가시다님의 Kubernetes Advanced Networking Study에 참여하게 되어, 스터디 때 다룬 주제를 정리하려고 합니다.
4주차는 Service(ClusterIP, NodePort)를 주제로 진행되었습니다.
아래 포함된 테스트는 kind로 구성한 클러스터에서 진행되었습니다.
kube-proxy 모드
kube-proxy는 서비스에서 파드로의 연결이 가능하게 해주는 컴포넌트이기 때문에, 어떤 모드를 사용하느냐에 따라 통신 방식이 다릅니다.
userspace
서비스에 대한 요청이 netfilter에 의해 모두 kube-proxy(user space에 속함)로 보내집니다.
패킷에 대해 user space <-> kernel space 로 전환이 필요하기 때문에 비효율적이며, kube-proxy 다운 시 이슈가 발생합니다.
더 이상 지원하지 않는 모드입니다.
iptables(기본값)
서비스에 대한 요청이 커널 영역의 netfilter에 의해서 처리되고, kube-proxy에 전달되지 않습니다. 따라서 user space <-> kernel space 전환이 필요하지 않습니다. 이때, kube-proxy는 iptables을 통해 netfilter 규칙 관리를 담당합니다.
서비스, 파드 추가/삭제 시 다수의 규칙 변경이 필요하므로 서비스 수가 많은 경우 지연이 발생할 수 있습니다.
때문에 규칙을 효율적으로 관리하기 위하여 minSyncPeriod 옵션을 통해 sync 시간을 상향 조정하는 경우도 있습니다.
또한, 트래픽이 파드에 대해 동일한 확률로 분배되지만, 랜덤이기 때문에 실제로는 정확한 부하 분산이 이루어지지는 않습니다.
IPVS
netfilter에서 동작하는 IPVS(L4 Load Balancer)가 Service Proxy 역할을 수행합니다.
iptables에 비해 성능(처리 성능, 규칙 수 측면)이 좋고, 다양한 부하 분산 알고리즘을 제공합니다.
nftables
iptables를 nftables로 대체하여 사용하는 모드입니다.
netfilter를 사용하기 때문에 커널 영역에서 패킷이 처리되며, iptables보다 커널 내 패킷을 효율적으로 처리합니다.
kernel 5.13 버전 이상인 경우 사용이 가능합니다.
이 외에도 최근에는 kube-proxy를 사용하지 않고, eBPF를 활용하는 경우도 있다고 합니다.
참고) https://isovalent.com/blog/post/why-replace-iptables-with-ebpf/
Endpoint, EndpointSlice
서비스가 생성되면, 서비스에 연결된 파드를 관리하는 endpoint, endpointslice가 자동으로 생성됩니다.
기존 endpoint를 개선하기 위해 endpointslice가 나왔고 두 개의 차이점을 알아보겠습니다.
Endpoint
하나의 서비스의 모든 endpoint가 하나의 endpoint에 속하기 때문에, 규모가 큽니다.
endpoint에 대해 변경 사항(scaling, rollout 등)이 잦은 경우 변경을 위해 수행하는 작업(API Server <-> 노드, 컨트롤플레인 내 트래픽 등)에 부담이 있습니다.
EndpointSlice
하나의 Endpointslice에는 최대 100개의 endpoint(파드)가 속할 수 있습니다. flag를 통해 최대 1000개로 변경할 수 있습니다.
단일 파드를 추가하거나 제거할 때, 변경 사항을 감시하는 클라이언트에게 전달되는 업데이트의 수는 동일하지만, 대규모에서 업데이트 메시지의 크기가 훨씬 작아집니다.
의도적으로 비정상 파드를 생성한 뒤 endpoint, endpointslices 변경 사항을 확인해보겠습니다.
[비정상]
$ watch kubectl get pod,svc,ep,endpointslice -owide
Every 2.0s: kubectl get pod,svc,ep,endpointslice -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/mario-94dc76454-x5zsq 0/1 Running 1 (3m17s ago) 3m57s 10.10.0.5 myk8s-control-plane <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/kubernetes ClusterIP 10.200.1.1 <none> 443/TCP 91m <none>
service/mario NodePort 10.200.1.103 <none> 80:30001/TCP 3m57s app=mario
NAME ENDPOINTS AGE
endpoints/kubernetes 172.18.0.5:6443 91m
endpoints/mario 3m57s
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
endpointslice.discovery.k8s.io/kubernetes IPv4 6443 172.18.0.5 91m
endpointslice.discovery.k8s.io/mario-zskpv IPv4 8080 10.10.0.5 3m57s
$ k get endpoints mario -oyaml | k neat
apiVersion: v1
kind: Endpoints
metadata:
name: mario
namespace: default
subsets:
- notReadyAddresses:
- ip: 10.10.0.5
nodeName: myk8s-control-plane
targetRef:
kind: Pod
name: mario-94dc76454-x5zsq
namespace: default
uid: 92de98d0-8a96-443e-a6d9-17973b82c560
ports:
- name: mario-webport
port: 8080
protocol: TCP
$ k get endpointslices mario-zskpv -oyaml | k neat
addressType: IPv4
apiVersion: discovery.k8s.io/v1
endpoints:
- addresses:
- 10.10.0.5
conditions:
ready: false
serving: false
terminating: false
nodeName: myk8s-control-plane
targetRef:
kind: Pod
name: mario-94dc76454-x5zsq
namespace: default
uid: 92de98d0-8a96-443e-a6d9-17973b82c560
kind: EndpointSlice
metadata:
labels:
endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io
kubernetes.io/service-name: mario
name: mario-zskpv
namespace: default
ports:
- name: mario-webport
port: 8080
protocol: TCP
- endpoint : notReadyAddress에 포함
- endpointslices : condition false(ready, serving)
[정상]
$ watch kubectl get pod,svc,ep,endpointslice -owide
Every 2.0s: kubectl get pod,svc,ep,endpointslice -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/mario-94dc76454-x5zsq 1/1 Running 1 (5m44s ago) 6m24s 10.10.0.5 myk8s-control-plane <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/kubernetes ClusterIP 10.200.1.1 <none> 443/TCP 93m <none>
service/mario NodePort 10.200.1.103 <none> 80:30001/TCP 6m24s app=mario
NAME ENDPOINTS AGE
endpoints/kubernetes 172.18.0.5:6443 93m
endpoints/mario 10.10.0.5:8080 6m24s
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
endpointslice.discovery.k8s.io/kubernetes IPv4 6443 172.18.0.5 93m
endpointslice.discovery.k8s.io/mario-zskpv IPv4 8080 10.10.0.5 6m24s
$ k get endpoints mario -oyaml | k neat
apiVersion: v1
kind: Endpoints
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2024-09-28T06:55:50Z"
name: mario
namespace: default
subsets:
- addresses:
- ip: 10.10.0.5
nodeName: myk8s-control-plane
targetRef:
kind: Pod
name: mario-94dc76454-x5zsq
namespace: default
uid: 92de98d0-8a96-443e-a6d9-17973b82c560
ports:
- name: mario-webport
port: 8080
protocol: TCP
$ k get endpointslices mario-zskpv -oyaml | k neat
addressType: IPv4
apiVersion: discovery.k8s.io/v1
endpoints:
- addresses:
- 10.10.0.5
conditions:
ready: true
serving: true
terminating: false
nodeName: myk8s-control-plane
targetRef:
kind: Pod
name: mario-94dc76454-x5zsq
namespace: default
uid: 92de98d0-8a96-443e-a6d9-17973b82c560
kind: EndpointSlice
metadata:
annotations:
endpoints.kubernetes.io/last-change-trigger-time: "2024-09-28T06:55:50Z"
labels:
endpointslice.kubernetes.io/managed-by: endpointslice-controller.k8s.io
kubernetes.io/service-name: mario
name: mario-zskpv
namespace: default
ports:
- name: mario-webport
port: 8080
protocol: TCP
- endpoint : address에 포함
- endpointslice : condition true(ready, serving)
참고) https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/#motivation
Cluster IP
클러스터 내부에서만 접근이 가능한 서비스입니다.
서비스 생성 시 모든 노드에 iptables 규칙이 설정됩니다.
[서비스 및 파드 배포]
cat <<EOT> 3pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: webpod1
labels:
app: webpod
spec:
nodeName: myk8s-worker
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
name: webpod2
labels:
app: webpod
spec:
nodeName: myk8s-worker2
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
name: webpod3
labels:
app: webpod
spec:
nodeName: myk8s-worker3
containers:
- name: container
image: traefik/whoami
terminationGracePeriodSeconds: 0
EOT
## 통신 테스트용 파드 생성
cat <<EOT> netpod.yaml
apiVersion: v1
kind: Pod
metadata:
name: net-pod
spec:
nodeName: myk8s-control-plane
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOT
## 서비스 생성
cat <<EOT> svc-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-clusterip
spec:
ports:
- name: svc-webport
port: 9000 # 서비스 IP 에 접속 시 사용하는 포트 port 를 의미
targetPort: 80 # 타킷 targetPort 는 서비스를 통해서 목적지 파드로 접속 시 해당 파드로 접속하는 포트를 의미
selector:
app: webpod # 셀렉터 아래 app:webpod 레이블이 설정되어 있는 파드들은 해당 서비스에 연동됨
type: ClusterIP # 서비스 타입
EOT
$ kubectl apply -f 3pod.yaml,netpod.yaml,svc-clusterip.yaml
$ k get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
net-pod 1/1 Running 0 71s 10.10.0.6 myk8s-control-plane <none> <none>
webpod1 1/1 Running 0 71s 10.10.3.2 myk8s-worker <none> <none>
webpod2 1/1 Running 0 71s 10.10.2.2 myk8s-worker2 <none> <none>
webpod3 1/1 Running 0 71s 10.10.1.2 myk8s-worker3 <none> <none>
$ k get svc svc-clusterip
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
svc-clusterip ClusterIP 10.200.1.52 <none> 9000/TCP 77s
테스트용 파드(10.10.0.6) -> 서비스(10.200.1.52:9000) -> 다른 노드 내 파드
curl 요청 시 dst ip, port가 변경되는 것을 확인할 수 있습니다.
테스트용 파드2 (10.10.3.3) -> 서비스(10.200.1.52:9000) -> 같은/다른 노드 내 파드
myk8s-worker 노드에 테스트용 파드를 하나 더 생성하여, 같은 노드 내 파드에 접근하는 경우도 살펴보겠습니다.
$ k get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
net-pod 1/1 Running 0 37m 10.10.0.6 myk8s-control-plane <none> <none>
net-pod2 1/1 Running 0 67s 10.10.3.3 myk8s-worker <none> <none>
webpod1 1/1 Running 0 37m 10.10.3.2 myk8s-worker <none> <none>
webpod2 1/1 Running 0 37m 10.10.2.2 myk8s-worker2 <none> <none>
webpod3 1/1 Running 0 37m 10.10.1.2 myk8s-worker3 <none> <none>
서비스에 연결된 파드에 랜덤하게 접근하기 때문에 같은 노드에 파드가 있어도 다른 노드의 파드로 요청이 전달될 수 있습니다.
또한, 같은 노드의 파드로 요청이 전달될 때는 노드의 eth0 인터페이스는 사용하지 않습니다.
NodePort
노드의 포트를 통해 클러스터 외부에서도 접근이 가능한 서비스입니다.
서비스 생성 시 모든 노드에 iptables 규칙이 설정되고 Cluster IP도 설정됩니다.
[서비스 및 파드 배포]
cat <<EOT> echo-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-echo
spec:
replicas: 3
selector:
matchLabels:
app: deploy-websrv
template:
metadata:
labels:
app: deploy-websrv
spec:
terminationGracePeriodSeconds: 0
containers:
- name: kans-websrv
image: mendhak/http-https-echo
ports:
- containerPort: 8080
EOT
cat <<EOT> svc-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-nodeport
spec:
ports:
- name: svc-webport
port: 9000 # 서비스 ClusterIP 에 접속 시 사용하는 포트 port 를 의미
targetPort: 8080 # 타킷 targetPort 는 서비스를 통해서 목적지 파드로 접속 시 해당 파드로 접속하는 포트를 의미
selector:
app: deploy-websrv
type: NodePort
EOT
$ kubectl apply -f echo-deploy.yaml,svc-nodeport.yaml
$ kubectl get pod -owide;echo; kubectl get svc,ep svc-nodeport
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deploy-echo-5c689d5454-97sht 1/1 Running 0 47s 10.10.3.4 myk8s-worker <none> <none>
deploy-echo-5c689d5454-bpfsr 1/1 Running 0 47s 10.10.1.3 myk8s-worker3 <none> <none>
deploy-echo-5c689d5454-wjt2t 1/1 Running 0 47s 10.10.2.3 myk8s-worker2 <none> <none>
net-pod 1/1 Running 0 55m 10.10.0.6 myk8s-control-plane <none> <none>
net-pod2 1/1 Running 0 19m 10.10.3.3 myk8s-worker <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/svc-nodeport NodePort 10.200.1.108 <none> 9000:31379/TCP 48s
NAME ENDPOINTS AGE
endpoints/svc-nodeport 10.10.1.3:8080,10.10.2.3:8080,10.10.3.4:8080 48s
$ kubectl get no -owide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
myk8s-control-plane Ready control-plane 5h3m v1.31.0 172.18.0.5 <none> Debian GNU/Linux 12 (bookworm) 6.6.31-linuxkit containerd://1.7.18
myk8s-worker Ready <none> 5h3m v1.31.0 172.18.0.2 <none> Debian GNU/Linux 12 (bookworm) 6.6.31-linuxkit containerd://1.7.18
myk8s-worker2 Ready <none> 5h3m v1.31.0 172.18.0.3 <none> Debian GNU/Linux 12 (bookworm) 6.6.31-linuxkit containerd://1.7.18
myk8s-worker3 Ready <none> 5h3m v1.31.0 172.18.0.4 <none> Debian GNU/Linux 12 (bookworm) 6.6.31-linuxkit containerd://1.7.18
참고로, 서비스 생성 후 노드의 listen 포트를 살펴보면 노드 포트에 설정된 포트는 열려있지 않은 것을 확인할 수 있습니다.
[테스트용 외부 컨테이너 생성]
NodePort 서비스에 대한 요청을 보내기 위해 별도의 컨테이너를 생성합니다.
$ docker run -d --rm --name mypc --network kind --ip 172.18.0.100 nicolaka/netshoot sleep infinity
## 통신 확인
$ docker exec -it mypc ping -c 1 172.18.0.1
NodePort 서비스의 endpoint가 없는 컨트롤 플레인 노드에 요청을 보내보겠습니다.
mypc 컨테이너(172.18.0.100) -> 172.18.0.5:31379 (컨트롤플레인 노드 ip:노드 포트)로의 요청이 10.10.1.3:8080 (파드 ip:서비스 타겟 포트)로 보내질 때는 컨트롤플레인 노드의 ip를 갖고(SNAT) 나가는 것을 볼 수 있습니다.
파드가 없는 노드를 통해서도 정상적으로 접근 가능한 것을 확인할 수 있습니다.
또한, 클러스터 내부에서 NodePort Service의 Cluster IP로 통신 가능한 것을 확인할 수 있습니다.
iptables 규칙과 conntrack 테이블을 살펴보겠습니다.
Chain KUBE-EXT-VTR7MTHHNMFZ3OFS (2 references)
target prot opt source destination
KUBE-MARK-MASQ all -- anywhere anywhere /* masquerade traffic for default/svc-nodeport:svc-webport external destinations */
KUBE-SVC-VTR7MTHHNMFZ3OFS all -- anywhere anywhere
Chain KUBE-NODEPORTS (1 references)
target prot opt source destination
KUBE-EXT-VTR7MTHHNMFZ3OFS tcp -- anywhere 127.0.0.0/8 /* default/svc-nodeport:svc-webport */ tcp dpt:31379 nfacct-name localhost_nps_accepted_pkts
KUBE-EXT-VTR7MTHHNMFZ3OFS tcp -- anywhere anywhere /* default/svc-nodeport:svc-webport */ tcp dpt:31379
...
...
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-SVC-NPX46M4PTMTKRN6Y tcp -- anywhere 10.200.1.1 /* default/kubernetes:https cluster IP */ tcp dpt:https
KUBE-SVC-TCOU7JCQXEZGVUNU udp -- anywhere 10.200.1.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:domain
KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- anywhere 10.200.1.10 /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:domain
KUBE-SVC-JD5MR3NA4I4DYORP tcp -- anywhere 10.200.1.10 /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
KUBE-SVC-VTR7MTHHNMFZ3OFS tcp -- anywhere 10.200.1.108 /* default/svc-nodeport:svc-webport cluster IP */ tcp dpt:9000
KUBE-NODEPORTS all -- anywhere anywhere /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
Chain KUBE-SVC-ERIFXISQEP7F7OF4 (1 references)
target prot opt source destination
KUBE-MARK-MASQ tcp -- !10.10.0.0/16 10.200.1.10 /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:domain
KUBE-SEP-XVHB3NIW2NQLTFP3 all -- anywhere anywhere /* kube-system/kube-dns:dns-tcp -> 10.10.0.2:53 */ statistic mode random probability 0.50000000000
KUBE-SEP-ZEA5VGCBA2QNA7AK all -- anywhere anywhere /* kube-system/kube-dns:dns-tcp -> 10.10.0.3:53 */
Chain KUBE-SVC-JD5MR3NA4I4DYORP (1 references)
target prot opt source destination
KUBE-MARK-MASQ tcp -- !10.10.0.0/16 10.200.1.10 /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
KUBE-SEP-RT3F6VLY3P67FIV3 all -- anywhere anywhere /* kube-system/kube-dns:metrics -> 10.10.0.2:9153 */ statistic mode random probability 0.50000000000
KUBE-SEP-6GODNNVFRWQ66GUT all -- anywhere anywhere /* kube-system/kube-dns:metrics -> 10.10.0.3:9153 */
Chain KUBE-SVC-NPX46M4PTMTKRN6Y (1 references)
target prot opt source destination
KUBE-MARK-MASQ tcp -- !10.10.0.0/16 10.200.1.1 /* default/kubernetes:https cluster IP */ tcp dpt:https
KUBE-SEP-OJCTP5LCEHQJ3D72 all -- anywhere anywhere /* default/kubernetes:https -> 172.18.0.5:6443 */
Chain KUBE-SVC-TCOU7JCQXEZGVUNU (1 references)
target prot opt source destination
KUBE-MARK-MASQ udp -- !10.10.0.0/16 10.200.1.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:domain
KUBE-SEP-2XZJVPRY2PQVE3B3 all -- anywhere anywhere /* kube-system/kube-dns:dns -> 10.10.0.2:53 */ statistic mode random probability 0.50000000000
KUBE-SEP-XWEOB3JN6VI62DQQ all -- anywhere anywhere /* kube-system/kube-dns:dns -> 10.10.0.3:53 */
Chain KUBE-SVC-VTR7MTHHNMFZ3OFS (2 references)
target prot opt source destination
KUBE-MARK-MASQ tcp -- !10.10.0.0/16 10.200.1.108 /* default/svc-nodeport:svc-webport cluster IP */ tcp dpt:9000
KUBE-SEP-XEXGJWEWSC2GPNPZ all -- anywhere anywhere /* default/svc-nodeport:svc-webport -> 10.10.1.3:8080 */ statistic mode random probability 0.33333333349
KUBE-SEP-2AEFSWYPQGZTCWEI all -- anywhere anywhere /* default/svc-nodeport:svc-webport -> 10.10.2.3:8080 */ statistic mode random probability 0.50000000000
KUBE-SEP-C5MCUQGLTHD455UI all -- anywhere anywhere /* default/svc-nodeport:svc-webport -> 10.10.3.4:8080 */
...
# conntrack
conntrack v1.4.7 (conntrack-tools): 5 flow entries have been shown.
tcp 6 67 TIME_WAIT src=172.18.0.100 dst=172.18.0.5 sport=40262 dport=31379 src=10.10.2.3 dst=172.18.0.5 sport=8080 dport=21474 [ASSURED] mark=0 use=1
tcp 6 20 TIME_WAIT src=172.18.0.100 dst=172.18.0.5 sport=44104 dport=31379 src=10.10.3.4 dst=172.18.0.5 sport=8080 dport=56485 [ASSURED] mark=0 use=1
KUBE-NODEPORTS 체인에 규칙이 생성된 것을 볼 수 있습니다.
목적지 31379 포트로 들어온 패킷에 KUBE-EXT-VTR7MTHHNMFZ3OFS 체인이 적용됩니다.
KUBE-EXT-VTR7MTHHNMFZ3OFS에서는 마스커레이딩을 통해 SNAT을 하고 KUBE-SVC-VTR7MTHHNMFZ3OFS 체인이 적용됩니다.
KUBE-SVC-VTR7MTHHNMFZ3OFS 에서는 서비스에 연결된 endpoint 중 어느 endpoint로 보낼지 결정됩니다.
참고로 해당 체인은 서비스의 Cluster IP가 목적지인 패킷에도 동일하게 적용됩니다.
추가적으로 conntrack 테이블에서는 아래와 같은 정보를 확인할 수 있습니다.
• src=172.18.0.100, dst=172.18.0.5, dport=31379로 요청이 발생
• NAT 후 요청이 10.10.3.4(Pod)로 전달되었으며, 포트는 8080(서비스 타겟 포트)
참고 문서
블로그에서 같이 보면 좋은 글
'Kubernetes > Network Study' 카테고리의 다른 글
kube-proxy IPVS 모드 (2) | 2024.10.18 |
---|---|
kube-proxy 모니터링 환경 구성 실습(serviceMonitor, podMonitor) (3) | 2024.10.16 |
Calico 개념 및 실습 (3) | 2024.09.21 |
Flannel CNI 실습 (3) | 2024.09.07 |
Pause Container 이해하기 (11) | 2024.09.02 |