본문 바로가기
CNCF/Kubernetes

ExternalTrafficPolicy 설정을 위한 kubernetes service 분석

물리적인 로드밸런서 혹은 Metallb와 같은 Kubernetes 에서 논리/물리적인 로드밸런싱이 상단에서 이뤄진다면, 로드밸런싱이 2번 이상 될테고 그럴경우 다양한 문제를 발생 할 수 있기에 service 의 트래픽 flow 를 확인 하고 대처 하는 방안데 대하여 이야기 하고자 한다.

해당 kubernetes 버전은 1.19를 kubespray 로 배포되어 있는 상태이다.

테스트 환경은 다수의 worker 노드들 중에서 test-node=nginx 인 worker 노드들을 대상으로 진행 된다. 해당 kubernetes cluster의 cni는 calico를 ip-in-ip 터널링이 always 설정되어 있는 구조 이다.

root@cy01-ceph120:~# kubectl  get nodes -L test-node
NAME           STATUS   ROLES    AGE     VERSION   TEST-NODE
cy01-ceph141   Ready    master   4d18h   v1.19.7   nginx
cy01-ceph142   Ready    master   4d18h   v1.19.7   nginx
cy01-ceph143   Ready    master   4d18h   v1.19.7   nginx
cy01-ceph144   Ready    <none>   4d18h   v1.19.7
cy01-ceph145   Ready    <none>   4d18h   v1.19.7
cy01-ceph146   Ready    <none>   4d18h   v1.19.7
cy01-ceph147   Ready    <none>   4d18h   v1.19.7
cy01-ceph148   Ready    <none>   4d18h   v1.19.7
cy01-ceph149   Ready    <none>   4d18h   v1.19.7

위 label이 설정된 노드들에 nginx container 가 동작하는 pod와 meltalLB로 구성되어 있는 LoadBalancer 서비스를 생성 한다.

참고로 해당 label이 설정된 노드들에는 metallb speaker 가 동작중이며,  외부에서 External IP 로 들어오는 패킷들이 해당 노드로 먼저 유입 된다.

# vi test.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: ng-test-rs
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: con-test
  template:
    metadata:
      labels:
        app: con-test
    spec:
      nodeSelector:
        test-node: nginx
      containers:
        - image: 'cy01-ceph120:5000/stenote/nginx-hostname:latest'
          name: ng-con
---
apiVersion: v1
kind: Service
metadata:
  name: ng-svc
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: con-test
  type: LoadBalancer
# kubectl  create -f test.yaml
 
# kubectl  get svc ng-svc
NAME     TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)        AGE
ng-svc   LoadBalancer   10.233.36.144   172.19.100.2   80:31778/TCP   16s
 
# kubectl  get pod  -o wide
NAME               READY   STATUS    RESTARTS   AGE   IP              NODE           NOMINATED NODE   READINESS GATES
ng-test-rs-cwqr5   1/1     Running   0          4s    10.233.98.39    cy01-ceph142   <none>           <none>
ng-test-rs-djnj7   1/1     Running   0          4s    10.233.110.33   cy01-ceph141   <none>           <none>
ng-test-rs-kllg4   1/1     Running   0          4s    10.233.125.24   cy01-ceph143   <none>           <none>

당연한 결과이겠지만, 외부에서 ExternalIP 와 노드에서 pod 네트워크가 가능하면 ClusterIP로 각각 라운드로빈되어 각각의 pod로 응답이 오는것을 알 수 있으며 배포된 pod 와 service는 아래 그림과 같은 구조를 갖는다.

# curl 172.19.100.2
ng-test-rs-kllg4
# curl 172.19.100.2
ng-test-rs-djnj7
# curl 172.19.100.2
ng-test-rs-cwqr5
# curl 10.233.39.58
ng-test-rs-cwqr5
# curl 10.233.39.58
ng-test-rs-djnj7
# curl 10.233.39.58
ng-test-rs-kllg4

배포된 pod 의 container 에서 ip 가 eth0@if15에 10.233.110.31 아이피로 해당 노드에서 사용가능한 pod network 의 대역대로 ip 가 할당 되어 있으며, 해당 container가 동작중인 노드의 calicaca1cdbf39 인터페이스와 veth로 연결 중인것을 알 수 있다.

# kubectl  exec -it ng-test-rs-jkbkn  -- ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN 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 qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
4: eth0@if15: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue state UP
    link/ether 7a:f9:73:04:1e:4a brd ff:ff:ff:ff:ff:ff
    inet 10.233.110.33/32 brd 10.233.110.31 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::78f9:73ff:fe04:1e4a/64 scope link
       valid_lft forever preferred_lft forever
...#  해당 호스트에서 실행
# 15: calicaca1cdbf39@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netns cni-cf0d3428-1998-fb98-3d9d-0b99ee9ae350
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever

위, container network 는 다른 노드들에 있는 container들도 해당노드에 할당된 container network 대역만 다를뿐 동일하게 구성 된다.

외/내부에서는 실제로 service를 통하여 요청을 보내게 되는데, Cluster IP 와 Loadbalancer IP 모두 kube-proxy의 ipvs 를 통하여 로드밸런싱 된다.

# ipvsadm -ln | grep 10.233.36.144 -A 3
TCP  10.233.36.144:80 rr
  -> 10.233.98.39:80              Masq    1      0          0
  -> 10.233.110.33:80             Masq    1      0          0
  -> 10.233.125.24:80             Masq    1      0          0
...
# ipvsadm -ln | grep  172.19.100.2 -A 3
TCP  172.19.100.2:80 rr
  -> 10.233.98.39:80              Masq    1      0          0
  -> 10.233.110.33:80             Masq    1      0          0
  -> 10.233.125.24:80             Masq    1      0          0

여기서 바인된 VIP 는  kube-ipvs 라는 dummy interface로 아이피가 할당 되어 ipvs 정책에 있는 real ip 즉 container에 실제 할당된  아이피로 로드밸런싱 된다.

...
7: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
    link/ether 0e:da:12:c6:f6:cd brd ff:ff:ff:ff:ff:ff
    inet 10.233.35.114/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.33.184/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.36.77/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 172.19.100.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.0.3/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.59.206/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.0.1/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.47.127/32 scope global kube-ipvs0
       valid_lft forever preferred_lft forever
    inet 10.233.36.144/32 scope global kube-ipvs0  #<---  Cluster IP
       valid_lft forever preferred_lft forever
    inet 172.19.100.2/32 scope global kube-ipvs0  #<---  Loadbalancer IP
       valid_lft forever preferred_lft forever

이제 앞서서 service, 더정확하게 하면 kube-proxy 의 ipvs로 로드밸런싱 되는 과정을 확인 했는데, 여기서 source ip를 확인해보자.

container 에서 확인되는 로그를 보면 10.10.1.142(worker노드의 아이피)  아이피와 10.233.98.0(worker노드의 cluster ip 대역)  아이피로 출발지 아이피가 확인이 된다. 

일반적으로 외부에서 요청시 해당 노드의 전 홉(hop)인 L3의 아이피가 남아야 한다고 생각 하지만 예상과 다른 결과 이다.

### 외부에서 Loadbalancer VIP 로 요청
# curl 172.19.100.2
# curl 172.19.100.2
# curl 172.19.100.2
...
 
 
# kubectl  logs -l app=con-test -f
10.10.1.142 - - [23/Mar/2021:02:36:57 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
2021/03/23 02:36:57 [info] 7#7: *2 client 10.10.1.142 closed keepalive connection
10.233.98.0 - - [23/Mar/2021:02:37:02 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
2021/03/23 02:37:02 [info] 8#8: *2 client 10.233.98.0 closed keepalive connection
10.233.98.0 - - [23/Mar/2021:02:37:03 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
2021/03/23 02:37:03 [info] 7#7: *2 client 10.233.98.0 closed keepalive connection
10.10.1.142 - - [23/Mar/2021:02:37:09 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
2021/03/23 02:37:09 [info] 7#7: *3 client 10.10.1.142 closed keepalive connection

상세하게 살피기 위하여 다시 한번 요청을 보낸다.  External IP 요청을 보내는 142노드의 pod 로 응답이 왔으며 access log 상으로도 10.10.6.91 이라는 상단 l3 장비의 아이피를 출력 한것을 볼수 있다.

즉, service 에 들어온 패킷과 pod 가 같은 노드일 경우는 해당 노드의 IP를 출력하는것으로 확인 된다.

# curl 172.19.100.2
ng-test-rs-cwqr5
 
# kubectl  get pod ng-test-rs-cwqr5 -o wide
NAME               READY   STATUS    RESTARTS   AGE   IP             NODE           NOMINATED NODE   READINESS GATES
ng-test-rs-cwqr5   1/1     Running   0          10m   10.233.98.39   cy01-ceph142   <none>           <none>
 
142:~# tcpdump  -i ens4 tcp port 80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens4, link-type EN10MB (Ethernet), capture size 262144 bytes
02:45:43.146731 IP 10.10.6.91.41068 > 172.19.100.2.http: Flags [S], seq 2395335491, win 64240, options [mss 1460,sackOK,TS val 309382349 ecr 0,nop,wscale 7], length 0
02:45:43.147087 IP 172.19.100.2.http > 10.10.6.91.41068: Flags [S.], seq 487118850, ack 2395335492, win 64308, options [mss 1410,sackOK,TS val 3954271762 ecr 309382349,nop,wscale 7], length 0
02:45:43.200911 IP 10.10.6.91.41068 > 172.19.100.2.http: Flags [.], ack 1, win 502, options [nop,nop,TS val 309382396 ecr 3954271762], length 0
02:45:43.200911 IP 10.10.6.91.41068 > 172.19.100.2.http: Flags [P.], seq 1:77, ack 1, win 502, options [nop,nop,TS val 309382397 ecr 3954271762], length 76: HTTP: GET / HTTP/1.1
02:45:43.201410 IP 172.19.100.2.http > 10.10.6.91.41068: Flags [.], ack 77, win 502, options [nop,nop,TS val 3954271817 ecr 309382397], length 0
 
# kubectl  logs -l app=con-test -f
10.10.1.142 - - [23/Mar/2021:02:43:47 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"

이번에 외부에서는 External IP 요청을 보내는 143노드의 pod 로 응답이 왔으며, Tcpdump 를 확인 하면 142 로 먼저 들어 와서 142노드의 pod network 를 사용 하기 위한  tunl0인터페이스의 아이피인  10.239.98.0 출발지 아이피로 accesslog 가 확인 된다. 즉, service 로 들어온 패킷이 ipvs 로 라운드로빈 되어 다른 노드의 container로 포워딩 할 경우 출발지 아이피가 변경된  tunl0인터페이스 아이피로 snat 가 된것을 알 수 있다.

# curl 172.19.100.2
ng-test-rs-kllg4
 
# kubectl  get pod ng-test-rs-kllg4 -o wide
NAME               READY   STATUS    RESTARTS   AGE     IP              NODE           NOMINATED NODE   READINESS GATES
ng-test-rs-kllg4   1/1     Running   0          8m47s   10.233.125.24   cy01-ceph143   <none>           <none>
 
 
142:~# tcpdump  -i ens4 tcp port 80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens4, link-type EN10MB (Ethernet), capture size 262144 bytes
02:45:43.146731 IP 10.10.6.91.41068 > 172.19.100.2.http: Flags [S], seq 2395335491, win 64240, options [mss 1460,sackOK,TS val 309382349 ecr 0,nop,wscale 7], length 0
02:45:43.147087 IP 172.19.100.2.http > 10.10.6.91.41068: Flags [S.], seq 487118850, ack 2395335492, win 64308, options [mss 1410,sackOK,TS val 3954271762 ecr 309382349,nop,wscale 7], length 0
02:45:43.200911 IP 10.10.6.91.41068 > 172.19.100.2.http: Flags [.], ack 1, win 502, options [nop,nop,TS val 309382396 ecr 3954271762], length 0
02:45:43.200911 IP 10.10.6.91.41068 > 172.19.100.2.http: Flags [P.], seq 1:77, ack 1, win 502, options [nop,nop,TS val 309382397 ecr 3954271762], length 76: HTTP: GET / HTTP/1.1
02:45:43.201410 IP 172.19.100.2.http > 10.10.6.91.41068: Flags [.], ack 77, win 502, options [nop,nop,TS val 3954271817 ecr 309382397], length 0
# kubectl  logs -l app=con-test -f
10.233.98.0 - - [23/Mar/2021:02:41:49 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
 
# route -n  | grep 10.233.98
10.233.98.0     10.10.4.142     255.255.255.0   UG    0      0        0 tunl0

좀더 자세히 보면 처음 client 가 VIP 인 172.16.100.2 를 도착지로 요청을 보내 worker 노드들중 하나인 worker2의 인터페이스로 들어 오게 된다.

만약 같은 노드의 pod로 dnat 가 ipvs 에 의하여 된다면 아래와 같이 될것이다. 하지만 위에서 테스트한것과 같이 다른 노드로 들어오는 패킷에 대하여 snat 되는 것을 확인 했다.

snat 가 진행 된다면 해당 노드에서 iptables 로 확인 해볼 필요가 있다. 우선 nat 테이블상에서 처음 들어오는 PREROUTING 체인에서는 모든 트래픽을  KUBE-SEVICE를  taget 으로 설정된것을 알수 있다.

root@cy01-ceph142:~# iptables -nvL -t nat
Chain PREROUTING (policy ACCEPT 16 packets, 960 bytes)
 pkts bytes target     prot opt in     out     source               destination
 302K   18M KUBE-SERVICES  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */

이어서 KUBE-SERVICE 체인에서는 가장 먼저 KUBE-LOAD-BALANCER 라는 target으로 match-set이 되어 있다. 이런경우 ipset에 해당 패킷을 전달하는데 ipset 은 netfilter 하위 시스템의 iptables 부분과 같으며 IP 또는 네트워크 목록 생성을 지원해준다.  주로 iptables 의 정책이 많을경우 ipset 이라는 곳에 ip정보등을 저장 해두고 불러와서 matching 시키는 역할로 사용 된다.

root@cy01-ceph142:~# iptables -nvL -t nat
...
Chain KUBE-SERVICES (2 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-LOAD-BALANCER  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* Kubernetes service lb portal */ match-set KUBE-LOAD-BALANCER dst,dst
    0     0 KUBE-MARK-MASQ  all  --  *      *      !10.233.64.0/18       0.0.0.0/0            /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst
  113  6780 KUBE-NODE-PORT  all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL
    0     0 ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set KUBE-CLUSTER-IP dst,dst
    0     0 ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set KUBE-LOAD-BALANCER dst,dst

여기서는 KUBE-LOAD-BALANCER  라는 ipset 에 있는 meber 정보가 맞을 경우  KUBE-LOAD-BALANCER   체인으로 넘긴다.

root@cy01-ceph142:~# ipset  -L   KUBE-LOAD-BALANCER
Name: KUBE-LOAD-BALANCER
Type: hash:ip,port
Revision: 5
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 384
References: 2
Number of entries: 3
Members:
172.19.100.2,tcp:80
...

UBE-LOAD-BALANCER 체인에서는 KUBE-LOAD-BALANCER-LOCAL 의 ipset 정보와 확인 externalTrafficPolicy=local 설정일 경우 해당 노드의 pod 로 전달 하기 위한  룰 이 있다.

기본적으로 생성된 service 에는externalTrafficPolicy=cluster 설정이라서   KUBE-LOAD-BALANCER-LOCAL 의 정보가 없어서 다음 정책인  KUBE-MARK-MASQ 체인으로 전달한다. 

root@cy01-ceph142:~# iptables -nv  -t nat -L KUBE-LOAD-BALANCER
Chain KUBE-LOAD-BALANCER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* Kubernetes service load balancer ip + port with externalTrafficPolicy=local */ match-set KUBE-LOAD-BALANCER-LOCAL dst,dst
    0     0 KUBE-MARK-MASQ  all  --  *      *       0.0.0.0/0            0.0.0.0/0
 
root@cy01-ceph142:~# ipset  -L   KUBE-LOAD-BALANCER-LOCAL
Name: KUBE-LOAD-BALANCER-LOCAL
Type: hash:ip,port
Revision: 5
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 320
References: 1
Number of entries: 2
Members:
172.19.100.1,tcp:443
172.19.100.1,tcp:80
...

이제 해당 패킷들은 KUBE-MARK-MASQ 체인에서 나중에 마스쿼레이드를 하기 위한 패킷으로 MARK 한다.

root@cy01-ceph142:~#  iptables -nv  -t nat -L KUBE-MARK-MASQ
Chain KUBE-MARK-MASQ (3 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK or 0x4000

KUBE-MARK-MASQ 체인에서 나온 패킷은 ipvs 에서 라운드로빈된 아이피로 DNAT되어 나가기 위해서 POSTROUTING으로 들어 오는데 해당 체인은 KUBE-POSTROUTING 체인으로 전달 한다.

Chain POSTROUTING (policy ACCEPT 67 packets, 3750 bytes)
 pkts bytes target     prot opt in     out     source               destination
1791K  100M KUBE-POSTROUTING  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */

KUBE-POSTROUTING 체인에서는 가장 먼저  KUBE-LOOP-BACK 에 있는 ip set 정보 와 동일 할 경우 Masquerade 시킨다.

KUBE-LOOP-BACK IPset에 Packet의 출발지,도착지의 아이피와 동일한 pod의 아이피일 수 있는 모든 경우가 포함 되어 해당 내용이 맞다면  Masquerade 시킨다. 해당 패킷은 해당되지 않기에 다음 룰로 넘어 간다.

다음 룰에는 0x4000 mark 가 없는 경우 해당 패킷을 Return 하고 다음 룰에서 0x4000 mark 를 해제한다.

해당 패킷을 이미 KUBE-MARK-MASQ 체인에서 Mark 되어있기 때문에 Mark 를 해제 한다.

그리고 KUBE-LOOP-BACK 에 있는 ip set 정보와 맞지 않는 나머지 패킷에 대하여  random-fully옵션을 추가 하여 Masquerade 한다.

여기서 이렇게 강제로 snat를 시켜서 패킷이 다시 응답할때 자신의 호스트를 통해서 응답할수 있도록 하는 기법을 Hairpin 기법이라고 불린다. 이렇게 하지 않으면 nat 뒤에 있는 호스트는 실제 client 와 통신 할 수 없기 때문에 해당 기법이 들어 간것이다.

지금 들어온 패킷은 다른 노드에 있는 container ip를 도착지로 하기 때문에 마지막 룰을 통하여 0x4000 mark 해제 되어 Masquerade 된다.  해당 노드에서는 다른 노드를 가기 위하여  tunl0 인터페이스를 통하여 나가는것을 알수 있으며, 해당 노드에 할당된 tunl0 의 아이피인 10.233.98.0 아이피로 snat 되어 다른 노드에 있는 container 로 패킷이 전달 된다.

root@cy01-ceph142:~# iptables -nv -t nat -L  KUBE-POSTROUTING
Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source               destination
    1    60 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* Kubernetes endpoints dst ip:port, source ip for solving hairpin purpose */ match-set KUBE-LOOP-BACK dst,dst,src
   32  1785 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            mark match ! 0x4000/0x4000
    0     0 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK xor 0x4000
    0     0 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ random-fully
 
root@cy01-ceph142:~# ipset -L KUBE-LOOP-BACK
Name: KUBE-LOOP-BACK
Type: hash:ip,port,ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 760
References: 1
Number of entries: 7
Members:
10.233.98.36,tcp:8443,10.233.98.36
10.233.98.39,tcp:80,10.233.98.39
10.10.2.142,tcp:80,10.10.2.142
10.10.2.142,tcp:8443,10.10.2.142
10.233.98.36,tcp:443,10.233.98.36
10.233.98.36,tcp:80,10.233.98.36
10.10.2.142,tcp:9283,10.10.2.142
 
 
# ip route | grep 10.233.110
10.233.110.0/24 via 10.10.4.141 dev tunl0 proto bird onlink
 
 
# ip a
8: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 10.233.98.0/32 brd 10.233.98.0 scope global tunl0
       valid_lft forever preferred_lft forever

다시 외부 client 로 응답 보낼때는 container 에서는 출발지 아이피를 자신의 container ip로 하고, 도착지 아이피를 worker2 의 tunl0인터페이스 아이피로 전달 하게 된다. worker2에서는snat,dnat 된 것을 다시 반복 하여 client로 통신 하게 되는 구조를 갖는다.

만약 자기 자신의 노드에 있는 container로 가는 경우는 동일하게 SNAT되어 패킷이 해당 노드의 Container 로 전달 되지만 Masquerade 되는 인터페이스가 해당 노드의 tunl0 인터페이스가 아닌 로컬의 아이피로 snat 되어 Container 로 전달 되는 것이다.

이때 KUBE-LOAD-BALANCER체인으로 다시 들어온 패킷은 Cluster IP가 아니기 때문에 Mark 하지 않고 INPUT 체인을 거쳐서 container 로 가게 된다.

 

위 환경은 단순 metallb bgp 모드를 이용하여 로드밸런싱 된 vip를 외부에서 실제 container 까지 service를 통하여 통신 하는 패킷 플로우를 확인 하였다.  이러한 구조의 장점은 모든 노드를 균등 하게 사용 한다는 장점이 있지만 단점이 존재 한다.

가장 큰 문제가 패킷의 라우팅 되는 hop이 추가되어 부하가 상승 할것이며, metallb 또는 물리적인 LB를 사용하는 경우도 비슷하겠지만 2번 로드밸런싱 되는 단점이 있다. 또한, 네트워크의 복잡도가 상승 하여 트러블 슈팅 하는데 시간이 오래 걸릴수 있다.

그래서 사용할수 있는 service 의 옵션이 service.spec.externalTrafficPolic 옵션이다. 기본적으로service.spec.externalTrafficPolic는 기본적으로  cluster 로설정 되어 외부 트래픽에 대한 정책을 해당 cluster 의 endpoint 까지로 트래픽을 전달 하지만 local로 설정할 경우 오직 로컬 엔드포인트로만 프록시 요청하고 다른 노드로 트래픽 전달하지 않는다.

즉 이럴경우 snat가 없이 container에 패킷이 전달 하게 된다.

해당 설정을 현재 service 에 적용을 해본다.

# kubectl patch svc ng-svc  -p '{"spec":{"externalTrafficPolicy":"Local"}}'
service/ng-svc patched

다시 외부에서 요청을 하게 되면 응답은 동일하게 라운드로빈 되어 응답 했지만 container log를 확인 하면 상단 l3장비의 인터페이스 아이피로 source ip 가 나오는것으로 확인할 수 있다.

root@cy01-openstack91:~# curl 172.19.100.2
ng-test-rs-cwqr5
root@cy01-openstack91:~# curl 172.19.100.2
ng-test-rs-cwqr5
root@cy01-openstack91:~# curl 172.19.100.2
ng-test-rs-cwqr5
...
10.10.6.91 - - [23/Mar/2021:11:58:48 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
2021/03/23 11:58:48 [info] 7#7: *79 client 10.10.6.91 closed keepalive connection
10.10.6.91 - - [23/Mar/2021:11:58:48 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
2021/03/23 11:58:48 [info] 7#7: *80 client 10.10.6.91 closed keepalive connection
10.10.6.91 - - [23/Mar/2021:11:58:49 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
2021/03/23 11:58:49 [info] 7#7: *81 client 10.10.6.91 closed keepalive connection
10.10.6.91 - - [23/Mar/2021:11:58:49 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
2021/03/23 11:58:50 [info] 7#7: *82 client 10.10.6.91 closed keepalive connection
10.10.6.91 - - [23/Mar/2021:11:58:50 +0000] "GET / HTTP/1.1" 200 28 "-" "curl/7.68.0"
2021/03/23 11:58:50 [info] 7#7: *83 client 10.10.6.91 closed keepalive connection

iptables nat 테이블을 확인 하면 앞서 cluster 로 설정한거과 동일하게 우선 PREROUTING -> KUBE-SERVICES -> KUBE-LOAD-BALANCER 의 체인까지는 동일하게 패킷이 들어 간다.

root@cy01-ceph142:~# iptables -nvL -t nat
Chain PREROUTING (policy ACCEPT 16 packets, 960 bytes)
 pkts bytes target     prot opt in     out     source               destination
 302K   18M KUBE-SERVICES  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */
 
 
root@cy01-ceph142:~# iptables -nv -t nat -L KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 KUBE-LOAD-BALANCER  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* Kubernetes service lb portal */ match-set KUBE-LOAD-BALANCER dst,dst
    0     0 KUBE-MARK-MASQ  all  --  *      *      !10.233.64.0/18       0.0.0.0/0            /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst
   14   840 KUBE-NODE-PORT  all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL
    0     0 ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set KUBE-CLUSTER-IP dst,dst
    0     0 ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            match-set KUBE-LOAD-BALANCER dst,dst
 
 
 
root@cy01-ceph142:~# iptables -nv -t nat -L KUBE-LOAD-BALANCER
Chain KUBE-LOAD-BALANCER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* Kubernetes service load balancer ip + port with externalTrafficPolicy=local */ match-set KUBE-LOAD-BALANCER-LOCAL dst,dst
    0     0 KUBE-MARK-MASQ  all  --  *      *       0.0.0.0/0            0.0.0.0/0

KUBE-LOAD-BALANCER 체인에서 한가지 룰을 확인 해야 하는데 첫번째 룰에서 KUBE-LOAD-BALANCER-LOCAL의 ipset 의 정보와 동일할 경우 해당 패킷을 Return 한다. 일반적으로 cluster 설정으로 되어 있을 경우 해당 룰에서 Return이 안되고 KUBE-MARK-MASQ 체인으로 넘기지만 ipset 룰에 service.spec.externalTrafficPolicy 를 local 로 한 해당 서비스의 vip 정보가 추가 되었기때문에 Return 되어 KUBE-MARK-MASQ  체인으로 안가고  Return 된다.

root@cy01-ceph142:~# iptables -nv -t nat -L KUBE-LOAD-BALANCER
Chain KUBE-LOAD-BALANCER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* Kubernetes service load balancer ip + port with externalTrafficPolicy=local */ match-set KUBE-LOAD-BALANCER-LOCAL dst,dst
    0     0 KUBE-MARK-MASQ  all  --  *      *       0.0.0.0/0            0.0.0.0/0
root@cy01-ceph142:~# ipset  -L KUBE-LOAD-BALANCER-LOCAL
Name: KUBE-LOAD-BALANCER-LOCAL
Type: hash:ip,port
Revision: 5
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 384
References: 1
Number of entries: 3
Members:
172.19.100.2,tcp:80
....

 KUBE-MARK-MASQ 체인에서 나중에 마스쿼레이드를 하기 위한 패킷으로 MARK하는데 Mark 를 하지 않은상태로 패킷이 전달 된다는 의미이다.

root@cy01-ceph142:~#  iptables -nv  -t nat -L KUBE-MARK-MASQ
Chain KUBE-MARK-MASQ (3 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK or 0x4000

POSTROUTING ->KUBE-POSTROUTING 체인으로 이동된 패킷은 1번째 라인의 KUBE-LOOP-BACK 의 ipset 정보와 동일 하면 Masquerade 한다. 해당 패킷에서는 해당 룰에는 적용되지 않고, 두번째 룰인 mark가 되지 않는 패킷에 대한 Return 정책에 걸리게 되어 Masquerade 되지 않은 상태로 패킷이 전달 되는 구조를 갖는다.

Chain POSTROUTING (policy ACCEPT 67 packets, 3750 bytes)
 pkts bytes target     prot opt in     out     source               destination
1791K  100M KUBE-POSTROUTING  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes postrouting rules */
root@cy01-ceph142:~# iptables -nv -t nat -L  KUBE-POSTROUTING
Chain KUBE-POSTROUTING (1 references)
 pkts bytes target     prot opt in     out     source               destination
    1    60 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* Kubernetes endpoints dst ip:port, source ip for solving hairpin purpose */ match-set KUBE-LOOP-BACK dst,dst,src
   32  1785 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            mark match ! 0x4000/0x4000
    0     0 MARK       all  --  *      *       0.0.0.0/0            0.0.0.0/0            MARK xor 0x4000
    0     0 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes service traffic requiring SNAT */ random-fully

해당 패킷은 return 되어 host 의 network namepce를 거쳐 다시 위와 동일 하게 다시 prerouting 체인을 거쳐서 container 로 직접 들어가는 구조가 된다.

 

참고사이트

https://www.qikqiak.com/post/how-to-use-ipvs-in-kubernetes/

https://kubernetes.io/ko/docs/tutorials/services/source-ip/

https://www.zevenet.com/wp-content/uploads/2019/10/Zevenet-Kube-proxy-deep-dive-Kubernetes-Sevilla-Octubre-2019.pdf

https://ssup2.github.io/

 

반응형

'CNCF > Kubernetes' 카테고리의 다른 글

MetalLB BGP 모드를 이용한 Loadbalaning  (2) 2021.03.14
Antrea Overview  (0) 2020.07.30
Kubernetes1.17 Hard Way (Libvirt) using docker,flannel  (0) 2020.03.22