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

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

OOMKilled·Evicted·FailedCreate — 세 갈래의 죽음

TT Lab 에서 이어서 보기

한 줄 요약

컨테이너가 죽는 길은 세 갈래입니다. 자기 상한(limits)을 넘으면 커널이 그 cgroup 안에서 죽이고(OOMKilled), 노드 전체가 모자라면 kubelet 이 골라서 내보내고(Evicted), 할당량을 넘으면 API 서버가 처음부터 만들지 않습니다(FailedCreate). 셋은 진단하는 자리도, 고치는 손잡이도 다릅니다.

왜 이게 필요했나

요청(requests)과 상한(limits)은 흔히 「대충 넉넉하게」 적힙니다. 요청은 스케줄러가 자리를 셀 때 쓰는 숫자이고, 상한은 커널이 cgroup 에 거는 벽입니다. 둘 다 실제 사용량과 어긋나 있으면 장애는 엉뚱한 곳에서 납니다 — 노드는 한가한데 컨테이너가 재시작을 되풀이하거나, 요청을 작게 적은 파드가 압박 때 가장 먼저 쫓겨나거나, 할당량 때문에 배포가 조용히 반쯤만 됩니다. CoreDNS 에 메모리 상한 100Mi 를 걸어 두었다가 질의 폭주 때 OOM 이 반복되며 클러스터 DNS 가 통째로 멈춘 한 이커머스 회사의 공개 포스트모템(2019)이 대표적입니다.

어떻게 동작하나

QoS 는 커널 점수가 됩니다. 요청=상한이면 Guaranteed, 요청이 없으면 BestEffort, 나머지는 Burstable 입니다. kubelet 은 이 클래스를 컨테이너 프로세스의 oom_score_adj 로 옮깁니다. 이 실습의 node01 에서 읽은 값은 Guaranteed −997, BestEffort 1000, 요청 256Mi 인 Burstable 866, 요청 32Mi 인 Burstable 984 였습니다 — 공식 문서의 식 1000 − 1000×요청/노드 메모리(1902MiB 노드)와 맞습니다. 점수가 높을수록 노드 전체가 메모리를 잃었을 때 커널이 먼저 죽입니다.

상한을 넘으면 노드가 멀쩡해도 죽습니다. 실제로 160MiB 를 쓰는 프로그램에 상한 128Mi 를 걸자 커널이 Memory cgroup out of memory 로 죽였고, 파드에는 OOMKilled, 종료 코드 137 이 남았습니다. cgroup v2 에서 kubelet 은 memory.oom.group 을 켜 두어 컨테이너 안의 프로세스를 한꺼번에 죽입니다(실측 dmesg). 사용량을 재는 정확한 자리는 그 컨테이너 cgroup 의 memory.peak 입니다 — 측정값 161MiB.

축출은 kubelet 의 판단입니다. kubelet 은 memory.available 같은 신호를 주기적으로 재다가 임계값 아래로 내려가면 파드를 하나씩 내보냅니다. 기본 임계값은 memory.available<100Mi 인데, 이 실습에서 그 값을 400Mi 로 고치며 하나만 적었더니 디스크·inode 임계값이 모두 사라졌습니다 — kubelet 이 실제로 쓰는 설정(configz)에 memory.available 하나만 남았습니다(공식 문서도 같은 경고를 합니다). 다섯 개를 모두 적은 뒤 메모리를 초당 20MiB 씩 늘리는 BestEffort 파드를 띄우자, 56초쯤 사용 가능량이 318MiB 일 때 그 파드가 축출되었습니다. 메시지는 「Threshold quantity: 400Mi, available: 318244Ki. Container hog was using 1028628Ki, request is 0, has larger consumption of memory」. 노드에는 MemoryPressure=True 와 node.kubernetes.io/memory-pressure:NoSchedule 테인트가 붙었고, Guaranteed 파드들은 재시작 없이 남았습니다.

순서는 QoS 가 아닙니다. 공식 문서는 kubelet 이 ① 사용량이 요청을 넘었는가 ② 우선순위 ③ 요청을 넘은 양 순으로 고른다고 적고, QoS 클래스로 순서를 정하지 않는다고 따로 밝힙니다(QoS 문서의 「BestEffort → Burstable → Guaranteed」 서술은 대개 결과가 그렇게 된다는 뜻입니다). 그래서 우선순위가 높은 BestEffort 파드가 요청을 넘은 우선순위 0 의 Burstable 보다 나중에 나갈 수 있습니다. 요청을 실제 사용량 이상으로 적어 둔 파드는 ①에서 빠집니다 — 요청을 맞추는 것이 가장 싼 보험입니다.

In-place Pod Resize(1.35 GA). 파드의 CPU·메모리를 resize 하위 리소스로 바꾸면 파드를 다시 만들지 않습니다. 이 실습에서 CPU 를 200m→400m 로 바꾸자 UID·재시작 횟수 그대로 cgroup 의 cpu.max 가 40000 100000 이 되었습니다. 메모리는 resizePolicy 에 따라 컨테이너를 재시작할 수 있습니다.

현장에서 만나는 모습

할당량에 막힌 Deployment 는 Pending 파드를 남기지 않습니다. 화면에는 「2/3」 만 보이고, 이유는 ReplicaSet 의 이벤트(exceeded quota: team-a-quota, requested: requests.memory=256Mi, used: 512Mi, limited: 512Mi)에만 있습니다. 노드 압박 축출은 PDB 와 종료 유예를 무시합니다(hard 임계값) — 그래서 압박이 잦은 노드는 PDB 로 지키는 서비스도 한꺼번에 잃을 수 있고, 그 해법은 PDB 가 아니라 요청을 실제에 맞추는 것입니다.

다음 실습에서 할 것

세 파드의 QoS 와 OOM 점수를 노드에서 직접 읽고, 재시작을 되풀이하는 보고서 생성기의 실제 사용량을 재어 요청·상한을 고칩니다. 할당량에 막힌 배포를 할당량을 건드리지 않고 풀고, 축출 임계값을 바르게 고친 뒤 실제 압박에서 누가 나가는지 봅니다. 마지막으로 CPU 를 재시작 없이 바꾸고, 축출 순서를 직접 매겨 봅니다.