Skip to content
목록으로

쿠버네티스 아키텍처와 요청이 처리되는 과정

Kubernetes2025년 5월 11일
쿠버네티스 아키텍처와 요청이 처리되는 과정

저번 포스팅에서는, 쿠버네티스가 왜 나왔는지와 선언형 · 컨트롤 루프라는 사고방식을 공부하였습니다. 이번에는 그 루프를 실제로 누가 돌리는지, 즉 쿠버네티스의 아키텍처를 정리해보려 합니다. 구성 요소를 하나씩 나열하는 데서 그치지 않고 kubectl apply 한 줄이 실제로 Pod 가 뜨기까지 어떤 경로를 지나는지까지 따라가 보겠습니다.

면접에서 가장 자주 나오는 영역이기도 합니다. "Pod 가 생성되는 과정을 설명해보세요" 같은 질문이 결국 이 편의 내용입니다.


1. 큰 그림


쿠버네티스 클러스터는 크게 두 부분으로 나뉩니다.

  1. 컨트롤 플레인(Control Plane) : 클러스터 전체의 두뇌입니다. 무엇을 어디에 띄울지 결정하고, 상태를 저장하고, 컨트롤 루프를 돕니다.
  2. 워커 노드(Worker Node) : 실제로 컨테이너가 도는 곳입니다. 컨트롤 플레인이 내린 결정을 받아 실행합니다.
[ 컨트롤 플레인 ]
  kube-apiserver          모든 요청의 유일한 관문
  etcd                    클러스터의 모든 상태를 담는 저장소
  kube-scheduler          Pod 를 어느 노드에 놓을지 결정
  kube-controller-manager 각종 컨트롤 루프를 돌림
  cloud-controller-manager 클라우드 연동 (선택)

[ 각 워커 노드 ]
  kubelet                 그 노드의 Pod 를 책임지는 에이전트
  kube-proxy              Service 트래픽을 라우팅
  컨테이너 런타임          실제로 컨테이너를 띄움

여기서 가장 먼저 잡아야 할 것은, 이 요소들이 서로 직접 대화하지 않는다는 점입니다. 스케줄러가 kubelet 을 호출하지 않고 kubelet 이 컨트롤러를 호출하지 않습니다. 모두가 오직 kube-apiserver 하고만 이야기합니다.

이것을 허브 앤 스포크(Hub and Spoke) 구조라고 합니다. 이 구조 덕분에 구성 요소를 추가하거나 교체하기가 쉽고 인증과 감사 로그를 한곳에서 처리할 수 있습니다.


2. 컨트롤 플레인


2.1. kube-apiserver

클러스터의 유일한 관문입니다. kubectl 도, 컨트롤러도, kubelet 도 전부 여기로 옵니다. 하는 일은 이렇습니다.

  1. 인증(Authentication) : 요청을 보낸 것이 누구인가
  2. 인가(Authorization) : 그 사람이 이 일을 할 권한이 있는가
  3. 어드미션 컨트롤(Admission Control) : 이 요청을 그대로 받아도 되는가, 손볼 것은 없는가
  4. 검증과 저장 : 스키마에 맞는지 확인하고 etcd 에 기록

이 네 단계는 7편에서 자세히 다루겠습니다. 여기서 기억할 것은 apiserver 자체는 아무 판단도 하지 않는다는 점입니다. Pod 를 어디에 놓을지, 몇 개를 띄울지는 결정하지 않습니다. 그저 상태를 받아 적고, 물어보면 알려주고, 바뀌면 알려줄 뿐입니다.

또 하나 중요한 성질은 무상태(stateless) 라는 것입니다. apiserver 자신은 아무것도 기억하지 않고 전부 etcd 에 둡니다. 그래서 여러 개를 띄워 앞에 로드밸런서를 두면 그대로 수평 확장이 됩니다. 고가용성 구성에서 apiserver 를 3대 두는 것이 기본인 이유입니다.

2.2. etcd

클러스터의 모든 상태가 저장되는 유일한 장소입니다. Pod 목록도, 설정도, 권한도, 노드 정보도 전부 여기 있습니다. etcd 는 분산 Key-Value 저장소이고 Raft 라는 합의 알고리즘을 씁니다. 하둡 시리즈에서 공부한 분산 합의 이야기가 여기서 다시 나옵니다.

기억해야 할 성질들입니다.

  1. 강한 일관성 : Raft 로 리더를 뽑고 쓰기는 반드시 리더를 거칩니다. 과반수(quorum)가 기록을 받아들여야 커밋됩니다.
  2. 홀수 대로 구성한다 : 3대면 1대까지, 5대면 2대까지 죽어도 과반이 유지됩니다. 4대를 두면 과반이 3이라 3대일 때와 견딜 수 있는 장애 수가 같으면서 쓰기 지연만 늘어납니다. 그래서 3대나 5대로 둡니다.
  3. Watch 를 제공한다 : 특정 키가 바뀌면 알려주는 기능이 있습니다. 쿠버네티스의 이벤트 전달이 여기에 기대고 있습니다.
  4. 리비전을 관리한다 : 모든 변경에 전역 리비전 번호가 붙습니다. "이 리비전 이후의 변화를 달라" 는 요청이 가능해, 컨트롤러가 잠시 끊겼다 붙어도 놓친 변화를 이어받을 수 있습니다.

운영에서 etcd 는 가장 예민한 부분입니다. 디스크 지연에 민감해서 SSD 가 사실상 필수이고 etcd 가 느려지면 클러스터 전체가 느려집니다. 그리고 etcd 백업이 곧 클러스터 백업입니다.

2.3. kube-scheduler

아직 노드가 정해지지 않은 Pod 를 보고 어느 노드에 놓을지 결정합니다. 결정만 하고 실행은 하지 않습니다. 동작은 두 단계입니다.

  1. 필터링(Filtering) : 이 Pod 를 놓을 수 있는 노드를 걸러냅니다. 자원이 충분한가, 요구한 라벨이 있는가, taint 를 견딜 수 있는가, 요청한 볼륨을 붙일 수 있는가 등을 봅니다.
  2. 스코어링(Scoring) : 남은 후보에 점수를 매겨 가장 높은 곳을 고릅니다. 자원이 고르게 퍼지는 쪽, 이미지를 이미 가진 노드, 같은 서비스의 Pod 가 한 노드에 몰리지 않는 쪽 등이 반영됩니다.

결정이 끝나면 스케줄러는 Pod 의 nodeName 필드를 채워 apiserver 에 기록합니다. 이것이 스케줄러가 하는 일의 전부입니다. 실제로 컨테이너를 띄우는 것은 그 노드의 kubelet 입니다. 자세한 조건들은 6편에서 다루겠습니다.

2.4. kube-controller-manager

수많은 컨트롤 루프를 하나의 프로세스 안에서 돌리는 묶음입니다. 이름은 하나지만 안에는 수십 개의 컨트롤러가 들어 있습니다.

  1. Deployment 컨트롤러 : Deployment 를 보고 ReplicaSet 을 만듭니다.
  2. ReplicaSet 컨트롤러 : ReplicaSet 을 보고 Pod 개수를 맞춥니다.
  3. Node 컨트롤러 : 노드가 응답하지 않으면 상태를 바꾸고 일정 시간이 지나면 그 위의 Pod 를 정리합니다.
  4. Job 컨트롤러 · Endpoint 컨트롤러 · ServiceAccount 컨트롤러 …

여기서 컨트롤러가 계층을 이룬다는 점이 중요합니다. Deployment 컨트롤러는 Pod 를 직접 만들지 않습니다. ReplicaSet 을 만들 뿐이고 Pod 는 ReplicaSet 컨트롤러가 만듭니다. 각 컨트롤러는 자기 바로 아래 단계만 책임집니다.

2.5. cloud-controller-manager

클라우드 제공사와 엮이는 부분을 따로 떼어낸 것입니다. LoadBalancer 타입 서비스를 만들면 실제 클라우드 로드밸런서를 생성하는 일, 노드가 클라우드에서 삭제되었는지 확인하는 일 등을 맡습니다. 원래는 controller-manager 안에 있었는데 쿠버네티스 본체가 특정 클라우드에 의존하지 않도록 분리되었습니다.


3. 워커 노드


3.1. kubelet

각 노드에 하나씩 떠서 그 노드의 Pod 를 책임지는 에이전트입니다.

  1. apiserver 에게 "내 노드에 배정된 Pod" 를 계속 지켜봅니다.
  2. 새 Pod 가 배정되면 컨테이너 런타임에게 띄우라고 지시합니다.
  3. 볼륨을 마운트하고 Secret 과 ConfigMap 을 가져와 넣어줍니다.
  4. 컨테이너 상태를 확인하고(probe) 결과를 apiserver 에 보고합니다.
  5. 노드 자원이 부족해지면 Pod 를 쫓아낸다(eviction).

한 가지 짚어둘 것은 kubelet 이 Pod 의 spec 만 본다는 점입니다. Deployment 도 ReplicaSet 도 모릅니다. "이 Pod 를 이 노드에서 돌려라" 만 알면 되고 그것이 왜 생겼는지는 관심사가 아닙니다. 관심사가 잘 갈려 있는 설계입니다.

kubelet 은 apiserver 없이도 동작할 수 있는 경로가 하나 있습니다. 특정 디렉토리에 YAML 을 두면 그것을 그대로 띄우는 스태틱 Pod(Static Pod) 입니다. 컨트롤 플레인 자체를 Pod 로 띄우는 kubeadm 구성에서 이 방식이 쓰입니다. 닭과 달걀 문제를 푸는 장치입니다.

3.2. 컨테이너 런타임과 CRI

실제로 컨테이너를 띄우는 것은 컨테이너 런타임입니다. kubelet 은 CRI(Container Runtime Interface) 라는 표준 인터페이스로 런타임과 대화합니다. 초기에는 kubelet 안에 Docker 를 직접 호출하는 코드가 들어 있었습니다. 그러다 런타임을 갈아끼울 수 있도록 CRI 가 정의되었고 Kubernetes 1.24 에서 Docker 를 직접 호출하던 경로(dockershim)가 제거되었습니다.

이때 "쿠버네티스가 도커를 버린다" 는 이야기가 돌았는데 정확히는 그런 것이 아닙니다. Docker 로 만든 이미지는 그대로 쓸 수 있습니다. 이미지 형식은 OCI 표준을 따르기 때문입니다. 바뀐 것은 kubelet 이 런타임을 부르는 방법일 뿐이고 지금은 containerd 나 CRI-O 가 주로 쓰입니다.

3.3. kube-proxy

Service 로 들어온 트래픽을 실제 Pod 로 보내는 규칙을 각 노드에 설치하는 역할입니다. 자신이 트래픽을 직접 중계하는 것이 아니라, 커널의 패킷 처리 규칙을 넣어두는 일을 합니다. 동작 모드가 몇 가지 있는데 그 차이와 성능 특성은 4편에서 자세히 다루겠습니다.


4. Pod 하나가 뜨기까지


이제 조각들을 이어 붙여, kubectl apply -f deployment.yaml 한 줄이 실제 컨테이너가 되기까지를 따라가겠습니다. 면접에서 가장 자주 나오는 흐름입니다.

4.1. 요청이 들어온다

kubectl 이 YAML 을 읽어 apiserver 에 HTTP 요청을 보냅니다. apiserver 는 인증 → 인가 → 어드미션 → 검증 을 거친 뒤 Deployment 오브젝트를 etcd 에 저장합니다.

여기서 이미 사용자에게는 응답이 돌아갑니다. 저장된 시점에 "성공" 이고 아직 컨테이너는 하나도 뜨지 않았습니다. 선언형 시스템이라 가능한 일입니다.

4.2. Deployment 컨트롤러가 ReplicaSet 을 만든다

controller-manager 안의 Deployment 컨트롤러는 Deployment 의 변화를 지켜보고 있다가, 새 Deployment 를 발견합니다. "이 Deployment 에 맞는 ReplicaSet 이 없네" 를 확인하고 ReplicaSet 을 만들어 apiserver 에 제출합니다.

4.3. ReplicaSet 컨트롤러가 Pod 를 만든다

이번에는 ReplicaSet 컨트롤러가 그 변화를 봅니다. "replicas 는 3인데 이 셀렉터에 맞는 Pod 가 0개네" 를 확인하고 Pod 3개를 만들어 제출합니다. 이때 만들어진 Pod 에는 아직 nodeName 이 비어 있습니다.

4.4. 스케줄러가 노드를 정한다

스케줄러는 nodeName 이 비어 있는 Pod 를 지켜보고 있습니다. 새 Pod 3개를 발견하면 각각에 대해 필터링과 스코어링을 수행해 노드를 고르고 nodeName 을 채워 apiserver 에 기록합니다.

4.5. kubelet 이 컨테이너를 띄운다

각 노드의 kubelet 은 "nodeName 이 나인 Pod" 를 지켜보고 있습니다. 자기에게 배정된 Pod 를 발견하면 이제 실제 작업이 시작됩니다.

  1. 이미지를 받는다(이미 있으면 생략).
  2. 볼륨을 준비해 마운트합니다.
  3. pause 컨테이너를 먼저 띄워 네트워크 네임스페이스를 만듭니다.
  4. CNI 플러그인을 호출해 Pod 에 IP 를 할당합니다.
  5. initContainer 가 있으면 순서대로 실행합니다.
  6. 본 컨테이너들을 띄웁니다.
  7. 상태를 apiserver 에 보고합니다.

4.6. 그리고 계속 돈다

Pod 가 다 떴다고 끝이 아닙니다. 모든 컨트롤러는 계속 루프를 돌고 있습니다. 누군가 Pod 하나를 지우면 ReplicaSet 컨트롤러가 "3개여야 하는데 2개네" 를 발견하고 새로 만듭니다. 그러면 스케줄러가 노드를 정하고 kubelet 이 띄웁니다. 같은 경로를 그대로 다시 지나갑니다.

장애 복구가 특별한 코드 경로가 아니라 평상시 동작과 동일하다는 것이 이 설계의 핵심입니다.


5. 컨트롤러는 어떻게 변화를 알아채는가


여기까지 "지켜보고 있다가" 라는 표현을 여러 번 썼습니다. 이 부분이 실제로 어떻게 구현되어 있는지는, 면접에서 깊이를 보여줄 수 있는 지점입니다.

5.1. 폴링이 아니라 Watch 다

컨트롤러가 1초마다 "Pod 목록 주세요" 를 반복한다면, 컨트롤러 수십 개가 각자 이러는 순간 apiserver 와 etcd 가 버티지 못합니다. 그래서 쿠버네티스는 Watch 를 씁니다. 연결을 열어두고 변화가 생길 때만 apiserver 가 밀어주는 방식입니다.

5.2. List-Watch 패턴

Watch 만 쓰면 문제가 있습니다. 연결이 끊긴 동안의 변화를 놓치기 때문입니다. 그래서 실제로는 두 단계를 씁니다.

  1. List : 먼저 전체 목록을 한 번 받아 현재 상태를 파악합니다. 이때 리소스 버전(resourceVersion) 을 함께 받습니다.
  2. Watch : 그 버전 이후의 변화만 스트림으로 받습니다.

연결이 끊기면 마지막으로 받은 버전부터 다시 Watch 를 겁니다. 그 버전이 너무 오래되어 apiserver 가 더 이상 갖고 있지 않으면, 다시 List 부터 시작합니다. 이것을 리싱크(resync)라고 합니다.

5.3. 인포머와 로컬 캐시

컨트롤러들은 이 List-Watch 를 직접 구현하지 않고 인포머(Informer) 라는 공용 장치를 씁니다. 인포머가 하는 일은 두 가지입니다.

  1. 로컬 캐시를 유지한다 : 받은 오브젝트를 메모리에 들고 있어서, 컨트롤러가 조회할 때 apiserver 를 부르지 않습니다. 읽기 부하가 apiserver 로 가지 않는 것이 핵심입니다.
  2. 이벤트 핸들러를 호출한다 : 추가 · 변경 · 삭제가 생기면 등록해둔 함수를 불러줍니다.

그리고 한 프로세스 안의 여러 컨트롤러가 같은 종류를 볼 때는 공유 인포머(SharedInformer) 로 Watch 연결 하나를 나눠 씁니다. controller-manager 가 수십 개 컨트롤러를 갖고도 apiserver 에 연결을 몇 개만 유지하는 이유입니다.

5.4. 워크큐

이벤트를 받자마자 바로 처리하지 않고 워크큐(Workqueue) 에 넣습니다. 여기에 중요한 성질이 있습니다.

  1. 중복 제거 : 같은 오브젝트에 대한 이벤트가 여러 번 와도 큐에는 한 번만 들어갑니다. 어차피 처리할 때 최신 상태를 다시 읽기 때문에(레벨 트리거) 여러 번 처리할 이유가 없습니다.
  2. 지수 백오프 재시도 : 처리에 실패하면 간격을 늘려가며 다시 시도합니다.
  3. 속도 제한 : 한 오브젝트가 문제를 일으켜 무한 재시도에 빠져도 전체가 마비되지 않습니다.

1편에서 본 레벨 트리거가 여기서 실제 이득으로 나타납니다. 이벤트를 합쳐도 되고, 놓쳐도 되고, 순서가 바뀌어도 됩니다. 판단은 항상 현재 상태로 하기 때문입니다.

5.5. 낙관적 동시성 제어

여러 컨트롤러가 같은 오브젝트를 동시에 고치려 하면 어떻게 될까요. 쿠버네티스는 락을 걸지 않습니다. 대신 모든 오브젝트가 metadata.resourceVersion 을 갖고 있고 수정 요청을 보낼 때 제가 읽었던 버전을 함께 보냅니다. 그 사이 누가 바꿨다면 apiserver 가 409 Conflict 로 거절합니다.

거절당한 컨트롤러는 다시 읽고 다시 시도합니다. 그래서 컨트롤러 코드에서 conflict 재시도가 흔한 패턴입니다. 이것을 낙관적 동시성 제어(Optimistic Concurrency Control) 라고 합니다.

5.6. 리더 선출

controller-manager 와 scheduler 는 고가용성을 위해 여러 대를 띄웁니다. 그런데 세 대가 동시에 같은 Pod 를 만들면 안 됩니다. 그래서 이들은 리더 선출(Leader Election) 을 합니다. Lease 라는 오브젝트를 두고 먼저 잡은 쪽이 리더가 되어 실제 일을 하고 나머지는 대기만 합니다. 리더가 주기적으로 Lease 를 갱신하지 못하면 다른 쪽이 이어받습니다.

apiserver 는 액티브-액티브(전부 일함), 스케줄러와 컨트롤러 매니저는 액티브-스탠바이(하나만 일함) 라는 차이를 기억해두면 좋습니다. apiserver 는 무상태라 여러 대가 동시에 처리해도 문제가 없지만 컨트롤러는 판단을 내리기 때문에 하나여야 합니다.


6. 고가용성 구성


운영 환경에서 컨트롤 플레인을 어떻게 구성하는지도 자주 묻습니다.

  1. apiserver 3대 + 앞단 로드밸런서 : 무상태라 그냥 늘리면 됩니다.
  2. etcd 3대 또는 5대 : 과반수 유지를 위해 홀수로 둡니다.
  3. scheduler · controller-manager 각 3대 : 리더 선출로 하나만 실제로 동작합니다.

etcd 를 컨트롤 플레인 노드에 함께 두는 스택형(stacked) 과, 별도 서버에 두는 외부(external) 구성이 있습니다. 스택형은 서버 수가 적어 간단하지만 노드 하나를 잃으면 apiserver 와 etcd 를 함께 잃습니다. 외부 구성은 서버가 더 들지만 장애 영향 범위가 좁습니다.

그리고 컨트롤 플레인이 전부 죽어도 이미 떠 있는 Pod 는 계속 동작합니다. kubelet 과 컨테이너 런타임은 노드에서 독립적으로 돌기 때문입니다. 다만 그동안 새 배포, 스케일링, 자기 치유는 멈춥니다. 이 점도 면접에서 자주 확인하는 부분입니다.


7. 조금 더 들어가보기 : etcd 와 Watch 의 실제


앞에서 List-Watch 와 인포머까지 봤는데 그 아래에서 실제로 무슨 일이 벌어지는지까지 내려가 보겠습니다. 클러스터가 커졌을 때 어디가 먼저 막히는지도 이 층을 알아야 짚을 수 있습니다.

7.1. etcd 의 쓰기는 디스크를 기다린다

etcd 는 Raft 로 합의합니다. 쓰기 하나가 커밋되려면 과반수 노드가 그 기록을 디스크에 확정해야 합니다. 메모리에 받아둔 것으로는 부족합니다. 전원이 나가도 남아 있어야 합의가 의미를 갖기 때문입니다.

그래서 etcd 의 쓰기 지연은 디스크의 fsync 지연에 직접 묶입니다. SSD 가 사실상 필수인 이유이고 네트워크 스토리지를 쓰면 위험한 이유이기도 합니다. 여기서 체인이 생깁니다.

디스크가 느려짐  →  etcd 쓰기가 느려짐  →  apiserver 응답이 느려짐
                 →  컨트롤러와 kubelet 이 전부 느려짐  →  클러스터 전체가 느려짐

클러스터가 전반적으로 느린데 원인을 못 찾겠다면 etcd 의 디스크 지연부터 봐야 합니다. 리더 선출도 같은 영향을 받습니다. 리더가 정해진 시간 안에 신호를 보내지 못하면 선출이 다시 일어나는데 디스크가 밀려 신호가 늦으면 멀쩡한 리더가 교체되는 일이 생깁니다. 그동안 쓰기가 멈춥니다.

7.2. 리비전과 "너무 오래된 버전" 오류

etcd 는 모든 변경에 전역 리비전 번호를 붙이고 이전 버전들도 함께 보관합니다. 그래서 "이 리비전 이후의 변화를 달라" 는 요청이 가능합니다. 그런데 무한히 보관할 수 없으니 주기적으로 오래된 리비전을 압축합니다. 여기서 컨트롤러가 겪는 상황이 나옵니다.

컨트롤러가 잠시 끊겼다가 마지막 리비전부터 다시 구독하려는데 그 리비전이 이미 압축되어 사라졌다면 이어서 받을 수 없습니다. 이때 나오는 것이 "리소스 버전이 너무 오래되었다" 는 오류이고 컨트롤러는 전체 목록을 다시 받는 것부터 시작합니다. 평소에는 문제가 없는데 컨트롤러가 오래 멈춰 있었거나 변경이 아주 잦은 클러스터에서는 이 다시 받기가 반복되어 apiserver 부하가 커집니다.

7.3. Watch 는 어떻게 전달되는가

apiserver 는 etcd 를 대신 구독하고 자기 메모리에 캐시를 둔 뒤 구독자들에게 나눠 보냅니다. 모든 컨트롤러가 각자 etcd 를 보는 구조였다면 etcd 가 버티지 못했을 것입니다. 전달은 연결을 열어둔 채 변화가 생길 때마다 밀어주는 방식입니다. 그래서 구독자가 많고 변경이 잦으면 apiserver 의 부하가 구독자 수 × 변경 수 로 늘어납니다.

오퍼레이터를 많이 설치한 클러스터에서 apiserver 가 먼저 힘들어지는 이유가 이것입니다. 컨트롤러 하나를 추가하는 것이 그 컨트롤러의 자원만 쓰는 일이 아닙니다.

7.4. 큰 오브젝트가 자주 바뀌면 나쁘다

여기서 앞서 본 EndpointSlice 이야기가 설명됩니다. Pod 목록을 오브젝트 하나에 담으면, Pod 하나가 바뀔 때마다 그 오브젝트 전체가 다시 전송됩니다. Pod 가 3,000개인 서비스에서 하나가 재시작되면, 3,000개가 담긴 오브젝트가 모든 구독자에게 다시 갑니다.

배포 중에는 이것이 초당 수십 번 일어납니다. 전송량이 곱으로 늘어나 apiserver 와 네트워크를 함께 압박합니다. 조각으로 나누면 바뀐 조각만 전송하면 됩니다. 분산 시스템에서 "큰 것 하나" 보다 "작은 것 여럿" 이 유리한 전형적인 경우입니다.

7.5. 요청이 몰릴 때 누구를 먼저 받는가

apiserver 에는 요청을 분류해 종류별로 처리 자리를 나눠 두는 장치가 있습니다. 그렇지 않으면 잘못 만든 컨트롤러 하나가 요청을 쏟아부어 apiserver 를 독차지하고 그동안 kubelet 의 상태 보고나 리더 선출 갱신까지 밀립니다.

리더 선출이 밀리면 멀쩡한 컨트롤러가 리더 자격을 잃고 교체가 일어납니다. 부하가 장애로 번지는 경로입니다. 그래서 중요한 요청은 별도 자리를 갖고 나머지는 공정하게 나눠 쓰도록 되어 있습니다. 어느 요청이 밀려도 되는가를 미리 정해둔 것이라고 보면 됩니다.

7.6. 대규모에서 먼저 막히는 곳

  1. etcd 저장 크기 : 한도를 넘으면 쓰기가 거부되고 클러스터가 읽기 전용이 됩니다. 완료된 Job, 오래된 이벤트, 쓰지 않는 오브젝트가 쌓이는 것이 주된 원인입니다.
  2. etcd 디스크 지연 : 위의 체인 그대로입니다.
  3. apiserver 의 Watch 부하 : 구독자 수와 변경 빈도의 곱입니다.
  4. 오브젝트 하나의 크기 : 1.5MB 안팎의 상한이 있어 큰 ConfigMap 은 아예 들어가지 않습니다.

반대로 컨트롤 플레인이 전부 죽어도 이미 떠 있는 Pod 는 계속 돕니다. kubelet 과 컨테이너 런타임은 노드에서 독립적으로 동작하기 때문입니다. 다만 그동안 새 배포와 자기 치유가 멈추므로, "서비스는 살아 있지만 아무것도 고칠 수 없는 상태" 가 됩니다.


결론


아키텍처를 정리하면서 가장 인상 깊었던 것은, 모든 구성 요소가 서로를 모른다는 점이었습니다. 스케줄러는 kubelet 을 모르고 kubelet 은 Deployment 를 모릅니다. 각자 apiserver 만 보면서 자기 몫의 판단을 하고 그 결과를 다시 apiserver 에 적을 뿐이었습니다. 처음에는 돌아가는 구조가 답답해 보였는데 구성 요소 하나가 죽어도 나머지가 멈추지 않는 이유가 바로 이것이라는 것을 알고 나니 이해가 되었습니다.

두 번째로 크게 배운 것은 List-Watch 와 워크큐였습니다. 컨트롤러가 "지켜본다" 는 말 뒤에 이렇게 많은 장치가 있는 줄 몰랐습니다. 특히 워크큐가 같은 오브젝트의 이벤트를 합쳐버리는 것이, 1편에서 본 레벨 트리거 설계 덕분에 가능하다는 연결이 보였을 때 가장 재미있었습니다. 설계 원칙 하나가 구현 곳곳에서 이득으로 돌아오는 것을 확인한 셈입니다.

세 번째는 낙관적 동시성 제어입니다. 분산 환경에서 여러 주체가 같은 데이터를 고치는 문제를 락으로 풀지 않고 버전을 비교해 충돌하면 다시 하는 방식으로 풀었습니다. 락을 걸면 누군가 죽었을 때 락이 남는 문제를 또 풀어야 하는데 그 문제 자체를 피해 간 것입니다.


공부 참고 자료


📑 1. Kubernetes 공식 문서 - Cluster Architecture

📑 2. Kubernetes 공식 문서 - Controllers

📑 3. Kubernetes 공식 문서 - kube-apiserver

📑 4. Kubernetes 공식 문서 - Container Runtime Interface

📑 5. Kubernetes 공식 문서 - Operating etcd clusters

📑 6. etcd 공식 문서

📑 7. The Raft Consensus Algorithm

📑 8. Kubernetes 공식 문서 - API Concepts (Watch)

📑 9. Dockershim Removal FAQ

Comments