PDB 잘못 걸어놓고 새벽 3시에 노드 드레인이 멈춘 이야기
지난주 화요일 새벽이었다. EKS 컨트롤 플레인 마이너 버전 업그레이드에 맞춰 노드 그룹 rolling 교체를 걸어놨는데, 새벽 3시쯤 슬랙에 "노드 3대가 30분째 SchedulingDisabled 상태로 안 빠진다"는 알림이 떴다. 새벽에 눈이 번쩍 떠졌다.
원인은 결국 우리가 반년 전쯤 붙여둔 PodDisruptionBudget 이었다. 그런데 이게 왜 하필 이번 업그레이드에서 터졌는지, 어디서 계산이 어긋났는지를 정리해두려고 이 글을 쓴다.
뭐가 문제였나
우리 팀은 결제 관련 워크로드 몇 개에 minAvailable: 2 로 PDB를 걸어뒀다. 처음 붙일 때 replica가 4~5개였고, "최소 2개는 살아있어야 한다"는 서비스 팀 요구를 반영한 값이었다.
문제는 그 사이에 트래픽 특성이 바뀌면서 몇몇 서비스의 replica 수가 조용히 2로 줄었다는 거다. HPA min을 낮췄고, 새벽 시간대엔 실제로 2개만 떠 있었다. 이 상태에서 minAvailable: 2 는 곧 "아무도 못 죽인다" 와 같은 뜻이다. kubectl get pdb로 보면 ALLOWED DISRUPTIONS 컬럼이 그냥 0이었다.
Karpenter 가 노드 하나를 뽑으려고 파드에 eviction 요청을 보내면, API 서버는 이렇게 답한다.
Cannot evict pod as it would violate the pod's disruption budget.
이걸 Karpenter 는 계속 재시도하고, 노드는 계속 SchedulingDisabled 상태로 남아있고, 결과적으로 노드 rolling 이 그냥 멈춰버린다. 재밌는 건 kubectl drain 처럼 대놓고 에러가 뜨는 게 아니라 그냥 "느리다"로 보인다는 점이다. 그래서 처음엔 IMDS 문제인가, 이미지 pull 문제인가 한참 헛다리를 짚었다.
새벽에 뭘 했나
일단 무너지지 않게 시간부터 벌었다. 사내에서 쓰는 대응 순서는 대충 이렇다.
먼저 어떤 파드가 걸려있는지 확인.
kubectl get pdb -A -o wide | awk '$5==0 {print}'
ALLOWED DISRUPTIONS 가 0인 PDB를 뽑아 봤더니 결제 팀 서비스 두 개, 그리고 별로 안 중요한 사이드카 워크로드 하나가 걸려 있었다. 사이드카 쪽은 그냥 PDB를 임시로 지웠고, 결제 쪽은 판단이 필요했다.
새벽에 결제 팀 온콜을 깨우기가 좀 그래서, 일단 HPA min을 손으로 3으로 올렸다. 파드 하나가 더 뜨자마자 PDB ALLOWED DISRUPTIONS 가 1이 됐고, 그 순간부터 노드 드레인이 슬금슬금 진행됐다.
이게 완전한 해결은 아니었지만, 그날 밤은 이걸로 넘겼다. 아침에 결제 팀과 얘기해서 PDB를 다시 설계하기로 했다.
다음 날 어떻게 고쳤나
우리가 PDB를 다시 설계할 때 지킨 규칙 두 가지가 있다.
첫 번째는 minAvailable 대신 maxUnavailable 을 쓴다. minAvailable: 2 는 replica가 2로 떨어지는 순간 자멸한다. maxUnavailable: 1 이나 maxUnavailable: 25% 는 replica 수와 무관하게 최소 한 개는 항상 뽑아낼 수 있다는 걸 보장한다. 예전엔 "N개는 살아있어야 한다"는 표현이 더 직관적이라 minAvailable 을 썼는데, 운영해보니 파드 수가 시간에 따라 변한다는 걸 자꾸 잊는다.
두 번째는 unhealthyPodEvictionPolicy: AlwaysAllow. Kubernetes 1.27부터 정식으로 들어온 옵션인데, 이걸 안 걸어두면 CrashLoopBackOff 상태의 파드도 PDB 계산에 "healthy"가 아니라는 이유로 eviction이 안 될 수 있다. 진짜 어처구니 없는 상황이지만, 옛날 우리 클러스터에서 이걸로 발목 잡힌 적이 있다.
수정한 매니페스트는 대충 이렇게 생겼다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: payments-api
spec:
maxUnavailable: 25%
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app: payments-api
25% 는 replica 4개 기준이면 1개, 8개면 2개까지 허용된다. HPA min이 어떻게 바뀌어도 최소 한 개는 evict 되니까 노드 드레인이 무한히 멈추는 상황은 사라진다.
이걸 어떻게 미리 잡을 수 있을까
돌이켜보면 이건 예방 가능한 사고였다. 우리가 놓친 건 "PDB의 ALLOWED DISRUPTIONS가 0인 상태" 자체를 알림으로 걸어두지 않았다는 거다. 그래서 어제부터 다음 두 개의 Prometheus 알럿을 추가했다.
첫 번째는 kube_poddisruptionbudget_status_current_healthy 와 kube_poddisruptionbudget_status_desired_healthy 를 비교해서, desired - current == 0 (즉 여유가 없는 상태) 가 15분 이상 지속되면 warning. 두 번째는 노드가 SchedulingDisabled 상태로 20분 이상 남아있으면 warning. 이 두 개만 있어도 새벽에 깨는 대신 저녁에 조용히 처리할 수 있었을 거다.
혹시 다른 팀은 PDB를 어떻게 관리하는지, 특히 HPA와 PDB의 min 값이 어긋나지 않게 하는 자동화가 있는지 궁금하다. 우리는 아직 매니페스트 리뷰 때 눈으로 잡는 수준이라 완벽하지 않다.