TT Lab
시작하기
배우기 러닝패스 코스

쿠버네티스 운영 실무

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

TT Lab 에서 이어서 보기

한 줄 요약

드레인은 파드를 지우는 것이 아니라 축출 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 drain 은 파드를 지우지 않는다. 막혔을 때 화면에 보이는 한 줄 뒤에서 일어나는 일을 순서대로 놓았다.

  1. 노드를 먼저 예약 차단한다축출을 시작하기 전에 노드를 새 파드를 받지 않는 상태로 바꾼다. 이후 축출이 막혀 명령을 중단해도 이 표시는 남는다.
  2. 파드마다 축출 요청을 보낸다delete 가 아니라 파드의 eviction 서브리소스에 POST 한다. 이 요청이 API 서버 안에서 PDB 심사를 받는다.
  3. 모자라면 429, 그리고 재시도예산이 모자라면 429 Too Many Requests 로 거절된다. drain 은 포기하지 않고 다시 시도하므로 실패로 끝나지 않고 계속된다.

여기서 구분할 것 막힌 드레인이 오래 가는 것은 고장이 아니라 설계다. 반복해서 찍히는 error when evicting pods 에는 어떤 예산이 몇 개 모자라는지가 적혀 있지 않다.

잠깐, 예측해 보세요 드레인이 30분째 같은 줄만 찍고 있다. 그동안 이 노드에는 새 파드가 배치될 수 있을까?

설명 확인 · 채점 없는 자가 점검

배치되지 않는다. drain 이 축출보다 먼저 노드를 예약 차단 상태로 바꿨기 때문이다. 명령을 중단해도 표시는 남아 클러스터 용량이 노드 한 대만큼 줄어든 채로 지나갈 수 있다.

근거 문서

어떻게 동작하나

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

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

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

minAvailable 과 maxUnavailable 은 둘 중 하나만 쓸 수 있다. 백분율을 쓸 때의 올림 방향이 중요하다. 파드가 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 로 받는다. 실무에서도 유지보수 창을 잡기 전에 같은 방법으로 미리 물어볼 수 있다.

예산이 있어서 막히는 곳과 없어서 뚫리는 곳

드레인 사고는 두 방향으로 난다. 본문의 현장 사례를 나란히 놓았다.

  • 레플리카 1개에 minAvailable: 1허용 중단이 0 이라 그 워크로드가 있는 노드를 영원히 비울 수 없다. 서비스를 끊지 않으려던 의도가 업그레이드 계획을 통째로 멈춘다.
  • PDB 가 없는 워크로드드레인이 아무 저항 없이 통과한다. 심사할 예산이 없으니 레플리카가 한꺼번에 내려갈 수 있다.

여기서 구분할 것 한쪽은 너무 단단하고 다른 쪽은 아예 없다. 유지보수 창을 설계할 때 가장 먼저 만들 자료는 레플리카가 2 이상인데 어떤 PDB 에도 덮이지 않은 워크로드의 목록이다.

잠깐, 예측해 보세요 레플리카 3개인 서비스에 PDB 가 없다. 드레인이 429 없이 깔끔하게 끝났다. 서비스가 안전하게 비워졌다는 뜻일까?

설명 확인 · 채점 없는 자가 점검

그렇게 단정할 수 없다. 429 가 없었던 것은 심사할 예산이 없어서 막을 것이 없었다는 뜻일 수 있다. 그 사이 세 레플리카가 한꺼번에 내려갔는지는 따로 봐야 한다.

근거 문서

다음 실습에서 할 것

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

참고 문서: