PostgreSQL 메모리 누수 디버깅: heaptrack 활용 가이드
글쓴이: Phil Eaton
작성일: 2025년 5월 22일
이번 글에서는 PostgreSQL에 인위적으로 두 가지 메모리 누수를 주입한 뒤, heaptrack을 사용해 이를 디버깅하는 과정을 소개합니다. 지난 글에서 다룬 memleak과 마찬가지로 heaptrack도 리눅스 환경이 필요하지만, Mac 사용자도 리눅스 가상 머신(VM)에서 실습할 수 있습니다.
Postgres를 기반 코드로 사용하긴 하지만, 이 글에서 소개하는 기법은 C, C++, Rust 프로젝트 전반에 걸쳐 메모리 누수를 추적할 때 유용합니다. 포스트 리뷰를 도와준 동료 Jacob Champion에게 감사드립니다.
Postgres 빌드 준비
먼저 Postgres 코드를 클론하고 디버깅 옵션을 활성화하여 빌드합니다:
$ git clone https://github.com/postgres/postgres$ cd postgres$ git checkout REL_17_STABLE$ ./configure --enable-debug \--prefix=$(pwd)/build \--libdir=$(pwd)/build/lib$ make -j16 && make install
사례 1: Postmaster 프로세스에서의 메모리 누수
Postgres에서 각 프로세스를 실행할 때 postmaster가 스케줄링을 담당합니다. 여기에 누수를 삽입해 보겠습니다.
누수 코드 주입
$ git diff src/backend/postmasterdiff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.cindex d032091495b..e0bf8943763 100644--- a/src/backend/postmaster/postmaster.c+++ b/src/backend/postmaster/postmaster.c@@ -3547,6 +3547,13 @@ BackendStartup(ClientSocket *client_sock)Backend *bn; /* for backend cleanup */pid_t pid;BackendStartupData startup_data;+ MemoryContext old;+ int *s;++ old = MemoryContextSwitchTo(TopMemoryContext);+ s = palloc(80);+ *s = 12;+ MemoryContextSwitchTo(old);/** Create backend data structure. Better before the fork() so we can
Postgres는 MemoryContext라는 구조로 메모리를 할당합니다. 최상위 컨텍스트인 TopMemoryContext는 프로세스 종료 시 메모리가 정리되므로 valgrind memcheck나 LeakSanitizer는 이를 감지하지 못할 수 있습니다. 그러나 프로세스가 살아 있는 동안에는 실제로 누수가 발생한 상태입니다.
누수를 유발하는 가장 쉬운 방법
다수의 psql 클라이언트를 생성해 많은 백엔드 프로세스를 실행시키면 위의 누수가 발생합니다:
for run in {1..100000}; do psql postgres -c 'select 1'; done
Postgres 빌드 및 실행
변경된 코드를 기반으로 다시 빌드합니다:
$ make -j16 && make install
서버가 다음과 같이 실행됩니다:
$ ./build/bin/initdb testdb$ ./build/bin/postgres -D $(pwd)/testdb2025-05-22 12:05:15.995 EDT [260576] LOG: starting PostgreSQL 17.5 on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit2025-05-22 12:05:15.996 EDT [260576] LOG: listening on IPv6 address "::1", port 54322025-05-22 12:05:15.996 EDT [260576] LOG: listening on IPv4 address "127.0.0.1", port 54322025-05-22 12:05:15.997 EDT [260576] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432"2025-05-22 12:05:16.001 EDT [260579] LOG: database system was shut down at 2025-05-22 11:37:53 EDT2025-05-22 12:05:16.004 EDT [260576] LOG: database system is ready to accept connection
출력된 로그에서 대괄호 안에 있는 숫자가 postmaster의 PID입니다. 해당 PID를 사용해 heaptrack을 attach 합니다:
$ sudo heaptrack -p 260576
해당 프로세스는 Postgres 서버가 종료될 때까지 실행됩니다.
이제 또 다른 터미널에서 누수 유발 작업을 실행합니다:
$ for run in {1..100000}; do ./build/bin/psql postgres -c 'select 1'; done
원한다면 또 다른 터미널에서 top 명령어로 메모리 사용량이 점점 증가하는 것을 확인할 수 있습니다.
heaptrack 결과 확인
작업이 완료되면, Postgres 프로세스를 Ctrl + C로 종료하고 heaptrack 결과를 분석합니다:
$ sudo heaptrack -p 260576heaptrack output will be written to "/home/phil/postgres/heaptrack.postgres.260645.zst"injecting heaptrack into application via GDB, this might take some time...warning: 44 ./nptl/cancellation.c: No such file or directoryinjection finishedheaptrack stats:allocations: 19leaked allocations: 19temporary allocations: 0Heaptrack finished! Now run the following to investigate the data:heaptrack --analyze "/home/phil/postgres/heaptrack.postgres.260645.zst"
heaptrack이 생성하는 로그 파일 이름에는 우리가 추적 대상으로 지정한 PID와는 다른 PID가 포함되어 있을 수 있습니다. 그러나 이는 정상이며 문제되지 않습니다. 이제 heaptrack_print를 사용해 결과를 보기 좋게 출력해 보겠습니다.
$ heaptrack_print --print-leaks 1 ./heaptrack.postgres.260645.zst > leak.txt
이제 leak.txt 파일을 열어 MEMORY LEAKS 섹션을 찾아보면 다음과 같은 내용을 확인할 수 있습니다:
MEMORY LEAKS16.77M leaked over 11 calls fromAllocSetAllocFromNewBlockat /home/phil/postgres/src/backend/utils/mmgr/aset.c:908in /home/phil/postgres/build/bin/postgres16.77M leaked over 11 calls from:ServerLoop::BackendStartupat /home/phil/postgres/src/backend/postmaster/postmaster.c:3554in /home/phil/postgres/build/bin/postgresServerLoopat /home/phil/postgres/src/backend/postmaster/postmaster.c:1676PostmasterMainat /home/phil/postgres/src/backend/postmaster/postmaster.c:1374in /home/phil/postgres/build/bin/postgresmainat /home/phil/postgres/src/backend/main/main.c:199in /home/phil/postgres/build/bin/postgres
이는 우리가 앞서 코드에 삽입한 메모리 누수와 정확히 일치하는 결과입니다.
정말로 누수가 존재하는지 검증하기
혹시 우리가 착각하고 있는 건 아닌지 확인하기 위해, 누수를 유발한 palloc 코드를 주석 처리한 후 동일한 작업과 heaptrack 실행을 반복해 보았습니다. 결과 파일인 leak.txt는 다음과 같이 나옵니다:
$ cat leak.txtreading file "./heaptrack.postgres.504295.zst" - please wait, this might take some time...Debuggee command was: ./build/bin/postgres -D /home/phil/postgres/testdbfinished reading file, now analyzing data:MOST CALLS TO ALLOCATION FUNCTIONSPEAK MEMORY CONSUMERSMEMORY LEAKSMOST TEMPORARY ALLOCATIONStotal runtime: 146.664000s.calls to allocation functions: 0 (0/s)temporary memory allocations: 0 (0/s)peak heap memory consumption: 0Bpeak RSS (including heaptrack overhead): 21.93Mtotal memory leaked: 0
이번에는 메모리 누수가 전혀 없음을 확인할 수 있습니다.
또 다른 Postgres 프로세스에서의 메모리 누수 실험
이번에는 클라이언트 백엔드 프로세스에서 메모리 누수를 일으키고 이를 추적할 수 있는지 살펴보겠습니다.
클라이언트 백엔드에서의 누수
클라이언트 백엔드 프로세스는 세션 단위로 실행되고 종료되기 때문에, 메모리 누수를 추적하기 까다로운 편입니다. 하지만 해당 프로세스의 PID를 알 수 있다면 heaptrack을 attach하여 추적이 가능합니다.
이번엔 random() 함수 구현 내부에서 TopMemoryContext를 사용해 메모리 누수를 유도해보겠습니다.
$ git diff src/backend/utils/diff --git a/src/backend/utils/adt/pseudorandomfuncs.c b/src/backend/utils/adt/pseudorandomfuncs.cindex 8e82c7078c5..886efbfaf78 100644--- a/src/backend/utils/adt/pseudorandomfuncs.c+++ b/src/backend/utils/adt/pseudorandomfuncs.c@@ -20,6 +20,7 @@#include "utils/fmgrprotos.h"#include "utils/numeric.h"#include "utils/timestamp.h"+#include "utils/memutils.h"/* Shared PRNG state used by all the random functions */static pg_prng_state prng_state;@@ -84,6 +85,13 @@ Datumdrandom(PG_FUNCTION_ARGS){float8 result;+ int* s;+ MemoryContext old;++ old = MemoryContextSwitchTo(TopMemoryContext);+ s = palloc(120);+ MemoryContextSwitchTo(old);+ *s = 90;initialize_prng();
random() 함수를 여러 번 호출하면 이 누수가 발생합니다. 예를 들어 다음 쿼리를 통해 테스트할 수 있습니다:
SELECT sum(random()) FROM generate_series(1, 100_0000);.
앞서 수정한 코드를 기반으로 Postgres를 다시 빌드하고 설치합니다.
$ make -j16 && make install
그 다음, 이전에 생성한 testdb 데이터 디렉토리를 기반으로 Postgres 서버를 시작합니다.
$ ./build/bin/postgres -D $(pwd)/testdb2025-05-22 14:37:11.322 EDT [704381] LOG: starting PostgreSQL 17.5 on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit2025-05-22 14:37:11.323 EDT [704381] LOG: listening on IPv6 address "::1", port 54322025-05-22 14:37:11.323 EDT [704381] LOG: listening on IPv4 address "127.0.0.1", port 54322025-05-22 14:37:11.324 EDT [704381] LOG: listening on Unix socket "/tmp/.s.PGSQL.5432"2025-05-22 14:37:11.327 EDT [704384] LOG: database system was shut down at 2025-05-22 14:31:00 EDT2025-05-22 14:37:11.329 EDT [704381] LOG: database system is ready to accept connections
클라이언트 백엔드 PID 확인 및 heaptrack 연결
다른 터미널에서 psql 클라이언트를 실행한 뒤, 해당 세션의 백엔드 프로세스 PID를 확인합니다:
$ ./build/bin/psql postgrespsql (17.5)Type "help" for help.postgres=# select pg_backend_pid();pg_backend_pid----------------704389(1 row)postgres=#
이 세션을 종료하지 말고 유지한 상태로, 새로운 터미널을 열어 heaptrack을 해당 PID에 attach 합니다:
$ sudo heaptrack -p 704389heaptrack output will be written to "/home/phil/heaptrack.postgres.704409.zst"injecting heaptrack into application via GDB, this might take some time...warning: 44 ./nptl/cancellation.c: No such file or directoryinjection finished
이제 다시 앞서 열어둔 704389번 psql 세션으로 돌아가 메모리 누수를 유발하는 워크로드를 실행합니다.
postgres=# SELECT sum(random()) FROM generate_series(1, 10_000_000);sum-------------------499960.8137393289(1 row)
쿼리가 완료되면 Ctrl + D를 눌러 psql 세션을 정상 종료합니다.
클라이언트 백엔드의 heaptrack 결과 확인
heaptrack이 attach되어 있던 터미널에서 분석 결과가 출력됩니다:
$ sudo heaptrack -p 704389heaptrack output will be written to "/home/phil/heaptrack.postgres.704409.zst"injecting heaptrack into application via GDB, this might take some time...warning: 44 ./nptl/cancellation.c: No such file or directoryinjection finishedheaptrack stats:allocations: 206leaked allocations: 177temporary allocations: 8removing heaptrack injection via GDB, this might take some time...ptrace: No such process.No symbol table is loaded. Use the "file" command.The program is not being run.Heaptrack finished! Now run the following to investigate the data:heaptrack --analyze "/home/phil/heaptrack.postgres.704409.zst"
이전과 같이 heaptrack_print를 실행합니다.
$ heaptrack_print --print-leaks 1 ./heaptrack.postgres.704409.zst > leak.txt
leak.txt 파일의 MEMORY LEAKS 섹션을 열어보면 다음과 같은 결과가 나타납니다:
MEMORY LEAKS1.37G leaked over 180 calls fromAllocSetAllocFromNewBlockat /home/phil/postgres/src/backend/utils/mmgr/aset.c:908in /home/phil/postgres/build/bin/postgres1.37G leaked over 170 calls from:drandomat /home/phil/postgres/src/backend/utils/adt/pseudorandomfuncs.c:92in /home/phil/postgres/build/bin/postgresExecInterpExprat /home/phil/postgres/src/backend/executor/execExprInterp.c:740in /home/phil/postgres/build/bin/postgresExecAgg::agg_retrieve_direct::advance_aggregates::ExecEvalExprSwitchContextat ../../../src/include/executor/executor.h:356in /home/phil/postgres/build/bin/postgresExecAgg::agg_retrieve_direct::advance_aggregatesat /home/phil/postgres/src/backend/executor/nodeAgg.c:820ExecAgg::agg_retrieve_directat /home/phil/postgres/src/backend/executor/nodeAgg.c:2454ExecAggat /home/phil/postgres/src/backend/executor/nodeAgg.c:2179standard_ExecutorRun::ExecutePlan::ExecProcNode
우리가 의도적으로 random() 함수 안에 삽입한 메모리 누수가 정확하게 탐지된 것을 확인할 수 있습니다.
참고 사항
memleak과 달리, heaptrack은 추적 중인 프로세스가 종료된 후에야 메모리 누수 보고서를 출력합니다. 따라서 실시간으로 상태를 보고 싶거나, 프로세스를 종료시키기 어려운 상황에서는 다소 불편할 수 있습니다.
하지만 제 경험상 heaptrack은 다양한 환경에서 스택 트레이스와 코드 라인 번호를 memleak보다 훨씬 더 안정적이고 정확하게 제공해 줍니다.
요약하면, 프로그램 종료 시 자동으로 메모리가 정리되어버리는 환경에서는 Valgrind memcheck나 LeakSanitizer보다 heaptrack과 memleak이 더 적합합니다.
결론적으로, heaptrack과 memleak은 C/C++ 또는 PostgreSQL 기반 시스템을 다룰 때 꼭 도구 상자에 넣어두어야 할 훌륭한 메모리 분석 도구입니다.
본문: Debugging memory leaks in Postgres, heaptrack edition
이메일: salesinquiry@enterprisedb.com
PostgreSQL 메모리 누수 디버깅 FAQ
PostgreSQL 메모리 누수 디버깅에 heaptrack을 사용하는 이유는 무엇인가요?
heaptrack은 실행 중인 PostgreSQL 프로세스의 메모리 할당을 추적하고, 누수가 발생한 스택 트레이스와 코드 라인 번호를 확인하는 데 유용합니다. 특히 프로세스 종료 시 메모리가 자동 정리되어 Valgrind memcheck나 LeakSanitizer가 놓칠 수 있는 누수를 분석할 때 활용할 수 있습니다.
heaptrack으로 Postgres postmaster 프로세스를 어떻게 추적하나요?
Postgres 서버 로그에서 postmaster PID를 확인한 뒤 sudo heaptrack -p PID 명령으로 해당 프로세스에 연결합니다. 누수를 유발하는 워크로드를 실행하고 프로세스를 종료한 다음 heaptrack_print로 결과 파일의 MEMORY LEAKS 섹션을 분석합니다.
PostgreSQL 클라이언트 백엔드 PID는 어떻게 확인하나요?
psql 세션에서 select pg_backend_pid();를 실행하면 현재 세션의 백엔드 프로세스 PID를 확인할 수 있습니다. 세션을 유지한 상태에서 별도 터미널을 열고 heaptrack을 해당 PID에 연결합니다.
heaptrack 결과는 언제 확인할 수 있나요?
heaptrack은 추적 대상 프로세스가 종료된 후 메모리 누수 보고서를 출력합니다. 따라서 실시간 상태 확인이나 종료하기 어려운 프로세스 분석에는 제약이 있을 수 있습니다.
heaptrack과 memleak은 어떤 상황에 적합한가요?
본문에서는 프로그램 종료 시 메모리가 자동으로 정리되는 환경에서 heaptrack과 memleak이 Valgrind memcheck나 LeakSanitizer보다 적합하다고 설명합니다. 저자의 경험상 heaptrack은 여러 환경에서 스택 트레이스와 코드 라인 번호를 안정적으로 제공한다는 장점이 있습니다.