Kubernetes Service에는 clusterIP, nodePort, LoadBalancer 3가지 타입이 존재하고, Service는 kube-proxy에 의해 동작하게 된다.
각각에 대하여 상세히 알아보자.
서비스 통신 동작에 대한 설정을 관리하는 역할로 모든 Kubernetes 노드에 daemonset으로 배포된다.
kube-proxy에는 iptables proxy모드, ipvs proxy모드, nftable모드, eBPF모드 + XDP 가 있다.
clusterIP는 클러스터 내부 통신 목적의 서비스로, Control Plane의 iptables 분산 룰이 kube-proxy에 의해 모든 Worker Node에 적용된 이후 ,클라이언트가 clusterIP 접속 시 노드의 iptables 룰에 의해서 DNAT 처리가 되어 목적지와 통신하게 된다.
$ kubectl get svc -A
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default kubernetes ClusterIP 10.200.1.1 <none> 443/TCP 153m
default svc-clusterip ClusterIP 10.200.1.145 <none> 9000/TCP 139m
kube-system kube-dns ClusterIP 10.200.1.10 <none> 53/UDP,53/TCP,9153/TCP 153m
kube-system kube-ops-view NodePort 10.200.1.108 <none> 8080:30000/TCP 151m
$ docker exec -it myk8s-control-plane iptables -t nat -S | grep 10.200.1.145
-A KUBE-SERVICES -d 10.200.1.145/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SVC-KBDEBIL6IU6WL7RF ! -s 10.10.0.0/16 -d 10.200.1.145/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-MARK-MASQ
$ for i in worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i iptables -t nat -S | grep 10.200.1.145; echo; done
>> node myk8s-worker <<
-A KUBE-SERVICES -d 10.200.1.145/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SVC-KBDEBIL6IU6WL7RF ! -s 10.10.0.0/16 -d 10.200.1.145/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-MARK-MASQ
>> node myk8s-worker2 <<
-A KUBE-SERVICES -d 10.200.1.145/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SVC-KBDEBIL6IU6WL7RF ! -s 10.10.0.0/16 -d 10.200.1.145/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-MARK-MASQ
>> node myk8s-worker3 <<
-A KUBE-SERVICES -d 10.200.1.145/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SVC-KBDEBIL6IU6WL7RF ! -s 10.10.0.0/16 -d 10.200.1.145/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-MARK-MASQ
$ docker exec -it myk8s-control-plane ss -tnlp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.1:37521 0.0.0.0:* users:(("containerd",pid=104,fd=10))
LISTEN 0 4096 127.0.0.11:35057 0.0.0.0:*
LISTEN 0 4096 172.18.0.4:2379 0.0.0.0:* users:(("etcd",pid=656,fd=9))
LISTEN 0 4096 172.18.0.4:2380 0.0.0.0:* users:(("etcd",pid=656,fd=7))
LISTEN 0 4096 127.0.0.1:2381 0.0.0.0:* users:(("etcd",pid=656,fd=16))
LISTEN 0 4096 127.0.0.1:2379 0.0.0.0:* users:(("etcd",pid=656,fd=8))
LISTEN 0 4096 127.0.0.1:10257 0.0.0.0:* users:(("kube-controller",pid=562,fd=3))
LISTEN 0 4096 127.0.0.1:10259 0.0.0.0:* users:(("kube-scheduler",pid=550,fd=3))
LISTEN 0 4096 127.0.0.1:10249 0.0.0.0:* users:(("kube-proxy",pid=830,fd=13))
LISTEN 0 4096 127.0.0.1:10248 0.0.0.0:* users:(("kubelet",pid=723,fd=21))
LISTEN 0 4096 *:10250 *:* users:(("kubelet",pid=723,fd=9))
LISTEN 0 4096 *:10256 *:* users:(("kube-proxy",pid=830,fd=12))
LISTEN 0 4096 *:6443 *:* users:(("kube-apiserver",pid=570,fd=3))
$ docker exec -it myk8s-control-plane ip -c route
default via 172.18.0.1 dev eth0
10.10.0.2 dev vethee7e19dd scope host
10.10.0.3 dev vethb5c52334 scope host
10.10.0.4 dev vetha420baee scope host
10.10.0.5 dev vethc9967629 scope host
10.10.0.6 dev veth321e7cdd scope host
10.10.1.0/24 via 172.18.0.5 dev eth0
10.10.2.0/24 via 172.18.0.2 dev eth0
10.10.3.0/24 via 172.18.0.3 dev eth0
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.4
Cluster IP로 curl 접속 시 3개의 파드로 거의 균일하게 부하 분산 접속을 확인할 수 있다.
$ kubectl exec -it net-pod -- zsh -c "for i in {1..1000}; do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
345 Hostname: webpod3
328 Hostname: webpod2
327 Hostname: webpod1
$ kubectl exec -it net-pod -- zsh -c "for i in {1..100}; do curl -s $SVC1:9000 | grep Hostname; sleep 0.1; done"
Hostname: webpod2
Hostname: webpod1
Hostname: webpod3
Hostname: webpod2
Hostname: webpod3
Hostname: webpod2
Hostname: webpod3
Hostname: webpod1
Hostname: webpod1
Hostname: webpod2
Hostname: webpod2
Hostname: webpod2
Hostname: webpod3
Hostname: webpod1
Hostname: webpod3
Hostname: webpod3
Hostname: webpod2
Hostname: webpod3
Hostname: webpod2
Hostname: webpod3
...
iptables 모드에서 service 생성 시, 연동된 파드 갯수 퍼센트(%)를 기반으로 분산률을 정하는 코드를 분석해보자.
관련 코드는 kubernetes의 proxy/iptables/proxier.go 하위에 있다.
https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/iptables/proxier.go
1) writeServiceToEndpointRules
svc가 생성되고, kube-proxy가 iptables 규칙을 생성하는 함수이다.
해당 함수의 매개 변수는 아래와 같다.
func (proxier *Proxier) writeServiceToEndpointRules(natRules proxyutil.LineBuffer, svcPortNameString string, svcInfo proxy.ServicePort, svcChain utiliptables.Chain, endpoints []proxy.Endpoint, args []string) {
...
// Now write loadbalancing rules.
numEndpoints := len(endpoints)
// 서비스 endpoint를 돌면서 iptables 규칙을 한 줄 씩 생성한다.
for i, ep := range endpoints {
epInfo, ok := ep.(*endpointInfo)
if !ok {
continue
}
// 예시 ) "default/svc-clusterip:svc-webport -> 10.10.2.2:80"
comment := fmt.Sprintf(`"%s -> %s"`, svcPortNameString, epInfo.String())
// 예시) -A KUBE-SVC-KBDEBIL6IU6WL7RF
args = append(args[:0], "-A", string(svcChain))
args = proxier.appendServiceCommentLocked(args, comment)
// i < (numEndpoints - 1) 규칙을 통해 마지막 서비스 Endpoint의 경우, probability를 추가하지 않음으로써 모든 남은 트래픽을 해당 엔드포인트로 보내는 것을 보장한다.
if i < (numEndpoints - 1) {
// Each rule is a probabilistic match.
// 예시) -m statistic --mode random --probability 0.33333333349
args = append(args,
"-m", "statistic",
"--mode", "random",
// proxier.probability함수에 서비스 Endpoint 개수가 넘겨지고, 그 개수에 의해 확률이 정해진다.
"--probability", proxier.probability(numEndpoints-i))
}
// The final (or only if n == 1) rule is a guaranteed match.
// 예시) -j KUBE-SEP-TBW2IYJKUCAC7GB3
natRules.Write(args, "-j", string(epInfo.ChainName))
}
}
2) probability
writeServiceToEndpointRules에서 호출되어 서비스 Endpoint의 분산 확률 값을 계산하고, 반환하는 코드이다.
type Proxier struct {
...
// Since converting probabilities (floats) to strings is expensive
// and we are using only probabilities in the format of 1/n, we are
// precomputing some number of those and cache for future reuse.
precomputedProbabilities []string
...
}
//precomputedProbabilities string slice에 저장된 분산 확률 값 목록인 preComputedProbabilities(n)를 반환하고, n번째 확률 값이 없다면 computeProbability를 호출하여 확률값을 추가하고 반환한다.
func (proxier *Proxier) probability(n int) string {
if n >= len(proxier.precomputedProbabilities) {
proxier.precomputeProbabilities(n)
}
return proxier.precomputedProbabilities[n]
}
func (proxier *Proxier) precomputeProbabilities(numberOfPrecomputed int) {
if len(proxier.precomputedProbabilities) == 0 {
proxier.precomputedProbabilities = append(proxier.precomputedProbabilities, "<bad value>")
}
// 서비스 Endpoint 개수에 따라 계산한 확률 값을 preComputedProbabilities에 저장한다.
for i := len(proxier.precomputedProbabilities); i <= numberOfPrecomputed; i++ {
proxier.precomputedProbabilities = append(proxier.precomputedProbabilities, computeProbability(i))
}
}
//서비스 Endpoint 개수에 따라 확률값을 계산한다. 서비스 Endpoint 개수 n에 대해 1/n의 확률 값을 소수점 10자리 형식으로 반환한다.
func computeProbability(n int) string {
return fmt.Sprintf("%0.10f", 1.0/float64(n))
}
3) Netfilter에서 iptables의 규칙을 기반으로 실제 트래픽 라우팅
iptables의 -m statistic --mode random --probability 옵션은 Netfilter에 의해 처리된다.
해당 기능은 net/netfilter/xt_statistic.c에 구현되어 있다.
https://github.com/torvalds/linux/blob/master/net/netfilter/xt_statistic.c
statistic mode가 random일 때 처리되는 방식에 대해 확인해보자.
//statistic이 호출될 때 해당 함수 실행
statistic_mt(const struct sk_buff *skb, struct xt_action_param *par)
{
const struct xt_statistic_info *info = par->matchinfo;
//ret는 statictic 모듈의 규칙이 패킷에 매칭되는지를 나타내는 bool변수이며, default가 false이다.
bool ret = info->flags & XT_STATISTIC_INVERT;
int nval, oval;
switch (info->mode) {
//statistic mode가 random : iptables에 명시된 probability 비율보다 무작위로 생성한 숫자(0~최대값)가 작다면 ret의 값을 반전시킨다.(true->false, false->true)
//ret이 true이면 패킷이 규칙에 매칭되었다 판단하여 트래픽을 보내고, false이면 매칭되지 않았다 판단하여 라우팅시키지 않고, 다음 규칙으로 넘어간다.
//최종적으로는 probability에 설정한 확률값보다 작은 랜덤 숫자가 생성되면 정상적으로 라우팅 하고, 더 큰 랜덤 숫자가 생성되면 라우팅 하지 않고 다음 규칙으로 넘어감으로써 probability 수치에 맞게 트래픽 랜덤 분산을 수행 한다.
case XT_STATISTIC_MODE_RANDOM:
if ((get_random_u32() & 0x7FFFFFFF) < info->u.random.probability)
ret = !ret;
break;
...
}
return ret;
}
클러스터 내부 pod에서 ClusterIP:port로 통신 시 control-plane 내에서 conntrack을 통해 Netfilter connection tracking 테이블을 조회할 수 있다.
src:dst가 클러스터 내부 podIP:ClusterIP 였다가, 서비스와 연결된 podIP:클러스터 내부 podIP로 바뀌는 것을 확인할 수 있다.
# SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
# kubectl exec -it net-pod -- zsh -c "for i in {1..100}; do curl -s $SVC1:9000 | grep Hostname; sleep 1; done"
Hostname: webpod2
Hostname: webpod3
Hostname: webpod3
Hostname: webpod3
Hostname: webpod2
...
# docker exec -it myk8s-control-plane sh
# conntrack -L --src 10.10.0.6
# conntrack -L --dst 10.200.1.189
tcp 6 2 TIME_WAIT src=10.10.0.6 dst=10.200.1.189 sport=50638 dport=9000 src=10.10.3.3 dst=10.10.0.6 sport=80 dport=50638 [ASSURED] mark=0 use=1
tcp 6 118 TIME_WAIT src=10.10.0.6 dst=10.200.1.189 sport=39762 dport=9000 src=10.10.1.2 dst=10.10.0.6 sport=80 dport=39762 [ASSURED] mark=0 use=1
tcp 6 0 TIME_WAIT src=10.10.0.6 dst=10.200.1.189 sport=50624 dport=9000 src=10.10.3.3 dst=10.10.0.6 sport=80 dport=50624 [ASSURED] mark=0 use=1
tcp 6 12 TIME_WAIT src=10.10.0.6 dst=10.200.1.189 sport=45118 dport=9000 src=10.10.3.3 dst=10.10.0.6 sport=80 dport=45118 [ASSURED] mark=0 use=1
tcp 6 15 TIME_WAIT src=10.10.0.6 dst=10.200.1.189 sport=45152 dport=9000 src=10.10.3.3 dst=10.10.0.6 sport=80 dport=45152 [ASSURED] mark=0 use=1
tcp 6 20 TIME_WAIT src=10.10.0.6 dst=10.200.1.189 sport=58170 dport=9000 src=10.10.2.2 dst=10.10.0.6 sport=80 dport=58170 [ASSURED] mark=0 use=1
클러스터 내부 Pod에서 ClusterIP 서비스로 통신할 때, 각 워커노드에서의 packet dump를 확인해본다.
tcpdump -i eth0 tcp port 80 -nn
클러스터 내부 pod <-> 서비스 Pod 간의 패킷이 확인된다.
tcpdump -i eth0 tcp port 9000 -nn
패킷 통신이 확인되지 않는다.
tcpdump -i $VETH tcp port 80 -nn
클러스터 내부 pod <-> 서비스 Pod 간의 패킷이 확인된다.
tcpdump -i $VETH tcp port 9000 -nn
패킷 통신이 확인되지 않는다.
ClusterIP의 경우 클라이언트 Pod가 ClusterIP:ClusterIP port로 통신을 시도할 때, (출발지 = 클라이언트 PodIP:랜덤port | 목적지 = ClusterIP:port) 패킷이 iptables rule에 의해 DNAT되어 (출발지 = 클라이언트 PodIP:랜덤port | 목적지 = 대상 PodIP:Pod port)로 변환된다.
이미 DNAT이 된 패킷이 NIC를 통해 빠져나가 다른 Node로 향하기 때문에, 위 실습에서 ClusterIP port로 packet dump가 찍히지 않고, pod port로 packet dump가 찍히게 된다.
Control-Plane에서 iptables rules를 확인해본다.
PREROUTING 체인 : 클러스터 내부 트래픽이 Kubernetes Service iptables에 의해 처리되도록 하는 역할.
# iptables -v --numeric --table nat --list PREROUTING
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
387 23318 KUBE-SERVICES 0 -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
KUBE-SERVICES 체인: Cluster IP 10.200.1.189의 tcp 9000포트에 대한 요청을 KUBE-SVC-KBDEBIL6IU6WL7RF 체인이 처리함.
# iptables -v --numeric --table nat --list KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SVC-KBDEBIL6IU6WL7RF 6 -- * * 0.0.0.0/0 10.200.1.189 /* default/svc-clusterip:svc-webport cluster IP */ tcp dpt:9000
KUBE-SVC-XXX 체인 :
# iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF
Chain KUBE-SVC-KBDEBIL6IU6WL7RF (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ 6 -- * * !10.10.0.0/16 10.200.1.189 /* default/svc-clusterip:svc-webport cluster IP */ tcp dpt:9000
67 4020 KUBE-SEP-TBW2IYJKUCAC7GB3 0 -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport -> 10.10.1.2:80 */ statistic mode random probability 0.33333333349
70 4200 KUBE-SEP-DOIEFYKPESCDTYCH 0 -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport -> 10.10.2.2:80 */ statistic mode random probability 0.50000000000
97 5820 KUBE-SEP-K7ALM6KJRBAYOHKX 0 -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport -> 10.10.3.3:80 */
KUBE-SEP-YYY 체인 :
# iptables -v --numeric --table nat --list KUBE-SEP-TBW2IYJKUCAC7GB3
Chain KUBE-SEP-TBW2IYJKUCAC7GB3 (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ 0 -- * * 10.10.1.2 0.0.0.0/0 /* default/svc-clusterip:svc-webport */
67 4020 DNAT 6 -- * * 0.0.0.0/0 0.0.0.0/0 /* default/svc-clusterip:svc-webport */ tcp to:10.10.1.2:80
POSTROUTING 체인 : 패킷이 NIC를 빠져나가기 전에 적용되는 규칙
KUBE-POSTROUTING 체인 :
# iptables -v --numeric --table nat --list POSTROUTING
Chain POSTROUTING (policy ACCEPT 10724 packets, 643K bytes)
pkts bytes target prot opt in out source destination
183K 11M KUBE-POSTROUTING 0 -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */
# iptables -v --numeric --table nat --list KUBE-POSTROUTING
Chain KUBE-POSTROUTING (1 references)
pkts bytes target prot opt in out source destination
3903 234K RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0 mark match ! 0x4000/0x4000
0 0 MARK 0 -- * * 0.0.0.0/0 0.0.0.0/0 MARK xor 0x4000
0 0 MASQUERADE 0 -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ random-fully
Kubernetes 네트워크 단에서 이슈 발생 시 기본적으로 iptables를 분석하면서 이슈를 해결하게 된다.
운영을 하며 이슈가 발생하였을 때, iptables를 참고한 몇가지 사례이다.
(1) 현상
Kubernetes 버전 1.19 -> 1.23으로 순차적으로 업그레이드 하는 상황에서 Kubernetes 버전 1.21 -> 1.22 업그레이드 시 ingress 통신이 실패하는 현상 발생
(2) 현상 분석
Kubernetes 버전 1.21 -> 1.22로 업그레이드를 한 후 nginx-ingress-controller pod가 새롭게 뜨면 기존의 CNI-HOSTPORT-DNAT 체인이 사라지고 새로운 CNI-HOSTPORT-DNAT 체인이 생기는 것이 아니라 계속해서 체인이 쌓이면서 통신이 불가능해짐.
# iptables -t nat -L CNI-HOSTPORT-DNAT
Chain CNI-HOSTPORT-DNAT (2 references) target prot opt source destination
CNI-DN-927a1e1c4bd5758f249e2 tcp -- anywhere anywhere /* dnat name: "k8s-pod-network" id: "4c75c2d6bf09093eb41fed19414ce577a5d4f0e0196a435d63b2c3b9795bb2c5" */ multiport dports http,https,pcsync-https
CNI-DN-35351e11e6944203cb9a5 tcp -- anywhere anywhere /* dnat name: "k8s-pod-network" id: "3de4fe737cdd5b3787ca3e9d50ea40e4815179e1be62e7002e137a8e18916c3f" */ multiport dports http,https,pcsync-https
(3) 원인
kubelet에서 dockershim을 사용하는 경우에 해당하는 이슈였다.
kubelet의 dockershim은 hostport로 뜨는 Pod에 대해서 kubelet 컨테이너의 /var/lib/dockershim/sandbox하위에 Portmapping 정보를 넣어놓고, pod가 바뀔 때마다 기존의 정보는 삭제하고, 최신 정보를 iptables에 등록하게 된다.
# docker exec -it kubelet /bin/sh
sh-5.1# cd /var/lib/dockershim/sandbox/
sh-5.1# ls
7dded8712b8dbafacafda34d56a4f28aa5445f96c2274e0e509028a55b1d29ec
sh-5.1# cat 7dded8712b8dbafacafda34d56a4f28aa5445f96c2274e0e509028a55b1d29ec
# 현재 해당 노드에 떠있는 hostport 파드의 정보와 portmapping 정보
{"version":"v1","name":"nginx-ingress-controller-dd8rh","namespace":"ingress-nginx","data":{"port_mappings":[{"protocol":"tcp","container_port":80,"host_port":80},{"protocol":"tcp","container_port":443,"host_port":443},{"protocol":"tcp","container_port":8443,"host_port":8443}]},"checksum":2543962669}
Kubernetes의 1.22에서 Portmap 플러그인이 v0.8.6에서 v1.0.0으로 업그레이드 되었는데, 해당 버전의 Portmap 플러그인에는 Portmapping 정보가 주어지지 않으면 아무 작업을 수행하지 않도록 코드가 업데이트 되었다.
kubelet은 /var/lib/dockershim에 Pod 메타데이터를 저장하는데, 업그레이드 직후에는 kubelet이 재시작되면서 해당 디렉토리의 메타데이터가 사라진다.
hostPort pod의 경우, portMappings의 Pod 메타데이터를 기반으로 portmap 바이너리가 iptables 규칙을 변경하게 된다.
일반적으로 kubelet은 Pod 메타데이터가 없더라도 정상적으로 Pod를 종료하지만, hostPort와 새로운 버전의 Portmap 플러그인을 사용할 경우, pod 메타데이터가 사라지고 portMappings 리스트가 빈 값으로 반환되면서 iptables에서 사라진 Pod 관련 규칙 처리가 정상적으로 이루어지지 않고, 결과적으로 iptables duplicate 현상이 발생한다.
(4) 해결 방법
제일 간단한 해결 방법은 portmap 플러그인 버전을 업그레이드 하는 것이나, 해당 플러그인만 업그레이드 할 수 없는 상황이었고, W/A로 kubelet의 /var/lib/dockershim 하위 pod metadata 정보를 VM에 기록하여 portMappings 리스트를 정상적으로 반환하도록 하여 iptables에 삭제된 Pod의 규칙이 정상 제거되도록 하였다.
Cluster에 아래와 같은 설정을 추가하면 VM에 /var/lib/dockershim 경로가 생성되고, 하위에 Pod의 메타데이터 정보가 기록된다.
services:
kubelet:
extra_binds:
- "/var/lib/dockershim:/var/lib/dockershim"
이 후 업그레이드를 진행했을 때, Pod 메타데이터 정보를 기반으로 Pod가 iptables에서 정상적으로 삭제되고, 추가되어 정상적으로 ingress 통신이 되었다.
(1) 현상
Kubernetes 버전 업그레이드 이후 Worker Node에서 모든 Pod 기동이 실패함.
(2) 현상 분석
Worker Node에 접속하여 iptables를 확인해보았을 때 Kubernetes 관련 규칙이 사라짐.
iptables 업데이트를 관장하는 kube-proxy 상태를 확인해보았을 때, kube-proxy가 정상적으로 버전 업데이트 되지 않음.
(3) 원인
Kubernetes에는 Static Pod와 Mirror Pod라는 개념이 있다.
Static Pod는 Kubelet이 직접 노드의 파일 시스템에서 관리하는 Pod로, /etc/kubernetes/manifests 등 경로에 있는 yaml 파일을 주기적으로 확인하여 해당 파일에 정의된 Static Pod를 생성한다.
Mirror Pod는 Static Pod의 상태를 Kubernetes API 서버에 반영하기 위해 생성되는 리소스로, Static Pod의 내용을 Kubernetes API 서버에서 조회하거나 관리할 수 있도록 생성되는 리소스이다.
Kubelet이 Static Pod를 생성하면, 이를 Kubernetes API 서버에 Mirror Pod로 등록하고 동기화 상태를 유지하게 된다.
해당 이슈는 Kubelet과 Kubernetes API서버간의 타이밍 이슈로 인한 문제로, Kubelet이 새로운 kube-proxy Static Pod를 생성하기 전에 기존의 Mirror Pod가 먼저 Kubernetes API 서버에 등록되면서 Kubernetes API에서는 새로운 kube-proxy Pod가 존재한다고 인식하고, 새로운 Pod가 생성되지 않는다.
(4) 해결 방법
kube-proxy 관련 pod manifest파일을 삭제하거나 이동시킨다.
이를 통해 Kubelet이 kube-proxy 파드 삭제 후 생성 과정을 순서대로 명확히 밟으면서 Static Pod 삭제 -> Mirror Pod 삭제 -> 새로운 Static Pod 생성 -> 새로운 Mirror Pod 생성 순서가 보장되고 kube-proxy의 정상 업데이트가 가능해진다.