리눅스 장애 대응 · 메모리와 OOM · 이론
OOM Killer 는 누구를 고르는가
한 줄 요약
OOM Killer 의 점수는 top 의 VIRT 가 아니라 RSS 기준이고, oom_score_adj 는 총 메모리의 1000분의 1을 단위로 하는 가감점이다. 그리고 종료 코드 137 은 "SIGKILL 로 죽었다"는 뜻일 뿐 OOM 을 뜻하지 않는다.
왜 이게 필요했나
"서비스가 죽었는데 애플리케이션 로그에 에러가 없습니다. 마지막 줄이 정상 요청 처리입니다."
이 신고에서 로그가 깨끗하다는 것 자체가 단서다. SIGKILL 은 잡을 수 없고 핸들러도 돌지 않으므로 애플리케이션은 아무것도 남길 기회가 없다. 흔적은 애플리케이션이 아니라 커널이 남긴다.
여기서 흔한 오해가 네 가지 있다.
1. 137 = OOM 이 아니다. 128+9, 즉 SIGKILL 로 죽었다는 뜻일 뿐이다. liveness probe 실패 후 유예 만료도, 수동 종료도, 노드 종료도 전부 137 이다.
2. 범인은 VIRT 가 아니다. 64GB 를 mmap 으로 예약만 하고 200MB 만 실제로 쓰는 프로세스는 후보조차 아니다.
3. 스왑을 넣으면 OOM 이 안 난다 - 절반만 맞다. 막는 게 아니라 늦추고, 그 구간의 스래싱이 즉사보다 나쁠 수 있다.
4. 컨테이너가 OOMKilled 인데 limit 을 안 넘었을 수 있다. 노드 전역 OOM 이 컨테이너 안 프로세스를 죽여도 같은 표시가 나온다. 이 경우 limit 을 올려도 재발한다.
어떻게 동작하나
점수 계산은 대략 이렇다.
points = rss + swapents + (pgtables / PAGE_SIZE) # 단위: 페이지adj = oom_score_adj * (totalpages / 1000)badness = points + adj즉 oom_score_adj 200 은 64GiB 머신에서 약 12.8GiB 상당의 가점이다. RSS 2GB 프로세스가 이 가점으로 11GB JVM 을 제칠 수도 있다. 반대로 -1000 은 특별하다 - 점수 계산에서 아예 제외되어 절대 선택되지 않는다. 배포판이 sshd 에 이 값을 주는 이유다.
언제 발동하나. "메모리가 0 이 됐을 때"가 아니라 "회수(reclaim)를 시도했는데 진전이 없을 때" 다. 그래서 스왑이 있으면 회수가 진전을 만들어 OOM 이 미뤄지고, 그 구간에 스래싱이 생긴다.
전역 OOM 과 cgroup OOM 은 다른 사건이다. dmesg 리포트 마지막의 oom-kill: 줄에 있는 constraint 가 이를 가른다. CONSTRAINT_NONE 은 전역, CONSTRAINT_MEMCG 는 cgroup 이다. cgroup OOM 일 때만 memory: usage/limit/failcnt 줄이 함께 나온다. 이 한 줄이 대응을 완전히 가른다 - 전역이면 노드 전체 배치를 봐야 하고, cgroup 이면 그 컨테이너의 한도와 실사용량 문제다.
컨테이너의 진짜 한도는 free 가 아니다. 컨테이너 안에서 free -m 은 호스트 전체를 보여 준다. 실제 상한은 /sys/fs/cgroup/memory.max 이고, 현재 사용량은 memory.current 다. 그리고 memory.events 의 세 카운터가 상태를 알려 준다 - max 는 상한에 부딪혀 회수한 횟수, oom 은 회수 실패로 OOM 경로에 진입한 횟수, oom_kill 은 실제로 죽인 횟수. max 만 크고 oom_kill 이 0 이면 아직 버티는 중이라는 뜻이라, 사후 부검보다 이걸 잡는 편이 낫다.
현장에서 만나는 모습
MemFree 와 MemAvailable 은 다르다. 리눅스는 남는 메모리를 페이지 캐시로 쓴다. MemFree 가 작은 것은 정상이고, 판단 기준은 회수 가능한 캐시까지 계산에 넣은 MemAvailable 이다. free 명령에서도 free 열이 아니라 available 열을 본다.
RSS 를 합산하면 실제보다 커진다. 여러 프로세스가 공유하는 라이브러리 페이지를 RSS 는 각각 전부 센다. 워커 20개짜리 웹 서버에서 단순 합산하면 실제의 몇 배가 나온다. 용량 산정에는 공유 페이지를 참조 프로세스 수로 나눈 PSS(/proc/PID/smaps_rollup)를 쓴다.
쿠버네티스의 request 는 스케줄러를 속이는 숫자가 아니라 생존 순위다. Burstable 파드의 oom_score_adj 는 request 비율로 계산되므로, request 를 작게 적고 많이 쓰는 파드가 노드 압박 시 가장 먼저 죽는다.
다음 실습에서 할 것
/proc/meminfo 에서 MemTotal 과 MemAvailable 을 직접 읽고 둘의 차이를 확인한다. 메모리를 실제로 붙잡는 프로세스를 띄워 VmSize 와 VmRSS 가 어떻게 다른지 보고, oom_score_adj 를 올려 점수가 실제로 변하는 것을 관찰한다. 마지막에는 RSS 상위 프로세스를 뽑는 도구와 임계 초과를 감시하는 스크립트를 만든다.