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

쿠버네티스 전문가 — 장애를 진단하고 클러스터를 지킨다

스케줄링 — Pending 을 읽고, 드레인 교착을 풀고, 우선순위를 다룬다

TT Lab 에서 이어서 보기

목표

존 분산 규칙 때문에 하나도 뜨지 못한 서비스를 규칙을 지킨 채 띄우고, 정비 드레인이 PDB 와 분산 규칙에 걸려 멈추는 교착을 풉니다. 이어서 우선순위와 선점을 일부러 일으켜 보고, 우선순위가 사고가 되지 않게 울타리(선점하지 않는 클래스, 클래스별 할당량)를 칩니다. 마지막으로 이유가 다른 Pending 셋을 가려냅니다.

왜 중요한가

스케줄링 규칙은 하나씩 보면 모두 옳습니다 — 존에 고르게 퍼져라, 한 번에 하나 이상 내려가지 마라, 중요한 것을 먼저 놓아라. 사고는 그것들이 함께 걸릴 때 납니다. 정비 드레인이 몇 시간째 끝나지 않거나, 새로 배포한 워크로드가 높은 우선순위를 받아 운영 파드를 연쇄로 밀어낸 공개 사례(2019년 한 모니터링 SaaS 회사의 공개 포스트모템)가 그렇습니다. 이 실습은 그 맞물림을 진짜 스케줄러 위에서 겪고, 규칙을 지우지 않고 푸는 손을 익힙니다.

단계

  1. kx-incident sched-spread 를 실행하면 네임스페이스 web 의 Deployment web(4개)이 하나도 뜨지 않습니다. 원인을 읽고, web 의 분산 규칙(존마다 고르게, 어기면 놓지 않음)은 그대로 둔 채 두 노드를 서로 다른 존(controlplane → zone-a, node01 → zone-b)으로 표시해 web 이 존마다 둘씩 뜨게 하세요.
  2. node01 을 정비하려고 비웁니다(kubectl drain node01 --ignore-daemonsets --delete-emptydir-data). 그대로 하면 drain 이 끝나지 않습니다. 막는 것을 찾아, web 의 PDB 는 지우지 말고 「한 번에 하나까지만 내려간다」 로, web 의 분산 규칙은 DoNotSchedule 을 유지한 채 고쳐 drain 을 끝내세요. 끝난 drain 의 출력을 /root/kx/sched/drain.txt 에 저장하고, node01 은 cordon 된 채로 둡니다(정비 중). 그동안 web 은 4개 모두 Available 이어야 합니다.
  3. node01 을 다시 스케줄 가능하게 하고, web 이 다시 존마다 둘씩 놓이게 하세요. 그동안 web 이 4개 중 3개 아래로 내려가면 안 됩니다.
  4. kx-incident sched-filler 를 실행하면 우선순위가 낮은 배치(batch/filler)가 남은 CPU 를 거의 다 잡고, 결제 API(shop/checkout)가 배포됩니다. checkout 이 왜 뜨지 않는지 찾아, 필요한 PriorityClass critical-api(값 1000000, 낮은 우선순위를 선점함, 기본 클래스 아님)를 만들어 checkout 이 배치를 밀어내고 뜨게 하세요.
  5. PriorityClass nightly(값 1000, 선점하지 않음)를 만들고, 그 클래스로 Job nightly-report 를 네임스페이스 batch 에 만드세요(CPU 요청 400m, 이미지 e2e busybox, 30초 일하고 끝남). 클러스터가 꽉 차 있으므로 이 Job 의 파드는 아무 파드도 선점하지 않고 Pending 으로 기다려야 합니다.
  6. 네임스페이스 batch 에서는 PriorityClass critical-api 를 쓰는 파드를 하나도 만들 수 없게 ResourceQuota no-critical 을 만드세요. 다른 파드(우선순위를 쓰지 않거나 다른 클래스)는 그대로 만들 수 있어야 합니다.
  7. kx-incident sched-pending 을 실행하면 네임스페이스 triage 에 Pending 파드 셋(pending-a·pending-b·pending-c)이 생깁니다. 각각의 원인을 아래 참고의 코드에서 골라 /root/kx/sched/pending.txt 에 <파드> <코드> 꼴로 한 줄씩 적으세요. 파드는 고치지 않습니다.

참고

web 넷이 전부 Pending 이다

kx-incident sched-spread 를 실행하면 네임스페이스 web 의 Deployment web(4개)이 하나도 뜨지 않습니다. 원인을 읽고, web 의 분산 규칙(존마다 고르게, 어기면 놓지 않음)은 그대로 둔 채 두 노드를 서로 다른 존(controlplane → zone-a, node01 → zone-b)으로 표시해 web 이 존마다 둘씩 뜨게 하세요.

kubectl -n web describe pod <하나> 의 FailedScheduling 메시지를 끝까지 읽으세요 — 노드마다 왜 안 되는지 괄호 안에 적혀 있습니다. 토폴로지 분산 제약의 topologyKey 라벨이 없는 노드는 도메인으로 세지 않습니다. 존은 topology.kubernetes.io/zone 라벨로 표시합니다. 제약을 지우거나 ScheduleAnyway 로 바꾸면 Pending 은 풀리지만 존 장애에 대비한다는 목적도 사라집니다. 라벨은 한 노드씩 붙는다는 것도 기억하세요 — 첫 라벨이 붙는 순간 세상에는 존이 하나뿐이고, 기다리던 파드가 모두 그리로 갑니다. 이미 놓인 파드는 다시 놓이지 않습니다. 아직 아무것도 돌지 않는 지금은 0 으로 줄였다가 되돌리는 것이 가장 깨끗합니다.

node01 정비 — drain 이 끝나지 않는다

node01 을 정비하려고 비웁니다(kubectl drain node01 --ignore-daemonsets --delete-emptydir-data). 그대로 하면 drain 이 끝나지 않습니다. 막는 것을 찾아, web 의 PDB 는 지우지 말고 「한 번에 하나까지만 내려간다」 로, web 의 분산 규칙은 DoNotSchedule 을 유지한 채 고쳐 drain 을 끝내세요. 끝난 drain 의 출력을 /root/kx/sched/drain.txt 에 저장하고, node01 은 cordon 된 채로 둡니다(정비 중). 그동안 web 은 4개 모두 Available 이어야 합니다.

drain 은 파드를 Eviction API 로 하나씩 내보내고, PDB 가 허락하지 않으면 5초마다 다시 시도합니다(Cannot evict pod as it would violate the pod's disruption budget). PDB 를 고쳐도 두 번째 벽이 있습니다 — 쫓겨난 web 파드가 갈 곳이 zone-a 뿐인데, cordon 된 node01(zone-b)도 여전히 분산 계산에 들어가 skew 가 1 을 넘습니다. 그 파드가 Pending 이면 PDB 가 다음 축출을 막고 drain 은 다시 멈춥니다. 분산 제약의 nodeTaintsPolicy(기본 Ignore)를 읽어 보세요 — cordon 은 노드에 node.kubernetes.io/unschedulable 테인트를 붙입니다.

정비가 끝났다 — 다시 고르게

node01 을 다시 스케줄 가능하게 하고, web 이 다시 존마다 둘씩 놓이게 하세요. 그동안 web 이 4개 중 3개 아래로 내려가면 안 됩니다.

uncordon 은 「앞으로 놓아도 된다」 일 뿐, 이미 zone-a 에 몰린 파드를 옮기지 않습니다 — 스케줄러는 새 파드를 놓을 때만 판단하고, 쿠버네티스에는 다시 고르게 해 주는 기본 장치가 없습니다(그 일을 하는 것이 Descheduler 입니다). 롤링 재시작은 파드를 새로 만들므로 다시 판단할 기회를 주지만, 그대로 하면 고르게 되지 않습니다 — 롤링 중에는 아직 지워지지 않은 옛 파드까지 세어 새 파드를 놓기 때문입니다. 분산 제약의 matchLabelKeys(값 pod-template-hash)를 읽어 보세요. 롤링의 maxUnavailable 과 PDB 가 함께 「3개 아래로 내려가지 않음」 을 지킵니다.

결제 API 가 뜨지 않는다

kx-incident sched-filler 를 실행하면 우선순위가 낮은 배치(batch/filler)가 남은 CPU 를 거의 다 잡고, 결제 API(shop/checkout)가 배포됩니다. checkout 이 왜 뜨지 않는지 찾아, 필요한 PriorityClass critical-api(값 1000000, 낮은 우선순위를 선점함, 기본 클래스 아님)를 만들어 checkout 이 배치를 밀어내고 뜨게 하세요.

이번에도 Pending 파드가 없습니다. ReplicaSet 의 이벤트가 이유를 말합니다(kubectl -n shop describe rs). 존재하지 않는 PriorityClass 를 가리키는 파드는 만들어지지 않습니다. 클래스를 만들면 파드가 만들어지고, 자리가 없으니 스케줄러가 우선순위가 낮은 파드를 골라 내보냅니다(이벤트 Preempted). 사용자 클래스의 값은 10억 이하입니다.

밤 배치는 기다리되 아무도 밀어내지 않게

PriorityClass nightly(값 1000, 선점하지 않음)를 만들고, 그 클래스로 Job nightly-report 를 네임스페이스 batch 에 만드세요(CPU 요청 400m, 이미지 e2e busybox, 30초 일하고 끝남). 클러스터가 꽉 차 있으므로 이 Job 의 파드는 아무 파드도 선점하지 않고 Pending 으로 기다려야 합니다.

preemptionPolicy: Never 인 파드는 스케줄 대기열에서는 우선순위대로 앞에 서지만(filler 보다 먼저 놓인다), 자리를 만들려고 다른 파드를 내보내지는 않습니다. 한 모니터링 SaaS 회사가 공개한 2019년 사고는 새 워크로드가 높은 우선순위를 받아 운영 파드를 연쇄로 선점한 것이었습니다 — 「급하지만 남을 해치면 안 되는」 일에 이 설정을 씁니다.

아무나 critical-api 를 쓰지 못하게

네임스페이스 batch 에서는 PriorityClass critical-api 를 쓰는 파드를 하나도 만들 수 없게 ResourceQuota no-critical 을 만드세요. 다른 파드(우선순위를 쓰지 않거나 다른 클래스)는 그대로 만들 수 있어야 합니다.

PriorityClass 는 클러스터 전체의 객체라 누구나 이름만 적으면 씁니다. 공식 문서가 권하는 울타리는 ResourceQuota 의 scopeSelector(scopeName PriorityClass)입니다 — 그 클래스를 쓰는 파드만 세는 할당량을 pods: "0" 으로 둡니다. scopeSelector 를 빼면 네임스페이스의 모든 파드가 막힙니다. kubectl -n batch run x --image=... --dry-run=server 로 두 경우를 확인하세요.

Pending 셋, 이유 셋

kx-incident sched-pending 을 실행하면 네임스페이스 triage 에 Pending 파드 셋(pending-a·pending-b·pending-c)이 생깁니다. 각각의 원인을 아래 참고의 코드에서 골라 /root/kx/sched/pending.txt 에 <파드> <코드> 꼴로 한 줄씩 적으세요. 파드는 고치지 않습니다.

스케줄러가 남긴 FailedScheduling 메시지가 노드마다의 이유를 셉니다 — 0/2 nodes are available: .... 메시지에 PVC 가 나오면 PVC 쪽(kubectl -n triage describe pvc)을 한 번 더 보세요. 코드 이름이 메시지의 낱말과 똑같지는 않습니다.