IPVS

Gyullbb·2024년 10월 5일
0

K8S

목록 보기
9/13

IPVS란

IPVS 는 리눅스 커널에서 동작하는 소프트웨어 로드밸런서이다. 백엔드(플랫폼)으로 Netfilter 를 사용하며, TCP/UDP 요청을 처리 할 수 있다.

IPVS 설정 확인

strictARP

IPVS 클러스터를 생성하고 난 후 kube-proxy configmap을 확인하면 strictARP: true 설정을 볼 수 있다.

# kubectl describe cm -n kube-system kube-proxy
ipvs:
  excludeCIDRs: null
  minSyncPeriod: 0s
  scheduler: ""
  strictARP: true
  syncPeriod: 0s
  tcpFinTimeout: 0s
  tcpTimeout: 0s
  udpTimeout: 0s

strictARP: true

  • 노드가 자신이 소유한 IP에 대해서만 ARP 응답을 보내도록 제한하는 설정
  • IPVS에서는 클러스터 내부 Pod간, 외부와 클러스터 내부 Pod간 트래픽을 처리하는데, ARP 응답이 올바르게 처리되지 않으면 잘못된 노드에 트래픽이 전달될 수 있기 때문에 strictARP 설정이 필수이다.

kube-ipvs0

클러스터의 노드에서 ip정보를 확인해보면 kube-ipvs0이라는 인터페이스가 새롭게 생긴 것을 확인할 수 있다.
kube-ipvs0의 정보는 모든 노드에서 동일하며, 이는 클러스터에 배포된 SVC IP 이다.

# for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ip -br -c addr show kube-ipvs0; echo; done

>> node myk8s-control-plane <<
kube-ipvs0       DOWN           10.200.1.10/32 10.200.1.1/32 

>> node myk8s-worker <<
kube-ipvs0       DOWN           10.200.1.10/32 10.200.1.1/32 

>> node myk8s-worker2 <<
kube-ipvs0       DOWN           10.200.1.1/32 10.200.1.10/32 

>> node myk8s-worker3 <<
kube-ipvs0       DOWN           10.200.1.10/32 10.200.1.1/32 

# kubectl get svc,ep -A                        
NAMESPACE     NAME                 TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)                  AGE
default       service/kubernetes   ClusterIP   10.200.1.1    <none>        443/TCP                  11m
kube-system   service/kube-dns     ClusterIP   10.200.1.10   <none>        53/UDP,53/TCP,9153/TCP   11m

NAMESPACE     NAME                   ENDPOINTS                                            AGE
default       endpoints/kubernetes   172.18.0.5:6443                                      11m
kube-system   endpoints/kube-dns     10.10.0.2:53,10.10.0.4:53,10.10.0.2:53 + 3 more...   11m

kube-ipvs0이란?

Kubernetes에서 IPVS 모드로 네트워크 로드 밸런싱을 사용할 때 서비스의 클러스터 IP를 처리하는 가상 네트워크 인터페이스이다.
이 인터페이스는 서비스의 IP를 인지하고 트래픽을 서비스에 연결된 Pod로 전달한다.

kube-ipvs0이 SVC의 IP를 추적하는 방법

kube-proxy는 Kubernetes API 서버로부터 서비스와 엔드포인트의 상태를 감시하다가 새로운 서비스 추가/삭제, 엔드포인트 변경 시 이를 인지한다.
변경 사항이 생길 경우 IPset과 IPVS 가상서버를 설정하여 이를 IPVS 인터페이스에 반영한다.

결과적으로 kube-proxy가 kube-ipvs0 인터페이스에 서비스의 Cluster IP를 등록하고, 해당 Cluster IP로 도달하는 모든 트래픽은 kube-ipvs0를 통해 적절한 Pod로 전달된다.

해당 부분을 처리하는 코드를 확인해보자.

Service, Endpoint 동기화 후 SVC의 Cluster IP를 IPVS 테이블에 추가
https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/ipvs/proxier.go

func (proxier *Proxier) syncProxyRules() {
  ...
  // Build IPVS rules for each service.
	for svcPortName, svcPort := range proxier.svcPortMap {
		svcInfo, ok := svcPort.(*servicePortInfo)
    ...

    # 서비스의 Cluster IP를 가져온다.
    // Capture the clusterIP.
		// ipset call
		entry := &utilipset.Entry{
			IP:       svcInfo.ClusterIP().String(),
			Port:     svcInfo.Port(),
			Protocol: protocol,
			SetType:  utilipset.HashIPPort,
		}

    # IPSet 데이터 구조에 Cluster IP 정보를 저장한다.
		// add service Cluster IP:Port to kubeServiceAccess ip set for the purpose of solving hairpin.
		// proxier.kubeServiceAccessSet.activeEntries.Insert(entry.String())
    ...
		proxier.ipsetList[kubeClusterIPSet].activeEntries.Insert(entry.String())

    ...
    # IPVS에 가상 서버를 설정한다. 이 때, Cluster IP, Port, Protocol, 부하분산 방식 정보가 들어간다.
		// ipvs call
		serv := &utilipvs.VirtualServer{
			Address:   svcInfo.ClusterIP(),
			Port:      uint16(svcInfo.Port()),
			Protocol:  string(svcInfo.Protocol()),
			Scheduler: proxier.ipvsScheduler,
		}
    ...

    # IPVS 가상 서버를 kube-ipvs0 dummy 인터페이스에 바인딩한다. 이를 통해 kube-ipvs0 인터페이스에서 SVC의 ClusterIP를 확인할 수 있다.
    // We need to bind ClusterIP to dummy interface, so set `bindAddr` parameter to `true` in syncService()
		if err := proxier.syncService(svcPortNameString, serv, true, alreadyBoundAddrs); err == nil {
			activeIPVSServices.Insert(serv.String())
			activeBindAddrs.Insert(serv.Address.String())
}

ipset

IP주소나 포트의 집합으로, 이를 통해 IP와 포트의 집합을 빠르게 필터링할 수 있다.
위 코드에서 확인했다시피, kube-proxy에 의해 기록된다.

# docker exec -it myk8s-worker ipset -L | grep -i kube-cluster-ip -A12
Name: KUBE-CLUSTER-IP
Type: hash:ip,port
Revision: 7
Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0x2c7e0b73
Size in memory: 408
References: 3
Number of entries: 4
Members:
10.200.1.10,udp:53
10.200.1.1,tcp:443
10.200.1.10,tcp:53
10.200.1.10,tcp:9153

iptables 개수 비교

iptables 모드와 IPVS 모드에서의 iptables 규칙 개수를 비교해보자.

iptables

# iptables -nvL -t nat | wc -l
327

ipvs

# docker exec -it myk8s-worker iptables -nvL -t nat | wc -l 
61

iptables 모드와 비교했을 때 규칙 수가 현저히 줄어든 것을 확인할 수 있는데, 이는 서비스 부하 분산 관련 규칙을 따로 ipvs가 처리하기 때문이다.

KUBE-SERVICES 비교
ipvs모드에서 iptables kube-service 체인을 확인해보면 아래와 같다.
iptables모드와는 다르게 Service 에서 Endpoint로 부하분산하는 규칙이 사라졌다.

# docker exec -it myk8s-worker iptables -nvL -t nat
...
Chain KUBE-SERVICES (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     0    --  *      *       127.0.0.0/8          0.0.0.0/0           
    0     0 KUBE-MARK-MASQ  0    --  *      *      !10.10.0.0/16         0.0.0.0/0            /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst
    0     0 KUBE-NODE-PORT  0    --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL
    0     0 ACCEPT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            match-set KUBE-CLUSTER-IP dst,dst

서비스 접속

부하분산 확인 - ipvsadm

각 노드에서 ipvsadm을 통해 ClusterIP로의 통신 부하분산을 확인했을 때, 부하분산 방식이 Round Robin으로 설정되어 엔드포인트에 동일한 가중치로 분산됨을 확인할 수 있다.

# CIP=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.clusterIP}")
# CPORT=$(kubectl get svc svc-clusterip -o jsonpath="{.spec.ports[0].port}")
# echo $CIP $CPORT
10.200.1.33 9000

# kubectl get svc,ep                                       
NAME                    TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
service/kubernetes      ClusterIP   10.200.1.1    <none>        443/TCP    88m
service/svc-clusterip   ClusterIP   10.200.1.33   <none>        9000/TCP   17m

NAME                      ENDPOINTS                                AGE
endpoints/kubernetes      172.18.0.5:6443                          88m
endpoints/svc-clusterip   10.10.1.2:80,10.10.2.2:80,10.10.3.2:80   17m

# for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln -t $CIP:$CPORT ; echo; done
>> node myk8s-control-plane <<
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.200.1.33:9000 rr
  -> 10.10.1.2:80                 Masq    1      0          0         
  -> 10.10.2.2:80                 Masq    1      0          0         
  -> 10.10.3.2:80                 Masq    1      0          0         

>> node myk8s-worker <<
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.200.1.33:9000 rr
  -> 10.10.1.2:80                 Masq    1      0          0         
  -> 10.10.2.2:80                 Masq    1      0          0         
  -> 10.10.3.2:80                 Masq    1      0          0         

>> node myk8s-worker2 <<
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.200.1.33:9000 rr
  -> 10.10.1.2:80                 Masq    1      0          0         
  -> 10.10.2.2:80                 Masq    1      0          0         
  -> 10.10.3.2:80                 Masq    1      0          0         

>> node myk8s-worker3 <<
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.200.1.33:9000 rr
  -> 10.10.1.2:80                 Masq    1      0          0         
  -> 10.10.2.2:80                 Masq    1      0          0         
  -> 10.10.3.2:80                 Masq    1      0          0     

부하분산 확인 - ipvsadm rate 모니터링

ipvsadm rate를 통해 실제 연결이 엔드포인트로 얼마나 부하되었는지 개수를 확인할 수 있다.

# kubectl exec -it net-pod -- zsh -c "for i in {1..10000}; do curl -s $SVC1:9000 | grep Hostname; sleep 0.01; done"
Hostname: webpod2
Hostname: webpod3
Hostname: webpod1
Hostname: webpod2
Hostname: webpod3
...
^Ccommand terminated with exit code 130

# watch -d "docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --stats; echo; docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT --rate"
Every 2.0s: docker exec -it myk8s-control-plane ipvsadm -Ln -t 10.200.1.33:9000 ...  baggyuliui-MacBookPro.local: Sun Oct  6 01:33:47 2024

Prot LocalAddress:Port               Conns   InPkts  OutPkts  InBytes OutBytes
  -> RemoteAddress:Port
TCP  10.200.1.33:9000                  725     4350     2900   289275   380625
  -> 10.10.1.2:80                      242     1452      968    96558   127050
  -> 10.10.2.2:80                      241     1446      964    96159   126525
  -> 10.10.3.2:80                      242     1452      968    96558   127050

Prot LocalAddress:Port                 CPS    InPPS   OutPPS    InBPS   OutBPS
  -> RemoteAddress:Port
TCP  10.200.1.33:9000                    2       13        9      882     1160
  -> 10.10.1.2:80                        1        4        3      295      388
  -> 10.10.2.2:80                        1        4        3      293      386
  -> 10.10.3.2:80                        1        4        3      294      386

Conns를 보았을 때 거의 균일한 개수로 부하분산이 되고 있다.

0개의 댓글