Kubernetes

Kubernetes Kube-proxy의 iptables proxy mode, ClusterIP 심화 탐구

mokpolar 2024. 9. 28. 16:18
반응형

안녕하세요?

 

Kubernetes Service인 ClusterIP 구조를 Kube-proxy의 mode와 엮어서 평소보다 좀 더 깊게 파보겠습니다.
이 글은 CloudNeta팀 가시다님의 KANS(Kubernetes Advanced Network Study) 4주차 과제로 작성되었습니다.

환경 구성

클러스터 배포

이전에 했던 것과 같이 kind로 클러스터를 배포합니다.

kind manifest에서 아래 조건들을 확인합니다.


pod의 서브넷, service 서브넷에 유의합니다.

네트워킹 부분을 보면 podSubnet, serviceSunet에 대한 명시가 되어있습니다.

 

networking:
podSubnet: 10.10.0.0/16
serviceSubnet: 10.200.1.0/24

cat <<EOT> kind-svc-1w.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  "InPlacePodVerticalScaling": true
nodes:
- role: control-plane
  labels:
    mynode: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
- role: worker
  labels:
    mynode: worker1
- role: worker
  labels:
    mynode: worker2
- role: worker
  labels:
    mynode: worker3
networking:
  podSubnet: 10.10.0.0/16
  serviceSubnet: 10.200.1.0/24
EOT

 

생성해줍니다.
Kind로 쿠버네티스  클러스터를 배포하면 빠르게 생성이 됩니다.

kind create cluster --config kind-svc-1w.yaml --name myk8s --image kindest/node:v1.31.0

 

이제 미리 노드에 필요한 툴들을 설치합니다.

docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bridge-utils net-tools 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 bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done

 

tcpdump 등의 도구들이 설치됩니다.
Kubernetes 1.31.0 버젼의 클러스터입니다.

 

클러스터 정보 확인

배포할때 레이블을 넣었으니, 값들이 들어가있는것을 볼 수 있습니다.

kubectl get nodes -o jsonpath="{.items[*].metadata.labels}" | grep mynode

{"beta.kubernetes.io/arch":"arm64","beta.kubernetes.io/os":"linux","kubernetes.io/arch":"arm64","kubernetes.io/hostname":"myk8s-control-plane","kubernetes.io/os":"linux","mynode":"control-plane","node-role.kubernetes.io/control-plane":"","node.kubernetes.io/exclude-from-external-load-balancers":""} {"beta.kubernetes.io/arch":"arm64","beta.kubernetes.io/os":"linux","kubernetes.io/arch":"arm64","kubernetes.io/hostname":"myk8s-worker","kubernetes.io/os":"linux","mynode":"worker1"} 
...

 

컨트롤 플레인등의 아이피를 볼 수 있습니다.
그러니까 이건 노드의 아이피입니다.

docker ps -q | xargs docker inspect --format '{{.Name}} {{.NetworkSettings.Networks.kind.IPAddress}}'

/myk8s-control-plane 192.168.247.3
/myk8s-worker 192.168.247.4
/myk8s-worker3 192.168.247.5
/myk8s-worker2 192.168.247.2

 

컨트롤플레인 노드의 아이피에 유의합니다.
kind로 설치한 것과 같이 아래 대역들을 쪼개서 쓰게됩니다

kubectl get cm -n kube-system kubeadm-config -oyaml | grep -i subnet

      podSubnet: 10.10.0.0/16
      serviceSubnet: 10.200.1.0/24

 

 kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"

                            "--service-cluster-ip-range=10.200.1.0/24",
                            "--cluster-cidr=10.10.0.0/16",

 

개별 노드의 할당 받은 CIDR을 확인할 수 있습니다.
각각의 워커노드가 받은 대역을 이렇게 확인가능합니다.


그러니까 이 Pod CIDR 대역에 속하는 IP를 받아서 Pod가 생성됩니다.

 kubectl get nodes -o jsonpath="{.items[*].spec.podCIDR}"

10.10.0.0/24 10.10.1.0/24 10.10.2.0/24 10.10.3.0/24%

 

kube-proxy iptable mode

kube-proxy의 configmap 정보를 확인해보겠습니다.
kube-proxy의 컨피그맵을 보면, 기본값으로 설치되어, kube-proxy mode가 iptables 임을 볼 수 있습니다.

kubectl describe cm -n kube-system kube-proxy

Data
====
kubeconfig.conf:
----
apiVersion: v1
kind: Config
clusters:
- cluster:
...
mode: iptables
...

 

iptable모드를 쓰면 iptables에 대한 옵션 파라미터를 쓸 수 있습니다.

for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-$i cat /etc/cni/net.d/10-kindnet.conflist; echo; done

>> node myk8s-control-plane <<
{
    "cniVersion": "0.3.1",
    "name": "kindnet",
    "plugins": [
    {
        "type": "ptp",
        "ipMasq": false,
        "ipam": {
            "type": "host-local",
            "dataDir": "/run/cni-ipam-state",
            "routes": [
                { "dst": "0.0.0.0/0" }
            ],
            "ranges": [
                [ { "subnet": "10.10.0.0/24" } ]
            ]
        }
        ,
        "mtu": 1500
    },
    {
        "type": "portmap",
        "capabilities": {
            "portMappings": true
        }
    }
    ]
}
>> node myk8s-worker <<
{
    "cniVersion": "0.3.1",
    "name": "kindnet",
    "plugins": [
    {
        "type": "ptp",
        "ipMasq": false,
        "ipam": {
            "type": "host-local",
            "dataDir": "/run/cni-ipam-state",
            "routes": [
                { "dst": "0.0.0.0/0" }
            ],
            "ranges": [
                [ { "subnet": "10.10.1.0/24" } ]
            ]
        }
        ,
        "mtu": 1500
    },
    {
        "type": "portmap",
        "capabilities": {
            "portMappings": true
        }
    }
    ]
}

노드 네트워크 정보 확인

라우팅 정보를 확인해보면
컨트롤플레인과 워커노드들이 PodCIDR설정한 subnet을 할당받은 모습을 볼 수 있습니다.

 

각 노드에서 ip route를 실행해서 어떤 정보들이 들어있는지를 자세히 볼 수 있습니다.

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

>> node myk8s-control-plane <<
default via 192.168.247.1 dev eth0
10.10.0.2 dev veth1627bc48 scope host
10.10.0.3 dev vethc6bed53f scope host
10.10.0.4 dev veth94987eb4 scope host
10.10.0.5 dev veth56366fde scope host
10.10.1.0/24 via 192.168.247.4 dev eth0
10.10.2.0/24 via 192.168.247.5 dev eth0
10.10.3.0/24 via 192.168.247.2 dev eth0
192.168.247.0/24 dev eth0 proto kernel scope link src 192.168.247.3

>> node myk8s-worker <<
default via 192.168.247.1 dev eth0
10.10.0.0/24 via 192.168.247.3 dev eth0
10.10.2.0/24 via 192.168.247.2 dev eth0
10.10.3.0/24 via 192.168.247.5 dev eth0
192.168.247.0/24 dev eth0 proto kernel scope link src 192.168.247.4
...

 

그림으로 그리면 이런 모습일까요?

 

아이피도 확인해보면

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 <<
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1000
    link/tunnel6 :: brd :: permaddr 2e68:1042:e0c0::
4: vethc6bed53f@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 4e:0a:ce:61:60:99 brd ff:ff:ff:ff:ff:ff link-netns cni-d1d21b7c-5b8a-38a6-98f6-6a9d506e23e4
    inet 10.10.0.1/32 scope global vethc6bed53f
       valid_lft forever preferred_lft forever
    inet6 fe80::4c0a:ceff:fe61:6099/64 scope link
       valid_lft forever preferred_lft forever
5: veth1627bc48@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 26:28:36:49:7e:b9 brd ff:ff:ff:ff:ff:ff link-netns cni-ab985a5d-37c4-1400-9df4-1ebdad2d6bb8
    inet 10.10.0.1/32 scope global veth1627bc48
       valid_lft forever preferred_lft forever
    inet6 fe80::2428:36ff:fe49:7eb9/64 scope link
       valid_lft forever preferred_lft forever
6: veth94987eb4@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether ca:9f:56:e2:b4:f1 brd ff:ff:ff:ff:ff:ff link-netns cni-bb0d0bc2-ae66-17f9-c70d-b71a70cd9b31
    inet 10.10.0.1/32 scope global veth94987eb4
       valid_lft forever preferred_lft forever
    inet6 fe80::c89f:56ff:fee2:b4f1/64 scope link
       valid_lft forever preferred_lft forever
7: veth56366fde@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether ae:a1:0d:0c:f5:0e brd ff:ff:ff:ff:ff:ff link-netns cni-fff894bf-df33-476d-31ff-fe5af767ab68
    inet 10.10.0.1/32 scope global veth56366fde
       valid_lft forever preferred_lft forever
    inet6 fe80::aca1:dff:fe0c:f50e/64 scope link
       valid_lft forever preferred_lft forever
11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:c0:a8:f7:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.168.247.3/24 brd 192.168.247.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fc00:f853:ccd:e793::3/64 scope global nodad
       valid_lft forever preferred_lft forever
    inet6 fe80::42:c0ff:fea8:f703/64 scope link
       valid_lft forever preferred_lft forever
...

 

컨트롤플레인은 veth가 4개 호스트 네트워크를 안쓰는 파드가 4개 있다는 뜻일 것 같습니다.
eth0은 노드의 물리적 인터페이스입니다.

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

>> node myk8s-control-plane <<
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default  link-netnsid 0
    inet 192.168.247.3/24 brd 192.168.247.255 scope global eth0
       valid_lft forever preferred_lft forever

>> node myk8s-worker <<
18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default  link-netnsid 0
    inet 192.168.247.4/24 brd 192.168.247.255 scope global eth0
       valid_lft forever preferred_lft forever
...

네트워크 테스트 준비

이제 네트워크 테스트를 위한 준비를 해보겠습니다.

먼저 상태 확인을 위해 컨트롤플레인에서 arp-scan을 돌립니다


현재 설정된 네트워크 상에서 정상적으로 통신할 수 있는 노드들이 있는지 확인해봅니다.
연결된 각 노드들의 MAC주소를 확인할 수 있습니다.

 

arp-scan은 Layer 2에서 동작하므로, IP 레벨이 아니라 물리적인 MAC 주소 기반으로 네트워크에 연결된 장치들을 탐색하는 데 유용합니다.

docker exec -it myk8s-control-plane arp-scan --interfac=eth0 --localnet
Interface: eth0, type: EN10MB, MAC: 02:42:c0:a8:f7:03, IPv4: 192.168.247.3
Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.247.0    02:42:2a:04:8e:6a    (Unknown: locally administered)
192.168.247.1    02:42:2a:04:8e:6a    (Unknown: locally administered)
192.168.247.2    02:42:c0:a8:f7:02    (Unknown: locally administered)
192.168.247.4    02:42:c0:a8:f7:04    (Unknown: locally administered)
192.168.247.5    02:42:c0:a8:f7:05    (Unknown: locally administered)

5 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.10.0: 256 hosts scanned in 1.908 seconds (134.17 hosts/sec). 5 responded

 

이제 kind 밖에 있는 서버가 필요한데,
쿠버네티스 범위에 포함되지 않지만 같은 네트워크에 있는 어떤 노드를 새로 만들어야 합니다.

 

kind는 kind라고 하는 브릿지 네트워크를 씁니다.

mypc의 아이피를 고정하려고 옵션을 줬습니다.


netshoot 으로 기동하고 sleep으로 고정해둡니다.

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

35f52c6c3f6cd98890bb1c4f568e46cd18605421c914f52edf3b6e0fab70349b

Service와 Kube-proxy mode

Kubernetes Service IP와 Endpoint

Kubernetes의 Pod는 언제든 재실행 될 수 있습니다.
Pod의 IP는 그 경우 변경이 되고 Kubernetes의 Service는 이 Pod로 재연결을 해야 합니다.

 

그래서 Kubernetes Service IP 로 통신 요청시, Service는 Endpoint로 묶여있는 Pod의 IP로 전달합니다.
Endpoint는 Service에 연동된 Pod의 집합을 말합니다.

 

만약 Pod가 재실행되어 IP가 바뀌어도, Service는 이를 알고 있으며 재실행된 Pod의 IP로 전달합니다.
Service는 일종의 L4역할을 한다고 할 수 있습니다.

 

Kubernetes의 Endpoint controller는 모니터링을 통해 Service의 selector와 매칭되는 label이 있는 pod들을 ednpoint로 연결하고 있습니다.

아래 그림 처럼 동작합니다.

iptables proxy mode

kube-proxy는 서비스 통신 동작의 설정을 관리하며 Daemonset으로 배포되서 노드마다 존재합니다.
그리고 이 kube-proxy에는 3가지 모드가 있는데, (이제 사용하지 않는) user space proxy mode, iptables proxy mode, IPVS proxy mode 입니다.

 

그 중 비효율적이라서 사용하지 않는 user space 모드의 구조는 아래와 같습니다.

클라이언트의 traffic이 node1의 nic 1 에 도착합니다.
이는 kernel space의 netfilter에 의해서 user space의 kube-proxy 로 전달됩니다.
여기서 목적지 정보를 확인하고 nic 2를 통해 목적이 node 2로 전달됩니다.

 

이때 kernel , user space의 전환이 이루어지고, user space에서 kube-proxy가 동작하기 때문에 비효율적이며 성능 감소가 있습니다.

 

그리고 iptables 모드는 아래와 같습니다.

이 경우, kube-proxy는 트래픽 전달 과정에 개입되지 않습니다.
kube-proxy는 단순히 netfilter의 규칙을 수정합니다.

 

그렇게 서비스 설정시 관련된 iptables 규칙이 netfilter에 적용됩니다.

그리고 client의 트래픽은 kernel space의 netfilter에 적용되어 있는 규칙으로 전달됩니다.

 

이렇게하면 user space proxy mode에 비해 시스템 오버헤드가 줄어듭니다.

Kubernetes Service별 통신 흐름 : ClusterIP

ClusterIP의 통신흐름

TestPod를 Controlplane에 생성하여 트래픽 테스트를 시도한다고 가정해보겠습니다.

ClusterIP로 접근해왔다면, 이 Controlplane 노드에 있는 iptables 분산룰에 의해서 DNAT처리가 되어서 목적지 pod로 가게 됩니다.
ClusterIP를 설정하면 모든 노드에 동일한 룰이 세팅이 됩니다.
이때 부하분산 룰은 랜덤 부하분산입니다.

DNAT은 아래와 같은 형태로 이루어집니다.

처음에 TestPod에서 출발합니다.
Service(ClusterIP)가 10.96.0.1:9000 이고
출발지 아이피가 10.0.0.1에 랜덤포트 51234로 소스포트가 잡힙니다.

 

그리고 iptables rule에 의해 source ip는 그대론데.
destination ip가 바뀝니다.

ClusterIP의 통신 테스트 준비

테스트용 파드를 생성해보겠습니다.

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

 

클라이언트도 만들어줍니다.
위 그림과 같이 control plane에 띄웁니다.

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

 

이제 접근할 ClusterIP 서비스도 생성합니다.

cat <<EOT> svc-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  ports:
    - name: svc-webport
      port: 9000        # 서비스 IP 에 접속 시 사용하는 포트 port 를 의미
      targetPort: 80    # 타킷 targetPort 는 서비스를 통해서 목적지 파드로 접속 시 해당 파드로 접속하는 포트를 의미
  selector:
    app: webpod         # 셀렉터 아래 app:webpod 레이블이 설정되어 있는 파드들은 해당 서비스에 연동됨
  type: ClusterIP       # 서비스 타입
EOT

 

파드들은 물론 레이블과 셀렉터로 연결이 됩니다.
이제 생성하겠습니다.

watch -d 'kubectl get pod -owide ;echo; kubectl get svc,ep svc-clusterip'
kubectl apply -f 3pod.yaml,netpod.yaml,svc-clusterip.yaml
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"

                            "--service-cluster-ip-range=10.200.1.0/24",
                            "--cluster-cidr=10.10.0.0/16",
kubectl get pod -owide
kubectl get svc svc-clusterip
NAME      READY   STATUS    RESTARTS   AGE   IP          NODE                  NOMINATED NODE   READINESS GATES
net-pod   1/1     Running   0          46s   10.10.0.5   myk8s-control-plane   <none>           <none>
webpod1   1/1     Running   0          46s   10.10.1.2   myk8s-worker          <none>           <none>
webpod2   1/1     Running   0          46s   10.10.2.2   myk8s-worker2         <none>           <none>
webpod3   1/1     Running   0          46s   10.10.3.2   myk8s-worker3         <none>           <none>
NAME            TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
svc-clusterip   ClusterIP   10.200.1.22   <none>        9000/TCP   47s
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"

                            "--service-cluster-ip-range=10.200.1.0/24",
                            "--cluster-cidr=10.10.0.0/16",

 

ip를 확인해보면 왜 pod ip는 10.0.0.5일까요?
cluser-cidr을 위와 같이 설정했기 때문입니다.

 

왜 서비스 아이피는 10.200.1.22일까요?
service-cluster-ip-range를 위와 같이 설정했기 때문입니다.

 

ClusterIP의 Endpoint를 보면 3개의 pod가 잡혀있는 모습을 볼 수 있습니다.

kubectl describe svc svc-clusterip

Name:                     svc-clusterip
Namespace:                default
Labels:                   <none>
Annotations:              <none>
Selector:                 app=webpod
Type:                     ClusterIP
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.200.1.22
IPs:                      10.200.1.22
Port:                     svc-webport  9000/TCP
TargetPort:               80/TCP
Endpoints:                10.10.2.2:80,10.10.1.2:80,10.10.3.2:80

 

마치 L4에 묶인 대상서버들처럼 동작합니다.

kubectl get endpoints svc-clusterip

NAME            ENDPOINTS                                AGE
svc-clusterip   10.10.1.2:80,10.10.2.2:80,10.10.3.2:80   2m55s

 

엔드포인트 슬라이스로 확인할 수 있습니다.

 kubectl get endpointslices -l kubernetes.io/service-name=svc-clusterip
NAME                  ADDRESSTYPE   PORTS   ENDPOINTS                       AGE
svc-clusterip-d76tl   IPv4          80      10.10.2.2,10.10.1.2,10.10.3.2   3m20s

ClusterIP의 통신 테스트

통신환경

kubectl get pod -l app=webpod -o jsonpath="{.items[*].status.podIP}"

10.10.1.2 10.10.2.2 10.10.3.2%

배포된 파드의 ip를 확인해보겠습니다.
1.2 2.2 3.2
입니다.

 

이걸 변수에 지정합니다.

WEBPOD1=$(kubectl get pod webpod1 -o jsonpath={.status.podIP})
WEBPOD2=$(kubectl get pod webpod2 -o jsonpath={.status.podIP})
WEBPOD3=$(kubectl get pod webpod3 -o jsonpath={.status.podIP})
echo $WEBPOD1 $WEBPOD2 $WEBPOD3

 

아래와 같이 확인하면
net-pod에서 접근합니다.

for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod; done

Hostname: webpod1
IP: 127.0.0.1
IP: ::1
IP: 10.10.1.2
IP: fe80::94f6:e4ff:fe40:66a2
RemoteAddr: 10.10.0.5:51252
GET / HTTP/1.1
Host: 10.10.1.2
User-Agent: curl/8.7.1
Accept: */*

Hostname: webpod2
IP: 127.0.0.1
IP: ::1
IP: 10.10.2.2
IP: fe80::d43c:5eff:feea:776a
RemoteAddr: 10.10.0.5:51150
GET / HTTP/1.1
Host: 10.10.2.2
User-Agent: curl/8.7.1
Accept: */*

Hostname: webpod3
IP: 127.0.0.1
IP: ::1
IP: 10.10.3.2
IP: fe80::e857:38ff:fe2f:1b6d
RemoteAddr: 10.10.0.5:37926
GET / HTTP/1.1
Host: 10.10.3.2
User-Agent: curl/8.7.1
Accept: */*

 

grep으로 host name만 필터링해서 보겠습니다.

for pod in $WEBPOD1 $WEBPOD2 $WEBPOD3; do kubectl exec -it net-pod -- curl -s $pod | egrep 'Host|RemoteAddr'; done

Hostname: webpod1
RemoteAddr: 10.10.0.5:39274
Host: 10.10.1.2
Hostname: webpod2
RemoteAddr: 10.10.0.5:41392
Host: 10.10.2.2
Hostname: webpod3
RemoteAddr: 10.10.0.5:52318
Host: 10.10.3.2

 

remote address는 접근한 클라이언트의 아이피입니다.
이 경우 net-pod의 것입니다.

 

서비스 ip를 봅니다.

k get svc
NAME            TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
kubernetes      ClusterIP   10.200.1.1    <none>        443/TCP    74m
svc-clusterip   ClusterIP   10.200.1.22   <none>        9000/TCP   31m

얘를 변수로 지정합니다.

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

 

추가로 컨트롤플레인 노드에서 iptable 정보를 봅니다.

 docker exec -it myk8s-control-plane iptables -t nat -S | grep $SVC1

-A KUBE-SERVICES -d 10.200.1.22/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-SVC-KBDEBIL6IU6WL7RF
-A KUBE-SVC-KBDEBIL6IU6WL7RF ! -s 10.10.0.0/16 -d 10.200.1.22/32 -p tcp -m comment --comment "default/svc-clusterip:svc-webport cluster IP" -m tcp --dport 9000 -j KUBE-MARK-MASQ

 

이렇게 보면,
서비스 생성시 kube-proxy에 의해서 iptable 규칙이 모든 노드에 추가됩니다.

for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-control-plane iptables -t nat -S | grep $SVC1; echo; done

iptable에 목적지 포트가 나옵니다.
grep으로 이 아이피로 필터링해서봅니다.

 

-> destintation 9000번이 보입니다.

이 내용이 뭐냐면

controlplane iptable 룰에
ClusterIP의
-dport destination port 9000번 포트로 오게되면

-j 점프, 전부 보냅니다.

 

이 controlplane iptable rule에 의해서
destination ip port가 iptable룰에 의해서 처리된다는 것을 볼 수 있습니다.

 

동일한 규칙이 컨트롤플레인 말고도 다른 노드에도 있을까요?

for i in control-plane worker worker2 worker3; do echo ">> node myk8s-$i <<"; docker exec -it myk8s-control-plane iptables -t nat -S | grep $SVC1; echo; done

마찬가지로 들어있습니다.
모든 노드에
kube-proxy가 이 iptable rule을 세팅합니다.

 

만약 test pod가 뜨면
이 타겟이 모든 노드에 분산되서 들어갑니다.

 

itpable룰이 있을거고 대상 파드가 1, 2 ,3 이니까 랜덤 부하분산이 됩니다.

iptable 룰에의해서,
netfilter 프레임워크에 의해서 이 규칙에 매칭되면 처리되어 버립니다.

부하분산 여부 테스트

지금까지 확인한 내용을 그림으로 그리면 이런 느낌 인것 같습니다.

이제 위 그림과 같이 netshoot pod에서 web pod로
lusterIP를 100번을 호출해서 각 Pod에 실제로 부하분산이 이루어지는지 확인해보겠습니다.

kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

     38 Hostname: webpod3
     32 Hostname: webpod1
     30 Hostname: webpod2

얼추 비슷합니다.
부하분산이 된다는 사실을 알 수 있습니다.

 

그러면 위 그림과 같이 각 eth, veth를 잘 지나는지도 확인해보겠습니다.

 

먼저 controlplane의 eth0에서 tcpdump를 떠보겠습니다.
그 전에 다시 한 번 이 도식도 같이 보겠습니다.

 

먼저 netshoot pod에서 연결된 veth에서 패킷 덤프를 해보겠습니다.

ip link
..
7: vetha892b16d@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether ea:be:41:cf:ca:2a brd ff:ff:ff:ff:ff:ff link-netns cni-52c09da0-29ab-b517-d834-7ef001fddc4f
..
VETH1=vetha892b16d

 

호출합니다.

kubectl exec -it net-pod -- zsh -c "for i in {1..2};  do curl -s $SVC1:9000 | grep Hostname; done | sort | uniq -c | sort -nr"

 

패킷을 확인해봅니다.

tcpdump -i $VETH1 tcp port 9000 -nnq
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on vetha892b16d, link-type EN10MB (Ethernet), snapshot length 262144 bytes
06:37:46.567380 IP 10.10.0.5.58434 > 10.200.1.22.9000: tcp 0
06:37:46.567484 IP 10.200.1.22.9000 > 10.10.0.5.58434: tcp 0
06:37:46.567491 IP 10.10.0.5.58434 > 10.200.1.22.9000: tcp 0
06:37:46.567538 IP 10.10.0.5.58434 > 10.200.1.22.9000: tcp 79
06:37:46.567546 IP 10.200.1.22.9000 > 10.10.0.5.58434: tcp 0
06:37:46.567898 IP 10.200.1.22.9000 > 10.10.0.5.58434: tcp 309
...

 

출발지의 랜덤포트 58434이고 대상 ClusterIP Service의 9000번 포트를 향해 갑니다.

그리고 그 다음 순서인 eth0입니다.

tcpdump -i eth0 tcp port 80 -nnq
tcpdump -i eth0 tcp port 80 -nnq
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
06:31:40.329330 IP 10.10.0.5.37246 > 10.10.2.2.80: tcp 0
06:31:40.329377 IP 10.10.2.2.80 > 10.10.0.5.37246: tcp 0
06:31:40.329384 IP 10.10.0.5.37246 > 10.10.2.2.80: tcp 0
06:31:40.329419 IP 10.10.0.5.37246 > 10.10.2.2.80: tcp 79
06:31:40.329423 IP 10.10.2.2.80 > 10.10.0.5.37246: tcp 0
06:31:40.331248 IP 10.10.2.2.80 > 10.10.0.5.37246: tcp 309
...

 

이번엔 대상 pod ip의 80번 포트로 변해서 가는 것을 볼 수 있습니다.
노드 내 iptable rule로 변환되었다는 사실을 알 수 있습니다.

 

이제 control plane이 아니라 worker 노드로 가서 보겠습니다.

worker 노드의 eth0과 veth입니다.

 

pod cidr을 통해 움직입니다.

tcpdump -i eth0 tcp port 80 -nnq
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
06:43:08.830840 IP 10.10.0.5.36064 > 10.10.2.2.80: tcp 0
06:43:08.830865 IP 10.10.2.2.80 > 10.10.0.5.36064: tcp 0
06:43:08.830877 IP 10.10.0.5.36064 > 10.10.2.2.80: tcp 0
06:43:08.830914 IP 10.10.0.5.36064 > 10.10.2.2.80: tcp 79
06:43:08.830918 IP 10.10.2.2.80 > 10.10.0.5.36064: tcp 0
06:43:08.831132 IP 10.10.2.2.80 > 10.10.0.5.36064: tcp 309
06:43:08.831151 IP 10.10.0.5.36064 > 10.10.2.2.80: tcp 0

VETH1=vethe7b90842

tcpdump -i $VETH1 tcp port 80 -nnq
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on vethe7b90842, link-type EN10MB (Ethernet), snapshot length 262144 bytes
06:42:24.820475 IP 10.10.0.5.51962 > 10.10.2.2.80: tcp 0
06:42:24.820485 IP 10.10.2.2.80 > 10.10.0.5.51962: tcp 0
06:42:24.820502 IP 10.10.0.5.51962 > 10.10.2.2.80: tcp 0
06:42:24.820536 IP 10.10.0.5.51962 > 10.10.2.2.80: tcp 79
06:42:24.820538 IP 10.10.2.2.80 > 10.10.0.5.51962: tcp 0
06:42:24.820844 IP 10.10.2.2.80 > 10.10.0.5.51962: tcp 309
06:42:24.820868 IP 10.10.0.5.51962 > 10.10.2.2.80: tcp 0

 

이제 이 패킷들을 다 떠서 wireshark로 머지해서 한번 보겠습니다.

반응형