110. OOM (Out of Memory) Killed - 컨테이너 강제 종료와 메모리 한계 통제
⚠️ 이 문서는 쿠버네티스 클러스터에서 자바(Java)로 짠 웹 서버 파드(Pod)가 트래픽을 신나게 받다가 어느 순간 갑자기 모니터에서 빨간 불빛을 내뿜으며 픽 하고 죽어버리고
CrashLoopBackOff의 무한 지옥에 빠지는, **클라우드 엔지니어들이 밤잠을 설치며 가장 많이 마주하는 공포의 빨간 글씨 "너한테 허락된 메모리(RAM) 밥그릇 용량을 다 처먹고 선을 넘었기 때문에 리눅스 커널(심장)이 널 전기톱으로 썰어버렸다"는 사형 선고인 'OOM Killed'**를 다룹니다.
핵심 인사이트 (3줄 요약)
- 본질: 쿠버네티스의 자원 통제 선언(Limits)을 위반한 파드에 대한 리눅스 OS(cgroups)의 즉각적이고 무자비한 암살(Kill) 조치다.
- 가치: 이 잔인한 사형 집행(OOM Killed)이 없다면, 미친 파드 하나가 서버(Node)의 램 100GB를 끝도 없이 다 빨아먹어 결국 그 물리 서버에 타고 있던 다른 착한 파드들 수십 개와 Kubelet(관리자)마저 다 같이 숨이 막혀 단체로 질식사(Node Out of Memory)해 버리는 끔찍한 동반 자살을 막아주는 최후의 방폭벽이다.
- 기술 체계: 개발자가 yaml에 적어둔
resources.limits.memory수치가 족쇄가 되며, 이 수치를 넘는 찰나의 순간 리눅스 커널의 **OOM Killer**가 튀어나와 9번 시그널(SIGKILL)로 파드의 목을 썰어버리고 상태 창에OOMKilled딱지를 붙여버린다.
Ⅰ. 폭식증 파드와 resources.limits의 족쇄
파드에게 무한리필 뷔페를 주면, 배가 터질 때까지 먹다 식당을 망친다.
- 상태 불일치: CPU와 RAM의 결정적 차이:
- 개발자가 yaml 파일에
resources.limits.cpu: 500m(반 개),memory: 512Mi라고 천장(제한선)을 걸어놨다. - 트래픽이 폭주해서 파드가 할당받은 CPU 한도를 넘으려 한다. K8s는 CPU를 뺏어 죽이지 않는다. 그냥 파드의 연산 속도를 멱살 잡고 늦춰버린다 (CPU Throttling, 렉 걸림). 고객 화면은 느리게 뜨지만 파드는 살아있다.
- 하지만 **메모리(RAM)**는 다르다. 파드가 512Mi를 꽉 채워 먹고 513Mi째 메모리를 달라고 입을 벌리는 순간! 리눅스는 자비 없이 전기톱으로 이 파드의 뇌(Process)를 1초 만에 썰어버려 즉사시킨다. 이것이 바로 OOM Killed의 무자비함이다.
- 개발자가 yaml 파일에
- 왜 죽여야만 하는가? (동반 자살의 공포):
- 만약
limits를 안 걸어두거나, 리눅스가 이 폭식증 파드를 안 죽이고 봐줬다면? - 자바(Java) 앱이 메모리 누수(Memory Leak) 버그로 물리 노드 서버의 램 전체(32GB)를 혼자 다 쳐먹는다.
- 서버의 램이 0이 되는 순간, 이 노드에 떠 있던 다른 회사의 정상적인 파드 10개도 램을 못 받아서 픽픽 쓰러지고, 심지어 노드를 관리하는 심장인
kubelet마저 기절해서 그 32GB짜리 노드 서버 기계 자체가 먹통(NotReady)이 되는 최악 노드 연쇄 붕괴가 터지기 때문이다.
- 만약
📢 섹션 요약 비유: CPU 초과는 고속도로 1차선만 쓰라고 했는데 차가 너무 몰린 상황입니다. 경찰(K8s)은 차들을 죽이지 않고 그냥 시속 10km로 서행(Throttling)하게 만들어 차가 늦게 도착할 뿐입니다. RAM 초과는 식당에서 밥그릇(512MB) 1개만 먹으라고 했는데 손님이 밥통 전체를 훔쳐 먹으려는 상황입니다. 손님이 밥통을 다 먹으면 식당에 있는 다른 100명의 손님이 굶어 죽습니다(노드 붕괴). 경찰(OOM Killer)은 밥그릇 1개를 넘기는 순간 그 손님의 머리통을 즉시 저격(Kill)해 버리고 식당 밖으로 시체를 던져버려 다른 손님들을 살리는 냉혹한 식량 통제법입니다.
Ⅱ. OOM Killer의 타겟팅 1순위: 누가 먼저 죽는가?
식량이 바닥나면 10명 중 누구부터 절벽으로 밀어 떨어뜨릴 것인가.
- 노드 레벨의 OOM (Node OOM):
- 파드에
limits를 넉넉하게 10GB 줬다. 근데 10GB짜리 파드 4개가 램 32GB짜리 노드(물리 서버)에 몰려 들어갔다. - 4놈이 신나게 밥을 먹다가 다 합쳐서 물리 서버의 총량인 32GB를 뚫어버리는 순간이 왔다 (Overcommit 상황).
- 리눅스 커널의 사신인 **
OOM Killer**가 낫을 들고 노드 전체를 스캔하며 "누굴 제일 먼저 찔러 죽여야 내(노드)가 살까?" 타겟팅 점수(oom_score_adj)를 매기기 시작한다.
- 파드에
- 사신의 3단계 우선순위 (QoS Class):
- K8s는 파드를 만들 때 밥그릇 설정(
requests,limits)에 따라 3개의 계급(QoS)을 부여하며, 이 계급표에 따라 죽는 순서가 100% 결정된다. - 1순위 암살 대상 (BestEffort 흙수저):
requests와limits를 아예 안 적은 파드. 제일 만만하므로 노드 램이 쪼들리면 사신이 0.1초 만에 제일 먼저 목을 쳐버린다. - 2순위 암살 대상 (Burstable 평민):
requests(1G)보다limits(2G)를 더 크게 잡아놔서 고무줄처럼 램을 늘려 쓰는 얌체 파드들. 흙수저가 다 죽고도 모자라면 두 번째로 썰려 나간다. - 절대 생존권 (Guaranteed 귀족):
requests(2G)와limits(2G)숫자를 완벽히 똑같이 꽉 채워 적어둔 철밥통 파드. 사신이 가장 늦게 건드리거나 끝까지 보호해 주는 VIP들이다. (진짜 중요한 DB 서버는 무조건 이 Guaranteed 세팅으로 묶어둬야 한다.)
- K8s는 파드를 만들 때 밥그릇 설정(
📢 섹션 요약 비유: 타이타닉호(물리 노드)가 가라앉고 있습니다. 구명보트(RAM) 자리가 모자랍니다. 사신(OOM Killer)은 총을 들고 승객을 바다로 던집니다. 1순위로 바다에 던져지는 놈(BestEffort)은 "나 밥 안 먹어도 돼"라며 티켓(resources) 옵션을 안 적고 무임승차한 놈들입니다. 2순위(Burstable)는 "평소엔 1그릇 먹고 삘받으면 2그릇 먹을게"라고 여지(Limits)를 둔 얌체들입니다. 가장 끝까지 구명보트에 살아남는 귀족(Guaranteed)은 "난 무조건 2그릇 정가 다 냈고, 2그릇만 딱 먹 거다!"라고 requests와 limits를 똑같이 명시한 정직한 1등석 승객입니다. DB 서버는 무조건 귀족 티켓을 끊어줘야 폭풍우 속에서도 살아남습니다.
Ⅲ. OOM Killed 대처법: 무엇을 고쳐야 하는가?
파드를 무한 재부팅시키는 악순환의 고리를 끊어라.
- 임시 땜빵:
limits상향 조정:kubectl describe pod를 쳤더니 빨간색Reason: OOMKilled가 찍혀있다.- 가장 쉬운 방법은 yaml 파일의
resources.limits.memory숫자를512Mi에서1Gi로 무식하게 올려버리고 다시 배포하는 것이다. - 하지만 이건 진통제일 뿐이다. 메모리 누수 버그라면 1시간 뒤에 1GB도 다 퍼먹고 또 터진다.
- 근본적 수술: Java JVM 힙 메모리 (Heap Sizing) 튜닝 $\star$:
- OOM의 90%는 백엔드 Java (Spring Boot) 컨테이너에서 터진다.
- 자바 뱃속에 있는 거대한 공룡인 JVM(가상머신)은 멍청해서 자기가 컨테이너 안에 갇힌 줄 모른다. 컨테이너 램은
512MB밖에 안 주어졌는데, JVM은 바깥쪽 32GB짜리 노드 서버가 다 자기 건 줄 착각하고 램을 1GB씩 마구 퍼먹다가 512MB 선을 넘어 K8s 사신에게 대가리를 썰린다. - 해결책: 도커 이미지를 구울 때 옵션(
-XX:MaxRAMPercentage=75.0또는-Xmx)을 강력하게 걸어준다. "야 JVM 공룡아! 너한테 허락된 램(512MB) 중에 딱 75% 까지만 밥그릇(Heap)으로 쓰고 그 이상은 절대 먹지 마!"라고 내부에서 목줄을 꽉 채워야만 파드가 OOM 암살을 피하고 평생 쌩쌩하게 돌아간다.
📢 섹션 요약 비유: 파드가 OOMKilled를 맞고 죽는 건, 작은 50cm 어항(컨테이너 Limits) 안에 상어(Java JVM)를 키우는 짓입니다. 상어는 어항 벽을 인지 못 하고 미친 듯이 커지려(메모리 할당) 하다가 어항 유리를 깨버리고 즉사합니다. 임시 처방은 어항을 100cm(Limits 상향)로 키워주는 거지만 결국 상어는 또 커져서 유리를 깹니다. 근본적 해결책은 상어의 유전자(Java 힙 메모리 옵션)를 조작하여 "넌 평생 40cm 이상 자라지 못하는 금붕어 체질이다(-Xmx 옵션)"라고 내부에서 성장 락(Lock)을 걸어버려, 평생 50cm 어항 안에서 안전하게 헤엄치며 살게 만드는 최적화 튜닝입니다.