LabHub
배우기 러닝패스 코스

쿠버네티스 운영 실무 · 드레인이 멈췄다 — 축출 API 와 유지보수 창 · 이론

끝나지 않는 드레인을 읽는 법

LabHub 에서 이어서 보기

한 줄 요약

드레인은 파드를 지우는 것이 아니라 축출 API 를 부르는 일이고, 그 요청을 PodDisruptionBudget 이 심사해
모자라면 429 를 돌려준다. 막힌 드레인의 원인은 거의 언제나 예산이 0 이거나, 셀렉터가 겹쳤거나,
준비되지 않은 파드가 이미 예산을 깨 놓은 세 가지 중 하나다.

왜 진단이 따로 필요한가

PDB 를 만드는 법은 어렵지 않다. 문서 한 쪽이면 된다. 그런데 현장에서 시간을 잡아먹는 것은 만드는
쪽이 아니라 막혔을 때다. 노드 한 대를 비우려고 친 명령이 30분째 같은 줄만 반복해서 찍고, 그 줄에는
error when evicting pods 밖에 없다. 어떤 예산이 막고 있는지, 몇 개가 모자란지, 지금 내보낼 수 있는
파드가 몇 개인지는 그 화면에 없다.

그래서 드레인이 실제로 무엇을 하는지부터 알아야 한다. kubectl drain 은 파드를 delete 하지 않는다.
파드마다 축출 서브리소스(/api/v1/namespaces/<ns>/pods/<pod>/eviction)에 POST 를 보낸다. 이 요청은
API 서버 안에서 PDB 심사를 거치고, 예산이 모자라면 429 Too Many Requests 로 거절된다. 그리고 drain 은
포기하지 않고 다시 시도한다. 막힌 드레인이 실패로 끝나지 않고 영원히 계속되는 이유가 이것이다.
반대로 kubectl delete pod 는 이 심사를 통째로 건너뛴다 — 그래서 급할 때 쓰고 싶어지지만, 그건 예산을
무시하겠다는 선언이다.

어떻게 동작하나

PDB 의 상태는 숫자 하나가 아니라 네 개다. kubectl get pdb <이름> -o json.status 를 보면 이렇게 있다.

| 필드 | 뜻 |
| --- | --- |
| expectedPods | 셀렉터에 걸리는 파드가 몇 개로 기대되는가 |
| currentHealthy | 그중 지금 건강한 파드가 몇 개인가 |
| desiredHealthy | 축출 뒤에도 남아 있어야 하는 최소 개수 |
| disruptionsAllowed | 지금 당장 내보낼 수 있는 개수 |

여기서 "건강하다" 는 말에는 정확한 정의가 있다. 공식 문서는 .status.conditionstype=Ready 이고
status=True 인 항목이 있는 파드를 건강한 파드로 센다고 적는다. Running 이면 건강한 것이 아니다.

minAvailablemaxUnavailable 은 둘 중 하나만 쓸 수 있다. 백분율을 쓸 때의 올림 방향이 중요하다.
파드가 7개이고 minAvailable: "50%" 이면 3.5 인데, 쿠버네티스는 올림해서 4개를 요구한다. 안전한
쪽이다. 그런데 maxUnavailable 을 백분율로 쓰면 내보낼 수 있는 개수 쪽이 올림된다. 그래서 공식
문서의 표현대로 실제 중단이 적어 둔 백분율을 넘어설 수 있고, 레플리카가 하나뿐일 때
maxUnavailable: 30% 는 그 하나를 내보낼 수 있게 만들어 결국 100% 중단이 된다.

두 번째 함정은 셀렉터가 겹친 예산이다. 한 파드가 두 개 이상의 PDB 에 걸리면, 각 예산에 여유가
있어도 축출이 거절된다. 축출 서브리소스가 그 상황을 지원하지 않기 때문이다. 문서도 겹치는 셀렉터를
피하라고 적고, 정당한 용례로는 파드를 한 예산에서 다른 예산으로 옮기는 과도기만 든다.

세 번째가 unhealthyPodEvictionPolicy 다. 1.31 에서 안정이 된 필드이고, 기본값은 IfHealthyBudget 이다.
이 기본값에서는 아직 건강하지 않은 파드를 내보내려면 그 애플리케이션이 이미 예산을 깨고 있지 않아야
한다. 즉 CrashLoopBackOff 에 빠진 파드나 Ready 를 보고하지 못하는 파드가 하나라도 있으면, 바로 그
망가진 파드조차 축출할 수 없다. 드레인은 그 자리에서 영원히 멈춘다. AlwaysAllow 로 바꾸면 실행 중이지만
건강하지 않은 파드는 예산과 무관하게 내보낼 수 있다. 공식 문서가 노드 드레인을 지원하려면 이 값을
권한다고 적는 이유다.

마지막으로 예산이 보호하지 못하는 것도 알아야 한다. PDB 는 자발적 중단만 막는다. 노드가 그냥
죽는 것은 막지 못하고, 그래서 문서는 예산이 그 개수를 늘 보장하지는 않는다고 못박는다.
그리고 maxUnavailable: 0 이나 minAvailable 을 레플리카 수와 같게 두면 자발적 축출을 0 으로 만든 것이라
그 파드가 있는 노드는 드레인이 영원히 끝나지 않는다. 이것은 버그가 아니라 문서가 적은 그대로의 의미다.

현장에서 만나는 모습

가장 자주 보는 것은 레플리카 1개짜리에 minAvailable: 1 을 건 것이다. 의도는 "이 서비스는 절대
끊기면 안 된다" 였는데, 결과는 그 워크로드가 있는 노드를 영원히 비울 수 없게 된 것이다. 업그레이드
계획이 통째로 멈춘다.

두 번째는 드레인이 막힌 뒤 노드를 되돌리지 않는 것이다. drain 은 축출을 시작하기 전에 노드를 먼저
예약 차단 상태로 바꾼다. 축출이 막혀 명령을 중단해도 그 표시는 남는다. 아무도 눈치채지 못한 채
클러스터 용량이 노드 한 대만큼 줄어 있고, 몇 주 뒤 트래픽이 몰릴 때 그 대가를 치른다.

세 번째는 예산이 없는 워크로드다. 이쪽은 반대 방향의 사고다 — 드레인이 아무 저항 없이 통과하고,
레플리카가 한꺼번에 내려간다. 그래서 유지보수 창을 설계할 때 가장 먼저 만들어야 할 자료가
"레플리카가 2 이상인데 어떤 PDB 에도 덮이지 않은 워크로드" 목록이다.

이 실습 환경의 한계

kwok 파드는 진짜 컨테이너가 아니라서 CrashLoopBackOff 를 진짜로 만들 수는 없다. 대신 파드의
status.conditions 를 직접 고쳐 Ready 를 False 로 만든다 — PDB 가 보는 것이 정확히 그 컨디션이라
중단 컨트롤러는 진짜로 currentHealthy 를 하나 줄이고, 축출 판정도 진짜로 달라진다. 확인해 보니
기본 정책에서는 429, AlwaysAllow 로 바꾸면 통과였다. 또 축출을 실제로 실행하면 뒤 단계가 쓸 파드가
사라지므로, 이 실습은 대부분의 판정을 ?dryRun=All 로 받는다. 실무에서도 유지보수 창을 잡기 전에
같은 방법으로 미리 물어볼 수 있다.

다음 실습에서 할 것

한 노드에 워크로드를 모아 놓고 허용 중단이 0 인 예산을 만든 다음, 축출 API 를 직접 불러 429 를 받아
본다. 이어서 드레인이 실제로 멈추는 것을 보고 노드를 되돌린 뒤, 예산을 고쳐 같은 요청이 통과하는
것을 확인한다. 그다음 7개 레플리카에 50% 예산을 걸어 올림 방향을 클러스터가 계산한 숫자로 확인하고,
셀렉터가 겹친 예산으로 교착을 만들어 그 메시지를 읽는다. 준비되지 않은 파드를 만들어 기본 정책과
AlwaysAllow 의 차이를 재고, 마지막으로 예산이 없는 워크로드를 클러스터 전체에서 찾는 감사
스크립트를 만든다.

참고 문서: