쿠버네티스 전문가 — 장애를 진단하고 클러스터를 지킨다
자원·QoS·OOM·축출 — 누가, 왜, 어떤 순서로 죽는가
목표
메모리 2GiB 짜리 워커(node01)에서 QoS 클래스가 커널의 OOM 점수로 어떻게 옮겨지는지 읽고, 상한에 걸려 죽는 컨테이너(OOMKilled)와 할당량에 막혀 만들어지지도 않는 파드를 진단합니다. kubelet 의 축출 임계값을 바르게 고친 뒤 실제 메모리 압박을 일으켜 누가 축출되는지 보고, 마지막으로 CPU 를 파드 재시작 없이 바꿉니다.
왜 중요한가
「파드가 죽었다」 에는 적어도 세 가지가 섞여 있습니다. 컨테이너가 자기 상한을 넘어 커널에게 죽는 것(OOMKilled, 노드는 멀쩡함), 노드 전체가 모자라 kubelet 이 골라 내보내는 것(Evicted), 처음부터 만들어지지 못한 것(할당량). 원인이 다르면 처방이 다릅니다 — 상한을 올릴지, 요청을 올릴지, 임계값을 바꿀지, 할당량 안으로 줄일지. 그리고 축출 순서는 흔히 믿는 것과 달리 QoS 클래스가 아니라 「요청을 넘었는가 → 우선순위 → 넘은 양」 입니다. 요청(requests)을 실제 사용량에 맞추는 일이 왜 용량 관리의 출발점인지를 이 실습에서 숫자로 봅니다.
단계
- 네임스페이스
shop의 파드qos-a·qos-b·qos-c(모두 node01)마다 QoS 클래스와, node01 커널에서 그 컨테이너 프로세스의oom_score_adj를 읽어/root/kx/res/qos.txt에<파드> <QoS> <oom_score_adj>꼴로 한 줄씩 적으세요. kx-incident res-oom을 실행하면shop/report가 재시작을 되풀이합니다. 원인을 확인하고, 상한을 잠시 넉넉히(예: 512Mi) 올려 그 컨테이너가 실제로 쓰는 메모리를 node01 의 cgroup(memory.peak)에서 재세요. 잰 값을/root/kx/res/report-peak.txt에peak_mib=<MiB 정수>로 적고, Deploymentreport를 요청은 실제 사용량 이상, 상한은 사용량의 1.2배 이상 512Mi 이하 로 고쳐 재시작 없이 돌게 하세요.kx-incident res-quota를 실행하면team-a/api가 3개 중 2개만 뜹니다. 원인을 찾아 할당량(ResourceQuotateam-a-quota)은 그대로 둔 채api3개가 모두 Available 이 되게 하세요.- node01 의 kubelet 이 사용 가능한 메모리가 400Mi 아래로 내려가면 파드를 축출하도록
/var/lib/kubelet/config.yaml의evictionHard를 고치고 kubelet 을 다시 띄우세요. 그러면서 다른 기본 임계값(디스크·inode)이 사라지지 않게 해야 합니다. kx-incident res-pressure를 실행하면 node01 에서 메모리를 초당 20MiB 씩 늘리는 파드가 돕니다. 지켜보며 어느 파드가 왜 축출되는지, 그때 노드에 어떤 조건과 테인트가 붙는지 보고/root/kx/res/eviction.txt에evicted=<파드>,signal=<축출 신호>,taint=<붙은 테인트 키>세 줄을 적으세요. Guaranteed 파드(ledger·qos-c)는 살아남아야 합니다.shop/ledger(Guaranteed, CPU 200m)의 CPU 요청·상한을 파드를 다시 만들지 않고, 컨테이너를 재시작하지 않고 400m 로 바꾸세요. 바뀐 뒤 node01 에서 그 컨테이너 cgroup 의cpu.max를 읽어/root/kx/res/resize.txt에cpu.max=<읽은 값>으로 적습니다.- 어떤 노드에서 메모리 압박이 와 kubelet 이 축출을 시작합니다. 아래 네 파드가 축출되는 순서를
/root/kx/res/rank.txt에 한 줄에 하나씩, 먼저 나가는 것부터 적으세요(파드 이름만).
7단계의 네 파드:
| 파드 | QoS | 우선순위 | 메모리 요청 | 지금 사용량 |
|---|---|---|---|---|
| api-a | Guaranteed(256Mi/256Mi) | 0 | 256Mi | 200Mi |
| batch-b | BestEffort | 100000 | 없음 | 300Mi |
| cache-c | Burstable | 0 | 128Mi | 400Mi |
| web-d | Burstable | 0 | 64Mi | 150Mi |
참고
- 이 터미널은 controlplane 입니다. node01 은
ssh node01. 쿠버네티스 1.36.4(kubeadm), cgroup v2, node01 은 2 vCPU·2GiB(할당 가능 약 1.47GiB)입니다. shop네임스페이스의 바탕 파드:qos-a(요청 없음),qos-b(요청 256Mi·상한 512Mi),qos-c(요청=상한),ledger(Guaranteed, 웹),cache(요청 32Mi, 실제 64MiB 사용). 모두 node01 에 있습니다.- 메모리를 쥐는 프로그램은
dd if=/dev/zero bs=<N>M count=1 | sleep …입니다. dd 가 N MiB 버퍼를 채운 채 파이프에 막혀 그 메모리를 놓지 않습니다. - 5단계는 실제로 node01 의 메모리를 바닥 가까이 씁니다. 1분 안팎이면 끝나고, 그동안 node01 의 셸이 느려질 수 있습니다.
- 세션이 끝나면 VM 둘과 작업 파일이 함께 사라집니다.
세 파드의 QoS 와 커널이 매긴 OOM 점수를 읽는다
네임스페이스 shop 의 파드 qos-a·qos-b·qos-c(모두 node01)마다 QoS 클래스와, node01 커널에서 그 컨테이너 프로세스의 oom_score_adj 를 읽어 /root/kx/res/qos.txt 에 <파드> <QoS> <oom_score_adj> 꼴로 한 줄씩 적으세요.
QoS 는 kubectl get pod <이름> -o jsonpath='{.status.qosClass}'. 프로세스는 node01 에서 crictl ps --label io.kubernetes.pod.name=<파드> 로 컨테이너를 찾고 crictl inspect <ID> | jq .info.pid 로 pid 를 얻어 /proc/<pid>/oom_score_adj 를 봅니다. Burstable 의 값은 메모리 요청이 노드 메모리에서 차지하는 비율로 정해집니다 — 계산이 맞는지도 확인해 보세요.
보고서 생성기가 계속 재시작된다
kx-incident res-oom 을 실행하면 shop/report 가 재시작을 되풀이합니다. 원인을 확인하고, 상한을 잠시 넉넉히(예: 512Mi) 올려 그 컨테이너가 실제로 쓰는 메모리를 node01 의 cgroup(memory.peak)에서 재세요. 잰 값을 /root/kx/res/report-peak.txt 에 peak_mib=<MiB 정수> 로 적고, Deployment report 를 요청은 실제 사용량 이상, 상한은 사용량의 1.2배 이상 512Mi 이하 로 고쳐 재시작 없이 돌게 하세요.
kubectl -n shop describe pod -l app=report 의 Last State 에 Reason·Exit Code 가 있습니다(137 = SIGKILL). 상한(limits)을 넘으면 커널이 그 cgroup 안에서 프로세스를 죽입니다 — 노드에 메모리가 남아 있어도요. 사용량은 node01 에서 컨테이너 pid 의 /proc/<pid>/cgroup 이 가리키는 /sys/fs/cgroup/... 디렉터리의 memory.peak·memory.current 입니다. 요청을 사용량보다 작게 두면 다음 단계의 노드 압박 때 이 파드가 축출 후보가 됩니다.
3개 중 2개만 떠 있고 Pending 은 없다
kx-incident res-quota 를 실행하면 team-a/api 가 3개 중 2개만 뜹니다. 원인을 찾아 할당량(ResourceQuota team-a-quota)은 그대로 둔 채 api 3개가 모두 Available 이 되게 하세요.
Pending 파드가 없다면 파드가 아예 만들어지지 않은 것입니다. 파드를 만드는 것은 ReplicaSet 이므로 그쪽 이벤트(kubectl -n team-a describe rs)를 봅니다. kubectl -n team-a describe quota 의 Used/Hard 를 보고, 세 파드가 한도 안에 들어가려면 파드 하나의 요청이 얼마여야 하는지 계산하세요. 할당량을 지우거나 키우는 것은 다른 팀의 몫을 가져오는 일입니다. 요청을 줄이는 롤아웃도 할당량에 걸립니다 — 롤링 업데이트는 기본으로 새 파드를 먼저 하나 더 띄우고(maxSurge), 그것을 0 으로 해도 만들어지지 못한 세 번째 파드가 이미 「사용 불가」 한 칸을 차지하고 있어 옛 파드를 내리지 못합니다. 원하는 수를 잠시 실제 수(2)에 맞춰 보세요.
node01 이 메모리를 400Mi 남기고 지키게 한다
node01 의 kubelet 이 사용 가능한 메모리가 400Mi 아래로 내려가면 파드를 축출하도록 /var/lib/kubelet/config.yaml 의 evictionHard 를 고치고 kubelet 을 다시 띄우세요. 그러면서 다른 기본 임계값(디스크·inode)이 사라지지 않게 해야 합니다.
기본 hard 임계값은 memory.available<100Mi, nodefs.available<10%, imagefs.available<15%, nodefs.inodesFree<5%, imagefs.inodesFree<5% 입니다(Linux). evictionHard 에 하나만 적으면 나머지가 기본값이 아니라 0 이 됩니다 — kubelet 이 실제로 쓰는 값은 kubectl get --raw /api/v1/nodes/node01/proxy/configz | jq .kubeletconfig.evictionHard 로 확인하세요. 100Mi 로는 커널의 OOM 이 kubelet 보다 먼저 오는 일이 많습니다(공식 문서의 알려진 문제).
node01 에 메모리 압박이 온다 — 누가 축출되는가
kx-incident res-pressure 를 실행하면 node01 에서 메모리를 초당 20MiB 씩 늘리는 파드가 돕니다. 지켜보며 어느 파드가 왜 축출되는지, 그때 노드에 어떤 조건과 테인트가 붙는지 보고 /root/kx/res/eviction.txt 에 evicted=<파드>, signal=<축출 신호>, taint=<붙은 테인트 키> 세 줄을 적으세요. Guaranteed 파드(ledger·qos-c)는 살아남아야 합니다.
kubectl get node node01 -w 대신 1, 2초마다 kubectl -n shop get pods 와 kubectl describe node node01 | grep -A6 Conditions 를 보세요. 축출된 파드의 status.message 에 임계값·사용량·요청이 그대로 적힙니다. 축출 신호 이름은 4단계에서 고친 키 그대로입니다.
ledger 의 CPU 를 재시작 없이 두 배로
shop/ledger(Guaranteed, CPU 200m)의 CPU 요청·상한을 파드를 다시 만들지 않고, 컨테이너를 재시작하지 않고 400m 로 바꾸세요. 바뀐 뒤 node01 에서 그 컨테이너 cgroup 의 cpu.max 를 읽어 /root/kx/res/resize.txt 에 cpu.max=<읽은 값> 으로 적습니다.
In-place Pod Resize 는 1.35 에서 GA 가 됐습니다. 파드 spec 의 resources 를 바꾸는 것은 resize 하위 리소스로만 됩니다 — kubectl patch pod ledger --subresource resize --patch '...'. Deployment 를 고치면 새 파드가 만들어집니다(UID 가 바뀜). 결과는 status.containerStatuses[0].resources 와 cgroup 의 cpu.max(할당량 주기 100000µs 당 몇 µs)로 확인합니다.
다음 압박 때 누가 먼저 나가는가
어떤 노드에서 메모리 압박이 와 kubelet 이 축출을 시작합니다. 아래 네 파드가 축출되는 순서를 /root/kx/res/rank.txt 에 한 줄에 하나씩, 먼저 나가는 것부터 적으세요(파드 이름만).
| 파드 | QoS | 우선순위 | 메모리 요청 | 지금 사용량 |
|---|---|---|---|---|
| api-a | Guaranteed(256Mi/256Mi) | 0 | 256Mi | 200Mi |
| batch-b | BestEffort | 100000 | 없음 | 300Mi |
| cache-c | Burstable | 0 | 128Mi | 400Mi |
| web-d | Burstable | 0 | 64Mi | 150Mi |
공식 문서의 순서는 세 열쇠입니다 — ① 사용량이 요청을 넘었는가 ② 우선순위(낮은 쪽이 먼저) ③ 요청을 넘은 양(많은 쪽이 먼저). kubelet 은 QoS 클래스로 순서를 정하지 않는다고 문서가 따로 적어 둡니다. 5단계에서 본 축출 메시지의 「request is …, has larger consumption」 이 ③ 입니다.