Cloud/Kubernetes

Pod 라우팅과 ExternalTrafficPolicy 옵션

Taemy 2020. 11. 23. 17:09

 Pod를 expose할 때 Kubernetes의 Service를 통해 다른 Pod혹은 외부에서 접근할 수 있도록 설정할 수 있다. 더 정확히는 Service를 생성하면 해당하는 Pod들에 대한 endpoint가 자동으로 생성되어, connection이 가능해진다. 물론 없어도 연결이 아예 불가능한 것은 아니지만 쿠버네티스적 관점에서는 Service가 필요하다.

 

 

노드 외부에서의 연결이 들어온 상태이고 Service를 NodePort나 LB타입으로 설정한 경우는 어떨까?

 

 

 

기본 설정에서는 Service에 여러 개의 Pod가 연결되어 있다면 쿠버네티스의 kube-proxy에 의해 iptables가 설정될 것이고, 패킷은 iptables에 따라 랜덤하게 전달될 것이다.

 

 

 

하지만 만약 패킷이 특정 노드에 전달된 경우 해당 노드 외에도 다른 노드에 파드가 있다면, 그 파드에도 패킷이 전달될 여지가 있다.

 

특정 Node로 들어온 패킷 전달

 

 

이런 경우 도착한 노드에 있는 파드로만 패킷을 전달하고 싶을 때에는 어떻게 해야할까?

이를 위해서 externalTrafficPolicy 라는 항목이 있다.

 

테스트

사전 준비

아래와 같이 replica 2개 짜리 deployment를 하나 생성하고, 서로 다른 노드에 올라가는 것을 확인한다.

(외부에서 curl 응답되는 것을 보기 위해 luksa 성님의 kubia 이미지를 사용했다)

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    run: test
  name: test
spec:
  replicas: 2
  selector:
    matchLabels:
      run: test
  template:
    metadata:
      labels:
        run: test
    spec:
      nodeSelector:
        app: test
      containers:
      - image: luksa/kubia
        name: test
        ports:
        - containerPort: 8080

 

두 개의 노드(worker0, worker1)에 각각 pod가 배포되었다.

[host ~]# kubectl get po -n default -o wide
NAME                    READY   STATUS    RESTARTS   AGE    IP             NODE                     NOMINATED NODE   READINESS GATES
test-86d7ff54f9-5kbwl   1/1     Running   0          5m5s   10.128.4.194   worker0                 <none>           <none>
test-86d7ff54f9-dt8ng   1/1     Running   0          15m    10.131.2.22    worker1                 <none>           <none>

 

worker0, worker1의 정보는 아래와 같다.

worker0가 ip 172.20.22.238을 가진다. 이 테스트에서는 이 ip로 curl을 보낼 예정이다.

[host ~]# kubectl get node -o wide
NAME                     STATUS   ROLES    AGE    VERSION           INTERNAL-IP     EXTERNAL-IP
worker0                  Ready    upf      152d   v1.18.3+2cf11e2   172.20.22.238   <none>        
worker1                   Ready    upf      153d   v1.18.3+2cf11e2   172.20.22.239   <none>    

이제 외부에서 expose할 수 있도록 nodePort를 가진 서비스를 생성한다.

동일 라벨을 사용하면 차후 라벨 제거 시 replica가 추가 생성되기 때문에, 라벨 셀렉터를 다른 것으로 변경하고 수동으로 replica에 라벨을 넣어줄 것이다.

 

[host ~]# kubectl expose deploy/test -n default --type NodePort --port 8080 --dry-run -o yaml > service.yaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    expose: test # Changed
  name: test
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    expose: test # Changed
  type: NodePort
status:
  loadBalancer: {}

 

Service 배포 전 아직 아무런 endpoint가 없는 상태이다.

[host ~]# kubectl get svc,po,ep -o wide
NAME                 TYPE           CLUSTER-IP   EXTERNAL-IP                            PORT(S)   AGE    SELECTOR
service/kubernetes   ClusterIP      172.30.0.1   <none>                                 443/TCP   160d   <none>

NAME                        READY   STATUS    RESTARTS   AGE     IP             NODE                     NOMINATED NODE   READINESS GATES
pod/test-86d7ff54f9-5kbwl   1/1     Running   0          5m32s   10.128.4.194   worker0   <none>           <none>
pod/test-86d7ff54f9-dt8ng   1/1     Running   0          15m     10.131.2.22    worker1   <none>           <none>

NAME                   ENDPOINTS                                            AGE
endpoints/kubernetes   ...                                                  160d

 

아래와 같이 라벨을 추가하고 Service를 Deploy하면 endpoint가 생성된 것을 볼 수 있다.

[host ~]# kubectl label po test-86d7ff54f9-5kbwl expose=test
pod/test-86d7ff54f9-5kbwl labeled
[host ~]# kubectl label po test-86d7ff54f9-dt8ng expose=test
pod/test-86d7ff54f9-dt8ng labeled
[host ~]# kubectl get po --show-labels
NAME                    READY   STATUS    RESTARTS   AGE   LABELS
test-86d7ff54f9-5kbwl   1/1     Running   0          11m   expose=test,pod-template-hash=86d7ff54f9,run=test
test-86d7ff54f9-dt8ng   1/1     Running   0          21m   expose=test,pod-template-hash=86d7ff54f9,run=test

 

 딱히 NodePort값을 특정하지 않았기 때문에 랜덤하게 포트값 35542를 받았다. 이제 worker0 ip:35542로 전달되는 패킷들은 test pod로 전달될 것이다.

 

curl 로 메시지 보내기

앞에서 말한 것처럼 worker0(172.20.22.238)로 curl을 보낸다.

[host ~]# curl -k 172.20.22.238:35542
You've hit test-86d7ff54f9-5kbwl
[host ~]# curl -k 172.20.22.238:35542
You've hit test-86d7ff54f9-dt8ng
[host ~]# curl -k 172.20.22.238:35542
You've hit test-86d7ff54f9-5kbwl
[host ~]# curl -k 172.20.22.238:35542
You've hit test-86d7ff54f9-dt8ng

 

보는 것과 같이 두 pod 모드에 패킷이 도착하고 있다.

처음 Service에 대해 배울 때 나오는 내용처럼, iptables에 의해 패킷이 공평하게 분배되는 것을 확인할 수 있다.

 

만약 worker0에 있는 Pod가 죽으면 어떻게 될까?

유사 파드 Missing 상태를 만들어내보자. 이번엔 아래와 같이 worker0위에 있는 Pod에 아까 붙였던 라벨을 제거한다.

[host ~]# kubectl label pod test-86d7ff54f9-5kbwl expose= --overwrite
pod/test-86d7ff54f9-5kbwl labeled

아래와 같이 endpoint가 삭제된다.

[host ~]# kubectl get svc,po,ep -o wide
NAME                 TYPE           CLUSTER-IP     EXTERNAL-IP                            PORT(S)          AGE     SELECTOR
service/kubernetes   ClusterIP      172.30.0.1     <none>                                 443/TCP          160d    <none>
service/test         NodePort       172.30.69.12   <none>                                 8080:35542/TCP   2m24s   expose=test

NAME                        READY   STATUS    RESTARTS   AGE   IP             NODE                     NOMINATED NODE   READINESS GATES
pod/test-86d7ff54f9-5kbwl   1/1     Running   0          16m   10.128.4.194   worker0   <none>           <none>
pod/test-86d7ff54f9-dt8ng   1/1     Running   0          26m   10.131.2.22    worker1   <none>           <none>

NAME                   ENDPOINTS                                            AGE
endpoints/test         10.131.2.22:8080                                     2m24s

 

동일하게 worker0로 curl을 보내면, 아래와 같이 worker1에 있는 Pod로 패킷이 전달되는 것을 확인할 수 있다.

[host ~]# curl -k 172.20.22.238:35542
You've hit test-86d7ff54f9-dt8ng
[host ~]# curl -k 172.20.22.238:35542
You've hit test-86d7ff54f9-dt8ng
[host ~]# curl -k 172.20.22.238:35542
You've hit test-86d7ff54f9-dt8ng
[host ~]# curl -k 172.20.22.238:35542
You've hit test-86d7ff54f9-dt8ng

 

 

endpoint가 제거될 경우, kube-proxy가 api-server를 통해 이를 전달받아 iptables를 업데이트한다. iptables에는 worker0 상의 Pod에 대한 정보가 없어졌기 때문에 worker1의 Pod 라우팅만 남는다.

따라서 자연스럽게 worker1의 Pod에 패킷이 전달된다.

이런 설정들은 Service를 생성하면 별도로 기재해놓지 않아도 externalTrafficPolicy가 Cluster로 설정되기 때문에 가능한 것이다.

externalTrafficPolicy가 Cluster로 설정되면, Cluster 에 있는 모든 Pod에 대해 Routing 하도록 iptables를 수정한다.

 

[host ~]# kubectl get svc test -o yaml
apiVersion: v1
kind: Service
metadata:
...
  labels:
    expose: test
...
  name: test
  namespace: default
spec:
...
  externalTrafficPolicy: Cluster

 

이 externalTrafficPolicy: Cluster를 통한 분배는 좋으면서 좋지 않다. 공평한 분배와 본래 Service 특성을 생각해봤을 때는 당연하고도 좋은 일이지만, 두 Node간 물리 거리가 멀리 떨어져 있는 경우(많은 Hop을 거쳐야 하는 경우) 굳이 다른 노드로 보내서 불필요한 라우팅을 초래한다.

 

Hop에 따른 서비스 지연을 고려하지 않아도 되는 서비스라면 상관없겠지만(그런 서비스가 많은지는 잘 모르겠지만), 그렇지 않다면 가급적 물리적으로 가까운 노드에 배포되도록 한다던가하는 노력이 필요하다.

 

이런 Hop들에 대한 영향을 없애기 위해서, externalTrafficPolicy: Local 설정이 있다.

 

externalTrafficPolicy: local 인 경우

externalTrafficPolicy가 Local로 설정될 경우, 같은 노드 내에 있는 Pod로만 패킷이 전달된다. 따라서 불필요하게 다른 Pod로 패킷이 가는 상황을 막을 수 있다. 그러면 만약 externalTrafficPolicy: Local일 때 해당 노드에 있는 Pod가 죽으면 어떻게 될까?

Policy를 Local로 변경 후, 동일하게 worker0로 패킷을 전달한다.

 

...
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
  externalTrafficPolicy: Local
  selector:
...

 

아래의 결과처럼 timed out이 발생하는 것을 볼 수 있다.

[host ~]# curl -k 172.20.22.238:35542
curl: (7) Failed connect to 172.20.22.238:35542; Connection timed out

 

 

이는 externalTrafficPolicy: Local특성 상 노드 상에 있는 Pod에만 패킷을 전달하고, 노드 바깥에 있는 Pod에 대해서는 고려하지 않기 때문이다.

 

아쉽게도 평소에는 노드 상에 있는 Pod에게 패킷을 보내는데 유사시에는 다른 Node로 패킷을 보내주는 완벽한 설정은 없다.

 

요약

  • Service는 externalTrafficPolicy: Cluster가 Default다. 패킷이 골고루 분배되지만 불필요한 Hop이 생길 수 있다.

  • Hop을 줄이려면 externalTrafficPolicy: local을 사용하자

  • 그러나 패킷이 안갈 수도 있다. 주의하자.

 

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

Admission Control  (0) 2021.07.27
Kubernetes NetworkPolicy 란?  (0) 2020.12.26
Istio를 알아보자 - 1. 기본 항목 구성  (0) 2020.12.13