개요
k8s의 Service 리소스의 종류와 동작 방식 그리고 kube-proxy mode에 대해 탐색
필요성
k8s의 최소 단위인 Pod는 Ephemeral (일시적)인 특성을 가짐
클라이언트가 Pod의 IP를 이용하여 통신을 하다가도, Pod가 죽었다가 새로 생성되었을 때는 IP가 바뀌기 때문에 내구성이 부족
서버들의 앞단에 Reverse Proxy를 두어 클라이언트는 Proxy로 연결하여 사용하는 방식이 필요
Proxy 역할 자체는 특정 목록의 서버를 관리하고, 살아 있는 서버로 트래픽을 전달하는 것이기 때문
이 때 Proxy는 내구성이 있어야하고, 트래픽 전달할 서버 목록이 있어야하고, 해당 서버들이 정상적인 상태인지 확인할 수 있어야하는데, k8s는 이와 같은 Proxy를 Service 리소스로 우아하게 해결
Service 이름 도메인 주소로 사용하고 별도의 VIP를 입혀서 클라이언트가 고정적으로 접속할 수 있는 방법을 제공
내부 동작
부하 분산
Service의 필요성에서 알 수 있듯이, Reverse Proxy 역할을 수행해주는 Service를 이용하여 Pod와 통신할 때 트래픽을 부하 분산 가능
Pod 연동
Service를 이용하여 Pod와 연동할 때는 port / targetPort, 그리고 label / selector를 사용
port는 Service의 포트를 지칭하고, 해당 번호로 Service에 접근
targetPort는 Service가 트래픽을 포워딩할 때 사용할 Pod의 포트를 지칭하고, Service는 해당 번호로 Pod에 접근
port: 3000, targetPort: 8080의 예시
클라이언트가 컨테이너로 접근하기 위해 Service를 이용할 때, <service-ip>:3000으로 접근했을 때 Pod 내에서 실행 중인 애플리케이션의 리스닝 포트인 8080으로 접근
접근 경로의 설정과 달리 Service가 어떤 Pod들을 대상으로 하는지 명시하기 위해 label과 selector를 사용
Pod를 생성할 때 spec 상에 <key>=<value> 형태로 레이블을 입힐 수 있는데, 이를 selector에 명시하여 특정 Pod들을 엔드포인트로 인식시키는 것이 가능
Pod들을 엔드포인트로 인식 가능한 것은 k8s의 Endpoints Controller가 지속적으로 레이블에 해당되는 Pod들을 모니터링하여 엔드포인트에 연동하기 때문
kube-proxy mode
kube-proxy는 Service의 통신 동작 설정을 관리
kube-proxy는 DaemonSet으로 배포되어, k8s의 모든 노드에 생성
kube-proxy는 user space proxy mode, iptables proxy mode, ipvs proxy mode로 총 3가지로 나뉨
mode별 특징
ipvs proxy mode를 가장 권장
iptables proxy mode는 k8s 설치 시 default mode
user space proxy mode는 비효율적인 이유로 비권장
위 방식들은 Netfilter를 공통적으로 이용하는데, 이는 Pod에 대한 헬스 체크 기능은 없기 때문에 문제가 발생한 상태의 Pod로 트래픽을 흘려 보낼 수도 있음
따라서 Readiness Probe를 Pod에 설정하여 문제가 발생하면 엔드포인트에서 삭제될 수 있도록 하여야 함
** nftables는 iptables API의 확장성과 성능을 고려한 후속작인 nftables API를 이용하는 프로그램으로, 이를 이용하는 proxy mode는 상대적으로 신규 기능이라 리눅스 시스템에서만 동작하며, 모든 네트워크 플러그인과 호환성이 있지는 않음
** 위 mode들은 L3 ~ L4를 거치는 proxy mode인데 반해, 오버헤드가 상대적으로 큰 커널 구조를 우회하여 L2에서만 동작하는 eBPF proxy mode도 존재 (XDP 네트워크 모듈을 이용)
user space proxy mode
위 도표 처럼 클라이언트 요청이 네트워크 인터페이스를 통해 들어오면, Netfilter를 통해 kube-proxy로 전달
kube-proxy를 통해 목적지인 노드를 찾고, Pod로 트래픽이 전달되는 구조
kube-proxy가 직접 개입하다보니 커널 영역과 사용자 영역의 전환이 필요하면서 성능 측면에서 비효율적
kube-proxy 자체에 문제가 생기는 경우 통신이 안 되는 문제 존재
iptables proxy mode
kube-proxy가 통신 과정에서 직접 개입하지 않고, Service와 관련된 iptables의 규칙을 Netfilter에 적용
kube-proxy는 단순히 Netfilter의 규칙을 적절히 수정하는 것 뿐이고, kube-proxy 문제 시 규칙 수정은 어렵지만 통신은 가능하여 user space proxy mode에 비해 안정적
트래픽이 들어오면 사용자 영역까지 올라가지 않고 Netfilter 규칙에 의해 바로 네트워크 인터페이스를 거쳐 목적지 노드로 이동
사용자 영역까지 올라가지 않아 성능 상 이점이 있지만, 부하 분산 알고리즘이 부족하고 iptables 규칙이 많아지면 지연이 발생 가능
ipvs proxy mode
iptables와 비슷하게 kube-proxy는 통신에 직접 개입하지 않음
ipvs는 IP Virtual Server를 의미하며 Netfilter에서 동작하는 L4 LB
iptables보다 높은 성능 처리가 가능하기에 가장 효율적인 mode로써 권장되는 방식
ipvs는 리눅스 커널에서 동작하는 S/W LB로써 Netfilter를 이용하여 TCP 및 UDP를 처리 가능
iptables로 5000건 이상의 규칙이 등록되었을 때 시스템 성능이 급격히 떨어지는 문제를 극복하기 위해 IP 주소들의 집합이라는 IPSET 개념을 도입하여 극복
ipvs에서 지원하는 부하 분산
1.
rr (Round Robin)
2.
wrr (Weighted Round Robin)
3.
lc (Least Connection)
4.
wlc (Weighted Least Connection)
5.
lblc (Locality Based Least Connection)
6.
lblcr (Locality Based Least Connection with Replication)
7.
sh (Source Hashing)
8.
dh (Destination Hashing)
9.
sed (Shortest Expected Delay)
10.
nq (Never Queue)
종류
Service의 종류는 크게 ClusterIP, NodePort, LoadBalancer 크게 3가지 형태로 나뉨
** 언급된 3가지 종류는 클라이언트가 Service를 통해 Pod로 접속하는 방식인데, ExternalName이라는 타입은 Service를 접속하려고 했을 때 CNAME 레코드를 반환 받아 이를 이용하여 접속하는 방식
ClusterIP
개념
가장 기본적인 타입으로 클러스터 내부에서 접속하기 위한 방식
클러스터 내부에서만 접속이 가능하기 때문에, Node Port를 이용하면 클러스터 외부에서 접속 가능
동작 방식
ClusterIP 타입 Service를 생성하면 k8s API Server → kubelet → kube-proxy 를 거쳐 iptables에 규칙이 추가되는데, 이를 이용하여 dNAT 처리되어 Pod와 통신
sessionAffinity: ClientIP
클라이언트가 접속했던 Pod에 최대한 접속을 고정할 수 있도록 지원하는 설정으로, 해당 설정 없이는 엔드포인트에 등록된 Pod들로 무작위 부하 분산
Service를 통해 Pod로 트래픽을 전달할 때 커넥션을 기록해두고, 동일 클라이언트의 접근 시도 시엔 기록해둔 커넥션 정보를 통해 Pod로 재전달
NodePort
개념
노드에서의 포트를 Service의 포트와 연결하여, 클러스터 외부에서 <node-ip>:<node-port>를 이용하여 Service에 접속할 수 있는 방식
외부에서 노드의 직접적인 정보를 통해 접속하므로 보안에 취약하고, 이 때문에 LoadBalancer 타입을 이용하면 정보 공개 최소화 가능
동작 방식
노드를 통해 외부에서 접속하기 위한 방식을 단순히 추가한 것으로 Pod에 접속할 때는 ClusterIP 방식을 포함하여 동작 (NodePort → ClusterIP로 통신)
<node-ip>:<node-port>로 접속 시 kube-proxy에 의해 설정된 iptables의 분산 규칙에 기반하여 sNAT / dNAT가 동작하고, Pod와 통신 후 응답 받는 트래픽은 최초 인입된 노드를 거쳐서 다시 외부로 반환 (Direct Server Return이 아님)
externalTrafficPolicy: Local
Service의 externalTrafficPolicy의 기본 값은 Cluster로 클러스터 내의 모든 Pod를 대상으로 트래픽이 분배될 수 있도록 동작하는데, 이는 노드의 IP로 sNAT 되어 클라이언트의 IP가 보존되지 않음
만일 클라이언트의 IP를 수집해야 한다면, externalTrafficPolicy를 Local로 두어 sNAT 되지 않고 해당 노드에 존재하는 Pod로 트래픽을 처리시키는 것이 가능
Local의 경우 Double Hop을 해결할 수 있지만, Cluster처럼 트래픽을 고르게 처리할 수는 없다는 Trade Off가 존재
다른 문제로는 클라이언트의 IP를 보존할 수는 있지만 트래픽을 받은 노드에 Pod가 위치해있지 않으면 요청이 실패할 수 있는데, 이와 같은 문제는 LoadBalancer 타입의 Service를 이용하면서 Probe로 대처 가능
LoadBalancer
개념
일반적으로 CSP (Cloud Service Provider: AWS, GCP, Azure)에서 제공하는 타입으로 외부에서 클러스터를 접속하는 방식
온프레미스에서도 LB를 구성하여 LoadBalancer 타입을 이용하는 것도 가능
동작 방식
노드에 접근하기 위한 과정을 단순히 추가한 것으로 노드에 접근하는 NodePort와 Pod에 접속하는 ClusterIP 방식을 포함하여 동작 (LoadBalancer → NodePort → ClusterIP로 통신)