..

KANS3) Service 2

service

kube-proxy

ipvs proxy mode

지난 주에 쓴 것과 같이, kube-proxy의 iptables proxy mode에서 무수한 규칙 수로 인해 지연이 발생하는 등의 단점이 있어 ipvs proxy mode가 나오게 되었다. 커널스페이스에서 직접 트래픽을 관리하여 iptables proxy mode보다 성능이 좋다.

ipvs(ip virtual server)는 4계층에서 부하분산으로 실행되기 위해 단일 vip를 제공하는 기술로, kube-proxy에서 ipvs mode로 사용하게되면, svc 에 연결된 endpoint ip들로 트래픽이 분배되도록 규칙이 만들어져서 동작한다.

부하분산 규칙에 따라 분산되는 것 말고도 다른점이 있다. iptables는 무조건 위에서부터 순차적으로 규칙을 조회하기 때문에 규칙이 늘어날 수록 조회가 증가하는 반면, ipvs는 해시테이블 기반으로 규칙을 관리 하기 때문에 규칙이 아무리 많아도 한 번만 조회하면 된다. 🔗 그렇다고 iptables를 아예 안쓰는 것이 아니다. NodePort 트래픽 처리나 네트워크 정책 필터링 등 일부 규칙은 iptables에서 관리된다. 아래 테스트에서 좀더 자세하게 살펴보기로..

테스트

설치

실습 환경은 가시다님이 제공해주신 아래 스택 파일 사용해서 ec2에 올려 실습했다. 이번에도 동일하게 kindnet cni를 사용한다. $ curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/kans/kans-5w.yaml

root@MyServer:~# 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"        
EOT

root@MyServer:~# kind create cluster --config kind-svc-2w-ipvs.yaml --name myk8s --image kindest/node:v1.31.0
Creating cluster "myk8s" ...
...
Thanks for using kind! 😊

# 지난주와 동일하게, 컨트롤플레인과 워커노드에 필요한 툴들 설치
(⎈|kind-myk8s:N/A) root@MyServer:~# 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'
(⎈|kind-myk8s:N/A) root@MyServer:~# 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

# ip 대역대 확인
(⎈|kind-myk8s:N/A) root@MyServer:~# kubectl get cm -n kube-system kubeadm-config -oyaml | grep -i subnet
      podSubnet: 10.10.0.0/16
      serviceSubnet: 10.200.1.0/24
(⎈|kind-myk8s:N/A) root@MyServer:~# kubectl get nodes -o custom-columns='NODE_NAME:.metadata.name,POD_CIDR:.spec.podCIDR'
NODE_NAME             POD_CIDR
myk8s-control-plane   10.10.0.0/24
myk8s-worker          10.10.2.0/24
myk8s-worker2         10.10.1.0/24
myk8s-worker3         10.10.3.0/24

환경 구경

ipvs mode로 설정한 부분 확인

(⎈|kind-myk8s:N/A) root@MyServer:~# kubectl describe cm -n kube-system kube-proxy
...
ipvs:
  excludeCIDRs: null
  # ipvs proxy가 건드리면 안되는 대역대
  minSyncPeriod: 0s
  # ipvs 규칙 재동기화 주기 
  # 0s: service나 endpointSlice변경 시 즉시 재동기화
  scheduler: ""
  # ipvs 분산 유형 (기본값 round robin)
  strictARP: true
  # true일 때, arp 관련 설정을 엄격하게 적용
  # arp_ignore와 arp_announce가 활성화됨
  syncPeriod: 0s
  # 다양한 재동기화, 정리 작업이 수행되는 빈도
  tcpFinTimeout: 0s
  # fin을 수신한 ipvs tcp 세션 타임 아웃 설정
  tcpTimeout: 0s
  # 유휴 ipvs tcp 세션 타임 아웃 설정
  udpTimeout: 0s
  # ipvs udp 패킷에 대해 타임 아웃 설정
mode: ipvs
...


# .ipvs.strictARP: true 로 설정시, 아래 두 값이 모든 노드에서 변경됨
# arp_ignore: 노드가 arp 요청을 처리하는 방식을 제어하는 파라미터(트래픽이 적절한 인터페이스에 라우팅되도록 도와준다.)
(⎈|kind-myk8s:N/A) root@MyServer:~# docker exec -it  myk8s-control-plane   sysctl net.ipv4.conf.all.arp_ignore
net.ipv4.conf.all.arp_ignore = 1
# arp_announce: 노드가 arp 응답 전송하는 방식을 제어(잘못된 인터페이스에서 arp 응답 전송 방지)
(⎈|kind-myk8s:N/A) root@MyServer:~# docker exec -it  myk8s-control-plane  sysctl net.ipv4.conf.all.arp_announce
net.ipv4.conf.all.arp_announce = 2

내부 확인

# 인터페이스를 확인해보면 kube-ipvs0 확인 가능하다. 
# dummy라고 되어있으며 ip도 두개 할당되어있다. 
(⎈|kind-myk8s:N/A) root@MyServer:~# docker exec -it myk8s-worker ip -d -c addr show kube-ipvs0
2: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether ba:f0:1b:0a:e2:16 brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536
    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


# 위의 인터페이스가 모든 노드에 있고 ip가 동일하다. 
(⎈|kind-myk8s:N/A) root@MyServer:~# 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.1/32 10.200.1.10/32

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

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

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


# 이 아이피들은 누구냐면.. svc cluster ip
(⎈|kind-myk8s:N/A) root@MyServer:~# kubectl get svc -A
NAMESPACE     NAME                 TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)                  AGE
default       service/kubernetes   ClusterIP   10.200.1.1    <none>        443/TCP                  7h3m
kube-system   service/kube-dns     ClusterIP   10.200.1.10   <none>        53/UDP,53/TCP,9153/TCP   7h3m




# ipvs를 구성하는 도구인 ipvsadm로 확인
# ipvs에서 현재 설정된 부하분산 규칙을 확인해보면, 어느 노드를 하든 연결 수 부분을 제외하고 똑같이 반환됨 
(⎈|kind-myk8s:N/A) root@MyServer:~# docker exec -it myk8s-worker ipvsadm -L -n
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
  -> 172.18.0.4: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


(⎈|kind-myk8s:N/A) root@MyServer:~# kubectl get no -o custom-columns='NODE_NAME:.metadata.name,NODE_IP:.status.addresses[?(@.type=="InternalIP")].address'
NODE_NAME             NODE_IP
myk8s-control-plane   172.18.0.4
myk8s-worker          172.18.0.5
myk8s-worker2         172.18.0.2
myk8s-worker3         172.18.0.3

가장 위의 규칙을 보면

TCP 10.200.1.1:443 rr -> 172.18.0.4:6443

10.200.1.1은 default svc로 해당 서비스 ip로 요청시, 172.18.0.4인 controlplane node의 6443포트(kubernetes api 서버가 사용하는 포트)로 전달된다. rr은 roundrobin을 의미하며, 어떻게 분산 처리를 할 것인지 정할 수가 있다. (물론 여기서는 엔드포인트가 혼자니까 분산되지 않는다.)

테스트

테스트 pod,svc(clusterIP) 생성

# 백엔드가 될 파드들
(⎈|kind-myk8s:N/A) root@MyServer:~# cat backend.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
  
# 백엔드 앞단에 있을 svc
(⎈|kind-myk8s:N/A) root@MyServer:~# cat svc4backend.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  ports:
    - name: svc-webport
      port: 9000        # svc port
      targetPort: 80    # target(pod) port 
  selector:
    app: webpod         
  type: ClusterIP       
  
# 테스트 시 사용 할 파드
(⎈|kind-myk8s:N/A) root@MyServer:~# cat testpod.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

동작 확인

# svc 생성 후, 맨 마지막에 10.200.1.216/32가 추가된 것을 확인
# 모든 노드에 동일하게 추가되어 있음
(⎈|kind-myk8s:N/A) root@MyServer:~# docker exec -it myk8s-worker ip -d -c addr show kube-ipvs0
2: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether ba:f0:1b:0a:e2:16 brd ff:ff:ff:ff:ff:ff promiscuity 0  allmulti 0 minmtu 0 maxmtu 0
    dummy numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 65536 tso_max_segs 65535 gro_max_size 65536
    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
    inet 10.200.1.216/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever

# 추가된 10.200.1.216/32은 새로 만든 svc ip
(⎈|kind-myk8s:N/A) root@MyServer:~# kubectl get svc -A
NAMESPACE     NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes      ClusterIP   10.200.1.1     <none>        443/TCP                  9h
default       svc-clusterip   ClusterIP   10.200.1.216   <none>        9000/TCP                 16m
kube-system   kube-dns        ClusterIP   10.200.1.10    <none>        53/UDP,53/TCP,9153/TCP   9h


# ipvsadm으로 현재 분산 규칙 확인
(⎈|kind-myk8s:N/A) root@MyServer:~# docker exec -it myk8s-worker ipvsadm -L -n
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
...
TCP  10.200.1.216: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

# svc ip에 지정했던 9000번 포드로 들어올 시, 각 파드의 80으로 가도록 규칙이 설정되어있다.
# 현재 부하 분산 방식은 기본값은 round robin으로 되어있다.
# net-pod(10.10.0.5)에서 svc(10.200.1.216:9000 -> 10.10.2.2:80)호출
(⎈|kind-myk8s:N/A) root@MyServer:~# kubectl exec -it net-pod -- curl -s --connect-timeout 1 10.200.1.216:9000
Hostname: webpod1


# worker node에서 tcpdump 떠보면..
root@myk8s-worker:/# tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on veth42459b76, link-type EN10MB (Ethernet), snapshot length 262144 bytes
# pod의 gateway인 10.10.2.1이 pod(10.10.2.2) mac 주소를 물어보고
19:38:28.346837 ARP, Request who-has ip-10-10-2-2.ap-northeast-2.compute.internal tell ip-10-10-2-1.ap-northeast-2.compute.internal, length 28
19:38:28.347027 ARP, Reply ip-10-10-2-2.ap-northeast-2.compute.internal is-at 9e:d3:48:0e:08:a9 (oui Unknown), length 28
# 전달되면, test pod(10.10.0.5)에서 호출이 잘되는 것을 볼 수 있다.
19:38:28.347030 IP ip-10-10-0-5.ap-northeast-2.compute.internal.43140 > ip-10-10-2-2.ap-northeast-2.compute.internal.http: Flags [S], seq 3445453562, win 64240, options [mss 1460,sackOK,TS val 4260853262 ecr 0,nop,wscale 7], length 0
19:38:28.347045 IP ip-10-10-2-2.ap-northeast-2.compute.internal.http > ip-10-10-0-5.ap-northeast-2.compute.internal.43140: Flags [S.], seq 587243439, ack 3445453563, win 65160, options [mss 1460,sackOK,TS val 3976301545 ecr 4260853262,nop,wscale 7], length 0
19:38:28.347104 IP ip-10-10-0-5.ap-northeast-2.compute.internal.43140 > ip-10-10-2-2.ap-northeast-2.compute.internal.http: Flags [.], ack 1, win 502, options [nop,nop,TS val 4260853262 ecr 3976301545], length 0
19:38:28.347184 IP ip-10-10-0-5.ap-northeast-2.compute.internal.43140 > ip-10-10-2-2.ap-northeast-2.compute.internal.http: Flags [P.], seq 1:81, ack 1, win 502, options [nop,nop,TS val 4260853262 ecr 3976301545], length 80: HTTP: GET / HTTP/1.1
19:38:28.347202 IP ip-10-10-2-2.ap-northeast-2.compute.internal.http > ip-10-10-0-5.ap-northeast-2.compute.internal.43140: Flags [.], ack 81, win 509, options [nop,nop,TS val 3976301545 ecr 4260853262], length 0
19:38:28.348365 IP ip-10-10-2-2.ap-northeast-2.compute.internal.http > ip-10-10-0-5.ap-northeast-2.compute.internal.43140: Flags [P.], seq 1:310, ack 81, win 509, options [nop,nop,TS val 3976301547 ecr 4260853262], length 309: HTTP: HTTP/1.1 200 OK
...
# 그렇다면, 부하분산 역할을 잘 할지.. 비율을 확인해보았다.
(⎈|kind-myk8s:N/A) root@MyServer:~# kubectl exec -it net-pod -- zsh -c "for i in {1..1000}; do curl -s 10.200.1.216:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
    334 Hostname: webpod2
    333 Hostname: webpod3
    333 Hostname: webpod1

# 일정하게 잘 간다...
(⎈|kind-myk8s:N/A) root@MyServer:~# for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i ipvsadm -Ln -t  10.200.1.216:9000 --rate ; echo; done
>> node myk8s-control-plane <<
Prot LocalAddress:Port                 CPS    InPPS   OutPPS    InBPS   OutBPS
  -> RemoteAddress:Port
TCP  10.200.1.216:9000                  36      215      143    14326    18815
  -> 10.10.1.2:80                       12       72       48     4786     6294
  -> 10.10.2.2:80                       12       72       48     4770     6261
  -> 10.10.3.2:80                       12       72       48     4770     6261
  
  
# 내부에서 통신하는거라 nat되진 않는다.
root@myk8s-worker:/# conntrack -L --src 10.10.0.5 
tcp      6 104 TIME_WAIT src=10.10.0.5 dst=10.10.2.2 sport=47602 dport=80 src=10.10.2.2 dst=10.10.0.5 sport=80 dport=47602 [ASSURED] mark=0 use=1
tcp      6 104 TIME_WAIT src=10.10.0.5 dst=10.10.2.2 sport=47626 dport=80 src=10.10.2.2 dst=10.10.0.5 sport=80 dport=47626 [ASSURED] mark=0 use=1
tcp      6 104 TIME_WAIT src=10.10.0.5 dst=10.10.2.2 sport=47576 dport=80 src=10.10.2.2 dst=10.10.0.5 sport=80 dport=47576 [ASSURED] mark=0 use=1

테스트 svc를 nodePort로 변경

# 기존 svc를 nodePort로 변경하였다.
(⎈|kind-myk8s:N/A) root@MyServer:~#  cat svc-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  ports:
    - name: svc-webport
      port: 9000        
      targetPort: 80    
      nodePort: 30001
  selector:
    app: webpod         
  type: NodePort
  
(⎈|kind-myk8s:N/A) root@MyServer:~# k apply -f svc-clusterip.yaml
service/svc-clusterip configured
(⎈|kind-myk8s:N/A) root@MyServer:~# k get svc
NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
kubernetes      ClusterIP   10.200.1.1     <none>        443/TCP          11h
svc-clusterip   NodePort    10.200.1.216   <none>        9000:30001/TCP   108m


# 외부에서 접근해야하기 때문에 mypc container를 생성
(⎈|kind-myk8s:N/A) root@MyServer:~# docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity
29ed3db401ca06fe9148205fd75d992b122afd7566c876e66f79cbb98c946f21
(⎈|kind-myk8s:N/A) root@MyServer:~# docker exec -it 29 zsh
 # 당연히 clusterIP로는 접근 안된다.
 29ed3db401ca  ~  curl 10.200.1.216:9000
 
 # nodeIP:ndePort로 접근
 # containerIP(172.18.0.6) -> workerNodeIP:nodePort(172.18.0.5:30001 -> 10.10.2.2:80)
 29ed3db401ca  ~  curl 172.18.0.5:30001
Hostname: webpod1
IP: 127.0.0.1
IP: ::1
IP: 10.10.2.2
IP: fe80::9cd3:48ff:fe0e:8a9
RemoteAddr: 10.10.2.1:14677
GET / HTTP/1.1
Host: 172.18.0.5:30001
User-Agent: curl/8.7.1
Accept: */*


# worker 노드에서 tcpdump
# pod의 gw인 10.10.2.1 에서 pod(10.10.2.2)로 패킷이 간다.
root@myk8s-worker:/# tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on veth42459b76, link-type EN10MB (Ethernet), snapshot length 262144 bytes
20:58:59.432284 IP ip-10-10-2-1.ap-northeast-2.compute.internal.17574 > ip-10-10-2-2.ap-northeast-2.compute.internal.http: Flags [S], seq 1880576606, win 64240, options [mss 1460,sackOK,TS val 1217925696 ecr 0,nop,wscale 7], length 0
20:58:59.432303 IP ip-10-10-2-2.ap-northeast-2.compute.internal.http > ip-10-10-2-1.ap-northeast-2.compute.internal.17574: Flags [S.], seq 3103781959, ack 1880576607, win 65160, options [mss 1460,sackOK,TS val 1094814017 ecr 1217925696,nop,wscale 7], length 0
20:58:59.432380 IP ip-10-10-2-1.ap-northeast-2.compute.internal.17574 > ip-10-10-2-2.ap-northeast-2.compute.internal.http: Flags [.], ack 1, win 502, options [nop,nop,TS val 1217925697 ecr 1094814017], length 0
20:58:59.432483 IP ip-10-10-2-1.ap-northeast-2.compute.internal.17574 > ip-10-10-2-2.ap-northeast-2.compute.internal.http: Flags [P.], seq 1:80, ack 1, win 502, options [nop,nop,TS val 1217925697 ecr 1094814017], length 79: HTTP: GET / HTTP/1.1
20:58:59.432491 IP ip-10-10-2-2.ap-northeast-2.compute.internal.http > ip-10-10-2-1.ap-northeast-2.compute.internal.17574: Flags [.], ack 80, win 509, options [nop,nop,TS val 1094814017 ecr 1217925697], length 0
20:58:59.432966 IP ip-10-10-2-2.ap-northeast-2.compute.internal.http > ip-10-10-2-1.ap-northeast-2.compute.internal.17574: Flags [P.], seq 1:309, ack 80, win 509, options [nop,nop,TS val 1094814017 ecr 1217925697], length 308: HTTP: HTTP/1.1 200 OK
...


# 테스트한 컨테이너(mypc)를 출발지로 잡고 conntract를 보면..
# 다른 워커 노드에 있는 파드에서 오는 친구들의 dst를 보게되면, myk8s-worker의 node ip이다.
# 기존과 동일하게 다른 노드에 있는 파드로 전달시, node ip로 snat 되는 것으로 보인다.
root@myk8s-worker:/# conntrack -L --src 172.18.0.6
tcp      6 83 TIME_WAIT src=172.18.0.6 dst=172.18.0.5 sport=34212 dport=30001 src=10.10.1.2 dst=172.18.0.5 sport=80 dport=29038 [ASSURED] mark=0 use=1
tcp      6 85 TIME_WAIT src=172.18.0.6 dst=172.18.0.5 sport=34232 dport=30001 src=10.10.2.2 dst=10.10.2.1 sport=80 dport=17574 [ASSURED] mark=0 use=1
tcp      6 38 TIME_WAIT src=172.18.0.6 dst=172.18.0.5 sport=55212 dport=30001 src=10.10.2.2 dst=10.10.2.1 sport=80 dport=14677 [ASSURED] mark=0 use=1
tcp      6 84 TIME_WAIT src=172.18.0.6 dst=172.18.0.5 sport=34218 dport=30001 src=10.10.3.2 dst=172.18.0.5 sport=80 dport=4766 [ASSURED] mark=0 use=1

# ipvsadm 분산테이블에 써있는걸로 확인했다.
# nodeIP:nodePort랑 clusterIP:Port만 생각했는데..
root@myk8s-worker:/#  ipvsadm -L -n
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  172.18.0.5:30001 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
TCP  10.10.2.1:30001 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
TCP  10.200.1.216: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
...


# gw로만 생각했던.. 저 ip로 호출해보면, cluster ip처럼 내부에서 호출이 된다.
(⎈|kind-myk8s:N/A) root@MyServer:~#  k exec net-pod -it -- curl 10.10.2.1:30001
Hostname: webpod1
IP: 127.0.0.1
IP: ::1
IP: 10.10.2.2
IP: fe80::9cd3:48ff:fe0e:8a9
RemoteAddr: 10.10.2.1:23003
GET / HTTP/1.1
Host: 10.10.2.1:30001
User-Agent: curl/8.7.1
Accept: */*


# 다른 노드들의 경우도 동일하게 분산테이블에 각 gw ip가 들어가 있다. 
(⎈|kind-myk8s:N/A) root@MyServer:~# 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  172.18.0.4:30001 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
TCP  10.10.0.1:30001 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
TCP  10.200.1.216: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 <<
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  172.18.0.5:30001 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
TCP  10.10.2.1:30001 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
TCP  10.200.1.216: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 <<
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  172.18.0.2:30001 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
TCP  10.10.1.1:30001 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
TCP  10.200.1.216: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 <<
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  172.18.0.3:30001 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
TCP  10.10.3.1:30001 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
TCP  10.200.1.216: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
...

각 pod의 gw가 뿌려주는 것인지.. 잘모르겠다. 이에 대해서 문서나 내용을 못찾아서 더이상 확인해보지는 못했다. 그냥 내 짤막한 지식으로 생각을 해보자면.. pod의 gateway가 ipvs 모드에서 nodePort 로 서비스하게되면, 이 트래픽을 처리해주기 위해서 중간에서 snat하여 내부로 라우팅하는 것 같다.

TCP 172.18.0.5:30001 rr -> 10.10.1.2:80
-> 10.10.2.2:80
-> 10.10.3.2:80

이렇게 되어있지만, tcpdump로 확인해보면 아래와 같이 10.10.2.1을 한번 거쳐서 가는 것으로 이해된다.

TCP 172.18.0.5:30001 rr -> TCP 10.10.2.1:30001 rr -> 10.10.1.2:80
-> 10.10.2.2:80
-> 10.10.3.2:80

iptable 확인

# ipvs proxy mode에서는 iptables 작성 할 때, ipset을 사용한다. 겹치는 규칙들은 반복해서 생성하는 것이 아니라 하나의 규칙에 그룹처럼 만들어서 사용하는 것이다. 
# 외부에서 노드포트를 호출해서 파드로 도달한다는 가정으로 Iptable을 따라가보면.. 

# 따라갈 규칙 -j KUBE-SERVICES 
root@myk8s-worker:/# iptables -t nat -S | grep PREROUTING
-P PREROUTING ACCEPT
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A PREROUTING -d 172.18.0.1/32 -j DOCKER_OUTPUT

# 따라갈 규칙 -j KUBE-NODE-PORT
# -m set --match-set KUBE-CLUSTER-IP dst,dst 이부분을 보면, KUBE-CLUSTER-IP ipset을 사용하는 것이다.
root@myk8s-worker:/# iptables -t nat -S | grep KUBE-SERVICES
-N KUBE-SERVICES
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A KUBE-SERVICES -s 127.0.0.0/8 -j RETURN
-A KUBE-SERVICES ! -s 10.10.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODE-PORT
-A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst -j ACCEPT


# ipset을 확인해보면 svc clusterIP 
root@myk8s-worker:/# ipset list KUBE-CLUSTER-IP
Name: KUBE-CLUSTER-IP
Type: hash:ip,port
Revision: 7
Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0x72343c95
Size in memory: 456
References: 3
Number of entries: 5
Members:
10.200.1.10,tcp:9153
10.200.1.10,udp:53
10.200.1.216,tcp:9000
10.200.1.1,tcp:443
10.200.1.10,tcp:53

-A KUBE-SERVICES ! -s 10.10.0.0/16 -m comment –comment “Kubernetes service cluster ip + port for masquerade purpose” -m set –match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ 그럼 이 규칙은 출발지 대역이 10.10.0.0/16 목적지 IP가 svc clusterIP가 아니라면 KUBE-MARK-MASQ로 가서 마스커레이딩 되어야한다는 뜻이다.

# 따라갈 규칙 -j KUBE-MARK-MASQ
root@myk8s-worker:/# iptables -t nat -S | grep  KUBE-NODE-PORT
-N KUBE-NODE-PORT
-A KUBE-NODE-PORT -p tcp -m comment --comment "Kubernetes nodeport TCP port for masquerade purpose" -m set --match-set KUBE-NODE-PORT-TCP dst -j KUBE-MARK-MASQ
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODE-PORT

# 생성되어있는 노드 포트 30001이 멤버로 들어가 있다.
root@myk8s-worker:/# ipset list KUBE-NODE-PORT-TCP
Name: KUBE-NODE-PORT-TCP
Type: bitmap:port
Revision: 3
Header: range 0-65535
Size in memory: 8264
References: 1
Number of entries: 1
Members:
30001

-A KUBE-NODE-PORT -p tcp -m comment –comment “Kubernetes nodeport TCP port for masquerade purpose” -m set –match-set KUBE-NODE-PORT-TCP dst -j KUBE-MARK-MASQ 위의 규칙을 타고 node ip로 snat되어서 ipvs를 타게 된다. 그래서 위에서 conntract 확인해봤을때, 파드가 다른 노드에 있는 경우 들어갔던 노드의 ip로 snat되어 내부에서 통신된 것을 확인했다. 파드가 위치한 노드로 들어간 경우는 노드의 ip로 snat되는 것이 아니라, 자신이 아는 네트워크이기 때문에 arp 통신을 처음에 하지 않고, 10.10.2.1로 snat되어 바로 파드의 10.10.2.2로 패킷이 전달된다. 이는 strictARP를 true로 설정하면서 arp_ignore 값이 활성화 되었기 때문이다.