free 후 RSS가 줄지 않는 이유: glibc 재사용·mmap·trim 구분

반응형

free(ptr)를 호출한 뒤에도 top이나 ps의 RSS가 그대로인 경우가 있다. 이른바 free RSS 문제를 볼 때 그 자체만으로 memory leak이라고 결론 내릴 수도, allocator가 memory를 정상 반환했다고 단정할 수도 없다.

핵심은 free()가 보장하는 범위와 RSS가 세는 범위가 다르다는 데 있다. 아래 내용은 Linux에서 glibc allocator를 사용하는 process를 기준으로 한다.

free는 allocation의 수명을 끝내는 API다

유효한 pointer를 free()하면 application은 그 allocation을 더 이상 사용하면 안 된다. 이후 같은 주소의 byte를 읽거나 쓰는 것은 undefined behavior다.

allocator 관점에서는 해당 chunk를 다시 allocation할 수 있게 된다. 하지만 그 순간마다 kernel에 page를 반환해야 하는 것은 아니다. arena 안의 free chunk를 보유하면 다음 malloc() 요청에 system call 없이 재사용할 수 있다.

application live allocation
          ↓ free
allocator가 재사용 가능한 chunk
          ↓ 조건이 맞을 때
kernel에서 mapping 제거 또는 page 반환 시도

따라서 free() 뒤에 application live bytes는 감소했지만 allocator footprint와 RSS는 유지되는 상태가 가능하다.

RSS가 줄 수 있는 경로도 있다

“free는 OS에 절대 돌려주지 않는다”도 정확하지 않다.

  • 별도 mmap() mapping으로 관리된 큰 allocation은 free 과정에서 munmap()될 수 있다.
  • arena 끝에 충분한 free space가 모이면 glibc가 top chunk 일부를 trim할 수 있다.
  • allocator는 free page에 madvise() 계열 동작을 적용할 수 있으며, resident page 감소가 나중에 관측될 수 있다.

mmap 경로의 기준과 trim 조건은 고정된 크기 하나가 아니다. glibc version, tunable, arena 상태, thread 수에 따라 달라진다. memory metric을 읽는 시점과 kernel reclaim 상태도 RSS 변화 시점에 영향을 준다.

GNU 확장인 malloc_trim(pad)은 free heap memory를 system에 돌려보내도록 시도한다. 성공적으로 일부를 release했는지를 반환하지만, 호출할 때마다 RSS가 원하는 만큼 줄어든다는 보장은 없다. production에서 주기적으로 호출하기 전에는 allocation latency와 CPU overhead까지 함께 검증해야 한다.

RSS 안의 anon·file·shmem을 나눠 본다

Linux의 /proc/<pid>/statusVmRSSRssAnon, RssFile, RssShmem으로 나눠 보여 준다. 같은 RSS 증가라도 원인 후보가 다르다.

pid=1234

grep -E '^(VmRSS|RssAnon|RssFile|RssShmem|VmSwap):' \
  "/proc/$pid/status"

cat "/proc/$pid/smaps_rollup"

이 명령은 대상 PID의 procfs 파일을 읽을 권한이 있을 때 쓰는 read-only 확인이다. smaps_rollup은 더 자세하지만 읽는 비용이 있으므로 고빈도로 polling하지 않는다.

해석할 때는 다음 단정을 피한다.

  • RssAnon 증가가 곧바로 malloc leak이라는 결론: stack, anonymous mmap, runtime·JIT 영역도 포함될 수 있다.
  • RssFile 증가는 항상 무해하다는 결론: mapped file page도 process working set과 memory pressure에 영향을 준다.
  • system 전체 page cache가 모두 특정 process RSS에 포함된다는 결론: read()로 cache된 file data와 process의 resident file mapping은 같은 개념이 아니다.

leak·retention·fragmentation을 구분한다

현상 확인할 질문
application leak 더 이상 필요 없는 allocation을 code가 계속 참조하는가
allocator retention free된 chunk가 allocator 안에서 재사용을 기다리는가
fragmentation free space는 있지만 요청 형태에 맞게 재사용하기 어려운가
mapped file 증가 어떤 file mapping의 resident page가 늘었는가

RSS 하나로는 네 경우를 분리하기 어렵다. smaps의 mapping별 변화, allocator profiler의 live allocation, 동일 workload 구간의 추이를 함께 본다. allocation stack을 수집하는 tool은 overhead와 민감 정보 노출 가능성이 있으므로 test에서 먼저 검증하고 수집 범위를 제한한다.

변경 전에 세우는 검증 기준

allocator tunable 변경, trim 호출, cache 축소를 적용하기 전에 같은 지표로 전후를 비교한다.

  1. workload와 traffic 구간
  2. application live bytes
  3. RSS·RssAnon과 cgroup memory
  4. allocation latency와 request tail latency
  5. page fault·reclaim·memory pressure

RSS만 줄고 latency가 악화되면 좋은 최적화라고 보기 어렵다. 반대로 RSS가 유지돼도 free chunk가 안정적으로 재사용되고 cgroup pressure가 없다면 당장 leak으로 확정할 근거가 부족하다.

allocator가 memory를 확보하는 경로는 glibc malloc의 brk·mmap 선택, mapping 단위 분석은 /proc/smaps로 메모리를 분해하는 방법, 전체 장애 분석 순서는 리눅스 메모리 분석 흐름으로 이어진다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글