LabHub
배우기 러닝패스 코스

Diagnosing CPU and Memory Leaks

What RSS, VSZ and PSS Each Measure

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

VSZ 는 약속, RSS 는 실제로 물린 물리 메모리, PSS 는 공유분을 나눠 가진 몫이다. 파드 여러 개의 RSS 를 더하면 실제보다 많이 나온다 — 그때 봐야 할 것이 PSS 다.

Concept map: 파드 여러 개의 RSS 를 더하면 실제보다 많이 나온다 · 합계를 낼 때 쓸 유일한 값 · 누수를 볼 때 이것 · 한 시점의 값이 아니라 기울기

왜 이게 필요했나

"메모리 누수 같다" 는 신고의 절반은 누수가 아니다. 프로세스가 커진 이유는 여러 가지다.

넷은 대처가 다르다. 구분하지 않고 "메모리를 늘리자" 로 가면 며칠 뒤 같은 일이 반복된다.

어떻게 동작하나

한 프로세스의 실제 사용량은 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 이다.

컨테이너의 메모리 한도는 어디서 보나

호스트의 freetop 은 컨테이너에 할당된 몫을 모릅니다. cgroup 을 직접 봅니다.

cat /sys/fs/cgroup/memory.max        # 한도 (max 면 제한 없음)
cat /sys/fs/cgroup/memory.current    # 지금 쓰는 양
cat /sys/fs/cgroup/memory.stat       # 항목별 내역

memory.current 에는 페이지 캐시가 포함됩니다. 파일을 많이 읽는 프로세스는 캐시 때문에 한도에 가까워 보이지만, 압박이 오면 커널이 캐시를 먼저 버리므로 OOM 이 나지 않습니다. 그래서 "메모리를 90% 쓰고 있다" 가 곧 위험은 아닙니다.

진짜로 위험한지는 memory.stat 의 두 줄로 봅니다.

anon      1234567890     ← 익명 메모리. 이건 버릴 수 없다
file       987654321     ← 페이지 캐시. 압박이 오면 버려진다

anon 이 한도에 가까우면 곧 OOM 입니다.

OOM 이 났는지 확인하는 법

파드가 갑자기 재시작했는데 로그에 아무것도 없으면 대개 OOM 입니다. 애플리케이션이 자기가 죽는다는 것을 알 방법이 없어 마지막 말을 남기지 못합니다.

kubectl describe pod <파드> | grep -A3 "Last State"
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137

# cgroup 쪽 기록
cat /sys/fs/cgroup/memory.events
oom 3
oom_kill 1

oom 은 한도에 닿아 회수를 시도한 횟수이고 oom_kill 이 실제로 죽인 횟수입니다. oom 만 늘고 oom_kill 이 0 이면 아직 죽지는 않았지만 계속 압박받고 있다 는 뜻이라, 그때부터 지연이 나빠집니다. 이 신호를 놓치면 "가끔 느리다" 로만 보입니다.

언어별로 무엇을 조절하나

컨테이너 한도를 줄여도 런타임이 그것을 모르면 소용이 없습니다.

런타임 조절할 것 안 하면
JVM -XX:MaxRAMPercentage=75 옛 JVM 은 호스트 메모리 기준으로 힙을 잡는다
Node.js --max-old-space-size=<MB> 기본 힙이 컨테이너 한도보다 클 수 있다
Go GOMEMLIMIT GC 가 늦게 돌아 한도를 넘는다
Python 별도 설정 없음 워커 수(gunicorn -w)로 조절한다

GOMEMLIMIT 은 Go 1.19 부터 있고, 한도의 90% 정도로 두면 GC 가 그 선에서 더 자주 돕니다. 이것 없이 Go 서비스를 좁은 한도에 넣으면 GC 가 여유롭게 굴다가 OOM 을 맞습니다.

실무에서 진짜 중요한 것

컨테이너는 호스트가 아니라 cgroup 한도로 죽는다.

cat /sys/fs/cgroup/memory.max        # 한도
cat /sys/fs/cgroup/memory.current    # 지금
cat /sys/fs/cgroup/memory.events     # oom_kill 횟수

memory.current 에는 페이지 캐시도 들어간다. 그래서 파일을 많이 읽는 프로세스는 실제 힙이 작아도 한도에 닿는다. memory.statanon(익명 메모리)과 file(캐시)을 나눠 봐야 진짜 원인이 보인다. OOM 킬은 커널이 하는 일이라 애플리케이션 로그에 아무것도 안 남는다 — memory.eventsoom_kill 이 늘었는지가 유일한 증거인 경우가 많다.