Kubernetes의 네트워크 이슈를 해결할 수 있는 network-node-manager

안녕하세요, 클라우드플랫폼팀 ssup이라고 합니다.
Kubernetes 기반 Kubernetes Platform을 개발, 운영하고 있습니다.

카카오 사내에서 DKOS라고 불리는 Kubernetes Platform을 운영하는 도중에 많은 Kubernetes 관련 Network Issue들을 경험할 수 있었습니다. 그중에 몇몇 Network Issue들은 Kubernetes Cluster를 구성하는 Node에 간단한 Network 설정을 추가하여 해결할 수 있는 Network Issue도 존재하였습니다. 이러한 Kubernetes 관련 Network Issue들을 해결하기 위해서 각 Node의 Network 설정을 관리하는 network-node-manager를 개발하게 되었습니다.

 


 

network-node-manager

 

network-node-manager는 Kubernetes Cluster를 구성하는 Node들의 Network 설정을 관리하는 Kubernetes Controller입니다. 현재 network-node-manager를 통해서 아래와 같은 2가지 문제를 해결할 수 있습니다. 각각의 문제에 대해서는 뒤에서 좀 더 자세히 설명할 예정입니다.

 

network-node-manager를 이용하기 위해서는 Kubernetes Cluster에 network-node-manager를 배포하기만 하면 됩니다. network-node-manager를 배포하기 위해서는 Kubernetes Cluster의 Pod Network CIDR와 kube-proxy의 Mode(iptables, IPVS)를 알아야 합니다. kube-proxy의 Mode에 따라서 network-node-manager의 방법이 달라집니다. network-node-manager는 IPv6를 이용하는 Service도 지원합니다.

배포 방법은 다음과 같습니다.

iptables proxy mode 
$ kubectl apply -f https://raw.githubusercontent.com/kakao/network-node-manager/master/deploy/network-node-manager_iptables.yml
$ kubectl -n kube-system set env daemonset/network-node-manager POD_CIDR_IPV4=[IPv4 POD CIDR]
$ kubectl -n kube-system set env daemonset/network-node-manager POD_CIDR_IPV6=[IPv6 POD CIDR]

IPVS proxy mode
$ kubectl apply -f https://raw.githubusercontent.com/kakao/network-node-manager/master/deploy/network-node-manager_ipvs.yml
$ kubectl -n kube-system set env daemonset/network-node-manager POD_CIDR_IPV4=[IPv4 POD CIDR]
$ kubectl -n kube-system set env daemonset/network-node-manager POD_CIDR_IPV6=[IPv6 POD CIDR]

 

Architecture

 

 

위의 그림은 network-node-manager의 Architecture를 나타내고 있습니다. network-node-manager는 DaemonSet을 통해서 Kubernetes Cluster의 모든 Node에 배포되어 동작합니다. 이때 network-node-manager는 Host Network Namespace에서 Network Privilege를 갖고 동작합니다. 따라서 network-node-manager는 Node(Host)의 Network 설정을 변경할 수 있게 됩니다.

network-node-manager는 Kubernetes API Server에게 Watch 요청을 통해서 특정 Object의 변화를 감시하고, 감시하는 Object의 변화에 따라서 Node의 Network 설정을 진행합니다. 현재 network-node-manager는 Service Object만을 Watch 하여 Service Object의 변화만을 감시하고, Service Object의 변화에 따른 Node Network 설정을 진행하고 있습니다.

 

Connection Reset Issue between Pod and Out of Cluster

Kubenetes Cluster 내부의 Client Pod와 외부의 Server와 통신 시 Client Pod 내부에서 “connection reset by peer” Error와 함께 TCP Connection이 갑자기 Reset되어 끊기는 문제가 발생할 수 있습니다. 일반적인 경우에는 거의 발생하지 않았지만 Client Pod에서 외부의 Server에게 파일과 같은 큰 Data를 전송하는 경우에 본 Issue를 경험할 수 있었습니다.

 

 

본 Issue를 이해하기 위해서는 먼저 Kubernetes Cluster 내부의 Client Pod와 외부의 Server과 통신하는 과정을 이해해야 합니다. 위의 그림은 이러한 통신 과정을 나타내고 있습니다. 일반적으로 Pod가 Overlay Network를 이용하는 경우, Client Pod가 외부 Server에게 전송한 요청 Packet은 Node에서 SNAT 됩니다. 즉 요청 Packet의 SrcIP는 Pod의 IP에서 Node의 IP로 변환됩니다. SNAT가 필요한 이유는 Pod의 IP는 Kubernetes Cluster 내부에서만 유효한 IP이기 때문에 Kubernetes Cluster 외부의 Server도 인식할 수 있는 Node의 IP로 변환해야 하기 때문입니다.

$ iptables -t nat -nvL
...
Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000
    0     0 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* Kubernetes endpoints dst ip:port, source ip for solving hairpin purpose */ match-set KUBE-LOOP-BACK dst,dst,src
...
Chain CILIUM_POST_nat (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  *      !cilium_+  10.240.2.128/25     !10.240.2.128/25      /* cilium masquerade non-cluster */
...

이러한 요청 Packet의 SNAT는 Kubenetes Cluster의 Network를 담당하는 kube-proxy 또는 CNI (Container Netowkr) Plugin이 설정한 iptables의 MASQUERADE Rule을 통해서 실행됩니다. 위에는 kube-proxy 또는 cilium CNI Plugin이 설정하는 iptables의 MASQUERADE Rule을 나타내고 있습니다.

Client Pod가 전송한 요청 Packet을 받은 Server는 Packet의 SrcIP가 Node의 IP인 것을 확인하고 응답 Packet을 Node에게 전송합니다. Server의 응답 Packet은 Node가 받는 것이 아니라 Node 안의 Client Pod가 받아야 하기 때문에 응답 Packet은 DNAT 됩니다. 즉 응답 Packet의 DstIP는 Node의 IP에서 Client Pod의 IP로 변경된 다음 Client Pod로 전달됩니다. 이러한 DNAT 과정은 Linux에서 Node의 Network Connection 정보를 관리하는 conntrack과 iptables에 의해서 암묵적으로 진행됩니다.

 

 

위의 그림은 본 Issue가 발생하는 경우를 나타내고 있습니다. 외부 서버의 응답 Packet이 외적인 요소로 인해서 순서가 변경(Out of Order) 되어 전송될 경우, 문제없는 Packet이지만 conntrack의 버그로 인해서 conntrack은 해당 Packet이 유효하지 않은(INVALID) Packet이라고 간주합니다. iptables는 유효하지 않는 Packet에 대해서는 암묵적 DNAT를 수행하지 않습니다. 따라서 해당 Packet은 DNAT 되지 않고 Client Pod가 아닌 Node에게 전송됩니다. Node는 Packet을 처리하는 과정 중에서 정체를 알 수 없는 Connection의 Packet을 발견하고 Packet의 SrcIP로 Connection Reset 요청을 전송합니다.

Node가 전송한 Connection Reset 요청을 받은 외부의 Server는 Connection을 종료하고 TCP Protocol에 의해서 다시 Connection Reset 요청을 전송합니다. Server가 전송한 Connection Reset 요청은 정상적으로 DNAT되어 Client Pod로 전송되어 “connection reset by peer” Error를 발생시킵니다. 즉 conntrack 버그로 인해서 Node가 Pod가 받아야 할 Packet을 대신 받고, Node가 전송한 Connection Reset 요청이 Client Pod와 Server Pod의 Connection을 종료시키게 됩니다.

Client Pod Packet Dump
349001 1199.001173634   10.251.0.1 → 110.76.141.111 TCP 43626 [TCP Window Full] 56284 → 80 [ACK] Seq=10278971 Ack=26 Win=43008 Len=43560 TSval=499458821 TSecr=3479336939 [TCP segment of a reassembled PDU]
349002 1199.001829309 110.76.141.111 → 10.251.0.1   TCP 66 80 → 56284 [ACK] Seq=26 Ack=10160447 Win=173056 Len=0 TSval=3479336939 TSecr=499458821
349003 1199.001867310 110.76.141.111 → 10.251.0.1   TCP 66 80 → 56284 [ACK] Seq=26 Ack=10194987 Win=173056 Len=0 TSval=3479336940 TSecr=499458821
349004 1199.001873632   10.251.0.1 → 110.76.141.111 TCP 20766 56284 → 80 [ACK] Seq=10322531 Ack=26 Win=43008 Len=20700 TSval=499458822 TSecr=3479336940 [TCP segment of a reassembled PDU]

Node Packet Dump
117897 291.391007869 10.205.13.221 → 110.76.141.111 TCP 43626 [TCP Window Full] 56284 → 80 [ACK] Seq=10278971 Ack=26 Win=43008 Len=43560 TSval=499458821 TSecr=3479336939 [TCP segment of a reassembled PDU]
117898 291.391020119 110.76.141.111 → 10.205.13.221 TCP 66 80 → 56284 [ACK] Seq=26 Ack=10110467 Win=173056 Len=0 TSval=3479336939 TSecr=499458820
117899 291.391054153 10.205.13.221 → 110.76.141.111 TCP 54 56284 → 80 [RST] Seq=10110467 Win=0 Len=0
117900 291.391596495 110.76.141.111 → 10.205.13.221 TCP 66 80 → 56284 [ACK] Seq=26 Ack=10160447 Win=173056 Len=0 TSval=3479336939 TSecr=499458821
117901 291.391646840 110.76.141.111 → 10.205.13.221 TCP 66 80 → 56284 [ACK] Seq=26 Ack=10194987 Win=173056 Len=0 TSval=3479336940 TSecr=499458821

위에는 본 Issue가 발생하였을 때의 tshark를 이용하여 Client Pod 내부와 Node에서 각각 Packet Dump를 수행한 모습입니다. Node에서만 RST Packet이 감지되는 것을 확인할 수 있습니다. 즉 Node에서 Connection Reset 요청을 전송하는 것을 확인할 수 있습니다.

 

 

본 Issue를 우회하기 하는 가장 직관적인 방법은 Node로 잘못 전달되는 외부의 Server의 응답 Packet을 차단하는 방법이 존재합니다. 위의 그림은 network-node-manager가 설정한 iptables의 Filter Rule에 의해서 Node로 잘못 전달되는 Packet을 차단하여, Node에서 Connection Reset 요청을 막는 과정을 나타내고 있습니다. 차단된 Packet은 Pod에게 전달되지 않기 때문에 Data 유실이 발생할 수 있을 거라 생각되지만 TCP 재전송에 의해서 보완되기 때문에 문제가 되지는 않습니다.

$ iptables -t filter -nvL
...
Chain INPUT (policy ACCEPT 3229 packets, 597K bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 NMANAGER_INPUT  all  --  *      *       0.0.0.0/0            0.0.0.0/0
...
Chain NMANAGER_INPUT (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 NMANAGER_DROP_INVALID_INPUT  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain NMANAGER_DROP_INVALID_INPUT (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate INVALID

위는 실제 network-node-manager가 설정하는 Filter Rule입니다. INPUT Chain부터 차례대로 따라가다 보면 NMANAGER_DROP_INVALID_INPUT Chain에서 DROP Rule이 설정되어 있는 것을 발견할 수 있습니다. conntrack에 의해서 유효하지 않은 (INVALID) Packet만을 DROP 하는 것을 확인할 수 있습니다.

 

External-IP Access Issue with IPVS Proxy Mode

Kubernetes Cluster에서 kube-proxy의 IPVS Mode를 이용 시, Kubernetes Cluster 내부의 Client Pod에서 ExternalTrafficPolicy Local 설정과 함께 생성된 LoadBalancer Service의 External-IP로 통신하지 못하는 문제가 존재합니다. 물론 Client Pod에서 LoadBalancer Service의 Cluster-IP를 통해서는 통신이 가능합니다. 하지만 몇몇 사내 개발자분들은 설정을 단순화하기 위해서 External-IP에 Domain을 설정한 다음, Client Pod가 해당 Domain으로 통신하도록 설정하였기 때문에 본 Issue가 종종 문제가 되었습니다.

 

 

위의 그림은 본 Issue가 발생하는 상황을 좀 더 자세히 나타내고 있습니다. LoadBalancer Service가 하나 존재하며 해당 LoadBalancer Service는 ExternalTrafficPolicy가 Local로 설정되어 있고 Service Pod A/B를 EndPoint로 갖고 있습니다. 따라서 Client Pod에서 LoadBalancer Service의 External-IP 또는 Cluster-IP로 접근 시 Service Pod A/B와 통신하게 됩니다.

Service Pod A/B 모두 Node A에 존재하고 있기 때문에, Node A에 존재하는 kube-proxy는 IPVS Rule에 LoadBalancer Service의 External-IP, NodePort를 통해서 전달된 Packet을 Service Pod A/B로 전달하도록 설정합니다. 반면 Node B에는 Service Pod가 존재하지 않기 때문에 LoadBalancer Service의 External-IP, NodePort를 통해서 전달된 Packet을 Drop 하도록 설정합니다.

Service Pod가 존재하지 않을 때 kube-proxy가 IPVS Rule에 Drop을 설정하는 이유는, Load Balancer에서 Service Pod가 존재하지 않는 Node로 Packet을 전송하지 않게 만들기 위해서입니다. ExternalTrafficPolicy Local 설정은 Node가 Load Balancer로부터 수신한 Packet을 동일 Node에 존재하는 Serivce Pod로만 Load Balancing을 수행하도록 만드는 설정입니다. 따라서 위의 그림에서 Load Balancer는 Service Pod가 존재하지 않는 Node B로 Packet을 전송하면 안 됩니다.

Load Balancer는 LoadBalancer Service의 NodePort 또는 External-IP를 통해서 Node에 존재하는 Service Pod의 Health Check를 수행합니다. Load Balancer는 Health Check에 실패하는 Node에게는 Packet을 전송하지 않습니다. 따라서 위의 그림의 Node B로는 Load Balancer가 Packet을 전송하지 않게 됩니다. 문제는 Node B에 위치하는 Client Pod에서도 LoadBalancer Service의 External-IP로 전송하는 Packet도 IPVS Rule에 의해서 Drop 된다는 점입니다.

 

 

본 Issue를 해결하기 위한 방법으로 network-node-manager는 Client Pod에서 LoadBalancer Service의 External-IP로 전송하는 Packet을 DNAT 하여 Cluster-IP로 변경하는 방법을 이용하고 있습니다. 위의 그림은 network-node-manager가 적용된 모습을 나타내고 있습니다. Client Pod B가 LoadBalancer Service의 External-IP인 20.0.0.20으로 전송한 Packet은 network-node-manager를 통해서 10.0.0.10인 Cluster-IP로 DNAT되어 IPVS에 전달됩니다. 이후에 IPVS의 Cluster-IP Rule에 따라서 Packet은 Service Pod A/B로 전송됩니다.

$ kubectl -n default get service
NAME           TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                      AGE
lb-service-1   LoadBalancer   10.231.42.164   10.19.20.201   80:31751/TCP,443:30126/TCP   16d
lb-service-2   LoadBalancer   10.231.2.62     10.19.22.57    80:32352/TCP,443:31549/TCP   16d

$ iptables -t nat -nvL
...
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 NMANAGER_PREROUTING  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain OUTPUT (policy ACCEPT 3 packets, 180 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 NMANAGER_OUTPUT  all  --  *      *       0.0.0.0/0            0.0.0.0/0
...
Chain NMANAGER_OUTPUT (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 NMANAGER_EX_CLUS_OUTPUT  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain NMANAGER_PREROUTING (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 NMANAGER_EX_CLUS_PREROUTING  all  --  *      *       0.0.0.0/0            0.0.0.0/0
...
Chain NMANAGER_EX_CLUS_OUTPUT (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  all  --  *      *       0.0.0.0/0            10.19.20.201         /* default/lb-service-1 */ ADDRTYPE match src-type LOCAL
    0     0 DNAT       all  --  *      *       0.0.0.0/0            10.19.20.201         /* default/lb-service-1 */ ADDRTYPE match src-type LOCAL to:10.231.42.164
    0     0 KUBE-MARK-MASQ  all  --  *      *       0.0.0.0/0            10.19.22.57          /* default/lb-service-2 */ ADDRTYPE match src-type LOCAL
    0     0 DNAT       all  --  *      *       0.0.0.0/0            10.19.22.57          /* default/lb-service-2 */ ADDRTYPE match src-type LOCAL to:10.231.2.62

Chain NMANAGER_EX_CLUS_PREROUTING (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  all  --  *      *       192.167.0.0/16       10.19.20.201         /* default/lb-service-1 */
    0     0 DNAT       all  --  *      *       192.167.0.0/16       10.19.20.201         /* default/lb-service-1 */ to:10.231.42.164
    1    60 KUBE-MARK-MASQ  all  --  *      *       192.167.0.0/16       10.19.22.57          /* default/lb-service-2 */
    1    60 DNAT       all  --  *      *       192.167.0.0/16       10.19.22.57          /* default/lb-service-2 */ to:10.231.2.62
...

위에는 Kubernetes Cluster에 존재하는 LoadBalancer Service 및 LoadBalancer Service에 따른 network-node-manager가 설정한 DNAT Rule을 보여주고 있습니다. NMANAGER_EX_CLUS_PREROUTING Chain은 일반적인 Pod에서 전송하는 Packet을 DNAT 하기 위한 Rule이 존재하는 Chain이고, NMANAGER_EX_CLUS_PREROUTING Chain은 Host(Node)의 Network Namespace를 이용하는 Pod에서 전송하는 Packet을 DNAT 하기 위한 Rule이 존재하는 Chain입니다.

192.167.0.0/16은 POD의 CIDR를 의미하는데, NMANAGER_EX_CLUS_PREROUTING Chain에서는 SRC IP가 POD의 CIDR인 경우에만 DNAT를 수행하도록 설정되어 있는 것을 확인할 수 있습니다. 즉 POD에서 전송한 Packet만 DNAT 되도록 설정되어 있습니다.

KUBE-MARK-MASQ Chain은 kube-proxy가 생성하는 Chain인데, SNAT를 수행해야 하는 Packet은 KUBE-MARK-MASQ Chain에서 Marking 되고 나중에 MASQUERADE Rule에 의해서 SNAT 됩니다. Client Pod에서 전송하는 Packet이 SNAT 되어 Packet의 SrcIP에 적절한 Host의 IP가 설정되는 것을 보장하기 위해서 KUBE-MARK-MASQ Chain 으로 Jump 하는 Rule을 NMANAGER_EX_CLUS_PREROUTING Chain과 NMANAGER_EX_CLUS_OUTPUT Chain에 설정합니다.

 

TO-DO

위에서 설명한 2가지의 Issue 말고도 Kubernetes 관련 몇 가지 Network Issue들이 존재하지만 Issue 해결을 위한 Network 설정이 Kubernetes Cluster에서 이용하는 CNI Plugin에 따라서 적용되지 않을 수 있기 때문에, 공개한 network-node-manager에는 적용하지 못하였습니다. 추후 CNI Plugin에 영향받지 않는 Network Issue 및 Issue 해결을 위한 Network 설정을 발견하게 되면 network-node-manager에 추가할 예정입니다.

감사합니다.

 


 

함께 하면 좋은 글 

 

 

카카오톡 공유 보내기 버튼

Latest Posts

제5회 Kakao Tech Meet에 초대합니다!

Kakao Tech Meet #5 트렌드와 경험 및 노하우를 자주, 지속적으로 공유하며 개발자 여러분과 함께 성장을 도모하고 긴밀한 네트워크를 형성하고자 합니다.  다섯 번째

테크밋 다시 달릴 준비!

(TMI: 이 글의 썸네일 이미지는 ChatGPT와 DALL・E로 제작했습니다. 🙂) 안녕하세요, Kakao Tech Meet(이하 테크밋)을 함께 만들어가는 슈크림입니다. 작년 5월에 테크밋을 처음 시작하고,