CPU·메모리 누수 판별 · 메모리를 읽는다 · 이론
RSS·VSZ·PSS 가 각각 재는 것
한 줄 요약
VSZ 는 약속, RSS 는 실제로 물린 물리 메모리, PSS 는 공유분을 나눠 가진 몫이다. 파드 여러 개의 RSS 를 더하면 실제보다 많이 나온다 — 그때 봐야 할 것이 PSS 다.
왜 이게 필요했나
"메모리 누수 같다" 는 신고의 절반은 누수가 아니다. 프로세스가 커진 이유는 여러 가지다.
- 진짜 누수: 해제하지 않은 할당이 계속 쌓인다
- 캐시 증가: 애플리케이션이 의도적으로 캐시를 채운다(도달하면 멈춘다)
- 힙 단편화: 해제는 했는데 OS 에 돌려주지 못했다
- 할당자 특성: glibc malloc 은 작은 블록을 OS 에 잘 반환하지 않는다
넷은 대처가 다르다. 구분하지 않고 "메모리를 늘리자" 로 가면 며칠 뒤 같은 일이 반복된다.
어떻게 동작하나
한 프로세스의 실제 사용량은 smaps_rollup 한 줄이 가장 정확하다.
grep -E '^(Rss|Pss|Private_Dirty|Swap):' /proc/<PID>/smaps_rollup| 항목 | 뜻 |
|---|---|
| Rss | 물리 메모리에 올라온 전체(공유 라이브러리 포함) |
| Pss | 공유분을 사용자 수로 나눈 몫 — 합계를 낼 때 쓸 유일한 값 |
| Private_Dirty | 이 프로세스만 쓰는, 파일로 되돌릴 수 없는 부분 — 누수를 볼 때 이것 |
누수 판별은 한 시점의 값이 아니라 기울기로 한다. 부하를 일정하게 준 상태에서 Private_Dirty 를 몇 분 간격으로 찍어 단조 증가하면 누수 쪽이고, 어느 선에서 평평해지면 캐시 쪽이다.
흔한 착각
VSZ 를 보고 놀라는 것. 64비트 JVM 이나 Go 런타임은 VSZ 로 수십 GB 를 잡는다. 주소 공간을 예약했을 뿐 물리 메모리가 아니다. VSZ 는 거의 항상 무시해도 된다.
RSS 를 더하는 것. 컨테이너 10개가 같은 libc 를 쓰면 그 페이지는 물리적으로 하나인데 RSS 에는 10번 잡힌다. 합계는 PSS 로 낸다.
free -h 의 free 가 작다고 걱정하는 것. 리눅스는 남는 메모리를 전부 페이지 캐시로 쓴다. 봐야 할 값은 available 이다.
실무에서 진짜 중요한 것
컨테이너는 호스트가 아니라 cgroup 한도로 죽는다.
cat /sys/fs/cgroup/memory.max # 한도cat /sys/fs/cgroup/memory.current # 지금cat /sys/fs/cgroup/memory.events # oom_kill 횟수memory.current 에는 페이지 캐시도 들어간다. 그래서 파일을 많이 읽는 프로세스는 실제 힙이 작아도 한도에 닿는다. memory.stat 의 anon(익명 메모리)과 file(캐시)을 나눠 봐야 진짜 원인이 보인다. OOM 킬은 커널이 하는 일이라 애플리케이션 로그에 아무것도 안 남는다 — memory.events 의 oom_kill 이 늘었는지가 유일한 증거인 경우가 많다.