서비스 - 가상 IP 매커니즘

개요

이 문서는 서비스의 핵심 작동 원리를 담고자 분리된 문서이다.
서비스 문서에 전부 담기에는 조금 하드하지 않나 싶기도 했고, 공식 문서[1]에서도 일단 분리를 하고 있기 때문에 나도 이 방향이 바람직하다고 생각한다.
다만 이 문서에서는 원리에 대한 간단한 내용을 담는 것을 핵심으로 하고, 세부 설정을 하는 것은 서비스 문서로 옮기려고 한다.
태반이 서비스 양식을 어떻게 작성할 것인가에 관한 부분이라 구태여 나누어 설명하는 것이 더 큰 혼란을 야기한다고 생각했다.
아직도 지금 문서는 진짜 개판이라고 생각한다

이 문서에는 구체적으로 다음의 두 가지 내용을 담는다.

서비스의 동작 원리를 설명하는 만큼, 관련한 기능을 제공하는 kube-proxy와도 밀접한 관련이 있다.

kube-proxy를 통한 가상 IP 프록싱

서비스가 만들어지면 어떤 ip를 부여 받게 되는데, 이것은 어떻게 부여받는가?
이 물음 이전에, 클러스터에서 서비스의 ip로 들어온 트래픽이 노드에 대한 정보가 없어도 원하는 엔드포인트로 가는지 알아보자.

프록시

서비스 문서에서 간략하게 설명했듯이, 실제로 모든 트래픽에 대한 설정을 해주는 것은 바로 kube-proxy(이하 프록시)이다.
모는 노드에 배치된 이 프록시는 세부 설정을 하지 않는 이상 동일하게 동작한다.
이 친구들은 kube-apiserver를 통해 서비스, 엔드포인트슬라이스 오브젝트를 감시한다.
그리고 오브젝트들에 변화가 생기면 이를 자신에 노드에 적용하는 일을 한다.

엔포슬 컨트롤러

엔드포인트슬라이스와 서비스의 관계는 엔드포인트슬라이스를 참고한다.
엔드포인트슬라이스 컨트롤러가 이들을 만들고, 연결되는 관계를 관리하는 일을 맡는다.
저 컨트롤러가 화이트칼라로서 문서를 정리해주면 프록시가 블루칼라마냥 실제 공구리를 쳐준다..

그렇다면 프록시는 어떤 설정을 노드에 가하는가?
그 방법은 바로 iptables에 있다.
모든 노드에는 패킷을 필터링해서 트래픽을 내부로 라우팅하거나 그대로 전달하는(forward) NetFilter가 존재한다.
이 넷필터를 조작하는 방법이 바로 iptables라는 것이고, 이것은 규칙 리스트를 지정하여 적용하는 식으로 동작한다.
프록시는 이렇게 규칙 리스트를 작성하여 실제로 노드로 들어온 트래픽이 원하는 곳으로 전달되도록 만드는 것이다.

iptables 외의 방법

참고로 iptables 말고 다른 프록시 모드를 사용하는 설정도 가능하다.
자세한 내용은 kube-proxy를 참조하자.

프록시 간단 예시

서비스가 만들어지면 임의의 가상 ip가 만들어지고, 엔드포인트슬라이스 컨트롤러가 이 서비스에 매칭되는 엔드포인트들을 묶어 관리한다.
새로운 서비스를 프록시가 관측하면 일련의 여러 iptables 규칙들을 만든다.
구체적으로는 서비스에서 엔포슬로 관리되는 각 엔드포인트로 프록시되는 규칙, 그리고 엔드포인트로의 트래픽이 실제 파드로 가도록 DNAT(가는 목적지 수정)하는 규칙을 관리한다.
그래서 궁극적으로 서비스의 가상 ip로의 트래픽은 엔드포인트로 흘러들어가고, 이후 백엔드까지 흘러가게 된다.

iptables 룰을 보기 쉽게 잘 설명한 듯..[2]
PREROUTING - KUBE-SERVICES - KUBE-SVC-... - KUBE-SEP-...의 순서로 체인이 연결된다.
SVC가 각 서비스를 나타내며, 다음 단계에서 주석 뒷부분 설정을 통해(random probability) 랜덤 부하분산된다.
마지막 체인에서 DNAT되어 실제 파드의 ip로 이어지게 된다.

image.png
실제로 어떻게 규칙들을 쓰는가 보니까 진짜로 일일히 iptables 명령어를 만들어서 명령을 내리는 방식이다;[3]

왜 프록시를 사용하나?

프록시를 사용하는 이유는 무엇일까?
가령 DNS를 활용해 한 서비스의 도메인네임을 쿼리하면 파드의 ip를 바로 찔러주는 A레코드를 만들면 안되는 걸까?
dns 차원의 로드 밸런싱(round-robin name resolution) 방법도 존재하니, 서비스가 바라는 기능을 충족할 수 있을 지도 모른다.
그러나 dns를 사용하지 않는 데에는 몇 가지 이유가 있다.

반면 kube-proxy는 커널 레벨의 룰을 수정한다.
그래서 패킷을 뜯어 어디로 가야 하는지 파악하고 이를 빠르게 라우팅하는데 효과적이다.

주의사항

참고로 프록시 규칙을 맘대로 커스텀하려 했다가는 노드를 재부팅하지 않는 이상 수정되지 않을 수도 있다..
그래서 이 프록시를 직접 다루는 건 낮은 레벨에서의 조작 결과를 아는 관리자만이 해야 한다.

프록시 모드

기본 원리는 iptables로 설명했으나, kube-proxy가 어떤 툴을 활용하여 프록시 규칙을 설정할 것인가에 따라 몇 가지 타입을 세분화시킬 수 있다.
각각의 차이를 깊게 보기에는 내 수준이 조금 얕고, 이해를 증진시키기 위한 차원에서만 설명한다.

iptables

|850
리눅스에서만 가능한 모드로, 커널의 넷필터 subsystem에 있는 iptables API를 이용한다.
기본적으로 각 엔드포인트는 무작위 파드와 매칭된다.

iptables 최적화

kube-proxy에 대해서 몇 가지 설정들을 해주는 방식으로 부하 최소화를 통한 최적화를 꾀할 수 있다.

iptables:
  minSyncPeriod: 1s
  syncPeriod: 30s

iptables에서는 모든 서비스와 모든 엔드포인트에 룰이 추가된다.
1000개의 서비스와 엔드포인트가 있다면? 룰을 변경하고 적용하는데 시간도 오래 걸릴 것이다.

이때 위의 설정들을 하는 것이 도움이 될 수 있다.
minSyncPeriod는 재동기화를 하는 최소 기간을 지정한다.
이 값이 0이면 서비스가 생기거나 바뀔 때마다 프록시는 값을 반영한다.
짧게 많이 서비스를 수정하는 경우 부하가 걸리게 될 것이다.
또 100개의 파드를 관리하는 디플을 없앤다고 한다면, 이 기간이 조금 여유가 있으면 룰을 수정할 때 한꺼번에 수정하게 돼서 부하가 줄어든다.
그래서 오히려 부하가 줄어들어 결과가 빠르게 반영되게 될 수도 있다.
물론 이 값이 크면 동기화에 걸리는 텀이 길어지니 안 좋을 수 있다.
기본 값은 1초이며, 규모가 큰 클러스터에서는 조금 더 큰 값이 필요할 수도 있다.
프록시 메트릭 중 sync_proxy_rules_duration_seconds라는 값의 평균이 1초보다 크다면 이 값을 늘리는 방향으로 효율화해보자.

syncPeriod 파라미터는 개별 서비스와 엔드포인트의 변경과 직접적으로는 관련 없는 동기화 작업을 제어한다.
구체적으로는 프록시와 관련 없는 외부 컴포넌트의 개입이 일어난 것을 얼마나 빠르게 감지하는가에 대한 것이다.
큰 클러스터에서는 한번씩 불필요한 작업을 정리하는 시간이 필요하긴 하다.
대체로는 이게 큰 영향을 끼치지는 않는데, 과거에는 아예 1시간으로 설정하는 케이스가 있었다고 한다.
지금은 추천되지 않는데 이게 오히려 기능성에 영향을 주기 때문이다.

ipvs

|850
사진으로는.. 뭐가 다른지는 잘 모르겠다.

리눅스 노드에서만 가능한 방식으로, ipvs는 커널의 ipvs와 iptables api를 사용한다.
iptables 모드와 비슷하나, 커널 스페이스에서 동작하는 해시 테이블을 사용한다.
그래서 레이턴시가 조금 더 줄어들고, 프록시 규칙 동기화의 성능이 올라간다.
심지어 처리량에서도 조금 더 좋은 성능을 보인다고 한다.

ipvs를 쓰려면 노드에서 먼저 ipvs가 가능하게 해야 한다.
이게 안 된 채로 프록시를 가동시키면 에러가 난다.

nftables

5.13 커널을 가진 리눅스 노드에서만 가능하며, nftables api를 사용한다.
iptables의 후속자로, 조금 더 좋은 성능과 유연성을 가지고 있다.
엔드포인트를 바꾸는 등 작업에 더 효율적이고, 커널 단의 패킷 처리도 더 빠르다고 한다.
그러나 수만 개의 서비스가 있는 클러스터 정도는 돼야 눈에 띈다고 한다.
Kubernetes v1.32 - Penelope에서도 아직 새로운 모드에 속하는 정도라, 클러스터에서 사용하는 CNI 플러그인이 이를 지원하는지는 확인이 꼭 필요하다.

iptables로부터 마이그레이션

마이그레이션을 할 때 조금 알아야 할 사항들이 있다.

kernelspace

윈도우에서 사용되는 모드인데, 윈도우 네트워크를 잘 몰라 매우 간략하게..
kube-proxy는 윈도우 vSwitch의 확장인 VFP(Virtual Filtering Platform)를 사용하게 된다.
노드 레벨의 가상 네트워크에 대해서 작업을 진행하고, DNAT를 해준다.
프록시에서 다른 노드의 파드로 가는 규칙을 쓰게 된다면, 윈도우의 HNS(Host Networking Service)가 응답패킷들이 잘 돌아올 수 있도록 보장해준다고 한다.

가상 IP가 서비스에 할당되는 원리


파드는 각 노드에서 할당되는 ip를 기본적으로 받는다.
(CNI마다 조금씩 설정이 다를 수도 있는데, Calico의 경우는 기본적으로 자체 ipam으로 대역을 설정해준다.)

그러나 서비스의 ip는 클러스터 전역에서 활용될 수 있는 범위를 토대로 ip를 부여받는다.
이것은 어떤 노드에 특정되지 않고 전역적으로 사용될 수 있는 가상 IP(Virtual IP)이다.

그래서 서비스의 ip는 단일 노드에서 값을 가진 채로 답을 내리는 것이 아니다.
서비스 ip에 대해 모든 노드의 프록시는 패킷 처리 로직을 사용하여, 투명하게(클라는 모르게) 리디렉트되도록 한다.
클라이언트가 가상 ip로 트래픽을 날려 어떤 노드에든 연결이 되면, 그들의 트래픽은 적절한 엔드포인트로 전송되고 문제가 발생하지 않는 것이다.
이런 원리 덕에 모든 노드에서 서비스의 ip를 자유롭게 사용할 수 있는 것이다.

ip 충돌 회피

쿠버네티스의 철학 중 하나는 관리자의 잘못이 아닌 이유로 작업 실패가 일어나지 않는 것이다.
그래서 서비스 ip를 지정할 때는 누군가의 선택과 충돌할 수 있는 선택이 허용되지 않는다. (이거 너무 번역투인데)
그래서 ip는 kube-apiserver에 명시된 범위 내에서 자동적으로 할당된다.
api서버에서 service-cluster-ip-range cidr 범위를 사용해 ip를 할당하며, 기본적으로 충돌이 일어나지 않게 한다.

구체적으로는 고유한 ip 주소를 얻는 것을 보장하기 위해 내부적인 할당자가 원자적으로 Etcd에 전역 할당 맵을 만들어둔다.
이 맵을 통해 정확하게 할당 가능한 ip 주소를 트래킹하며 ip를 할당해주는 것이다.
맵은 백그라운드 컨트롤러가 책임을 지는데, 이것은 관리자의 유효하지 않은 할당을 막고, 안 쓰이는 서비스에 대한 주소를 청소하는 일까지 해준다!

ip 범위 커스텀

근데.. Kubernetes v1.31 - Elli에서 MultiCIDRServiceAllocator 피처 게이트를 활성화하면 이걸 또 커스텀 가능하다.
IPAddress, ServiceCIDR 오브젝트를 내부의 전역 할당 맵 대신 사용하게 되는 것이다.
이때 IPAddress 오브젝트가 각 서비스의 클러스터 ip에 할당될 것이다.
그러나 Kubernetes v1.32 - Penelope에서조차 이 기능을 활성화하기 이전에서의 자동 마이그레이션은 제공하지 않으니 조심하자.
Kubernetes v1.33 - Octarine부터 기본적으로 사용이 가능해졌다!

이런 방식은 내 맘대로 서비스의 ip 범위를 지정할 수 있단 장점이 있다.
예를 들어 서비스를 위한 IP 대역이 부족해진다면 이 리소스를 만들어서 클러스터 재기동 없이 IP 대역을 확장하거나 추가할 수 있게 되는 것이다.
ipv4에서는 제한이 없고, ipv6에서도 /108까지이던 넷마스크를 /64나 그 아래까지도 지정할 수 있게 된다.

또한 유저의 커스텀으로 ip 주소를 지정할 수 있다는 장점도 있다(장점 맞냐).
Gateway API에서는 이걸 활용해 내부 네트워킹 능력을 확장한다고 한다.

직접 해보진 않을 것 같아서 사진만 남겨둔다.
자세한 내용은 문서 참고![4]

ip 밴드를 통한 정적 할당

자동이 아닌, 특정 ip를 정적으로 할당해야 하는 케이스가 있을 수 있다.[5]

대표적인 예시가 dns 서버인데, 주어진 ip 대역 내에서 관례적으로 dns 서버는 10번째 ip를 할당한다.
흔한 케이스는 아니겠지만, 이런 식으로 ip를 직접 할당할 때 ip 충돌 가능성은 더 커지기 마련이다.

이에 대해 쿠버네티스는 기본적으로 ip를 두 밴드로 나눈 후 정적으로 할당할 수 있는 범위를 제공해준다.
min(max(16, {cidrSize, 즉 확보된 호스트 주소 개수} / 16), 256)
공식은 이렇게 되는데, 간단하게 말하자면 클러스터 ip 범위에 따라 16~256개 정도의 ip를 수동으로 자유롭게 할당할 수 있다.
Pasted image 20250106141248.png
만약 서비스 대역 범위가 10.96.0.0/24라면?
cidrSize는 256일 것이고 위 공식에 따르면 min(max(16, 256/16), 256)으로 16의 값이 나온다.
그래서 16개의 ip가 충돌을 회피하는 정적 할당이 보장된다.
Pasted image 20250106141429.png
그럼 10.96.0.0/16의 범위 내에서는?
cidrSize는 65536, min(max(16, 65536/16), 256)으로 256이 나오게 된다.
그래서 10.96.0.1 ~ 10.96.1.0의 범위를 맘대로 쓸 수 있다!

관련 문서

이름 noteType created
Service knowledge 2024-12-29
서비스 - 가상 IP 매커니즘 knowledge 2025-01-02
가상 IP 매커니즘 knowledge 2025-05-04
EndpointSlice knowledge 2025-02-16
LoxiLB knowledge 2025-01-07
AWS Load Balancer Controller knowledge 2025-02-12
2W - ALB Controller, External DNS published 2025-02-15
E-레디네스 프로브와 레디네스 게이트 topic/explain 2024-08-15
I-EndpointSlice 분산 로직 분석 topic/idea 2025-01-03
S-flannel dns 질의 실패 topic/shooting 2024-09-11
T-스테이트풀셋과 연결되는 헤드리스 서비스에 관한 실험 topic/temp 2024-12-27
T-LoxiLB vs MetalLB topic/temp 2025-01-06

참고


  1. https://kubernetes.io/docs/reference/networking/virtual-ips/ ↩︎

  2. https://hackjsp.tistory.com/64#서비스(ClusterIP) 통신 흐름 분석-1 ↩︎

  3. https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/iptables/proxier.go ↩︎

  4. https://kubernetes.io/docs/tasks/network/extend-service-ip-ranges/ ↩︎

  5. https://kubernetes.io/docs/concepts/services-networking/cluster-ip-allocation/ ↩︎