Kubernetes Networking - 2. Service

squareBird·2022년 4월 25일
1

Kubernetes

목록 보기
13/17
post-thumbnail

Kubernetes Networking - 2. Service

지난 글 말미에 Kubernetes에서 실제 통신은 Service는 추상화된 layer를 통해 이루어진다는 말씀을 드렸습니다.

Service는 일종의 softeware-defined-proxy 역할을 수행하며 Kubernetes Cluster에서 Pod이나 Node가 변경되더라도 동일한 기능을 제공하는 애플리케이션에 접근할 수 있도록 해줍니다.

이번 글에서는 PodService와 어떤 방식으로 통신하는지에 대해 알아보겠습니다.


Service

Service란 어떤 Pod에 접근하기 위해 추상화된 layer입니다.
Kubernetes 환경에서는 문제가 발생했을 때 NodePod을 쉽게 교체할 수 있습니다.

그렇기 때문에 외부 사용자 또는 내부 Pod들간의 통신에서 Pod의 IP를 이용해 통신할 경우 문제가 생겨 Pod이나 Node가 교체되면 동일한 Pod에 접근할 수 없게 됩니다.

그렇기 때문에 Kubernetes에서는 어떤 상황에서라도 동일한 기능을 제공하는 Pod에 접근하기 위해 Pod의 IP가 아닌 Service라는 추상화된 접근 포인트를 이용하여 Pod에 접근합니다.


Service Network 구성도

이전 환경의 마지막 구성도는 여러개의 Container를 가진 Pod이 어떻게 구성되어 있는지를 보여줍니다.

위의 구성도는 하나의 Node에 하나의 Pod이 위치하고, Pod은 여러개의 Container를 가지고 있는 구성도였습니다.

하지만, 대부분의 경우 Pod는 하나의 책임을 가지는 것이 권장되기 때문에 단일 Container로 구성하고 추가적인 Container는 메인 Container의 기능을 강화하거나, 보조하는 방식으로 사용합니다(참고).

따라서, 이번 Service에 대한 글에서 참고할 구성도는 위와 같습니다.
하나의 Pod은 하나의 Contaienr만을 가지고, 2대의 Worker Node에 각각 2대와 1대의 Pod이 배포되어 있으며 네트워킹 정보는 아래와 같습니다.

Worker Node 1
IP : 10.100.0.2
Container Runtime : 10.0.1.0/24

Worker Node2
IP : 10.100.0.3
Contaienr Runtime : 10.0.2.0/24


패킷 전송

위 구성도의 왼쪽 Worker Node에 위치한 client pod에서 오른쪽 Worker Nodeserver pod1로 패킷을 전달하려고 한다고 생각해보겠습니다.

Destination IP는 10.3.241.152라는 Cluster IP입니다.
구성도를 보면 아시겠지만 어떤 인터페이스에도 10.3.241.152라는 IP는 할당되어 있지 않습니다.

veth1cbr0, eth0은 자신이 모르는 IP이기 때문에 자신의 Default Gateway로 패킷을 전달할 것입니다.

veth1cbr0으로, cbr0eth0으로 eth0은 10.100.0.1로 패킷을 전달할 것입니다.

그런데 위의 그림을 보면 eth0에서 router/gateway로 패킷이 전달될 때 Destiantion IP가 변경된 것을 확인할 수 있습니다.

과연 Worker Node는 어떻게 10.0.2.2라는 IP를 알게 되었을까요?


kube-Proxy

Kubernetes에서 Service를 하나 생성하게 되면 많은 컴포넌트에 영향을 미칩니다.

단순히 생각해도 etcdCluster IPEndpoint에 대한 정보를 저장해야 하며,
Cluster IP와의 통신을 위한 라우팅 테이블의 설정 등 많은 일들이 이루어질 것입니다.

그럼 이 모든 일들을 관리자가 직접 수행해주어야 할까요?
정말 다행스럽게도 Kubernetes에는 이런 Cluster 내부의 네트워킹에 대한 설정을 자동으로 관리해주는 kube-proxy라는 컴포넌트가 존재합니다.

kube-proxyproxy라는 이름에서 알 수 있는 것 처럼 ClientServer간의 트래픽을 전달하는 것이 주된 목적입니다.

kube-proxyKubernetes 오브젝트들이 생성될 때, 관리자가 별도의 작업을 수행하지 않아도 오브젝트들간에 통신이 이루어질 수 있도록 네트워크 룰을 설정하고, NodePort와 같이 외부로 노출되는 port들을 오픈합니다.

또한, kernel spacenetfilteruser spaceiptables 룰을 통해 Cluster IP에 대한 네트워크 정책을 생성합니다.

iptables는 리눅스상에서 방화벽을 설정하는 도구로 user space에 위치합니다.
netfilter 패킷 필터링 기능의 리눅스 커널 방화벽이라고도 정의하는데, iptables는 체인이라는 규칙을 생성하여 들어오는 패킷들을 체인에 따라 관리합니다.
iptables에서 룰을 정의하면 netfilter에도 룰이 생성됩니다.

netfilterkernel space에 위치하며 패킷의 생명주기를 관리하는 툴인데, netfilter에 설정된 규칙에 매칭되는 패킷을 발견하면 미리 정의된 action을 수행합니다.

iptables은 단지 netfilter의 룰을 쉽게 세워주기 위한 도구입니다.
iptables은 패킷필터링을 수행하지 않으며, 실제 패킷 필터링은 커널에 탑제된 netfilter가 수행합니다.

즉, kube-proxyiptables룰을 정의하고, iptables에 정의한 룰은 netfilter에도 생성되며, 실제로 패킷은 netfilter에 의해 특정 경로로 전달됩니다.


kube-proxy Mode

kube-proxy는 크게 3가지 모드가 존재합니다.

  1. userspace mode
  2. iptables mode
  3. ipvs mode

그러면 Kubernetes 환경에서 kube-proxy가 어떤 모드로 동작하고 있는지 알아보겠습니다.
만약, Kubernetes Cluster에서 직접 확인해보고 싶다면 Minikube 설치 글을 참고해 Minikube를 설치하거나, 다른 방법으로 Kubernetes Cluster를 구축해주세요.

# Minikube가 설치된 환경에서 사용중인 포트 정보 확인
$ netstat -ntlp

Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:6010          0.0.0.0:*               LISTEN      374438/sshd: root@p
tcp        0      0 127.0.0.1:34365         0.0.0.0:*               LISTEN      481/containerd
tcp        0      0 127.0.0.1:35523         0.0.0.0:*               LISTEN      2174/kubelet
tcp        0      0 127.0.0.1:10248         0.0.0.0:*               LISTEN      2174/kubelet
tcp        0      0 127.0.0.1:2379          0.0.0.0:*               LISTEN      2785/etcd
tcp        0      0 10.0.1.50:2379          0.0.0.0:*               LISTEN      2785/etcd
tcp        0      0 10.0.1.50:2380          0.0.0.0:*               LISTEN      2785/etcd
tcp        0      0 127.0.0.1:2381          0.0.0.0:*               LISTEN      2785/etcd
tcp        0      0 127.0.0.1:10257         0.0.0.0:*               LISTEN      2783/kube-controlle
tcp        0      0 127.0.0.1:10259         0.0.0.0:*               LISTEN      2724/kube-scheduler
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      399/systemd-resolve
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      62350/sshd: /usr/sb
tcp6       0      0 ::1:6010                :::*                    LISTEN      374438/sshd: root@p
tcp6       0      0 :::8443                 :::*                    LISTEN      2717/kube-apiserver
tcp6       0      0 :::10249                :::*                    LISTEN      3526/kube-proxy
tcp6       0      0 :::10250                :::*                    LISTEN      2174/kubelet
tcp6       0      0 :::8080                 :::*                    LISTEN      20779/java
tcp6       0      0 :::10256                :::*                    LISTEN      3526/kube-proxy
tcp6       0      0 :::22                   :::*                    LISTEN      62350/sshd: /usr/sb

포트 조회 결과를 보면 각 Kubernetes Componenet들이 어떤 포트를 사용하고 있는지 알 수 있습니다.

kube-proxy는 10249, 10256 포트를 사용하고 있습니다.
이 중 10249번 포트를 이용해 kube-proxy가 어떤 모드로 동작하고 있는지 확인할 수 있습니다.

# kube-proxy 동작 모드 조회
$ curl localhost:10249/proxyMode

iptables

조회 결과 Minikube를 설치하면 기본적으로 iptables mode로 동작하는 것을 확인할 수 있습니다.


1. userspace Mode

먼저 가장 legacy한 모드인 userspace mode를 기반으로 설명하고자 합니다.

userspace mode에서는 kube-proxy가 상당히 많은 역할을 수행합니다.

위의 그림은 userspcae mode의 동작방식입니다.

ClientService IP로 요청을 하면, iptables 룰에 의해 kube-proxy로 전달이 되고, kube-proxy가 대상 Pod들로 패킷을 전송해줍니다.

PodService의 등록부터 패킷의 전달까지의 과정을 자세히 나열하면 아래와 같습니다.

kube-proxy userspace mode

  1. kube-proxyapi-server와 통신하면서 ClusterPodService CRUD를 모니터링
  2. Service 오브젝트가 생성되면, kube-proxy는 자신이 현재 동작중인 Node에 랜덤한 port를 오픈하고 api-server와의 통신을 통해 알게된 ServicePod정보에 따라 Node에 오픈한 portPod을 연결시킴
  3. kube-proxy는 위에서 설정한 Service에 대한 트래픽을 자신이 받기 위해 Service로 가는 트래픽을 전부 자신이 받도록 iptables 룰을 설정
  4. ClientCluster IP로 request 패킷 전달
  5. 3번에서 설정한 iptables 룰에 의해 kube-proxy로 패킷 전달
  6. kube-proxy가 해당 Service에 매칭된 Pod 중 하나로 패킷 전달 (Round-Robin)

이 과정을 패킷 전송 부분의 구성도에 적용시켜보면 아래와 같이 동작합니다.

먼저 kube-proxy는 10.100.0.2라는 IP를 가진 Worker Node에서 동작하고 있습니다.
kube-proxy가 10400번 포트로 동작하고 있다고 가정하겠습니다.

  1. Client process(Pod내부의 Container)에서 Cluster IP로 요청
  2. Pod는 10.3.241.152:80이라는 Cluster IP 정보를 모르므로 Worker Node로 트래픽 전달
  3. Cluster IP가 생성될 때 kube-proxyiptables 룰을 설정함. 이때, netfilter의 룰이 생성되며, 해당 Cluster IP로 가려는 패킷은 kube-proxy로 가라는 정책이 적용됨
  4. Worker Nodekube-proxy로 패킷 전달
  5. kube-proxy는 해당 Cluster IP에 해당하는 Pod들의 정보를 알고 있기때문에 Cluster IP에 매칭된 Pod들 중에서 적절한 Pod의 IP와 포트 정보를 전달

userspace modekube-proxy는 로드밸런싱, 패킷 규칙 설정 등 대부분의 네트워크킹 작업을 대부분 processkube-proxy 자체에서 컨트롤 하기 때문에 userspace와 kernel 간 엑세스 해야하는 일이 굉장히 많아 성능이 떨어집니다.


2. iptables Mode

위에서 설명한 것 처럼 kube-proxyuserspace modeprocesskube-proxy가 대부분의 네트워킹 작업을 주도하기 때문에 성능이 떨어진다는 단점이 있습니다.

이를 해결하기 위해 나온 모드가 iptables mode입니다.

위의 구성도를 보면 기존에는 Cluster IP로 접근시 kube-proxy로 패킷이 전달되었지만, 이제는 iptables룰에 의해 바로 패킷이 전달되는 것을 확인할 수 있습니다.

iptables mode에서 kube-proxyiptables에 룰을 적용하는 것만을 담당하고, Pod의 정보를 전달하는 역할은 수행하지 않습니다.

구성도에서의 실제 과정은 다음과 같습니다.

  1. Client process(Pod내부의 Container)에서 Cluster IP로 요청
  2. Pod는 10.3.241.152:80이라는 Cluster IP 정보를 모르므로 Worker Node로 트래픽 전달
  3. Cluster IP가 생성될 때 kube-proxyiptables 룰을 설정함. 이때, netfilter의 룰이 생성되며, 해당 Cluster IPselector로 선택한 Pod들에 대한 정보까지 netfilter에 적용
  4. Worker Nodenetfilter에 의해 해당 Pod의 정보를 획득하고 전달

요청 과정은 userspace mode와 동일하지만 kube-proxy가 전달 과정에 관여하지 않는 것을 확인할 수 있으며, 이를 통해 네트워크의 성능이 향상되었습니다.

하지만, iptables mode의 경우 Cluster IP가 여러대의 Pod을 셀렉트 하고 있을 때, 하나의 Pod에 연결이 실패했을 경우 다른 Pod으로의 연결을 재시도하지 않습니다.
이는 Pod하나에 연결이 실패했을 때 다른 Pod 또는 동일한 Pod에 재시도를 하는 userspace mode와 비교했을때 단점이 됩니다.

또한, iptables 자체가 방화벽 목적으로 설계 되었는데 kube-proxy가 담당하고 있던 역할들까지 수행하다 보니 iptables 레코드가 과도하게 생성되어 병목현상이 발생할 수 있습니다.


3. ipvs Mode

IPVS Mode는 리눅스 커널의 L4 Loadbalnacer인 IPVS 기술을 사용합니다.

IPVS Mode에서 kube-proxy는 생성 및 삭제되는 ServicePod을 파악하고 파악한 정보에 따라 IPVS 규칙을 설정하는 것입니다.

Client에서 전달되는 request 패킷은 netfilter를 통해 IPVS로 전달되며, IPVS가 모든 네트워킹 작업을 처리해서 request 패킷을 해당하는 Pod으로 전달합니다.

IPVSPod에 대한 접근 실패 시 재시도 기능을 제공하며, 커널 영역에서 동작하는 hash table 방식을 활용하여 iptables와 비교했을 때 효율적인 데이터 구조를 통해 거의 무제한적인 확장이 가능합니다.

하지만, IPVS Mode를 사용하기 위해서는 모든 Node들이 IPVS를 사용할 수 있도록 미리 환경을 구성해 주어야 하며, IPVS 자체가 로드밸런싱은 가능하지만 패킷 필터링이나 hairpin-masquerade, SNAT 등 기존 iptables이 제공하던 기능들을 제공하지 않아 iptables를 함께 사용해야 합니다.
IPVS Modeiptables의 사용을 최소화 하기 위해 ipset 기술을 사용해 문제를 해결했습니다.

IPVS Mode에서는 iptables와 유사하게 kube-proxy가 직접적인 패킷 전달을 담당하지 않습니다.

iptables Mode와의 차이점은 iptables Mode에서는 kube-proxyiptables 룰을 추가하지만, IPVS Mode에서는 IPVS Hash TableService와 그에 매칭되는 Pod의 정보를 기록하고, iptablescluster IP, NodePort, LoadBalancer Type의 패킷을 IPVS로 전달하는 체인을 생성합니다.


Port는 어떻게 변경될까?

그렇다면 Cluster IP를 통해 패킷이 전송될 때 Pod의 어떤 Port로 패킷이 전송될까요?

Kubernetes 공식 문서에서 Service를 생성하는 예시로 아래와 같은 yaml 파일이 있습니다.

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376

여기를 보면 spec.ports에 porttargetPort가 존재합니다.

이때 portCluster IP의 포트이고, targetPortPod의 포트입니다.

예를 들어 위의 그림에서 Cluster IP의 80 포트로 전달된 패킷이 10.0.2.2의 8080 포트로 전달되었습니다.

이 때 해당 Cluster IP를 describe 명령어를 통해 확인하면

Cluster IP : 10.3.241.152
port : 80
targetPort : 8080

위와 같은 설정을 확인할 수 있을 것이고, 패킷 전달 대상에 10.0.2.2라는 IP를 가진 Pod를 확인할 수 있을 것입니다.


참고

https://ssup2.github.io/theory_analysis/Kubernetes_Service_Proxy/
https://coffeewhale.com/k8s/network/2019/05/11/k8s-network-02/

profile
DevOps...

0개의 댓글