Kubernetes

Kubernetes Kube-proxy의 IPVS 모드

mokpolar 2024. 10. 3. 20:29
반응형

안녕하세요?

이번 글에서는 Kube-proxy의 IPVS 모드에 대해서 알아보겠습니다.

이 글은 CloudNeta팀 가시다님의 KANS(Kubernetes Advanced Network Study) 5주차 과제로 작성되었습니다.

Kube-proxy 모드 중 IPVS 모드에 대해서

IPVS란?

지난 글 에서도 봤듯이,
Kube-proxy는 클러스터 내부에서 서비스 간의 네트워크 요청을 적절한 Pod로 라우팅하는 역할을 합니다.

이 라우팅 방식에는 세 가지 모드가 있으며, 그 중 하나가 IPVS 모드입니다.

 

IPVS(Internet Protocol Virtual Server)는 리눅스 커널 레벨에서 제공하는 고성능 (소프트웨어) 로드 밸런서로, 클러스터 내에서 트래픽을 처리하는 방식입니다.

 

리눅스 커널 모듈로 동작하며, 로드 밸런싱을 위해 L4에서 TCP와 UDP 트래픽을 처리합니다.
IPVS는 외부에서 들어오는 요청을 여러 백엔드로 분산하여 서비스의 가용성과 확장성을 높이는 데 주력합니다.

 

IPVS 모드는 userspace, iptables 모드보다 성능이 좋아서 고성능, 대규모 클러스터에서 더 유용하다고 합니다.

IPVS 의 특징

  • 커널에서 트래픽을 처리하기에 고성능
  • 다른 모드들과 달리 다양한 분산 알고리즘 지원 : Round Robin(RR), Least Connection, Destination Hashing, Source Hashing..
  • Iptable에 생성되는 rule이 적어서 더 많은 서비스를 운영 가능
  • IPVS 테이블은 일종의 hash table

IPSET

ipset은 iptables의 확장기능입니다.
IP들의 집합을 별도로 만들어서 관리할 수 있습니다.

 

iptable의 경우, 5000건 이상의 룰셋이 등록되었을 때, 시스템의 성능이 급격하게 떨어집니다.
IPSET은 이를 막기 위해 사용합니다.

 

IPVS 사용시, 아래와 같은 IPSET이 기본적으로 설정됩니다.


출처 : https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/

 

환경 구성

Kind K8S cluster 생성

Kind를 이용해 아래와 같은 환경을 구성합니다.


kube-proxy mode를 "IPVS"로 지정합니다.

cat <<EOT> kind-svc-2w-ipvs.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  "InPlacePodVerticalScaling": true
  "MultiCIDRServiceAllocator": true
nodes:
- role: control-plane
  labels:
    mynode: control-plane
    topology.kubernetes.io/zone: ap-northeast-2a
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  - containerPort: 30003
    hostPort: 30003
  - containerPort: 30004
    hostPort: 30004
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        runtime-config: api/all=true
    controllerManager:
      extraArgs:
        bind-address: 0.0.0.0
    etcd:
      local:
        extraArgs:
          listen-metrics-urls: http://0.0.0.0:2381
    scheduler:
      extraArgs:
        bind-address: 0.0.0.0
  - |
    kind: KubeProxyConfiguration
    metricsBindAddress: 0.0.0.0
    ipvs:
      strictARP: true
- role: worker
  labels:
    mynode: worker1
    topology.kubernetes.io/zone: ap-northeast-2a
- role: worker
  labels:
    mynode: worker2
    topology.kubernetes.io/zone: ap-northeast-2b
- role: worker
  labels:
    mynode: worker3
    topology.kubernetes.io/zone: ap-northeast-2c
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24
  kubeProxyMode: "ipvs"         # IPVS 모드
EOT
kind create cluster --config kind-svc-2w-ipvs.yaml --name myk8s --image kindest/node:v1.31.0
docker ps

 

테스트용 mypc 컨테이너도 올립니다.

docker run -d --rm --name mypc --network kind --ip 192.168.247.100 nicolaka/netshoot sleep infinity

네트워크 툴 설치

각 노드들에 테스트용 기본 툴도 설치합니다.

  • tree: 디렉토리 구조를 트리 형태로 표시하는 도구
  • psmisc: 프로세스 관련 유틸리티 모음
  • lsof: 열린 파일 목록을 보여주는 도구
  • wget: 파일을 다운로드하는 명령줄 도구
  • bsdmainutils: 다양한 유틸리티 모음
  • bridge-utils: 네트워크 브리지 관리 도구
  • net-tools: 네트워크 관련 도구 모음
  • dnsutils: DNS 관련 유틸리티
  • ipset: IP 집합 관리 도구
  • ipvsadm: IP 가상 서버 관리 도구
  • nfacct: NetFlow 수집 도구
  • tcpdump: 패킷 캡처 및 분석 도구
  • ngrep: 네트워크 패킷에서 문자열을 검색하는 도구
  • iputils-ping: Ping 명령을 제공하는 패키지
  • arping: ARP 요청을 보내는 도구
  • git: 버전 관리 시스템
  • vim: 텍스트 편집기
  • arp-scan: ARP 요청을 보내어 네트워크 호스트를 검색하는 도구
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools dnsutils ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping git vim arp-scan -y'
for i in worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools dnsutils ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done

 

IPVS 모드시 네트워크 정보 확인

먼저 kube-proxy configmap을 확인해보면 몇 가지 유의해야 하는 설정들을 볼 수 있습니다.

kubectl describe cm -n kube-system kube-proxy

 

그 중 strictARP에 대한 설명입니다.

  • strictARP: true는 ARP 패킷을 보다 엄격하게 처리하겠다는 설정입니다.
  • IPVS 모드에서 strict ARP가 활성화되면, 노드의 인터페이스는 자신에게 할당된 IP 주소에 대해서만 ARP 응답을 보내게 됩니다.
  • 이는 IPVS로 로드밸런싱할 때 ARP 패킷이 잘못된 인터페이스로 전달되는 문제를 방지합니다.
  • 이 설정은 특히 클러스터 내에서 여러 노드가 동일한 IP를 갖는 VIP(Virtual IP)를 사용하는 경우 중요합니다.

그리고
노드 별 네트워트 정보를 보면 iptable 모드와 다르게
kube-ipvs0라는 네트워크 인터페이스를 확인할 수 있습니다.

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

>> node myk8s-control-plane <<
...
4: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether ea:5d:81:86:81:d5 brd ff:ff:ff:ff:ff:ff
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
...

>> node myk8s-worker <<
...
4: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether f6:24:c5:98:63:df brd ff:ff:ff:ff:ff:ff
    inet 10.200.1.10/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.200.1.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
...

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

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

 

kube-ipvs0에 보이는 ip는 뭘까요?

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                  4m36s
kube-system   service/kube-dns     ClusterIP   10.200.1.10   <none>        53/UDP,53/TCP,9153/TCP   4m35s

NAMESPACE     NAME                   ENDPOINTS                                            AGE
default       endpoints/kubernetes   192.168.247.2:6443                                   4m36s
kube-system   endpoints/kube-dns     10.10.0.2:53,10.10.0.3:53,10.10.0.2:53 + 3 more...   4m29s

 

내부에서 호출할때 쓰는 kubernetes, kube-dns 의 ClusterIP가 박혀있는 것을 볼 수 있습니다.

Service ClusterIP 생성 시 kube-ipvs0 인터페이스에 해당 IP 가 할당됩니다.

IPVSADM

ipvsadm은 IPVS의 관리 유틸리티입니다.

ipvsadm을 통해

  • IPVS 테이블 관리
  • 로드밸런싱 설정 확인
  • 서비스와 백엔드 상태 확인
    등을 할 수 있습니다.

아래 rr이라고 표시된건 Round Robin입니다.
IPVS부터 제대로된 부하분산 알고리즘을 쓸 수 있습니다.

for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln ; echo; done

>> node myk8s-control-plane <<
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.200.1.1:443 rr
  -> 192.168.247.2:6443           Masq    1      3          0
TCP  10.200.1.10:53 rr
  -> 10.10.0.2:53                 Masq    1      0          0
  -> 10.10.0.3:53                 Masq    1      0          0
TCP  10.200.1.10:9153 rr
  -> 10.10.0.2:9153               Masq    1      0          0
  -> 10.10.0.3:9153               Masq    1      0          0
UDP  10.200.1.10:53 rr
  -> 10.10.0.2:53                 Masq    1      0          0
  -> 10.10.0.3:53                 Masq    1      0          0

>> node myk8s-worker <<
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.200.1.1:443 rr
  -> 192.168.247.2:6443           Masq    1      0          0
TCP  10.200.1.10:53 rr
  -> 10.10.0.2:53                 Masq    1      0          0
  -> 10.10.0.3:53                 Masq    1      0          0
TCP  10.200.1.10:9153 rr
  -> 10.10.0.2:9153               Masq    1      0          0
  -> 10.10.0.3:9153               Masq    1      0          0
UDP  10.200.1.10:53 rr
  -> 10.10.0.2:53                 Masq    1      0          0
  -> 10.10.0.3:53                 Masq    1      0          0

iptables와 IPVS 모드의 iptables 정책 수 비교

동일 조건에서 iptables 와 IPVS 모드의 iptable 정책 수를 한번 비교해보겠습니다.

먼저 iptable 모드의 경우입니다.

for i in filter nat mangle raw ; do
  echo ">> IPTables Type : $i <<"
  docker exec -it myk8s-2-control-plane iptables -t $i -S | wc -l
  echo
done

for i in filter nat mangle raw ; do
  echo ">> IPTables Type : $i <<"
  docker exec -it myk8s-2-worker iptables -t $i -S | wc -l
  echo
done

for i in filter nat mangle raw ; do
  echo ">> IPTables Type : $i <<"
  docker exec -it myk8s-2-worker2 iptables -t $i -S | wc -l
  echo
done

for i in filter nat mangle raw ; do
  echo ">> IPTables Type : $i <<"
  docker exec -it myk8s-2-worker3 iptables -t $i -S | wc -l
  echo
done
>> IPTables Type : filter <<
      27
>> IPTables Type : nat <<
      71
>> IPTables Type : mangle <<
       8
>> IPTables Type : raw <<
       2
>> IPTables Type : filter <<
      27
>> IPTables Type : nat <<
      71
>> IPTables Type : mangle <<
       8
>> IPTables Type : raw <<
       2
>> IPTables Type : filter <<
      27
>> IPTables Type : nat <<
      71
>> IPTables Type : mangle <<
       8
>> IPTables Type : raw <<
       2
>> IPTables Type : filter <<
      27
>> IPTables Type : nat <<
      71
>> IPTables Type : mangle <<
       8
>> IPTables Type : raw <<
       2

 

그리고 IPVS 모드입니다.

for i in filter nat mangle raw ; do
  echo ">> IPTables Type : $i <<"
  docker exec -it myk8s-control-plane iptables -t $i -S | wc -l
  echo
done

for i in filter nat mangle raw ; do
  echo ">> IPTables Type : $i <<"
  docker exec -it myk8s-worker iptables -t $i -S | wc -l
  echo
done

for i in filter nat mangle raw ; do
  echo ">> IPTables Type : $i <<"
  docker exec -it myk8s-worker2 iptables -t $i -S | wc -l
  echo
done

for i in filter nat mangle raw ; do
  echo ">> IPTables Type : $i <<"
  docker exec -it myk8s-worker3 iptables -t $i -S | wc -l
  echo
done

>> IPTables Type : filter <<
      30
>> IPTables Type : nat <<
      36
>> IPTables Type : mangle <<
       7
>> IPTables Type : raw <<
       2
>> IPTables Type : filter <<
      30
>> IPTables Type : nat <<
      36
>> IPTables Type : mangle <<
       7
>> IPTables Type : raw <<
       2
>> IPTables Type : filter <<
      30
>> IPTables Type : nat <<
      36
>> IPTables Type : mangle <<
       7
>> IPTables Type : raw <<
       2
>> IPTables Type : filter <<
      30
>> IPTables Type : nat <<
      36
>> IPTables Type : mangle <<
       7
>> IPTables Type : raw <<
       2

차이가 많이 나는 모습을 볼 수 있습니다.
그렇기 때문에 서비스가 많아질수록 ipvs가 훨씬 유리할 수 밖에 없습니다.

트래픽 테스트

테스트용 Pod 생성

테스트용 pod들의 manifest는 아래와 같습니다.

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  
      targetPort: 80
  selector:
    app: webpod
  type: ClusterIP
EOT

 

생성해줍니다.

kubectl apply -f 3pod.yaml,netpod.yaml,svc-clusterip.yaml
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.74 9000

 

ipvsadm 툴로 부하분산 되는 정보를 확인합니다.

 

svc-clusterip, 즉 10.200.1.74:9000 로 트래픽이 인입될 때
3곳의 타겟으로 라운드로빈(rr)로 부하분산하여 전달됨을 확인할 수 있습니다.

 

모든 노드에서 동일한 IPVS 분산 설정 정보 확인 가능합니다.
3곳의 타겟은 서비스에 연동된 endpoint 3곳입니다.

 

그리고 Masq를 보면 전달시 source ip는 매스커레이딩 된다는 사실을 알 수 있습니다.

docker exec -it myk8s-control-plane ipvsadm -Ln -t $CIP:$CPORT
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.200.1.74: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

IPVS 서비스 접속 확인

변수를 지정하고

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

 

컨트롤플레인 노드에서 ipvsadm 모니터링을 실행합니다.
ClusterIP 접속 시 연결 정보가 확인됩니다.

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"

 

svc-clusterip 의 ClusterIP주소를 변수로 지정하고 80, 9000 포트별 접속을 확인합니다.

SVC1=$(kubectl get svc svc-clusterip -o jsonpath={.spec.clusterIP})
echo $SVC1
10.200.1.74

kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname
kubectl exec -it net-pod -- curl -s --connect-timeout 1 $SVC1:9000 | grep Hostname

Hostname: webpod3
IP: 127.0.0.1
IP: ::1
IP: 10.10.1.2
IP: fe80::d8e1:4bff:fe88:e881
RemoteAddr: 10.10.0.5:52104
GET / HTTP/1.1
Host: 10.200.1.74:9000
User-Agent: curl/8.7.1
Accept: */*

Hostname: webpod2
Hostname: webpod1

 

이제 ClusterIP로의 부하분산이 잘 되는지 확인합니다.

Round Robin으로 잘 부하분산이 되는 것을 확인할 수 있습니다.

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"
    334 Hostname: webpod3
    333 Hostname: webpod2
    333 Hostname: webpod1

Reference

반응형