메모리 진단의 첫 질문은 “얼마나 많이 보이는가”가 아니라 “어떤 계층이 그 숫자를 소유하고 무엇을 포함하는가”입니다. 살아 있는 객체, allocator 보유량, RSS/PSS, cgroup 집계는 서로 다른 관찰이며, 단편화와 누수도 다른 원인입니다.
객체 해제와 페이지 반환 지연
부하가 끝나 살아 있는 객체량은 줄었는데 RSS가 높게 유지될 수 있습니다.
시간 trace를 부하 상승→생존 객체 증가/RSS 증가→부하 하강→생존 객체 감소/RSS plateau→동일 크기 재부하→RSS 재사용으로 기록하면 allocator 보유와 누적 leak을 먼저 구분할 수 있습니다. 매 회차 생존 객체·native·mapping도 함께 증가하면 owner 누수를 의심하고, 객체 수는 plateau인데 특정 size class와 arena만 남으면 반환 지연·단편화를 의심합니다. 단, 강제 trim 뒤 RSS 감소만으로 건강해졌다고 결론 내리지 말고 fault와 p99를 함께 봅니다. 할당자가 다음 요청에 재사용할 공간을 보관하거나 같은 페이지에 작은 생존 객체가 남아 전체 페이지를 반환하지 못할 수 있습니다. RSS만으로 누수라고 단정하면 정상 보관 공간과 실제 참조 누수를 혼동합니다.
RSS에는 관리 힙 외에 스레드 스택·native 할당·공유 라이브러리·파일 매핑도 포함될 수 있습니다. 런타임의 heap 사용량과 OS의 상주 메모리를 같은 값으로 비교하지 않습니다.
메모리 지표별 집계 대상
| 지표 | 대표 의미 | 주의점 |
|---|---|---|
| 생존 객체량 | 현재 도달 가능한 객체 | 불필요한 소유인지 별도 판단 |
| allocator 보유량 | 재사용 블록·arena 포함 | 객체 해제와 OS 반환은 다름 |
| RSS | 프로세스의 상주 매핑 | 공유 페이지가 여러 프로세스에 중복 집계 |
| PSS | 공유 페이지를 참여자별로 나눈 기여 | 실제 cgroup 과금 규칙과 동일하지 않음 |
| 가상 크기 | 주소 공간 예약·매핑 | 모두 RAM에 있는 것은 아님 |
두 프로세스가 같은 100MB 페이지를 공유하고 각각 private 20MB를 가진 단순 모형이면 RSS 합은 240MB, 실제 공유를 한 번만 센 페이지는 140MB입니다. PSS는 각 70MB가 될 수 있습니다. 공유 비율은 변할 수 있고 커널·cgroup의 다른 자원까지 포함한 전체 사용과는 별도입니다.
그림을 글로 읽기
- 살아 있는 객체
- allocator의 빈 블록: 프로세스 안에서 재사용 가능
- 새 객체에 재사용
- OS에 페이지 반환: 정책·페이지 전체 상태에 의존
- 살아 있는 객체 → allocator의 빈 블록: 객체 해제
- allocator의 빈 블록 → 새 객체에 재사용: 재할당
- allocator의 빈 블록 → OS에 페이지 반환: 반환 조건 충족
여유 메모리 합계와 연속 공간
빈 블록 네 개가 각각 256KiB라면 합은 1MiB지만, 연속 700KiB를 요구하는 가변 블록 할당자는 실패할 수 있습니다. 이것이 외부 단편화입니다. 요청 33바이트에 size class 48바이트를 배정해 15바이트가 남는 것은 내부 단편화입니다.
일반 사용자 버퍼는 연속 가상 주소만 필요하고 물리 프레임은 흩어져 있어도 될 수 있습니다. 대형 페이지·일부 장치 버퍼는 더 큰 물리 연속·정렬·고정 조건을 요구할 수 있습니다. 주소 공간 부족과 물리 단편화를 다른 도구로 확인해야 합니다.
size class는 비슷한 크기 블록을 재사용하기 쉽게 하지만 등급 반올림과 풀별 유휴 공간을 만듭니다. 등급을 촘촘히 하면 내부 낭비가 줄어도 메타데이터와 여러 풀의 유휴 공간이 늘 수 있습니다. thread cache·arena에 공간이 묶여 다른 요청에 바로 쓰지 못하는 경우도 있습니다.
대형 페이지의 TLB 이득과 추가 제약
큰 페이지는 한 변환으로 넓은 범위를 덮지만 연속 물리 공간 확보·초기화·compaction 비용과 내부 낭비가 생길 수 있습니다. pinned 페이지 등으로 이동이 제한되면 여유 총량이 충분해도 준비가 어려울 수 있습니다.
THP·명시 huge page·예약·fallback의 실제 정책을 확인하고, 강제 사용으로 첫 fault나 p99가 나빠지지 않는지 봅니다. 일반 가상 연속 할당의 성공을 큰 물리 페이지 확보 가능성의 증거로 쓰지 않습니다.
컨테이너 파일 캐시의 회수·writeback 비용
file cache의 clean·비활성 페이지는 회수 가능할 수 있지만 dirty 데이터는 writeback이 필요하고, 활발한 재사용은 회수 후 다시 I/O를 만듭니다. anonymous 메모리는 객체 수명·swap 정책에 영향을 받습니다. cgroup 버전·메모리 통계·pressure·OOM 사건을 실제 제한과 대조합니다.
호스트에 여유 메모리가 있어도 컨테이너에 허용된 메모리를 넘으면 OOM이 날 수 있습니다. 애플리케이션의 heap만 작게 유지한다고 충분하지 않은 이유입니다. 런타임 밖의 할당, 스택, 파일 캐시와 일시적인 버퍼도 해당 환경의 메모리 집계에 포함될 수 있습니다.
다만 이런 숫자를 무조건 더하면 중복 계산할 수 있습니다. 예를 들어 allocator가 보유한 영역이 이미 heap 지표에 들어 있는지부터 확인해야 합니다. 각 지표의 포함 범위를 맞춘 뒤 cgroup 사용량·압박·OOM 시점과 대조하고, 순간 최대 사용량과 회수 지연을 위한 여유를 둡니다.
대조 부하 기반 누수·단편화 구분
동일한 최종 생존 객체 수를 만들되 할당 크기와 수명 순서를 다르게 합니다. 큰 객체 사이에 작은 장수 객체를 남긴 경우와 같은 수명을 묶어 해제한 경우를 비교하면 페이지 반환과 단편화 차이를 볼 수 있습니다. 실제 누수는 더 이상 필요 없는 owner 경로가 객체를 붙잡는지 확인해야 합니다.
부하 상승·안정·하강을 반복하며 생존 객체·native·mapping·allocator·RSS/PSS를 기록합니다. plateau인지 매 회차 누적 증가인지 구분합니다. 강제 trim으로 RSS가 줄어도 다음 요청의 page fault·할당 비용이 늘 수 있으므로 사용자 지연까지 대조해야 합니다.
메모리 풀은 낭비를 없애는 것이 아니라 재사용·예측성과 보유 공간을 교환합니다. 총량 하나 대신 영역·수명·반환 경로를 확인하는 것이 진단의 출발점입니다.