eBPF로 사용자 공간 메모리 할당 추적하기: malloc 호출과 누수 구분

반응형

/proc/<PID>/smaps로 익명 메모리가 늘어나는 매핑을 찾았다면 다음 질문은 “어느 호출 경로가 할당을 만들었는가?”다. 이때 eBPF 기반 도구로 사용자 공간 allocator 함수에 uprobe를 붙여 호출 스택과 요청 크기를 관찰할 수 있다.

중요한 경계부터 말하면, malloc 호출 횟수만 세어서는 메모리 누수를 증명할 수 없다. 반환된 주소와 크기를 기록하고 대응하는 free를 제거한 뒤, 일정 시간 남아 있는 outstanding allocation을 봐야 누수 후보에 가까워진다.

smaps와 할당 추적이 답하는 질문

도구 답하기 좋은 질문
/proc/PID/smaps 어떤 매핑과 메모리 성격이 커졌는가?
heap profiler 언어·런타임이 아는 객체와 할당 지점은 어디인가?
eBPF uprobe 실행 중인 프로세스에서 어떤 allocator 함수와 호출 스택이 관찰되는가?

smaps에서 Anonymous가 늘었다고 곧바로 malloc만 추적하는 것도 성급하다. Java·Go처럼 자체 런타임과 GC를 쓰거나 jemalloc·tcmalloc을 쓰는 프로세스는 glibc malloc이 핵심 경로가 아닐 수 있다.

먼저 대상과 allocator를 확인한다

pid=1234
readlink -f "/proc/$pid/exe"
cat "/proc/$pid/maps" | grep -E 'libc|jemalloc|tcmalloc'

확인할 것은 다음과 같다.

  • 정확한 PID와 실행 파일
  • glibc, jemalloc, tcmalloc 또는 언어 런타임 중 무엇이 할당하는지
  • 심볼이 제거됐는지와 사용자 스택을 해석할 디버그 심볼이 있는지
  • 컨테이너·PID namespace에서 어느 호스트 PID에 붙어야 하는지

bpftrace로 요청한 할당량을 빠르게 보기

bpftrace는 라이브러리 이름을 /etc/ld.so.cache에서 해석할 수 있다. 다음 예시는 특정 PID의 malloc 진입점에서 호출 스택별 요청 바이트 합계를 집계한다.

pid=1234
sudo bpftrace -p "$pid" -e '
uprobe:libc:malloc
{
  @[ustack] = sum(arg0);
}'

arg0malloc(size_t size)의 첫 번째 인자다. 이 수치는 호출자가 요청한 누적 크기이지 현재 살아 있는 메모리도, RSS 증가량도 아니다. allocator가 내부적으로 반올림한 크기, 재사용한 arena, 실제 page fault와도 다를 수 있다.

호출 수가 필요하면 sum(arg0) 대신 count()를 쓸 수 있다. 그러나 높은 빈도의 할당에서 모든 사용자 스택을 수집하면 부하가 커질 수 있으므로 짧은 시간과 한 PID로 범위를 제한한다.

outstanding allocation은 BCC memleak으로 보기

BCC의 memleak 도구는 사용자 공간 할당과 해제 함수를 함께 추적하고, 아직 해제되지 않은 주소를 스택별로 집계한다.

sudo memleak -p 1234 5 6

위 형태는 PID 1234를 추적해 5초 간격으로 6회 보고하는 예시다. 배포판에 따라 실행 파일이 /usr/share/bcc/tools/memleak에 있을 수 있다.

할당이 매우 잦다면 샘플링과 최소 크기를 사용해 관찰 비용을 줄일 수 있다.

sudo memleak -p 1234 --sample-rate 10 --min-size 4096 5 6

남아 있는 할당은 어디까지나 후보다. 정상적으로 오래 사는 캐시나 초기화 데이터도 출력될 수 있다. 같은 부하를 반복해 오래된 할당량이 계속 증가하는지, 서비스의 heap 지표와 함께 확인한다.

malloc과 free를 직접 짝지을 때 필요한 것

직접 추적기를 만든다면 다음 상태가 모두 필요하다.

  1. malloc 진입 시 스레드별 요청 크기를 임시 저장한다.
  2. uretprobe에서 반환 주소와 크기, 호출 스택을 기록한다.
  3. free(ptr)가 호출되면 해당 주소를 맵에서 제거한다.
  4. calloc, realloc, posix_memalign 같은 다른 경로도 처리한다.
  5. 프로세스 종료, 실패한 할당과 주소 재사용을 처리한다.

bpftrace 문서에서 uprobe는 인자를, uretprobe는 반환값을 읽는 사용자 공간 probe로 구분한다. 런타임에 따라 return probe가 안전하지 않거나 스택 해석이 제한될 수 있으므로 대상별 검증이 필요하다.

운영 환경에서의 안전 체크

  • 먼저 스테이징이나 동일한 부하 재현 환경에서 오버헤드를 측정한다.
  • 호스트 전체가 아니라 PID, 함수, 크기와 시간으로 범위를 제한한다.
  • 스택·주소·문자열에 민감 정보가 포함될 수 있어 출력 접근과 보존 기간을 제한한다.
  • 심볼이 보이지 않는 결과를 추측으로 함수 이름에 연결하지 않는다.
  • 관찰을 끝내면 probe가 해제됐는지 확인한다.
  • eBPF 실행 권한은 강력하므로 최소 인원과 감사 가능한 절차로 관리한다.

분석 흐름을 한 줄로 연결하면

RSS 증가 확인
  → smaps로 증가 매핑과 Anonymous/PSS 확인
  → 런타임·allocator 확인
  → profiler 또는 eBPF로 호출 경로 추적
  → outstanding allocation 추세와 코드 수명주기 대조

eBPF는 코드를 수정하지 않고 관찰 지점을 추가할 수 있다는 장점이 있다. 하지만 항상 낮은 오버헤드이거나 자동으로 누수를 찾아 주는 도구는 아니다. 무엇을 추적했고 무엇은 보지 못했는지 함께 기록해야 결과를 믿을 수 있다.

관련 글: /proc/PID/smaps로 RSS·PSS를 나누는 법, eBPF가 안전한 이유와 위험해지는 조건

참고 문서

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

댓글