..

KANS3) Service 1

이번 주차는 서비스가 어떻게 파드로 패킷을 전달하는지와 서비스 종류에 대해 알아보는 주차였다. 테스트 환경은 가시다님이 제공주신 vagrant file을 사용하여 가상머신 올려서 사용했다.

테스트 환경 설정

curl -O https://raw.githubusercontent.com/gasida/KANS/main/kind/Vagrantfile
curl -O https://raw.githubusercontent.com/gasida/KANS/main/kind/init_cfg.sh

❯ vagrant up

❯ vagrant status
Current machine states:

default                   running (virtualbox)
...
❯ vagrant ssh

# kind cni를 사용하는 cluster를 배포
root@kind:~# cat <<EOT> kind-svc-1w.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  "InPlacePodVerticalScaling": true
  "MultiCIDRServiceAllocator": true
nodes:
- role: control-plane
  labels:
    mynode: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  kubeadmConfigPatches:
  - |
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        runtime-config: api/all=true
- role: worker
  labels:
EOTerviceSubnet: 10.200.1.0/24

root@kind:~# kind create cluster --config kind-svc-1w.yaml --name myk8s --image kindest/node:v1.31.0
Creating cluster "myk8s" ...
...

# docker ps로 노드 확인
(⎈|kind-myk8s:N/A) root@kind:~# docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED          STATUS          PORTS                                                             NAMES
ec7adbe5350c   kindest/node:v1.31.0   "/usr/local/bin/entr…"   31 minutes ago   Up 31 minutes                                                                     myk8s-worker
f15642db9183   kindest/node:v1.31.0   "/usr/local/bin/entr…"   31 minutes ago   Up 31 minutes   0.0.0.0:30000-30002->30000-30002/tcp, 127.0.0.1:42371->6443/tcp   myk8s-control-plane
9418511b3e6f   kindest/node:v1.31.0   "/usr/local/bin/entr…"   31 minutes ago   Up 31 minutes                                                                     myk8s-worker2
5d730ae049de   kindest/node:v1.31.0   "/usr/local/bin/entr…"   31 minutes ago   Up 31 minutes   

# 컨트롤플레인와 워커노드에 필요한 툴들 설치
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget bsdmainutils bridge-utils net-tools ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping git vim arp-scan -y'
(⎈|kind-myk8s:N/A) root@kind:~# 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 ipset ipvsadm nfacct tcpdump ngrep iputils-ping arping -y'; echo; done

# 노드에 붙은 라벨 확인
(⎈|kind-myk8s:N/A) root@kind:~# kubectl get nodes -o jsonpath="{.items[*].metadata.labels}" | jq | grep mynode
  "mynode": "control-plane",
  "mynode": "worker1"
  "mynode": "worker2"
  "mynode": "worker3"

# 노드 컨테이너 대역대 확인
(⎈|kind-myk8s:N/A) root@kind:~# docker ps -q | xargs docker inspect --format ' '
/myk8s-worker 172.18.0.2
/myk8s-control-plane 172.18.0.5
/myk8s-worker2 172.18.0.4
/myk8s-worker3 172.18.0.3

# 파드와 Service 대역 확인 
(⎈|kind-myk8s:N/A) root@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
      
# 노드마다 할당된 파드 고정 대역대 확인
(⎈|kind-myk8s:N/A) root@kind:~# 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.1.0/24
myk8s-worker2         10.10.2.0/24
myk8s-worker3         10.10.3.0/24

kindnet 🔗

# kind network를 사용
(⎈|kind-myk8s:N/A) root@kind:~# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
a7cb493939e6   bridge    bridge    local
bd171c4c05b9   host      host      local
b2152e16fdcb   kind      bridge    local
32658cbe2101   none      null      local

# 맨 마지막 이름(=kind network id)
(⎈|kind-myk8s:N/A) root@kind:~# ip -br -4 addr show
lo               UNKNOWN        127.0.0.1/8
enp0s3           UP             10.0.2.15/24 metric 100
enp0s8           UP             192.168.50.10/24
docker0          DOWN           172.17.0.1/16
br-b2152e16fdcb  UP             172.18.0.1/16


# controlplane에 들어가서 확인해보았다.
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it myk8s-control-plane bash

# 모든 veth인터페이스들이 동일한 IP 주소를 갖는다.
# 각 파드들이 기본 gw역할을 해서 bridge를 통해 통신 하는 것이 아님
root@myk8s-control-plane:/# ip -br -4 addr show
lo               UNKNOWN        127.0.0.1/8
vethe37588be@if2 UP             10.10.0.1/32
vethb19fa5cc@if2 UP             10.10.0.1/32
vethcbf739f8@if2 UP             10.10.0.1/32
veth5011a3b2@if2 UP             10.10.0.1/32
eth0@if13        UP             172.18.0.5/16

    
# type: ptp = point to point 로 두 엔트포인트간의 직접적인 연결을 의미한다. 
# 네트워크 레이어에서 ip 주소를 그대로 사용 하는 것
root@myk8s-control-plane:/# cat /etc/cni/net.d/10-kindnet.conflist
{
	"cniVersion": "0.3.1",
	"name": "kindnet",
	"plugins": [
	{
		"type": "ptp",
		"ipMasq": false,
		"ipam": {
			"type": "host-local",
	...

테스트 파드를 띄워서 확인해보았다.

(⎈|kind-myk8s:N/A) root@kind:~# cat test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod1
spec:
  containers:
  - name: pod1
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: pod2
spec:
  containers:
  - name: pod2
    image: nicolaka/netshoot
    command: ["tail"]
    args: ["-f", "/dev/null"]
  terminationGracePeriodSeconds: 0
  
  
(⎈|kind-myk8s:N/A) root@kind:~# k apply -f test.yaml
pod/pod1 created
pod/pod2 created

(⎈|kind-myk8s:N/A) root@kind:~# k get po -owide
pod2   1/1     Running             0          103s   10.10.1.2   myk8s-worker    <none>           <none>
pod1   1/1     Running             0          103s   10.10.2.3   myk8s-worker2   <none>           <none>


# pod1에서 pod2로 가는 길을 확인해보면,
# pod1에서 Pod2로 가기 위해 eht0인터페이스를 통해 10.10.2.1에 패킷을 전송하게 된다.
(⎈|kind-myk8s:N/A) root@kind:~# kubectl exec -it pod1 -- ip route get 10.10.1.2
10.10.1.2 via 10.10.2.1 dev eth0 src 10.10.2.3 uid 0

# 10.10.2.1은 pod1이 있는 worker2 node에서의 가상인터페이스
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it myk8s-worker2 ip -br -4 add show
lo               UNKNOWN        127.0.0.1/8
veth9fddd097@if2 UP             10.10.2.1/32
eth0@if11        UP             172.18.0.4/16

# worker2 node에서 pod2 경로를 화인해보면,
# eth0터널을 통해 172.18.0.2로 전달된다.
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it myk8s-worker2 ip route get 10.10.1.2
10.10.1.2 via 172.18.0.2 dev eth0 src 172.18.0.4 uid 0
    
# 172.18.0.2는 pod2가 위치한 myk8s-worker node
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it myk8s-worker ip -br -4 add show
lo               UNKNOWN        127.0.0.1/8
veth1af37920@if2 UP             10.10.1.1/32
eth0@if7         UP             172.18.0.2/16

# 해당 노드에서 pod2의 경로를 확인해보면, pod2에 연결된 가상인터페이스로 연결된다.
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it myk8s-worker ip route get 10.10.1.2
10.10.1.2 dev veth1af37920 src 10.10.1.1 uid 0

위와 같이 보면 브릿지를 사용한 오버레이 네트워크처럼 보일 수 있지만, 실제 통신은 l3 라우팅을 통해 이뤄지기 때문에 pod ip가 변경되지 않고 전달된다.

# pod1 > pod2
(⎈|kind-myk8s:N/A) root@kind:~# kubectl exec -it pod1 -- /bin/sh
~ # ping -c 3 10.10.1.2

# pod2에서 tcpdump로 확인해보면, pod1의 client ip를 확인 할 수 있다.
(⎈|kind-myk8s:N/A) root@kind:~# kubectl exec -it pod2 -- /bin/sh
~ # tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
18:58:36.180460 ARP, Request who-has pod2 tell 10.10.1.1, length 28
18:58:36.180469 ARP, Reply pod2 is-at 36:56:b7:dc:58:9d (oui Unknown), length 28
18:58:36.180471 IP 10.10.2.3 > pod2: ICMP echo request, id 57, seq 1, length 64
18:58:36.181208 IP pod2 > 10.10.2.3: ICMP echo reply, id 57, seq 1, length 64
...

Service

pod는 생성되었다 죽었다 하다보니 고정하여 호출 할 수 있는 고정된 vip나 domain 주소가 필요하여 등장한 것이 Service다. 이 Service의 요청을 파드로 전달하는 과정을 kube-proxy에서 기능을 제공해준다. 쿠버네티스를 설치하면 kube-system 네임스페이스에 데몬셋으로 생성되는데, 해당 컴포넌트가 클러스터의 서비스와 파드간의 네트워크 통신을 관리하는 것이다.

kube-proxy

kube-proxy 동작하는데에 여러 mode가 있는데, 이전에 사용되었던 user space proxy mode와 현재 default로 사용되는 iptables proxy mode, 그리고 ipvs proxy mode 등이 있다.

user space proxy mode

처음에는 user space proxy mode로 동작했었다.

  1. 클라이언트가 svc ip로 요청을 보내면, 먼저 커널 스페이스에 도착한다.
  2. 커널에서는 netfilter라는 프레임워크를 사용하여 네트워크 패킷을 필터링하고 조작할 수 있는데, 이 때, 서비스 IP로 들어온 패킷이 netfilter에 의해 가로채어지고, 이를 처리하기 위해 user space의 kube-proxy 프로세스가 호출된다.
  3. 패킷이 커널에서 user space로 이동하는 과정에서 커널은 user space에서 실행 중인 kube-proxy 프로세스에 패킷을 전달한다.
  4. user space에서 실행되는 kube-proxy는 패킷을 수신하고 이에 맞는 pod를 선택하고 그 정보를 커널 스페이스로 전달한다.
  5. 이렇게 커널로 다시 전달된 패킷은 netfilter를 통해 다시 한번 처리되면서, 최종적으로 pod에 전달되는 방식이다.

클라이언트 요청이 kernel space에서 user space로 넘어가고 kernel space 돌아오는 과정이 발생하게 되는데, 이런 컨텍스트 스위칭이 빈번하게 일어나면서 오버헤드가 증가하게되어 성능이 좋지 않아 더이상 사용하지 않게 되었다.

iptables proxy mode

현재 기본으로 쓰이는 것은 Iptables proxy mode이다.

  1. 클라이언트가 svc ip로 요청을 보내면, 커널스페이스에 도착한다.
  2. 커널의 netfilter는 이미 user space에서 설정한 iptables 규칙에 따라 설정되어있어, user space까지 가지 않고 netfilter에서 규칙에 맞는 파드로 연결해준다.

iptables mode에서는 kube-proxy가 클러스터의 서비스와 Pod 간의 라우팅 정보를 관리하며, 이 정보를 바탕으로 iptables 규칙을 업데이트하는데, 이러한 규칙들은 NAT 및 FORWARD 체인에 추가되어 실제 패킷을 필터링하고 라우팅하는데에 사용된다. kube-proxy에서는 직접적인 관여를 하지 않지만, iptables 규칙의 수가 많아지면 iptables 알고리즘에 따라 순차적으로 규칙을 조회해야하기 때문에 성능에 영향을 끼칠 수 있다. (규칙 수가 많을 경우 패킷 처리 지연 발생 가능)

이런 단점이 있어 l4 lb인 ipvs를 사용한 ipvs proxy mode가 있다. ipvs 모드는 커널 스페이스에서 동작하고, 데이터 구조를 해시 테이블로 저장하는 등 빠르고 성능이 좋다. 다양한 로드밸런싱 알고리즘을 지원되는데, ipvs에 대해서는 다음 주차에서 확인 할 예정이다.

Service type

이렇게 kube-proxy로 동작하는 svc 종류에는 cluster ip와 node port가 있다.

  • cluster ip: 클러스터 내부 통신
  • node port: 클러스터 외부 통신

현재 테스트 클러스터는 default인 iptables proxy mode로 설정되어있다.

(⎈|kind-myk8s:N/A) root@kind:~# kubectl describe cm -n kube-system kube-proxy
...
iptables:
  localhostNodePorts: null
  masqueradeAll: false
  masqueradeBit: null
  minSyncPeriod: 1s
  syncPeriod: 0s
...
mode: iptables
...

Custer IP

# 서비스 파드 배포
(⎈|kind-myk8s:N/A) root@kind:~# 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
(⎈|kind-myk8s:N/A) root@kind:~# k apply -f 3pod.yaml
pod/webpod1 created
pod/webpod2 created
pod/webpod3 created

# 테스트 파드 배포
(⎈|kind-myk8s:N/A) root@kind:~# 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
(⎈|kind-myk8s:N/A) root@kind:~# k apply -f netpod.yaml
pod/net-pod created

# 서비스 배포
(⎈|kind-myk8s:N/A) root@kind:~# cat <<EOT> svc-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-clusterip
spec:
  ports:
    - name: svc-webport
      port: 9000        # svc의 port
      targetPort: 80    # 연결 할 pod의 port
  selector:
    app: webpod         # 연결 할 pod의 selector
  type: ClusterIP       # 서비스 타입
EOT
(⎈|kind-myk8s:N/A) root@kind:~# k apply -f svc-clusterip.yaml
service/svc-clusterip created


(⎈|kind-myk8s:N/A) root@kind:~# kubectl get pod -owide
NAME      READY   STATUS    RESTARTS   AGE   IP          NODE                  NOMINATED NODE   READINESS GATES
net-pod   1/1     Running   0          24m   10.10.0.6   myk8s-control-plane   <none>           <none>
webpod1   1/1     Running   0          26m   10.10.1.3   myk8s-worker          <none>           <none>
webpod2   1/1     Running   0          26m   10.10.2.4   myk8s-worker2         <none>           <none>
webpod3   1/1     Running   0          26m   10.10.3.3   myk8s-worker3         <none>           <none>

(⎈|kind-myk8s:N/A) root@kind:~# kubectl get svc -owide
NAME            TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE   SELECTOR
kubernetes      ClusterIP   10.200.1.1    <none>        443/TCP    18h   <none>
svc-clusterip   ClusterIP   10.200.1.63   <none>        9000/TCP   23m   app=webpod

(⎈|kind-myk8s:N/A) root@kind:~# kubectl get endpoints svc-clusterip
NAME            ENDPOINTS                                AGE
svc-clusterip   10.10.1.3:80,10.10.2.4:80,10.10.3.3:80   22m

clustIP로 어떻게 접근하는지 확인

# 테스트 파드에서 svc와 pod ip로 접근 가능한 것을 확인
(⎈|kind-myk8s:N/A) root@kind:~# k exec -it net-pod -- /bin/sh
~ # curl -s  10.200.1.63:9000 | grep Hostname
Hostname: webpod3
~ # curl -s 10.10.1.3 | grep Hostname
Hostname: webpod1

# net-pod에서 svc clusterip:port로 호출하는 동안의 tcpdump를 확인해보면,
(⎈|kind-myk8s:N/A) root@kind:~# k exec -it net-pod -- curl -s 10.200.1.63:9000
Hostname: webpod1

# webpod1의 node에 접근하여 확인
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it myk8s-worker bash
root@myk8s-worker:/# tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on vethe9251aa3, link-type EN10MB (Ethernet), snapshot length 262144 bytes
22:08:23.864882 IP 10.10.0.6.38158 > 10.10.1.3.http: Flags [S], seq 2027460615, win 64240, options [mss 1460,sackOK,TS val 1215853224 ecr 0,nop,wscale 7], length 0
22:08:23.866038 IP 10.10.1.3.http > 10.10.0.6.38158: Flags [S.], seq 1612625038, ack 2027460616, win 65160, options [mss 1460,sackOK,TS val 2760627948 ecr 1215853224,nop,wscale 7], length 0
22:08:23.866107 IP 10.10.0.6.38158 > 10.10.1.3.http: Flags [.], ack 1, win 502, options [nop,nop,TS val 1215853226 ecr 2760627948], length 0
...

# 해당 워커 노드에서 들어오면 어떤 룰을 타고 가는지 iptables를 확인
# iptables로 들어올 때 가장 먼저 PREROUTING을 타서 여기서 먼저 확인
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it myk8s-control-plane bash

# packet 수 변한거를 >> 으로 하여 표시함 
# 계속해서 packet 수 변한 것을 보고 쫓아간다
root@myk8s-control-plane:/# iptables -v --numeric --table nat --list PREROUTING | column -t
Chain  PREROUTING  (policy        ACCEPT  242  packets,  14662  bytes)
pkts   bytes       target         prot    opt  in        out    source     destination
271    16510       KUBE-SERVICES  0       --   *         *      0.0.0.0/0  0.0.0.0/0    /*  kubernetes  service  portals  */
>>
275    16750       KUBE-SERVICES  0       --   *         *      0.0.0.0/0  0.0.0.0/0    /*  kubernetes  service  portals  */

# KUBE-SERVICES를 확인해보면
root@myk8s-control-plane:/# iptables -v --numeric --table nat --list KUBE-SERVICES | column
Chain KUBE-SERVICES (2 references)
 pkts bytes target     prot opt in     out     source               destination
   42  2520 KUBE-SVC-KBDEBIL6IU6WL7RF  6    --  *      *       0.0.0.0/0            10.200.1.63          /* default/svc-clusterip:svc-webport cluster IP */ tcp dpt:9000
>>
   43  2580 KUBE-SVC-KBDEBIL6IU6WL7RF  6    --  *      *       0.0.0.0/0            10.200.1.63          /* default/svc-clusterip:svc-webport cluster IP */ tcp dpt:9000
 
# KUBE-SVC-KBDEBIL6IU6WL7RF
 root@myk8s-control-plane:/# iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF | column
Chain KUBE-SVC-KBDEBIL6IU6WL7RF (1 references)
 pkts bytes target     prot opt in     out     source               destination
   16   960 KUBE-SEP-W5ROWLZT7Z7YVFGJ  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport -> 10.10.2.4:80 */ statistic mode random probability 0.50000000000
 >>
   17  1020 KUBE-SEP-W5ROWLZT7Z7YVFGJ  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport -> 10.10.2.4:80 */ statistic mode random probability 0.50000000000

# KUBE-SEP-K7ALM6KJRBAYOHKX
# 마지막으로  DNAT되는 것을 확인
root@myk8s-control-plane:/# iptables -v --numeric --table nat --list KUBE-SEP-K7ALM6KJRBAYOHKX
Chain KUBE-SEP-K7ALM6KJRBAYOHKX (1 references)
 pkts bytes target     prot opt in     out     source               destination
   17  1020 DNAT       6    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ tcp to:10.10.3.3:80
>>
   18  1080 DNAT       6    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport */ tcp to:10.10.3.3:80


# 나갈 때는 어떻게 나가는지 확인하기 위해 POSTROUTING 을 모니터링으로 확인
# RETURN에 걸려서 룰처리 되는 것을 확인함
root@myk8s-control-plane:/# watch -d 'iptables -v --numeric --table nat --list POSTROUTING | column; echo ; iptables -v --numeric --table nat --list KUBE-POSTROUTING | column'
Every 2.0s: iptables -v --numeric --table nat --list POSTROUTING | column; echo ; iptables -v --numeric --table nat --list...  myk8s-control-plane: Sat Sep 28 22:28:12 2024
...
Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source               destination
  853 51180 RETURN     0    --  *      *       0.0.0.0/0            0.0.0.0/0            mark match ! 0x4000/0x4000
    0     0 MARK       0    --  *      *       0.0.0.0/0            0.0.0.0/0            MARK xor 0x4000
    0     0 MASQUERADE  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ random-fully


# conntrack 명령어로 연결 상태 확인
# 테스트 파드(10.10.0.6)을 소스로 잡고 보면, svc ip(10.200.1.63): dport(9000)으로 호출하고
# 원본 파드(10.10.1.3혹은10.10.2.4): 원래 포트(80)에서 테스트 파드(10.10.0.6)로 연결한 것을 확인 할 수 있다.
root@myk8s-control-plane:/# conntrack -L --src 10.10.0.6
tcp      6 42 TIME_WAIT src=10.10.0.6 dst=10.200.1.63 sport=55702 dport=9000 src=10.10.1.3 dst=10.10.0.6 sport=80 dport=55702 [ASSURED] mark=0 use=1
tcp      6 72 TIME_WAIT src=10.10.0.6 dst=10.200.1.63 sport=42890 dport=9000 src=10.10.2.4 dst=10.10.0.6 sport=80 dport=42890 [ASSURED] mark=0 use=1
conntrack v1.4.7 (conntrack-tools): 2 flow entries have been shown.

clientIP로 분산 처리가 되는데 분산율에 대하여..

# 위에서 확인 한 것처럼 보면 각 ep에 퍼센트가 써있다.
root@myk8s-control-plane:/# iptables -v --numeric --table nat --list KUBE-SVC-KBDEBIL6IU6WL7RF | column
Chain KUBE-SVC-KBDEBIL6IU6WL7RF (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-MARK-MASQ  6    --  *      *      !10.10.0.0/16         10.200.1.63          /* default/svc-clusterip:svc-webport cluster IP */ tcp dpt:9000
   11   660 KUBE-SEP-2WDU4O4C4SDCYWRE  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport -> 10.10.1.3:80 */ statistic mode random probability 0.33333333349
   17  1020 KUBE-SEP-W5ROWLZT7Z7YVFGJ  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport -> 10.10.2.4:80 */ statistic mode random probability 0.50000000000
   17  1020 KUBE-SEP-K7ALM6KJRBAYOHKX  0    --  *      *       0.0.0.0/0            0.0.0.0/0            /* default/svc-clusterip:svc-webport -> 10.10.3.3:80 */

보기 편하게 끝에 컬럼만 떼오면…

  • 1/3 만큼 이곳으로 > default/svc-clusterip:svc-webport -> 10.10.1.3:80 */ statistic mode random probability 0.33333333349
  • 1/2 만큼 이곳으로.. > default/svc-clusterip:svc-webport -> 10.10.2.4:80 */ statistic mode random probability 0.50000000000
  • 하나 남았으니, 이곳에 모두 보내게 된다. > default/svc-clusterip:svc-webport -> 10.10.3.3:80 */ iptables 규칙이 위에서부터 순차적으로 처리 하기 때문에 위와 같이 확률 기반으로 적용된다.
# 실제로 보내보면 비슷한 비율로 호출된다.
(⎈|kind-myk8s:N/A) root@kind:~# kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s 10.200.1.63:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
     34 Hostname: webpod3
     33 Hostname: webpod2
     33 Hostname: webpod1

# Client IP로 세션을 설정하게 되면
(⎈|kind-myk8s:N/A) root@kind:~# kubectl patch svc svc-clusterip -p '{"spec":{"sessionAffinity":"ClientIP"}}'
service/svc-clusterip patched

# 최근에 연결한 pod로만 전송된다.
(⎈|kind-myk8s:N/A) root@kind:~# kubectl exec -it net-pod -- zsh -c "for i in {1..100};  do curl -s 10.200.1.63:9000 | grep Hostname; done | sort | uniq -c | sort -nr"
    100 Hostname: webpod2
    

# iptables 규칙을 확인보면, 모듈에 recent가 추가되어 최근 패킷을 추적할 수 있게 되었다.
# --rsource: source IP에 해당 규칙을 적용
root@myk8s-control-plane:/# iptables -t nat -S | grep recent
-A KUBE-SEP-2WDU4O4C4SDCYWRE -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m recent --set --name KUBE-SEP-2WDU4O4C4SDCYWRE --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.10.1.3:80
-A KUBE-SEP-K7ALM6KJRBAYOHKX -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m recent --set --name KUBE-SEP-K7ALM6KJRBAYOHKX --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.10.3.3:80
-A KUBE-SEP-W5ROWLZT7Z7YVFGJ -p tcp -m comment --comment "default/svc-clusterip:svc-webport" -m recent --set --name KUBE-SEP-W5ROWLZT7Z7YVFGJ --mask 255.255.255.255 --rsource -m tcp -j DNAT --to-destination 10.10.2.4:80
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport -> 10.10.1.3:80" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-2WDU4O4C4SDCYWRE --mask 255.255.255.255 --rsource -j KUBE-SEP-2WDU4O4C4SDCYWRE
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport -> 10.10.2.4:80" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-W5ROWLZT7Z7YVFGJ --mask 255.255.255.255 --rsource -j KUBE-SEP-W5ROWLZT7Z7YVFGJ
-A KUBE-SVC-KBDEBIL6IU6WL7RF -m comment --comment "default/svc-clusterip:svc-webport -> 10.10.3.3:80" -m recent --rcheck --seconds 10800 --reap --name KUBE-SEP-K7ALM6KJRBAYOHKX --mask 255.255.255.255 --rsource -j KUBE-SEP-K7ALM6KJRBAYOHKX

NodePort

외부 통신시, node ip에 svc에서 제공하는 node port를 붙여서 접근한다. (node port 할당범위: 30000~32767) node port 접근 후에는 cluster ip통신과 동일하다.

# 기존 파드와 서비스는 삭제 후 진행하였다.
(⎈|kind-myk8s:N/A) root@kind:~# cat echo-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deploy-echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: deploy-websrv
  template:
    metadata:
      labels:
        app: deploy-websrv
    spec:
      terminationGracePeriodSeconds: 0
      containers:
      - name: kans-websrv
        image: mendhak/http-https-echo
        ports:
        - containerPort: 8080

(⎈|kind-myk8s:N/A) root@kind:~# cat svc-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: svc-nodeport
spec:
  ports:
    - name: svc-webport
      port: 9000        # 내부 접근시 사용 (Cluster IP용)
      targetPort: 8080  # 연결 할 파드 port
  selector:
    app: deploy-websrv
  type: NodePort
  
(⎈|kind-myk8s:N/A) root@kind:~# kubectl apply -f echo-deploy.yaml,svc-nodeport.yaml
deployment.apps/deploy-echo created
service/svc-nodeport created

# 배포된 파드와 서비스 확인
(⎈|kind-myk8s:N/A) root@kind:~# k get po,svc,ep -owide
NAME                               READY   STATUS    RESTARTS   AGE     IP          NODE                  NOMINATED NODE   READINESS GATES
pod/deploy-echo-5c689d5454-mgn4p   1/1     Running   0          84m     10.10.3.5   myk8s-worker3         <none>           <none>
pod/deploy-echo-5c689d5454-w4nb6   1/1     Running   0          84m     10.10.2.8   myk8s-worker2         <none>           <none>
pod/net-pod                        1/1     Running   0          3h13m   10.10.0.6   myk8s-control-plane   <none>           <none>

NAME                   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE   SELECTOR
service/kubernetes     ClusterIP   10.200.1.1     <none>        443/TCP          21h   <none>
service/svc-nodeport   NodePort    10.200.1.217   <none>        9000:31662/TCP   84m   app=deploy-websrv

NAME                     ENDPOINTS                       AGE
endpoints/kubernetes     172.18.0.5:6443                 21h
endpoints/svc-nodeport   10.10.2.8:8080,10.10.3.5:8080   84m


# node ip 확인
(⎈|kind-myk8s:N/A) root@kind:~# k get node -owide
NAME                  STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION       CONTAINER-RUNTIME
myk8s-control-plane   Ready    control-plane   19h   v1.31.0   172.18.0.5    <none>        Debian GNU/Linux 12 (bookworm)   5.15.0-119-generic   containerd://1.7.18
myk8s-worker          Ready    <none>          19h   v1.31.0   172.18.0.2    <none>        Debian GNU/Linux 12 (bookworm)   5.15.0-119-generic   containerd://1.7.18
myk8s-worker2         Ready    <none>          19h   v1.31.0   172.18.0.4    <none>        Debian GNU/Linux 12 (bookworm)   5.15.0-119-generic   containerd://1.7.18
myk8s-worker3         Ready    <none>          19h   v1.31.0   172.18.0.3    <none>        Debian GNU/Linux 12 (bookworm)   5.15.0-119-generic   containerd://1.7.18


# 테스트 컨테이너 배포
(⎈|kind-myk8s:N/A) root@kind:~# docker run -d --rm --name mypc --network kind nicolaka/netshoot sleep infinity
0fa3ba5fe630519214de4ac59414d85ec7805dc5b23b56e27eae484c818d8ba4
(⎈|kind-myk8s:N/A) root@kind:~# docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED          STATUS          PORTS                                                             NAMES
0fa3ba5fe630   nicolaka/netshoot      "sleep infinity"         24 seconds ago   Up 24 seconds                                                                     mypc
...
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it 0f
# clusterIP로는 접근 불가
0fa3ba5fe630  ~  curl -s 10.200.1.217:9000

# nodeIP:nodePort로 호출
(⎈|kind-myk8s:N/A) root@kind:~# docker exec -it 0f zsh
0fa3ba5fe630  ~  curl -s  172.18.0.2:31662 | grep hostname
  "hostname": "172.18.0.2",
    "hostname": "deploy-echo-5c689d5454-w4nb6"
0fa3ba5fe630  ~  curl -s  172.18.0.3:31662 | grep hostname
  "hostname": "172.18.0.3",
    "hostname": "deploy-echo-5c689d5454-mgn4p"
0fa3ba5fe630  ~  curl -s  172.18.0.4:31662 | grep hostname
  "hostname": "172.18.0.4",
    "hostname": "deploy-echo-5c689d5454-mgn4p"
0fa3ba5fe630  ~  curl -s  172.18.0.5:31662 | grep hostname
  "hostname": "172.18.0.5",
    "hostname": "deploy-echo-5c689d5454-w4nb6"

myk8s-worker(172.17.0.2)나 myk8s-control-plane(172.17.0.5)에는 svc에 연결된 pod가 없는데에도 접근이 잘되고, hostname에도 노드 ip가 반환된다.

# worker 노드를 호출하고 worker 노드에서 tcpdump로 확인
0fa3ba5fe630  ~  curl -s  172.18.0.2:31662 | grep hostname
  "hostname": "172.18.0.2",
    "hostname": "deploy-echo-5c689d5454-w4nb6"

# myk8s-worker = w1
# myk8s-worker2 = w2
 
# 테스트 컨테이너(mypc)에서 들어온 패킷 확인
(w1) 00:57:28.215640 IP mypc.kind.40906 > myk8s-worker.31662: Flags [S], seq 2911406569, win 64240, options [mss 1460,sackOK,TS val 470432859 ecr 0,nop,wscale 7], length 0

# w1에서 10.10.2.8(svc와 연결된 pod ip)로 전달
(w2) 00:57:28.215722 IP myk8s-worker.kind.64570 > 10.10.2.8.http-alt: Flags [S], seq 2911406569, win 64240, options [mss 1460,sackOK,TS val 470432859 ecr 0,nop,wscale 7], length 0
(w2) 00:57:28.215736 IP 10.10.2.8.http-alt > myk8s-worker.kind.64570: Flags [S.], seq 2028762515, ack 2911406570, win 65160, options [mss 1460,sackOK,TS val 2654379599 ecr 470432859,nop,wscale 7], length 0

(w1) 00:57:28.215741 IP 10.10.2.8.http-alt > myk8s-worker.64570: Flags [S.], seq 2028762515, ack 2911406570, win 65160, options [mss 1460,sackOK,TS val 2654379599 ecr 470432859,nop,wscale 7], length 0
...
# 확인해보면, w1는 떠있는 파드가 없어서 파드가 있는 w2에 가서 응답을 가져온 것으로 보인다.

iptables를 따라가보자

root@myk8s-worker:/# iptables -t nat -S | grep PREROUTING
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
...

# node port를 타고 오기 때문에, 아래의 규칙을 따라간다.
root@myk8s-worker:/# iptables -t nat -S | grep KUBE-SERVICES
...
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS

# node port인 31662를 따라..
root@myk8s-worker:/# iptables -t nat -S | grep KUBE-NODEPORTS
...
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/svc-nodeport:svc-webport" -m tcp --dport 31662 -j KUBE-EXT-VTR7MTHHNMFZ3OFS

# source ip를 node ip로 변경한다
# (w2) 00:57:28.215722 에서, 이렇게 변환되어서 w2의 pod에게 요청이 간 것
root@myk8s-worker:/# iptables -t nat -S | grep KUBE-EXT-VTR7MTHHNMFZ3OFS
...
-A KUBE-EXT-VTR7MTHHNMFZ3OFS -m comment --comment "masquerade traffic for default/svc-nodeport:svc-webport external destinations" -j KUBE-MARK-MASQ

# 이렇게 변환이 된 후로는 node 에서 요청이 가는 것으로 보여, clusterIP로 통신 할 때와 동일하게 규칙을 타게 된다.