쿠버네티스 전문가 — 장애를 진단하고 클러스터를 지킨다
규칙은 각각 옳고, 함께 걸리면 서로를 막는다
한 줄 요약
스케줄러는 파드를 놓을 때 한 번만 판단합니다. Pending 은 「지금 놓을 곳이 없다」 이고 이유는 FailedScheduling 이벤트에 노드별로 적힙니다. 드레인·PDB·분산 규칙·우선순위는 각각 옳지만, 함께 걸리면 서로를 막습니다.
왜 이게 필요했나
운영 클러스터에는 규칙이 쌓입니다. 존 하나가 죽어도 버티려고 분산 제약을 걸고, 정비 중에도 서비스를 지키려고 PDB 를 걸고, 결제 같은 중요한 경로를 먼저 놓으려고 우선순위를 줍니다. 그런데 정비를 시작하는 순간 셋이 동시에 움직입니다. drain 은 PDB 가 허락할 때만 하나씩 내보내고, 내보낸 파드는 분산 규칙이 허락하는 곳에만 다시 놓이고, 다시 놓이지 못하면 PDB 는 다음 축출을 막습니다. 규칙을 하나 지우면 풀리지만, 그 규칙이 지키던 것도 함께 사라집니다.
어떻게 동작하나
분산 제약(topologySpreadConstraints). maxSkew 는 도메인(존·노드)마다 파드 수의 최대 차이이고, whenUnsatisfiable 기본값은 DoNotSchedule(어기면 놓지 않음)입니다. topologyKey 라벨이 없는 노드는 도메인으로 세지 않습니다 — 그래서 존 라벨이 없는 클러스터에 존 분산을 건 서비스는 하나도 뜨지 못합니다. 이 실습의 사고가 그것이고, 메시지는 「node(s) didn't match pod topology spread constraints (missing required label)」 입니다. 두 정책 손잡이도 있습니다. nodeAffinityPolicy(기본 Honor)는 파드의 노드 선택을 통과한 노드만 세고, nodeTaintsPolicy(기본 Ignore)는 테인트가 붙은 노드도 그대로 셉니다. 둘 다 1.33 에서 GA 가 됐습니다.
드레인 교착. cordon 은 노드에 node.kubernetes.io/unschedulable:NoSchedule 테인트를 붙입니다. 기본(Ignore)이면 cordon 된 노드의 존도 계산에 남으므로, 쫓겨난 파드를 남은 존에 놓으면 skew 가 커져 DoNotSchedule 에 걸립니다. 쫓겨난 파드가 Pending 이 되면 PDB 의 여유가 바닥나고 drain 은 다음 축출을 영원히 재시도합니다. 이 실습에서 PDB 를 「한 번에 하나까지」 로 고치고 nodeTaintsPolicy: Honor 를 주자 drain 이 끝났고 web 은 내내 넷이었습니다(실측). uncordon 해도 몰린 파드는 그대로입니다 — 스케줄러는 놓을 때만 판단합니다. 롤링 재시작으로 다시 놓게 해도 고르게 되지 않았습니다(실측 1 대 3). 롤링 중에는 옛 리비전의 파드까지 세기 때문이고, matchLabelKeys: [pod-template-hash] 로 리비전마다 세게 하자 2 대 2 가 되었습니다. 라벨을 붙이는 순서도 함정입니다 — 존 라벨을 한 노드에 먼저 붙이는 순간 세상에는 존이 하나뿐이라, 기다리던 파드 넷이 모두 그 노드로 갔습니다(실측 4 대 0).
우선순위와 선점. PriorityClass 를 가리키는 파드는 그 값을 받습니다. 존재하지 않는 클래스를 가리키면 파드가 승인 단계에서 거절되어 만들어지지도 않습니다(ReplicaSet 의 FailedCreate). 자리가 없으면 스케줄러는 우선순위가 낮은 파드를 골라 내보냅니다(이벤트 Preempted). 공식 문서의 주의: 선점은 PDB 를 최선을 다해 지킬 뿐 보장하지 않고, 노드를 넘나드는 선점은 하지 않으며, QoS 는 보지 않습니다. preemptionPolicy: Never 인 파드는 대기열에서는 앞에 서지만 남을 밀어내지 않습니다.
우선순위의 울타리. PriorityClass 는 클러스터 전역 객체라 이름만 알면 누구나 씁니다. 한 모니터링 SaaS 회사가 공개한 2019년 사고는 새로 띄운 워크로드가 높은 우선순위를 받아 운영 파드를 연쇄로 선점해 약 26분 장애가 난 것이었습니다. 공식 문서가 권하는 울타리는 ResourceQuota 의 scopeSelector(scopeName PriorityClass)로 네임스페이스마다 그 클래스를 쓸 수 있는 파드 수를 정하는 것입니다.
현장에서 만나는 모습
「drain 이 30분째 안 끝난다」 는 호출의 대부분은 PDB 하나 때문이 아니라 PDB 와 다른 규칙의 맞물림입니다. CrashLoopBackOff 파드가 PDB 의 여유를 갉아먹는 경우도 있습니다 — unhealthyPodEvictionPolicy(1.31 GA, 기본 IfHealthyBudget)를 AlwaysAllow 로 두면 건강하지 않은 파드는 예산과 무관하게 내보낼 수 있습니다. Pending 을 볼 때는 이벤트가 있는지 부터 봅니다. FailedScheduling 이 있으면 스케줄러가 판단을 마친 것이고, 아무 이벤트도 없으면 스케줄러가 없거나 파드가 아직 그 앞(승인·할당량)에서 막힌 것입니다.
다음 실습에서 할 것
존 라벨을 붙여 분산 규칙을 지킨 채 web 을 띄우고, drain 교착을 PDB 와 nodeTaintsPolicy 로 풉니다. 정비 뒤 다시 고르게 하고, 우선순위 선점을 일으킨 뒤 선점하지 않는 클래스와 클래스별 할당량으로 울타리를 칩니다. 마지막으로 이유가 다른 Pending 셋을 가려냅니다.