[Kubernetes in Action] Understanding Kubernetes Internals

2022. 1. 16. 13:36DevOps/Docker & Kubernetes

Kubernetes in Action 2nd Edition을 정리한 글입니다.

 

 

 

Understanding the architecture

쿠버네티스 클러스터는 다음과 같이 구성되어 있음을 알고 있습니다.

  • Kubernetes Control Plane: 전체 클러스터를 관리하고 저장합니다.
    • API Server
    • etcd Distributed Persistent Storage
    • Scheduler
    • Controller Manager
  • The Worker Nodes: 실제로 애플리케이션 컨테이너를 동작시키는 역할을 합니다.
    • Kubelet
    • Kubernetes Service Proxy
    • Container Runtime (Docker, rks,... etc)
  • Add on Components: 그 외에 리소스들을 관리하거나 사용하기 위해서 필요합니다.
    • Kubernetes DNS Server
    • Dashboard
    • Ingress controller
    • Container Network Interface Network Plugin
    • Heapster

 

The Distributed nature of Kubernetes Components

위에서 언급한 모든 컴포넌트들은 "개별 프로세스"로 실행됩니다. 각각의 컴포넌트들은 다음과 같은 의존 관계를 지닙니다. (뒤에서도 설명하겠지만 거의 모든 트래픽은 "API Server"를 거치는 것을 확인할 수 있습니다.) 

 

 

kubectl get componentstatuses 명령어를 사용하면 각각의 Control Plane Component의 health status를 확인할 수 있습니다. (단 v1.19+ 이상부터는 Deprecated 되었습니다)

 

 

How These Component Communicate

쿠버네티스 시스템 컴포넌트들은 반드시 API 서버와만 통신하고 직접적으로 다른 컴포넌트들과 통신하지 않습니다. API 서버는 유일하게 쿠버네티스의 상태를 관리하는 etcd와 통신하며 다른 컴포넌트들이 etcd를 수정하기 위해서는 반드시 API 서버를 거쳐야 합니다. 

 

Running Multiple Instances of Individual Components

워커 노드의 컴포넌트들(kubelet, kube-proxy)은 반드시 동일한 노드에서 실행되어야 하지만, Control Plane을 구성하는 컴포넌트들은 여러 개의 서버 인스턴스에서 나눠서 실행될 수 있습니다. 고가용성을 위해 Control Plane의 컴포넌트들은 하나 이상의 인스턴스에서 실행될 수 있습니다. (etcd의 인스턴스가 여러 개 일 수도 있고, API Server의 인스턴스가 여러개 일수도 있다는 의미입니다.) 다만, "Controller Manager"와 "Scheduler"의 경우에는 여러 개의 인스턴스가 있더라도 작업을 "병렬적"으로 수행할 수는 없습니다. 하나의 인스턴스가 실행 중이라면 다른 인스턴스는 "Standby"모드로 실행됩니다.

 

 

How Components are Run

Kubelet을 제외한 다른 Control Plane Components들은 시스템에서 직접 실행되거나 "파드"의 형태로 실행됩니다. 실제로 Kubelet은 Regular System Component로 실행되며 다른 모든 컴포넌트들을 파드의 형태로 실행합니다. 따라서 Control Plane Components들을 실행하기 위해 Kubelet은 마스터 노드에서도 deploy 됩니다. 실제로 다음과 같이 확인해보면 (minikube환경이라 싱글 노드이기는 하지만) etcd, apiserver, schduler 등등의 Control Plane Components들도 모두 POD의 형태로 떠있는 것을 확인할 수 있습니다.

 

 

How Kubernetes uses etcd

쿠버네티스가 클러스터를 관리하기 위해서는 컴포넌트 오브젝트들에 대한 "상태"를 어딘가에 저장하고 있어야 할 것입니다. 이 상태를 기준으로 파드를 생성하고, 수정하고, 삭제하기 때문입니다. 이를 위해서 쿠버네티스는 "etcd"를 사용합니다. etcd는 단순한 key-value pair를 저장하는 스토어이고 위에서 언급한 것처럼 여러 개의 인스턴스를 사용해서 고가용성을 제공할 수 있습니다.

 

 

Ensuring The Consistency and Validity of Stored Objects

쿠버네티스에서 etcd의 값을 수정하기 위해서는 위에서 언급한 것처럼 반드시 API 서버를 거쳐야 하기 때문에 Optimistic Locking을 사용해서 항상 일관적으로 업데이트를 할 수 있게 됩니다. 또한 뒤에서 자세하게 설명하겠지만, API Server를 거치면서 인증(Authentication)과 권한 부여(Authorization) 과정을 거치게 되며, 값을 Write 하기 전에 Validation을 거치므로(default 값 부여, format에 맞지 않는 값 reject 등등) etcd에 저장되는 정보는 항상 Valid 합니다.

 

Optimistic Locking이란 실제로 데이터를 읽고 쓸 때 데이터에 Lock을 거는 것이 아니라 데이터에 version Number를 부여해서 데이터를 읽고 쓰는 사이에 버전 넘버가 변경되었는지를 기준으로 데이터의 최신화 여부를 판단하는 것입니다.

만약 클라이언트(A)가 데이터를 읽고 쓰는 사이에 다른 클라이언트가 쓰기를 끝냈다면 A의 Write 요청은 거절되며, A는 데이터를 다시 읽어 업데이트를 시도해야 합니다.

 

 

Ensuring Consistency When etcd is Clustered

서비스 고가용성을 위해 여러 개의 etcd를 사용하는 경우, 데이터를 "분산 저장"하는 것이 아니기 때문에 etcd사이의 동기화가 이루어져야 합니다. 이때 분산 환경에서의 합의 알고리즘인 RAFT 알고리즘을 사용하며 이로 인해 etcd 노드들의 상태는 다수의 노드가 "현재" 동의한 상태이거나 과거에 동의했던 상태가 됩니다.

 

합의 알고리즘의 특성상 etcd 인스턴스의 개수는 홀수 개로 정하는 것이 좋으며, 클러스터가 분리되는 경우 하나의 그룹에 다수의 노드들이 포함되어 있을 것이므로 항상 다수의 노드가 있는 그룹만이 상태 변경의 권한을 가집니다. 나중에 클러스터가 다시 합쳐지게 되면 다수가 아니었던 노드들은 다수 노드들의 상태에 맞게 자신들의 상태를 업데이트하게 됩니다.

 

 

What the API Server Does

 

 

쿠버네티스 API 서버는 CRUD 인터페이스를 제공해서 다른 컴포넌트들이 RESTful 하게 etcd의 상태를 조회하거나 수정할 수 있도록 합니다. 이때 Optimistic Locking을 사용해서 동시성을 관리하며, Validation을 수행하여 항상 올바른 형태의 데이터만 저장할 수 있도록 합니다. 또한 인증과 권한 부여를 통해서 권한이 있는 요청만 올바르게 처리할 수 있는 방법을 제공합니다.

 

 

실제로 kubectl을 사용해서 리소스를 생성하게 되면 POST 요청이 API 서버로 향하게 되며 다음과 같은 순서로 진행됩니다.

  1. Authentication (인증)
    • API 서버에 하나 이상의 Authentication Plugin을 설정할 수 있습니다.
    • 요청이 들어오면 request의 header나 인증서를 검사하여 인증 과정을 거치게 됩니다.
  2. Authorization (권한 부여)
    • 요청을 어떤 사용자가 보냈는지를 판단했다면 해당 사용자가 요청을 실제로 수행할 수 있는지를 확인합니다.
    • 위의 인증과정과 마찬가지로 하나 이상의 Authorization Plugin을 설정할 수 있습니다.
  3. Validation
    • 요청이 POST, PUT, DELETE 라면 요청이 Admission Control로 전달되며, 이 또한 플러그인입니다.
    • 이경우 리소스 스펙에서 누락된 값을 채워주거나 요청을 기각할 수 있습니다.
    • Read의 경우에는 따로 Admission Control로 요청이 전달되지 않습니다.
  4. Store Persistently
    • 위 과정을 모두 거쳤다면 validation을 수행하고 ectd에 저장한 후에 클라이언트에 성공 응답을 내려줍니다.

 

 

Understanding how the API server notifies clients of resource changes

API 서버는 그 자체로 파드를 생성하거나 서비스 엔드포인트를 관리하지 않습니다. 이것은 아래에서 설명할 "Controller Manager"컴포넌트들이 하는 역할이며 이 때문에 실제로 서비스를 관리하는 컨트롤러 매니저들이 리소스 변경사항에 대해서 알림을 받을 필요성이 생깁니다. (실제로 etcd가 변경되었을 때 파드를 생성할 것인지, 서비스를 생성할 것인지 등을 판단하기 위해서)

 

 

API 서버 자체로는 변경사항을 다른 컴포넌트들에 알려주지 않습니다. 대신에 컨트롤러 매니저들과 컴포넌트들이 리소스의 변화를 "구독"할 수 있는 방법을 제공합니다. Control Plane의 컴포넌트들은 리소스 변경 사항에 대해서 알림을 받도록 요청할 수 있고, 클라이언트(변경 사항을 지켜보는 다른 컴포넌트)는 API 서버에 HTTP 요청 (watch true 옵션을 켠 상태로)을 해서 리소스 변경사항을 전달받을 수 있습니다.

 

 

 

Understanding the Scheduler

스케쥴러의 역할은 비교적 간단합니다. (자세한 Node-selection algorithm은 다루지 않겠습니다) 실제로 API 서버의 watch 메커니즘을 이용해서 새롭게 생성된 파드들을 확인하고 각각의 새로운 파드들에 노드를 할당하는 역할을 수행합니다. 단, 실제로 파드를 "생성"하는 것은 Kubelet이 담당하며 스케쥴러는 API 서버를 통해 Pod Definition을 수정합니다. 요청이 성공하면 API 서버는 Kubelet에게 해당 변경을 알려주며, Kubelet이 이를 통해 파드를 노드에 생성하게 됩니다.

 

 

 

What the Kubelet Does

다른 Controller(StatefulSet Controller, Endpoints Controller, PersistentVolume Controller.... etc)들이 마스터 노드에서 동작하는 Kubernetes Control Plane의 일부인 것과는 다르게 Kubelet과 Service Proxy는 워커 노드에서 동작합니다.

 

Understanding the Kubelet's Job

Kubelet은 쉽게 말해서 워커 노드에서 동작하는 모든 것을 책임지는 컴포넌트입니다. Kubelet은 처음 워커 노드가 생성될 때 API 서버에 노드 리소스가 생성되었음을 알려주는 역할을 합니다. 그리고 API 서버를 지속적으로 모니터링(watch)하면서 파드가 노드에 스케줄 되었는지를 확인하고, 파드의 컨테이너를 실행합니다.(파드의 컨테이너 실행은 오커와 같은 컨테이너 런타임에 명령을 전달함으로써 수행합니다.) 컨테이너를 실행한 후에는 지속적으로 컨테이너의 상태를 모니터링하면서 API 서버에 전달합니다. 

 

Kubelet은 또한 container liveness probe를 실행합니다 probe가 실패하면 컨테이너를 재시작하는 역할도 담당하고, API 서버로부터 파드가 지워졌다고 전달받으면 실제로 파드를 지우고 서버에 알려줍니다.

 

Running Static Pods without the API Server

기본적으로 Kubelet은 API 서버로부터 파드의 manifest를 받아서 파드에 대한 작업들을 수행하지만 노드의 local directory에 있는 파드의 manifest를 받아서 파드를 실행할 수도 있습니다. 실제로 쿠버네티스의 시스템 컴포넌트들을 바로 실행하지 않고 이 컴포넌트들의 manifest를 마스터 노드에 위치한 kubelet의 manifest directory에 넣은 후에 kubelet으로 하여금 해당 파드를 실행하도록 할 수 있습니다.  

 

 

The Role of the Kubernetes Service Proxy

kubelet뿐만 아니라 모든 워커 노드들은 kube-proxy를 실행합니다. kube-proxy를 사용하면 클라이언트들이 API 서버를 통해 생성한 파드들에 접근할 수 있습니다. Service IP와 port로 들어오는 요청이 하나의 파드로 연결되도록 보장하며, 서비스 뒤에 여러 개의 파드가 들어있는 경우 알아서 Load Balancing도 해줍니다.

 

 

이전에는 실제로 Proxy를 두었지만 최근 성능이 더 좋은 버전은 kube-proxy가 프록시 서버로서의 역할을 하는 것이 아니라 iptables를 수정하는 방식으로 변경되었습니다. (iptables proxy mode)

 

 

How Controllers Cooperate

실제로 쿠버네티스가 어떻게 동작하는지를 살펴보기 위해서 아래의 다이어그램을 이해하는 것이 좋습니다. etcd는 API 서버를 통해서만 접근할 수 있으므로 추상적인 단계에서 그 개념은 생략되었고, 리소스에 대한 정의는 API 서버에 저장되어 있다고 생각해도 됩니다. 마스터 노드의 여러 컨트롤러들과 스케쥴러가 API 서버를 watch 하고 있다는 사실을 기억해야 합니다.

 

 

The Chain of Events

kubectl을 사용해서 Deployment를 생성하게 되면, API 서버는 POST 요청을 받게 됩니다. 아래는 이를 표현한 다이어그램이며, 순차적으로 어떤 일들이 일어나는지를 살펴보겠습니다.

 

  1. The Deployment Controller Creates the ReplicaSet
    1. kubectl 명령어를 통해서 Post 요청이 일어나면 위에서 살펴보았던 인증, 권한 확인, validation 절차를 마친 이후에 Deployment 리소스를 새로 생성하게 됩니다.
    2. Deployment가 새로 생성되면 이를 watch 하고 있던 모든 클라이언트들은 새롭게 생성된 deployment에 대해 알게 되고, 클라이언트 중에는 Deployment Controller가 있으므로 변화를 감지하고 현재 spec에 맞게 ReplicaSet을 생성합니다. (9장에서 다뤘듯 Deployment는 내부적으로 ReplicaSet을 생성해서 파드의 생성 책임을 ReplicaSet에 위임합니다)
  2. The ReplicaSet Controller Create the Pod Resources
    1. 새롭게 ReplicaSet이 생성되면 마찬가지로 ReplicaSet Controller가 변화를 감지하게 되고  Replica Count와 Pod Selector를 사용해서 파드를 추가로 생성해야 하는지, 삭제해야 하는지 여부를 확인합니다.
    2. 파드를 새로 생성해야 하는 경우는 ReplicaSet의 Pod Template을 사용하여 API 서버에 파드 생성을 요청합니다. (여기서의 요청은 API 서버에 요청하는 것이고 실제로 워커 노드에 파드가 생성되는 것은 스케쥴러에서 파드를 생성할 노드를 선택한 이후입니다.)
  3. The Scheduler Assigns a Node to the newly created Pods
    1. 바로 위에서 언급한 것처럼 새롭게 생성된 파드는 etcd에는 저장되어 있지만 아직 노드가 결정되지 않은 상태입니다. (nodeName attribute가 없다.)
    2. 스케줄러는 이 nodeName이 없는 파드들을 watch 하고 있다가 발견되면 스케줄링해서 노드를 배정해줍니다 
    3. 여기까지가 Control Plane에서 일어나는 일입니다. 실제로 파드를 생성하고 컨테이너를 실행하지는 않고 API 서버와 통신하여 리소스 업데이트만 합니다.
  4. The Kubelet runs the Pod's Containers
    1. 파드가 특정 노드에 스케줄 되면, 해당 워커 노드의 Kubelet이 이를 감시하고 있다가 Docker와 같은 컨테이너 런타임을 실행합니다.
    2. 애플리케이션이 실행됩니다

 

Observing Cluster Events

Control Plane 컴포넌트와 Kubelet은 각자 액션을 수행할 때마다 API 서버에 event를 발생시키며 이는 "Event Resource"를 생성함으로써 수행됩니다. 실제로 Event는 쿠버네티스 리소스이며 이는 kubectl describe 명령어를 통해 확인할 수 있습니다. (하단 Events 필드) 또한 kubectl get events --watch 명령어를 사용하면 이벤트의 Name, Kind, Reason, Source 등의 정보를 확인할 수도 있습니다. 

 

 

Understanding What a running Pod is

실제로 위에서 Deployment를 생성했을 때 실제로 워커 노드에서 파드가 생성되고 컨테이너화 된 애플리케이션이 실행되는 것까지의 단계를 살펴보았습니다. 그렇다면 실제로 파드에서는 컨테이너를 어떻게 관리하는 걸까요? 다시 말해서 실제로 "Running Pod"는 어떤 상태를 가리키는 것일까요? 실제로 pod를 생성해서 이를 확인해보겠습니다. 다음 명령어로 nginx 이미지를 가진 파드를 생성해보겠습니다.

 

kubectl run nginx --image=nginx

 

 

파드가 생성된 뒤엔 minikube ssh(그냥 이대로 입력하면 됩니다)를 통해서 워커 노드에 직접 접속해보겠습니다. 워커 노드에서 docker ps 명령어를 통해 현재 running중인 컨테이너를 확인해 보면 다음과 같이 nginx 컨테이너가 있는 것을 확인할 수 있습니다. 여기서 주목할만한 것은 nginx가 생성되기 전에 (12 seconds ago) 먼저 생성된 컨테이너가 있다는 것입니다. 

 

 

 

이 컨테이너는 파드 안에 있는 모든 컨테이너들을 하나로 묶어주는 역할을 하고 namespace를 부여하는 컨테이너입니다. 이를 "infrastructure container"라고 부르며 목표는 파드 내의 여러 컨테이너들을 동일한 namespace에 묶어주는 것입니다. 따라서 애플리케이션 컨테이너들에 문제가 생겨 재시작되어도 이전과 동일한 namespace에 묶일 수 있게 되는 것입니다.

 

 

 

Running Highly Available Clusters

쿠버네티스를 사용하는 중요한 이유 중 하나는 고가용성 (High Availability)입니다. 인프라의 특정 부분에 문제가 생겨도 중단 없이 서비스를 제공할 수 있다는 점입니다. 이를 위해서는 애플리케이션이 계속 실행되는 상태를 유지하고 있어야 하기도 하지만, 마스터 노드의 Control Plane 컴포넌트들도 잘 작동해주어야 합니다.

 

Making Your Apps Highly Available

애플리케이션이 수평적으로 확장 가능하다면 여러 인스턴스를 만드는 것이 좋습니다. Application Downtime이 줄어들 것이기 때문입니다. 만약 애플리케이션이 확장 가능하지 않더라도 쿠버네티스의 Deployments를 사용하여 배포하는 것이 좋습니다. (replica count = 1로 설정하면 됩니다.) 이 경우, 문제가 생겼을 때 어쩔 수 없이 downtime이 생기겠지만 바로 쿠버네티스가 파드를 다시 생성해줄 것입니다.

 

수평적으로 확장 가능하지 않은 애플리케이션에 고가용성을 제공하기 위해서는 위의 방법 이외에 "Leder Election"방법을 사용할 수도 있습니다. 즉 비활성 상태의 Replica를 몇 개 더 만들어주고 Replica 중 반드시 하나(Leader)만 작동하도록 해주면 됩니다. 만약 나머지도 전부 동작한다면 하나만 Write이 가능하고 나머지는 Read만 가능하게 해도 괜찮을 것입니다. (다시 말해서, 동기화 문제가 발생하지 않는 구조로만 확장하면 됩니다.)

 

Making Kubenetes Control Plane Components Highly Available

Control Plane 컴포넌트에 고가용성을 달성하기 위해서는 마스터 노드를 여러 개 띄워야 합니다. 각각의 마스터 노드는 API 서버, etcd, Controller Manager, Scheduler를 각각 가지고 있어야 합니다.

 

etcd

etcd는 애초에 분산 시스템을 위해 설계되었기 때문에 여러 개의 etcd 인스턴스를 띄워도 괜찮습니다. 다만 동기화 알고리즘인 RAFT가 제대로 동작하도록 하기 위해서 홀수 개의 etcd 인스턴스를 유지하는 것이 좋고, 각 인스턴스가 다른 노드의 etcd인스턴스의 존재를 알고 있어야 합니다. 

 

API Server

API 서버는 사실상 stateless 하기 때문에 여러 인스턴스를 띄울 수 있고 인스턴스끼리 서로의 존재를 알 필요도 없습니다. 단, 인스턴스들 앞에 Load balancer를 두어 healthy 한 인스턴스에만 요청이 가도록 조정하는 것이 좋습니다.

 

Controllers & Scheduler

Controller와 Scheduler는 다수의 노드에 존재할 수는 있지만 동시성 문제가 발생할 경우 서비스 안정성에 문제를 일으킬 수 있습니다. (예를 들어 Deployment가 생성되었을 때 서로 다른 10개의 Deployment Controller가 생성을 하면 실제로 10개의 Deployment가 생성될 수도 있습니다.) 따라서 Controller와 Scheduler는 한 번에 한 인스턴스씩 일을 할 수 있도록 설계되어 있고 내부적으로 Leader-Election알고리즘을 통해 자신이 Leader인 경우에만 작업을 수행합니다. 

 

 

 

Reference

Raft Algorithm

 

Raft Algorithm - DINO ROMANTIST

RAFT 알고리즘은 Consensus Algorithm 은 분산환경에서 각 노드간 상태를 공유하는 알고리즘 중 하나이고 kubernetes 에서 etcd 의 고가용성을 위해 여러대로(홀수) 운영 했을 때 합의를 하는…

dinonotes.com

 

반응형