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

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

자원·QoS·OOM·축출 — 누가, 왜, 어떤 순서로 죽는가

TT Lab 에서 이어서 보기

목표

메모리 2GiB 짜리 워커(node01)에서 QoS 클래스가 커널의 OOM 점수로 어떻게 옮겨지는지 읽고, 상한에 걸려 죽는 컨테이너(OOMKilled)와 할당량에 막혀 만들어지지도 않는 파드를 진단합니다. kubelet 의 축출 임계값을 바르게 고친 뒤 실제 메모리 압박을 일으켜 누가 축출되는지 보고, 마지막으로 CPU 를 파드 재시작 없이 바꿉니다.

왜 중요한가

「파드가 죽었다」 에는 적어도 세 가지가 섞여 있습니다. 컨테이너가 자기 상한을 넘어 커널에게 죽는 것(OOMKilled, 노드는 멀쩡함), 노드 전체가 모자라 kubelet 이 골라 내보내는 것(Evicted), 처음부터 만들어지지 못한 것(할당량). 원인이 다르면 처방이 다릅니다 — 상한을 올릴지, 요청을 올릴지, 임계값을 바꿀지, 할당량 안으로 줄일지. 그리고 축출 순서는 흔히 믿는 것과 달리 QoS 클래스가 아니라 「요청을 넘었는가 → 우선순위 → 넘은 양」 입니다. 요청(requests)을 실제 사용량에 맞추는 일이 왜 용량 관리의 출발점인지를 이 실습에서 숫자로 봅니다.

단계

  1. 네임스페이스 shop 의 파드 qos-a·qos-b·qos-c(모두 node01)마다 QoS 클래스와, node01 커널에서 그 컨테이너 프로세스의 oom_score_adj 를 읽어 /root/kx/res/qos.txt 에 <파드> <QoS> <oom_score_adj> 꼴로 한 줄씩 적으세요.
  2. 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 이하 로 고쳐 재시작 없이 돌게 하세요.
  3. kx-incident res-quota 를 실행하면 team-a/api 가 3개 중 2개만 뜹니다. 원인을 찾아 할당량(ResourceQuota team-a-quota)은 그대로 둔 채 api 3개가 모두 Available 이 되게 하세요.
  4. node01 의 kubelet 이 사용 가능한 메모리가 400Mi 아래로 내려가면 파드를 축출하도록 /var/lib/kubelet/config.yaml 의 evictionHard 를 고치고 kubelet 을 다시 띄우세요. 그러면서 다른 기본 임계값(디스크·inode)이 사라지지 않게 해야 합니다.
  5. kx-incident res-pressure 를 실행하면 node01 에서 메모리를 초당 20MiB 씩 늘리는 파드가 돕니다. 지켜보며 어느 파드가 왜 축출되는지, 그때 노드에 어떤 조건과 테인트가 붙는지 보고 /root/kx/res/eviction.txt 에 evicted=<파드>, signal=<축출 신호>, taint=<붙은 테인트 키> 세 줄을 적으세요. Guaranteed 파드(ledger·qos-c)는 살아남아야 합니다.
  6. shop/ledger(Guaranteed, CPU 200m)의 CPU 요청·상한을 파드를 다시 만들지 않고, 컨테이너를 재시작하지 않고 400m 로 바꾸세요. 바뀐 뒤 node01 에서 그 컨테이너 cgroup 의 cpu.max 를 읽어 /root/kx/res/resize.txt 에 cpu.max=<읽은 값> 으로 적습니다.
  7. 어떤 노드에서 메모리 압박이 와 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

참고

세 파드의 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」 이 ③ 입니다.