Back to blog

PostgreSQL 메모리 누수 디버깅: heaptrack 활용 가이드

August 10, 2026

작성자: EDB Korea Team
작성일: 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/postmaster
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 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)/testdb
2025-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-bit
2025-05-22 12:05:15.996 EDT [260576] LOG: listening on IPv6 address "::1", port 5432
2025-05-22 12:05:15.996 EDT [260576] LOG: listening on IPv4 address "127.0.0.1", port 5432
2025-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 EDT
2025-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 260576
heaptrack 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 directory
injection finished
heaptrack stats:
allocations: 19
leaked allocations: 19
temporary allocations: 0
Heaptrack 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 LEAKS
16.77M leaked over 11 calls from
AllocSetAllocFromNewBlock
at /home/phil/postgres/src/backend/utils/mmgr/aset.c:908
in /home/phil/postgres/build/bin/postgres
16.77M leaked over 11 calls from:
ServerLoop::BackendStartup
at /home/phil/postgres/src/backend/postmaster/postmaster.c:3554
in /home/phil/postgres/build/bin/postgres
ServerLoop
at /home/phil/postgres/src/backend/postmaster/postmaster.c:1676
PostmasterMain
at /home/phil/postgres/src/backend/postmaster/postmaster.c:1374
in /home/phil/postgres/build/bin/postgres
main
at /home/phil/postgres/src/backend/main/main.c:199
in /home/phil/postgres/build/bin/postgres

이는 우리가 앞서 코드에 삽입한 메모리 누수와 정확히 일치하는 결과입니다.

정말로 누수가 존재하는지 검증하기

혹시 우리가 착각하고 있는 건 아닌지 확인하기 위해, 누수를 유발한 palloc 코드를 주석 처리한 후 동일한 작업과 heaptrack 실행을 반복해 보았습니다. 결과 파일인 leak.txt는 다음과 같이 나옵니다:

$ cat leak.txt
reading file "./heaptrack.postgres.504295.zst" - please wait, this might take some time...
Debuggee command was: ./build/bin/postgres -D /home/phil/postgres/testdb
finished reading file, now analyzing data:

MOST CALLS TO ALLOCATION FUNCTIONS

PEAK MEMORY CONSUMERS

MEMORY LEAKS

MOST TEMPORARY ALLOCATIONS

total runtime: 146.664000s.
calls to allocation functions: 0 (0/s)
temporary memory allocations: 0 (0/s)
peak heap memory consumption: 0B
peak RSS (including heaptrack overhead): 21.93M
total 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.c
index 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 @@ Datum
drandom(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)/testdb
2025-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-bit
2025-05-22 14:37:11.323 EDT [704381] LOG: listening on IPv6 address "::1", port 5432
2025-05-22 14:37:11.323 EDT [704381] LOG: listening on IPv4 address "127.0.0.1", port 5432
2025-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 EDT
2025-05-22 14:37:11.329 EDT [704381] LOG: database system is ready to accept connections

클라이언트 백엔드 PID 확인 및 heaptrack 연결

다른 터미널에서 psql 클라이언트를 실행한 뒤, 해당 세션의 백엔드 프로세스 PID를 확인합니다:

$ ./build/bin/psql postgres
psql (17.5)
Type "help" for help.
postgres=# select pg_backend_pid();
pg_backend_pid
----------------
704389
(1 row)
postgres=#

이 세션을 종료하지 말고 유지한 상태로, 새로운 터미널을 열어 heaptrack을 해당 PID에 attach 합니다:

$ sudo heaptrack -p 704389
heaptrack 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 directory
injection 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 704389
heaptrack 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 directory
injection finished
heaptrack stats:
allocations: 206
leaked allocations: 177
temporary allocations: 8
removing 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 LEAKS
1.37G leaked over 180 calls from
AllocSetAllocFromNewBlock
at /home/phil/postgres/src/backend/utils/mmgr/aset.c:908
in /home/phil/postgres/build/bin/postgres
1.37G leaked over 170 calls from:
drandom
at /home/phil/postgres/src/backend/utils/adt/pseudorandomfuncs.c:92
in /home/phil/postgres/build/bin/postgres
ExecInterpExpr
at /home/phil/postgres/src/backend/executor/execExprInterp.c:740
in /home/phil/postgres/build/bin/postgres
ExecAgg::agg_retrieve_direct::advance_aggregates::ExecEvalExprSwitchContext
at ../../../src/include/executor/executor.h:356
in /home/phil/postgres/build/bin/postgres
ExecAgg::agg_retrieve_direct::advance_aggregates
at /home/phil/postgres/src/backend/executor/nodeAgg.c:820
ExecAgg::agg_retrieve_direct
at /home/phil/postgres/src/backend/executor/nodeAgg.c:2454
ExecAgg
at /home/phil/postgres/src/backend/executor/nodeAgg.c:2179
standard_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 도입부터 운영까지, EDB와 함께하세요

PostgreSQL 도입, 운영, 성능 최적화 및 업그레이드에 대해 EDB 전문가와 상담해 보세요.

문의하기


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은 여러 환경에서 스택 트레이스와 코드 라인 번호를 안정적으로 제공한다는 장점이 있습니다.

Share this