IPVS 는 리눅스 커널에서 동작하는 소프트웨어 로드밸런서이다. 백엔드(플랫폼)으로 Netfilter 를 사용하며, TCP/UDP 요청을 처리 할 수 있다.
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정보를 확인해보면 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
Kubernetes에서 IPVS 모드로 네트워크 로드 밸런싱을 사용할 때 서비스의 클러스터 IP를 처리하는 가상 네트워크 인터페이스이다.
이 인터페이스는 서비스의 IP를 인지하고 트래픽을 서비스에 연결된 Pod로 전달한다.
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())
}
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 모드와 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을 통해 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를 통해 실제 연결이 엔드포인트로 얼마나 부하되었는지 개수를 확인할 수 있다.
# 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를 보았을 때 거의 균일한 개수로 부하분산이 되고 있다.