본문 바로가기
Computer Science/Kubernetes

Chapter 11. Understanding Kubernetes internals

by zxcvber 2021. 5. 30.

The chain of events that unfolds when a Deployment resource is posted to the API server (출처: https://livebook.manning.com/book/kubernetes-in-action/chapter-11/253)

주요 내용

  • Kubernetes 클러스터를 구성하는 컴포넌트의 기능과 역할에 대한 이해
  • Pod 생성시 일어나는 일에 대한 이해
  • Kubernetes 내부 네트워크와 Service 의 동작 방식 이해

Kubernetes 리소스들이 어떻게 구현되었는지 살펴보자!

11.1 Understanding the architecture

Kubernetes 클러스터는 다음과 같이 구성되어 있음을 1장에서 배웠다!

  • Kubernetes Control Plane: 클러스터의 상태를 저장하고 관리한다
    • etcd distributed persistent storage
    • API server
    • Scheduler
    • Controller Manager
  • (Worker) Node(s): 실제로 컨테이너를 동작하게 한다
    • Kubelet
    • Kubernetes Service Proxy (kube-proxy)
    • Container Runtime (Docker 등)

이와 별개로도 리소스들을 관리하거나 사용하기 위해서는 이하의 것들이 추가로 필요하다.

  • Kubernetes DNS server
  • Dashboard
  • Ingress controller
  • Heapster (14장)
  • Container Network Interface network plugin

11.1.1 The distributed nature of Kubernetes components

위에서 언급한 컴포넌트들은 각각 별도의 프로세스로 실행된다!

컴포넌트의 통신 방식

컴포넌트들은 반드시 API server 와만 통신하고, 서로 직접 통신하지 않는다. 그리고 etcd 와 통신하는 유일한 컴포넌트는 API server 이다. 클러스터의 상태를 변경하기 위해서는 무조건 API server 를 거쳐야 한다.

컴포넌트의 인스턴스 여러 개 만들기

Control Plane 의 컴포넌트들은 여러 개의 서버 사이에 분리될 수 있으며, Control Plane 을 여러 개 만들어 주면 high availability (HA/고가용성) 을 달성할 수 있다.

또한 etcd 와 API server 의 경우 병렬적인 작업 처리가 가능하지만, Scheduler 와 Controller Manager 의 경우 작업을 한 번에 한 인스턴스만 실행할 수 있다. 나머지는 standby 상태가 된다.

컴포넌트들의 실행 방식

Kubelet 만 시스템에서 실행하고 나머지는 Kubelet 이 pod 의 형태로 실행한다. (물론 Control Plane 의 컴포넌트들은 시스템에서 직접 실행 할수도 있다)

11.1.2 How Kubernetes uses etcd

Kubernetes 를 사용하면서 리소스를 생성하고 수정하게 되면 이러한 정보가 persistent 하게 어딘가에 저장되어야 한다. 그래야만 API server 가 재시작하는 등의 경우에도 리소스들이 유지될 수 있기 때문이다. Kubernetes 에서는 클러스터의 정보와 메타데이터를 저장하는 유일한 저장소가 etcd 이다. etcd 는 fast, distributed, consistent key-value store 이다. (Distributed 이므로 HA 를 위해 여러 개 생성 가능)

etcd 와 통신하는 유일한 컴포넌트는 API server 이고, 나머지 컴포넌트들은 API server 를 통해 간접적으로 etcd 에 접근하게 된다. 이렇게 구현된 이유는 validation 과 more robust optimistic locking system, 그리고 저장소와의 통신을 API server 에게 맡겨서 abstraction 의 효과를 얻기 위해서이다.

Optimistic Concurrency Control (Optimistic Locking): 데이터에 lock 을 걸어서 read/write 를 제한하지 않고, 데이터에 버전을 추가하는 방식이다. 대신 데이터가 수정될 때마다 버전이 증가하게 되며, 클라이언트가 데이터를 수정하려 할 때 데이터를 읽을 때와 쓰려고 할 때 버전을 비교하여 만약 다르다면 업데이트가 기각당하는 방식이다. 이 때 클라이언트는 데이터를 다시 읽어와서 업데이트를 다시 시도해야 한다.

모든 Kubernetes 리소스에는 metadata.resourceVersion field 가 있어 클라이언트 쪽에서 API server 에 업데이트 요청을 보낼 때 반드시 함께 전달해야 한다. 만약 etcd 에 저장된 버전과 다르다면, 업데이트가 기각된다.

etcd 에 리소스가 저장되는 방식

etcd v2 에서는 key 를 계층적으로 저장해서 (hierarchical key space) key-value pair 가 파일 시스템처럼 관리 되었다. 그래서 key 는 다른 key 를 포함하는 폴더이거나, 그냥 value 를 가졌다.

etcd v3 에서는 폴더를 지원하지 않는데, 대신 key 의 형태는 변하지 않고 유지되었다. / 를 포함할 수 있는 것이므로 폴더처럼 계층적으로 나뉘어 있다고 봐도 괜찮다.

Kubernetes 에서는 etcd 의 /registry 안에 정보를 저장한다.

etcd 설치하고 etcdctl 로 확인해보려 했으나 실패... 아래 내용은 책의 내용이다. May be outdated.

$ etcdctl ls /registry
/registry/configmaps
/registry/daemonsets
/registry/deployments
/registry/events
/registry/namespaces
/registry/pods
...

각 리소스 별로 저장되어 있음을 알 수 있다. 만약 pod 의 정보를 보고 싶다면

$ etcdctl ls /registry/pods
/registry/pods/default
/registry/pods/kube-system

Namespace 별로 구분이 되어있는 것을 확인할 수 있고, 더욱 자세히 확인하면

$ etcdctl ls /registry/pods/default
/registry/pods/default/kubia-159041347-xk0vc
/registry/pods/default/kubia-159041347-wt6ga
/registry/pods/default/kubia-159041347-hp2o5

Pod 마다 key 가 하나씩 존재하고 있음을 알 수 있다. Value 를 가져와 보면 pod definition 이 JSON 형태로 저장되어 있는 것을 확인할 수 있다.

Consistency and validity of stored objects

Kubernetes 에서는 다른 Control Plane 컴포넌트들이 무조건 API server 를 거쳐서 etcd 와 통신하기 때문에, optimistic locking 을 이용하여 항상 consistent 한 업데이트를 할 수 있게 된다. 또한 API server 가 validation 을 해주기 때문에 etcd 에 저장되는 정보는 항상 valid 하며, 업데이트 요청이 인증된 클라이언트로부터만 이뤄지도록 하고 있다.

Consistency when etcd is clustered

HA 를 위해 etcd 가 여러 개인 경우, 동기화가 이뤄져야 한다. etcd 에서는 RAFT 알고리즘을 사용하여 etcd 간의 상태를 동기화한다. 그래서 항상 노드의 상태는 다수의 노드가 동의한 현재 상태이거나, 과거에 동의했던 상태가 된다.

이 알고리즘은 consensus 알고리즘이기 때문에 '다수'가 동의해야 다음 상태로 나아갈 수 있게 된다. 그래서 만약 클러스터가 분리되어 두 그룹이 생긴다고 해도, 둘 중 한 그룹에는 분명 '다수'의 노드가 포함되어 있을 것이므로 해당 그룹만 상태를 변경할 수 있고, 다른 그룹은 상태를 변경할 수 없게 된다. 즉, 각 그룹의 상태는 diverge 할 수 없다.

나중에 클러스터가 다시 합쳐지게 되면 '다수'가 아니었던 노드들은 '다수'의 노드 상태를 확인하고 그에 맞게 자신의 상태를 변경하면 된다.

따라서 etcd 인스턴스의 개수는 홀수개로 정하는 것이 좋다!

11.1.3 What the API server does

API server 는 기본적인 CRUD interface 를 제공하여 RESTful API 로 클러스터 상태를 조회하거나 수정할 수 있도록 한다. 그리고 그 정보를 etcd 에 저장하며, 저장할 때 validation 을 수행하여 잘못된 데이터가 저장되지 않도록 한다. 또한 optimistic locking 을 이용해 동시성도 관리하고 있다.

kubectl 을 사용하게 되면 요청이 API server 로 오게 되는데, 이 과정을 상세하게 살펴본다.

우선 kubectl 을 이용해 리소스를 생성하게 되면 HTTP POST 요청이 API server 에 가게 된다.

Authenticating the client with authentication plugins

API server 입장에서는, 요청을 보낸 사람이 누구인지 먼저 확인할 필요가 있다. 내부에 authentication plugin 이 존재하므로, 이 plugin 을 사용해서 누가 요청을 보낸 것인지 판단한다. (HTTP 요청을 분석하는 것)

Authorizing the client with authorization plugins

요청을 어떤 사용자가 보냈는지 판단했다면, 해당 사용자가 요청의 내용을 실제로 수행할 수 있는지 확인한다. 이 때는 authorization plugin 을 사용하게 된다.

Validating and/or modifying the resource in the request with admission control plugins

요청이 리소스 생성/수정/삭제라면, Admission Control 로 요청이 전달된다. 이 또한 plugin 을 사용하게 되는데, 요청의 리소스 spec 에서 누락된 field 값을 채워주거나 (기본값으로 설정하거나) 다른 관련된 리소스의 값을 수정하기도 하며, 요청을 기각할 수도 있다.

리소스 읽기는 Admission Control 에 전달되지 않는다!

Docs: https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/

Validating the resource and storing it persistently

위 과정을 모두 거친 뒤에는 validation 을 수행하고 etcd 에 저장하여 클라이언트에게 응답을 돌려준다.

11.1.4 Understanding how the API server notifies clients of resource changes

Controller Manager 의 경우 ReplicaSet 을 관리하거나 service 의 endpoint 를 관리해야 하는데, 이를 위해서는 리소스의 정보가 필요하고, 리소스가 변경되었다면 변경 사실을 알 수 있어야 한다.

그래서 API server 는 리소스 변경사항을 다른 컴포넌트가 확인할 수 있도록 해준다. Control Plane 의 컴포넌트들은 리소스가 생성/수정/삭제될 때 알림을 받도록 요청할 수 있다. 이를 통해 다른 컴포넌트들은 클러스터 상태 변경에 대응할 수 있게 되는 것이다.

클라이언트(변경사항을 지켜보는 다른 컴포넌트)는 API server 에 HTTP 연결을 하게 된다. 이 연결을 통해 요청한 리소스의 수정 내역이 전달된다. 그래서 리소스 변경이 일어나면, 이 리소스의 변경 내역을 요청한 (watch 하고 있는) 모든 클라이언트에게 수정 내역이 전달된다.

11.1.5 Understanding the Scheduler

Scheduler 의 동작은 비교적 간단한 편이다. API server 의 watch 를 이용해 새로운 pod 가 생기면 pod 가 띄워질 노드를 하나 골라주면 된다.

Scheduler 는 노드에게 pod 를 띄우라고 절대 알려주지 않는다. Scheduler 가 하는 일은 API server 에 요청을 보내 pod definition 을 수정하는 것이다. 그러면 해당 노드의 Kubelet 이 watch 를 이용해 보고있다가 pod 가 schedule 되었음을 알게 되는 것이다. 이제 Kubelet 은 띄워야 할 pod 가 생긴 것이므로 pod 를 실행하게 된다.

큰 그림은 간단하지만, 노드를 고르는 일은 생각보다 복잡하다.

Default scheduling algorithm

  • Scheduling 이 가능한 노드(acceptable nodes)의 리스트를 만든다.
  • 만든 리스트의 노드들 중에서 가장 적합한 노드를 고르고, 만약 가장 적합한 노드가 여러 개라면, round-robin 을 이용하여 균등하게 scheduling 되도록 한다.

Finding acceptable nodes

Acceptable 인지 아닌지 판단하기 위해서는 미리 정의된 조건들을 확인한다. 몇 가지만 살펴보면..

  • Pod 가 요구하는 하드웨어 리소스가 노드에 충분한가?
  • 노드에 리소스가 거의 바닥나지는 않았는지?
  • Pod 에서 특정한 노드로의 scheduling 을 요청하지는 않았는지?
  • Node selector label 이 맞는지?
  • Pod 가 특정 포트로 요청을 받는다면 해당 포트가 노드에서 이미 bind 되어 있지는 않은지?
  • Pod 가 요구하는 volume 이 이 노드에서 mount 될 수 있는지?
  • Pod 에 node affinity/anti-affinity 설정이 있는지?

이러한 조건들을 모두 만족해야 acceptable node 가 된다.

Selecting the best node for the pod

위에서 다양한 조건으로 검사를 했지만, 그럼에도 불구하고 어떤 노드는 더 나은 선택일 수도 있다. 2-노드 클러스터가 있다고 하면, 둘다 acceptable 인데 한 노드가 10개의 pod 를 이미 실행하고 있는 반면 나머지 하나는 실행 중인 pod 가 없다고 하자. 그러면 자연스럽게 실행 중인 pod 가 없는 쪽으로 scheduling 을 할 것이다.

하지만 클라우드에서 실행하는 경우라면, 그냥 10개 pod 를 실행 중인 노드에 scheduling 하고 나머지 node 는 없애서 cloud provider 에게 메모리를 돌려주는 것이 나을 수도 있다.

Multiple schedulers

여러 개의 scheduler 를 사용할 수도 있으며, schedulerName field 를 사용해 어떤 scheduler 를 사용할지 정할 수 있다.

11.1.6 Introducing the controllers running in the Controller Manager

앞에서 언급한 것처럼 API server 는 etcd 에 저장하고 클라이언트에게 알리며, scheduler 는 노드를 선택하기만 하기 때문에, 변경 사항이 생겼을 때 실제로 변경된 클러스터 상태에 다가가도록 일할 컴포넌트가 필요하다. 이 일은 Controller Manager 안의 controller 들이 수행한다.

Controller 종류는 거의 모든 리소스 별로 하나씩 있는 느낌이다.

  • Replication Manager (ReplicationController)
  • ReplicaSet, DaemonSet, Job controllers
  • Deployment, StatefulSet controllers
  • Node controller
  • Service, Endpoints controller
  • Namespace controller
  • PersistentVolume controller
  • Others

즉 리소스는 클러스터 내부에서 돌아가야 하는 것들의 정보/명세이며 controller 는 그 정보를 바탕으로 실제로 실행하는 역할을 한다고 생각하면 된다.

Understanding what controllers do and how they do it

결국에는 API server 를 watch 하고 있다가 변경 내역이 생기면 일을 한다고 생각하면 된다. Controller 들은 서로의 존재를 모르며 각자 API server 와 통신할 뿐이다.

추가로 controller 들은 reconciliation loop 을 사용하여 목표 상태와 현재 상태를 비교한다. 또 status 에 현재 상태를 기록하며, API server 의 watch 기능이 모든 변경 이벤트를 준다는 보장은 없으므로 주기적으로 re-list operation 을 수행하여 놓친 변경 내역이 없는지 확인한다.

Replication Manager

ReplicationController 를 관리한다.

API server 에 ReplicationController 리소스를 watch 하고 있으며, replica count 에 변경 사항이 생기면 알림을 받게 된다. 또한 pod 리소스도 watch 하고 있어서 실제로 몇 개의 replica 가 실행 중인지 확인할 수 있다.

만약 replica count 와 실제 replica 수가 다르다면, POST/DELETE 요청을 API server 에게 보내서 pod 를 생성/삭제하도록 한다. 실제 생성과 삭제는 Scheduler 와 노드의 Kubelet 이 담당하게 된다.

ReplicaSet, DaemonSet, Job controllers

ReplicaSet 의 경우 Replication Manager 와 비슷하다.

한편 DaemonSet 과 Job controller 는 비슷한데, 자신들이 watch 하고 있는 리소스의 pod template 를 참고하여 API server 에게 pod 생성 요청을 보낸다. 마찬가지로 실제 pod 생성은 Kubelet 이 하게 된다.

Deployment controller

실행 중인 Deployment 의 상태가 리소스 정보와 동일하도록 계속 동기화하는 역할을 담당한다.

Deployment 리소스의 정보가 수정될 때마다 controller 는 새 버전을 rollout 을 하게 되는데, 이는 ReplicaSet 을 이용해서 한다. 그리고 deployment strategy 에 따라 각 ReplicaSet 의 replica 수를 적절히 조절하여 모든 pod 이 새로운 pod 로 교체되도록 한다.

9장에서 언급했듯이 Deployment 가 pod 를 직접 만들지 않는다.

StatefulSet Controller

(Deployment 에 state 가 추가된 것이니...) ReplicaSet controller 와 유사하다. 하지만 ReplicaSet controller 는 pod 만 관리하는 반면 StatefulSet controller 는 pod 의 PVC 까지 같이 관리하게 된다.

Node controller

클러스터의 worker node 정보를 담고있는 Node 리소스를 관리한다. 또한 노드의 health 를 확인하고 도달할 수 없는 (unreachable) 노드의 pod 는 제거한다.

Service controller

LoadBalancer service 를 관리하는 controller 이다.

Endpoints controller

Service object 들은 endpoints 를 확인하여 요청을 분산하는데, 이 endpoint 를 관리한다. Service 를 watch 하고 있고, label selector 에 맞는 pod 들을 watch 하고 있다가 pod 가 READY 상태가 되면 Endpoints 리소스에 pod 의 IP 와 포트를 추가한다.

Endpoints 는 standalone object 이므로 controller 가 직접 생성하고 삭제한다.

Namespace controller

Namespace 가 삭제될 때, 해당 namespace 안의 모든 리소스를 삭제하는 역할을 담당한다.

PersistentVolume controller

사용자가 PVC 를 생성했을 때, 적절한 PV 를 찾아 bind 해주는 역할을 담당한다.

PVC 가 생성됐을 때 access mode 를 만족하는 가장 작은 PV 를 찾아준다. 내부적으로 용량으로 정렬된 PV 목록을 가지고 있다.

11.1.7 What the Kubelet does

처음에 노드가 생성될 때는 API server 에 Node 리소스를 생성한다. 그리고 APi server 에 watch 하여 자신의 노드로 scheduling 된 pod 가 있으면 pod 의 컨테이너를 실행한다. Kubelet 은 실행중인 컨테이너들을 지속적으로 모니터링하여 상태와 이벤트 그리고 (하드웨어) 리소스 사용량을 API server 에 보고한다.

Liveness probe 를 실행하는 것도 Kubelet 이다. 실패하면 Kubelet 이 재시작 명령을 내린다.

Running static pods without the API server

일반적으로는 API server 에 pod definition 을 요청하여 pod 를 생성하겠지만, 로컬 저장소에 있는 definition 으로부터 pod 를 생성할 수도 있다. Pod manifest 를 Kubelet 의 manifest 폴더에 넣어주면 Kubelet 이 그것을 실행하고 관리해준다.

이 방식을 이용해서 Control Plane 의 컴포넌트들을 pod 형태로 실행할 수 있는 것이다.

11.1.8 The role of the Kubernetes service proxy

kube-proxy 는 클라이언트들이 생성된 service 에 연결될 수 있도록 해준다. Service IP 와 port 로 들어오는 요청이 service endpoints 중 하나의 pod 로 연결되도록 보장하며, 만약 endpoint 가 여러 개라면 로드 밸런싱도 해준다.

Why it's called a proxy

예전에는 리눅스 iptables 를 수정하여 kube-proxy 서버로 요청이 오도록 한 다음 처리하여 pod 에게 전달했었기 때문이다. 현재는 iptables 규칙을 수정하여 proxy 서버를 거치지 않고 바로 endpoint pod 중 랜덤한 하나에게 요청이 가도록 하고 있다. (packet redirection) 후자가 성능이 더 좋다.

11.1.9 Introducing Kubernetes add-ons

How add-ons are deployed

Pod 로 띄워지기도 하고 Deployment 나 ReplicationController 로 띄워지기도 한다. 결국 YAML 파일을 API server 에 POST 하는 점은 동일하다.

minikube 에서는 Ingress controller 와 coredns 가 deployment 로 띄워져있는 것을 확인했다.

How the DNS server works

DNS server pod 는 kube-dns service 를 통해 expose 되어 있으며, service 의 IP 주소는 pod 의 모든 컨테이너 내부의 /etc/resolv.conf 파일 안에 nameserver 로 들어가 있다.

kube-dns pod 는 Service 와 Endpoints 리소스를 watch 하고 있어서 변경 사항이 생기면 DNS record 를 업데이트한다. 이 업데이트에는 약간의 지연이 있을 수 있다.

How (most) Ingress controllers work

구현체마다 조금씩 다를 수 있지만 대부분 reverse proxy server 를 둔다. 그리고 reverse proxy server 의 세팅을 Ingress, Service, Endpoints 에 맞게 해준다. (API server 의 watch 기능 이용)

Ingress 를 사용하게 되면 service 를 거치지 않고 endpoint 로 바로 요청이 전달되는 것도 이 때문이다. 더불어 client IP 가 유지되는 장점도 있다.

11.1.10 Bringing it all together

Kubernetes 시스템이 각 역할과 책임을 가진 개별적인 컴포턴트들로 잘 분리되어있음을 알게 되었다.

사용자가 정의한 목표 상태에 클러스터가 도달할 수 있도록 컴포넌트들이 협동한다.

11.2 How controllers cooperate

Pod 를 생성할 때 어떤 일이 일어나는지 자세히 살펴본다. Pod 를 직접 생성하는 경우는 잘 없으므로, Deployment 를 생성하는 경우를 살펴볼 것이다.

11.2.1 Understanding which components are involved

시작하기 전, controllers, Scheduler, Kubelet 이 API server 를 watch 하고 있다는 사실을 기억해야 한다.

11.2.2 The chain of events

kubectl 을 이용해서 Deployment 를 생성하게 되면, Kubernetes API server 는 POST 요청을 받게 된다.

API server 는 spec 을 validation 하고, etcd 에 저장하며, kubectl 에 응답을 돌려준다.

그 다음부터는 연쇄적으로 반응이 일어난다.

Deployment controller creates the ReplicaSet

Deployment 리소스를 watch 하고 있던 모든 클라이언트들은 새롭게 생성된 deployment 에 대해 알게 된다. 이 클라이언트 중에 Deployment controller 가 있으므로, controller 는 새로운 deployment 를 감지하고 현재 specification 에 맞는 ReplicaSet 을 생성한다. 이 또한 API server 에 요청하는 것이다.

ReplicaSet controller creates the pod resources

ReplicaSet controller 가 새로운 ReplicaSet 을 감지하고, pod selector 의 값을 이용해서 replica count 와 같은 pod 개수가 존재하고 있는지 확인한다.

(새로 생성하면 당연히 개수가 부족하므로) 이제 controller 는 ReplicaSet spec 의 pod template 을 참고하여 API server 에 pod 생성을 요청한다.

Scheduler assigns a node to the newly created pods

새롭게 생성된 pod 는 etcd 에 저장되어 있지만 아직 노드가 결정되지 않은 상태라 nodeName field 가 없다. Scheduler 는 이렇게 nodeName field 가 없는 pod 들을 watch 하고 있다가 발견하면 scheduling 을 해준다. 이제 pod 정보에 nodeName 이 존재하게 된다.

여기까지는 Control Plane 에서 일어나는 일이다. Controller 들은 단지 API server 와 통신하여 리소스를 업데이트하기만 했다.

Kubelet runs the pod's containers

이제 worker node 들의 차례이다. Kubelet 은 자신의 노드에 schedule 된 pod 를 watch 하고 있으므로 pod definition 을 가져와 Docker (혹은 container runtime) 에게 특정 이미지를 기반으로 컨테이너를 실행하라고 명령한다. 이제 container runtime 이 컨테이너를 실행하게 된다.

11.2.3 Observing cluster events

위 작업이 수행되는 과정에서 Control Plane 컴포넌트와 Kubelet 은 API server 에 event 가 발생했다고 알려준다. Event 리소스를 생성하여 알려주며, 이는 kubectl get events 로 확인할 수 있다.

참고로 kubectl describe 하면 밑에 event 가 나왔었다.

명령어를 입력해 보면 SOURCE 에 어떤 컴포넌트/controller 가 event 를 발생시켰는지, KIND, NAME 에서 event 가 영향을 미친 리소스의 종류와 이름을 확인할 수 있다. REASONMESSAGE 에서는 상세한 설명을 확인할 수 있게 된다.

11.3 Understanding what a running pod is

Pod 를 실행했으므로, 실행중인 pod 가 무엇인지 살펴보자. Kubelet 이 컨테이너를 실행하는 것은 알았는데, 더 할 일이 있을까?

Pod 를 실행한 뒤, 노드ssh 연결하여 docker ps 를 입력해 보면 COMMAND/pause 인 컨테이너가 하나 있는데, 이 컨테이너가 한 pod 내의 컨테이너를 하나로 묶는 역할을 해준다.

Pod 내의 컨테이너는 linux namespace 와 네트워크를 공유하기 때문에, /pause 컨테이너는 이 linux namespace 를 붙잡고 있는 infrastructure 컨테이너인 것이다.

Pod 내의 컨테이너는 재시작 되는 경우가 많다. 이 때 같은 linux namespace 로 재시작 되어야 하는데, 위와 같이 infrastructure 컨테이너가 존재하기 때문에 같은 linux namespace 를 사용할 수 있게 된다. 이 infrastructure 컨테이너는 pod 와 lifecycle 이 같기 때문에 pod 가 삭제될 때 같이 지워진다. 만약 infrastructure 컨테이너를 삭제하면 Kubelet 이 다시 만들어 주고, pod 의 컨테이너도 전부 다시 만든다.

11.4 Inter-pod networking

Pod 들은 고유 IP 를 가지며, NAT 없이 flat network 구조를 갖는다. 작동 원리를 살펴본다.

11.4.1 What the network must be like

Kubernetes 는 특정 네트워크 기술을 요구하지는 않지만, 컨테이너들끼리는 어떤 노드에 있을지라도 서로 통신이 가능해야 한다.

또한 통신에 사용하는 IP 는 NAT 가 적용되지 않아서, 어디서 바라보더라도 자신의 IP 가 동일해야 한다. 이렇게 되야 하는 이유는 마치 같은 네트워크에 연결된 머신에서 동작하는 것처럼 보이고 또 간단해지기 때문이다.

이러한 요구사항을 만족하는 네트워크 기술은 많지만 구현체마다 다르고 상황에 따라 장단점이 다르기 때문에, 일반적인 방법에 대해 다룰 것이다.

11.4.2 Diving deeper into how networking works

Infrastructure 컨테이너에 IP 주소와 network namespace 가 설정된다는 것을 11.3 에서 확인했다. Pod 의 컨테이너는 해당 network namespace 를 사용하게 된다.

Enabling communication between pods on the same node

Infrastructure 컨테이너가 시작되기 전에 virtual Ethernet (veth) 인터페이스 pair 가 컨테이너에 생성된다. 한쪽은 노드의 namespace 에 남아있고, 다른 쪽은 컨테이너 안의 namespace 가 되어 eth0 으로 이름이 변경된다.

이제 노드의 namespace 에 남아있는 인터페이스는 container runtime 이 사용하는 network bridge 에 연결된다. 이제 eth0 인터페이스는 bridge 의 IP 대역 중 하나를 할당받아 IP 로 사용하게 된다.

컨테이너 내부에서 요청이 나가는 경우 eth0 -> vethXXX -> Bridge 의 순서로 가게 된다.

만약 이 요청의 destination 이 노드 내의 다른 pod 라면 Bridge -> vethYYY -> eth0 의 경로로 다른 pod 에 요청이 전달된다.

Enabling communication between pods on different nodes

다른 노드의 pod 와 통신하는 방법은 다양하지만, layer 3 routing 방법을 살펴볼 것이다.

우선 Pod IP 는 클러스터 내에서 유일해야 하므로, 노드의 network bridge 에 할당된 IP 대역은 서로 겹치지 않아야 한다.

각 노드에 layer 3 networking 을 적용하여 노드의 physical network interface 가 bridge 에 연결되도록 해주면 된다. 그리고 각 노드에서 routing table 을 설정해주면 된다.

이제 다른 노드로 통신하려는 경우 패킷의 이동 경로는 eth0 -> vethXXX -> Bridge -> Node's physical adapter -> Network Wire -> Other node's physical adapter -> Bridge -> vethYYY -> eth0 가 된다.

이 방법은 물론 노드가 같은 네트워크 스위치에 연결된 경우, 사이에 라우터가 없을 때만 유효하다. 물론 라우터를 사용하여 노드 사이에 오고가는 패킷을 routing 할 수 있겠지만, 노드 개수가 많아지거나 노드 사이의 라우터 개수가 많아지면 어렵고 오류가 발생하기 쉽다. 이러한 이유로 Software Defined Network (SDN) 을 사용하는 것이 편하다.

SDN 을 사용하게 되면 노드들이 같은 네트워크 스위치에 연결된 것처럼 보이게 되며, pod 에서 나가는 패킷은 캡슐화되어서 다른 pod 에 전달되고 un-캡슐화된다.

11.4.3 Introducing the Container Network Interface

컨테이너가 네트워크에 연결하는 것을 쉽게 하기 위해 Container Network Interface (CNI) 프로젝트가 시작되었다. Kubernetes 에서 해당 CNI 플러그인을 사용하도록 할 수 있다.

종류에는 Calico, Flannel, Romana, Weave Net 등이 있다.

플러그인 설치는DaemonSet 과 기타 리소스를 포함한 YAML 파일을 생성하면 된다. 참고로 Kubelet 실행시 --network-plugin=cni 로 옵션을 줘야 한다.

11.5 How services are implemented

복습!

  • 각 service 는 stable IP 주소와 포트를 갖고, 클라이언트는 이 주소로 연결하여 service 를 사용한다.
  • IP 주소는 가상 IP 주소로, 네트워크 인터페이스에 할당되어있지 않고, 노드 밖으로 나가는 패킷에 source/destination IP 로 들어가지 않는다.

11.5.1 Introducing the kube-proxy

Service 와 관련된 모든 것들은 각 노드에서 실행중인 kube-proxy 프로세스에 의해 관리된다.

예전에는 userspace proxy mode 를 통해 진짜 pod 로 연결을 proxy 해주는 역할을 했으나, 현재는 iptables 를 사용하고 있다.

11.5.2 How kube-proxy uses iptables

API server 가 service 생성 요청을 받으면, service 에 가상 IP 주소가 바로 할당된다. 그 다음, API server 는 worker node 의 kube-proxy 에게 새로운 service 가 생성됐음을 알린다. 그 다음 kube-proxy 는 해당 service 가 참조 가능하도록 자신의 노드의 iptables 규칙을 수정한다.

iptables 규칙을 수정하게 되면 service 를 목적지로 갖는 패킷을 가로채 목적지 주소가 변경되고 endpoint 중 한 곳으로 패킷이 연결된다.

kube-proxy 는 service 의 변화만 watch 하고 있는 것은 아니고, Endpoints 의 변화도 지켜보고 있다. 그래서 pod 가 생성/삭제되거나 readiness 상태가 바뀌거나 label 이 수정되었는지 확인한다.

예시

예를 들어, pod A 에서 172.30.0.1:80 에 있는 service 로 패킷을 보낸다고 하면, 패킷의 목적지 주소는 172.30.0.1:80 일 것이다. 네트워크로 패킷이 보내지기 전에, 노드의 iptables 규칙을 먼저 적용하게 된다.

규칙 중에서 적용할 규칙이 있는지 확인하게 되는데, service 가 생성된 상태이므로 '목적지가 172.30.0.1:80 인 패킷의 목적지는 임의로 선택된 pod 의 IP 로 교체되어야 한다'는 규칙이 있을 것이다.

그러므로 이 패킷의 목적지는 service 의 endpoint 중 한 pod 의 주소와 포트로 변경된다. 이때부터는 마치 클라이언트에서 선택된 pod 로 직접 (directly) 요청을 보낸 것처럼 보이게 된다.

11.6 Running highly available clusters

Kubernetes 를 사용하는 이유 중 하나로 인프라에 문제가 생겨도 중단 없이 서비스를 유지할 수 있다는 점이 있다. 중단 없이 서비스를 유지하기 위해서는 앱이 계속 실행되고 있어야 하기도 하지만, 노드에 있는 Control Plane 컴포넌트들이 잘 작동해 줘야 한다. 고가용성 (HA) 를 달성하기 위해 어떤 것들이 관련되어 있는지 살펴볼 것이다.

11.6.1 Making your apps highly available

노드에 문제가 생기더라도 Kubernetes 의 다양한 controller 덕분에 우리의 어플리케이션은 안정적으로 돌아갈 수 있게 된다. 어플리케이션의 HA 를 위해서는 Deployment 를 사용해서 충분한 replica 수를 정해주면, 나머지는 Kubernetes 가 알아서 해줄 것이다.

Running multiple instances to reduce the likelihood of downtime

당연히 여러 인스턴스를 만들어두면 downtime 이 줄어들 것이다. 물론 어플리케이션이 horizontally scalable 해야한다는 조건이 있다. 만약 scalable 하지 않더라도 Deployment 를 이용해 replica count 를 1로 설정해서 배포하는 것이 좋다. 문제가 생겼을 때 어쩔 수 없이 약간의 지연이 있긴 하겠지만 알아서 생성해주긴 할 것이다.

Using leader-election for non-horizontally scalable apps

Downtime 을 회피하기 위해서는 비활성 상태의 replica 를 몇 개 더 만들어두고, leader-election 방법을 이용해 replica 들 중 반드시 하나만 작동하도록 해주면 된다.

만약 나머지도 전부 동작한다면, 하나의 replica 만 write 가 가능하고 나머지는 read 만 가능하게 해도 괜찮을 것이다. 중요한 것은 인스턴스끼리 동기화 문제가 발생하지 않도록 하면 된다는 점이다.

그리고 이 leader-election 을 어플리케이션에 구현할 필요는 없고, sidecar 컨테이너에 담을 수 있도록 이미 구현체가 존재한다.

11.6.2 Making Kubernetes Control Plane components highly available

만약 Kubernetes 에 문제가 생기면 진짜 답이 없어지는데, 이런 경우를 위해서는 Control Plane 컴포넌트들에 HA 를 달성해야 한다. 이를 위해서는 마스터 노드를 여러개 띄워야 하는데, 각 노드들은 API server, etcd, Controller Manager, Scheduler 를 각각 가지고 있어야 한다.

Running an etcd cluster

etcd 는 애초에 분산 시스템을 위해 설계되었기 때문에 여러 개의 etcd 를 띄워도 괜찮다. 홀수 개의 etcd 만 유지하고, 각 etcd 인스턴스가 다른 노드에 있는 etcd 인스턴스의 존재를 알고 있으면 된다. 이는 각 인스턴스의 설정에 다른 인스턴스들의 리스트를 넘겨줘서 설정한다. (다른 etcd 의 IP/포트 정보를 넘겨준다)

etcd 는 앞에서 설명한 RAFT 로 데이터를 모든 인스턴스에 복제할 것이기 때문에, 인스턴스 몇 개에 문제가 생겨도 괜찮다. HA 를 위해서는 5

7개의 etcd 를 띄워서 2

3개 정도의 etcd failure 를 감당할 수 있도록 하면 된다.

Running multiple instances of the API server

API server 를 복제하는 것은 더 쉽다! API server 는 거의 stateless 하다. 물론 약간의 caching 이 있지만 데이터는 전부 etcd 에 저장되기 때문에 여러 인스턴스를 띄울 수 있게 된다. 인스턴스끼리 서로의 존재를 알 필요도 없다.

다만 API server 의 앞단에 로드 밸런서를 두어 API server 중 healthy 한 쪽으로만 클라이언트의 요청이 가도록 조절할 필요가 있다.

Ensuring high availability of the controllers and the Scheduler

얘네 둘은 간단하지 않다. Controller 와 Scheduler 는 클러스터의 상태를 계속 모니터링하고 상태가 변할 때 일하기 때문에 클러스터 상태가 변한 것을 감지한 다수의 controller 가 한꺼번에 반응하게 될 수도 있다. (예를 들면 replica count 가 변경된 것을 보고 5개의 controller 가 한꺼번에 pod 생성 요청을 보내면 pod 가 5개 추가될 것이다)

위와 같은 이유로 controller 와 Scheduler 는 한 번에 한 인스턴스씩 일을 할수 있도록 이미 설계되어 있다. 내부적으로 leader-election 알고리즘을 가지고 있어서, 자신이 leader 일 때만 작업을 수행한다. 나머지 인스턴스들은 leader 에게 문제가 생길 때까지 가만히 있는다.

Understanding the leader election mechanism used in Control Plane components

여기서 흥미로운 점은 leader election 을 위해 컴포넌트들이 서로 통신하지 않아도 된다는 점이다. Leader election 의 동작 과정을 보면 API server 에 Endpoints 리소스를 만들어서 한다. (저자는 Endpoints 리소스를 그렇게 쓰는거 아니라는 의미에서 abused 라고 표현했다. 다른 리소스를 사용했어도 괜찮았을 것이라고 한다.)

kube-scheduler 라는 이름을 가진 Endpoints 리소스를 확인해 보면 annotation 으로 control-plane.alpha.kubernetes.io/leader 를 가지고 있는 것을 확인할 수 있다. 여기에 holderIdentity 라는 필드가 있는데, 여기에 현재 leader 의 이름이 들어간다.

Optimistic locking 때문에 여러 개의 Scheduler 가 자신을 holderIdentity 에 넣으려고 하겠지만, 오직 한 인스턴스만 성공하며, 그 인스턴스가 leader 가 된다. 또한 자신이 계속 살아있음을 증명해야 하기 때문에 주기적으로(2초마다) 이 Endpoints 리소스를 업데이트하여 다른 Scheduler 에게 알린다. Leader 가 실패했음을 알게된다면 다른 인스턴스들은 모두 자신의 이름을 holderIdentity 에 넣고 leader 가 되기 위해 또다시 경쟁한다.


Discussion & Additional Topics

RAFT

Authentication vs Authorization

Leader Election Algorithm

댓글