(KO) PostgreSQL 메모리 컨텍스트 — 계층형 영역 할당과 longjmp 안전 정리
목차
- 이론적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 가이드
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
이론적 배경
섹션 제목: “이론적 배경”장시간 실행되는 서버는 공통적인 메모리 관리 문제에 직면한다. 요청 하나가 수십에서 수백 개의 작은 객체를 할당하는데, 이 객체들은 수명을 공유한다. 그런데 정상 종료 경로는 물론 에러 종료 경로를 포함해 모든 출구에서 단 하나라도 해제하지 않으면 프로세스가 누수를 낸다. 각 malloc()마다 짝을 맞춰 free()를 호출하는 방식은 두 가지 문제가 있다. 청크별 장부 관리로 느리고, 가지 하나를 빠뜨리면 영구 누수가 생겨 취약하다. 고전적인 해법은 영역 기반 메모리 관리(region-based memory management), 즉 아레나(arena) 또는 존(zone) 할당이다. 많은 객체를 하나의 영역에 할당한 뒤, 객체를 하나씩 해제하는 대신 영역 전체를 한 번에 버린다.
Database Internals (Petrov, “Implementation Details” 장)는 이 문제를 데이터베이스의 버퍼·작업 메모리 맥락에서 다룬다. 범용 할당기는 세 가지를 동시에 만족할 수 없다. 단편화(내부 단편화 — 요청을 크기 클래스로 올림; 외부 단편화 — 재사용하기엔 너무 작은 빈 구멍), 할당 속도, 회수 비용이 그것이다. 영역 할당기는 회수를 O(1)로 만든다. 포인터를 하나 움직여 할당하고, 아레나 전체를 버려 해제한다. 대가는 개별 객체의 free()를 포기하는 것이다. 영역 단위로만 회수한다. 한 요청 안에서 만들어진 객체들이 수명을 공유하는 워크로드(질의가 끝나면 질의가 건드린 모든 것이 사라지는 패턴)에서 이 트레이드오프는 정확히 맞아떨어진다.
영역 모델에서 두 가지 설계 선택이 파생되며, 이 두 선택이 모든 구현의 모양을 결정한다.
-
영역을 수명에 어떻게 묶는가. “프로세스당 큰 아레나 하나”로 단순하게 가면, 프로세스보다 짧은 수명을 가진 모든 것이 누수된다. 표준적인 개선책은 영역에 계층을 부여하는 것이다. 영역이 자식 영역을 가질 수 있게 하고, 부모를 버리면 모든 자손도 함께 버려지게 만든다. 이렇게 하면 중첩된 수명(트랜잭션 ⊃ 문장 ⊃ 튜플)을 영역 트리에 대응시킬 수 있다. “전부 해제”의 올바른 단위는 항상 트리에서 한 노드 거리에 있다.
-
빈 포인터에서 어떻게 영역을 찾는가.
free(p)와realloc(p)는 객체 포인터만 받을 뿐 영역 정보는 없다. 영역 할당기는 반환 주소 바로 앞에 헤더를 도장처럼 찍어야 한다. 최소한 헤더는 소유 영역을 인코딩하고, 유용하게는 할당기 종류(단일free()진입점이 올바른 구현으로 디스패치할 수 있도록)와 청크 크기(해제된 공간을 재활용하거나 집계할 수 있도록)도 인코딩한다. 헤더 너비는 밀도와 직접 맞바뀐다. 수백만 개의 작은 청크에 헤더가 붙으면 순수한 오버헤드다. 따라서 객체별free()가 필요 없는 고성능 할당기는 헤더를 아예 없애버린다.
PostgreSQL의 메모리 컨텍스트(memory context) 서브시스템은 이 두 가지 개선을 모두 채택한 영역 할당기다. 영역들은 트리(컨텍스트 포리스트)를 이루고, 청크들은 소유 할당기를 식별하는 간결한 헤더를 단다. 이 문서의 나머지는 src/backend/utils/mmgr/ 안에서 트리, 청크 헤더, 네 가지 교체 가능한 영역 구현이 어떻게 실현되는지를 추적한다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”교과서는 영역 모델을 알려준다. 이 절은 서버급 영역 할당기가 전체 코드베이스에서 실제로 쓸 만하게 만들기 위해 채택하는 엔지니어링 관례를 정리한다. 다음 절에서 다루는 PostgreSQL의 구체적 선택들은 이 공통 공간 안에서의 한 가지 조합으로 읽는 것이 가장 자연스럽다.
암묵적 “현재 영역”
섹션 제목: “암묵적 “현재 영역””할당을 할 수도 있는 모든 함수에 명시적 영역 인자를 꿰는 것은 감당하기 어렵다. copyObject(), 표현식 평가기, 수천 개의 헬퍼가 각자 파라미터를 하나씩 더 가져야 한다. 실제 시스템들은 **제어 흐름의 “현재 영역”**을 전역 변수(또는 스레드 로컬)에 두고, 단순 할당 호출(palloc 등)이 그 변수에서 가져가도록 한다. 영역을 전환하는 것은 그 전역을 저장하고 복원하는 저렴한 연산이며, 규칙은 “전환 → 할당 → 복원”이다.
중첩 수명에 묶인 영역 트리
섹션 제목: “중첩 수명에 묶인 영역 트리”이론 절의 계층 구조가 구체적인 패턴이 된다. 몇 가지 잘 알려진 장수 루트(프로세스, 트랜잭션, 질의, 튜플 단위)를 만들고, 수명이 짧은 자식들을 해당 루트 아래에 매달아 둔다. 정리는 그러면 “문장 사이에 per-statement 영역을 리셋”, “ExecutorEnd에서 per-query 영역을 삭제” 같은 식으로 단순해진다. 노드를 삭제하면 서브트리 전체가 삭제되므로, 깜빡하고 남겨진 자식 영역도 부모와 함께 회수된다.
타입 태그로서의 청크 헤더
섹션 제목: “타입 태그로서의 청크 헤더”free(p)는 포인터만 받으므로, 모든 영역 할당기는 반환 주소 바로 앞에 헤더를 찍는다. 헤더는 최소한 소유 영역을 인코딩하고, 유용하게는 할당기 타입(단일 free() 진입점이 올바른 구현으로 디스패치)과 청크 크기(회수 또는 집계)도 담는다. 헤더 너비는 밀도와 직접 맞바뀐다. 수백만 개의 작은 청크 앞에 헤더가 있으면 순수 오버헤드다. 고성능 할당기는 헤더를 줄이거나 아예 없앤다.
하나의 인터페이스 뒤에 여러 할당기
섹션 제목: “하나의 인터페이스 뒤에 여러 할당기”어떤 단일 할당 정책도 모든 패턴에 최적일 수 없다. 범용 워크로드는 크기 클래스 프리리스트를 원하고, 동일 크기 객체 스트림은 슬랩을 원하며, FIFO 생산자/소비자는 세대별 블록을 원하고, 한 번 쓰고 버리는 스크래치패드는 헤더 없는 범프 포인터를 원한다. 관례는 할당기 메서드 vtable(alloc, free, realloc, reset, delete)이다. 호출자는 영역 생성 시점에 정책을 고르고, 나머지 코드는 정책에 무관하게 동작한다.
영역 해제로 처리하는 에러 정리
섹션 제목: “영역 해제로 처리하는 에러 정리”데이터베이스에서 가장 결정적인 이점이다. 문장이 중단되면, 정리 코드는 데이터 구조를 돌며 객체를 해제하지 않는다. 문장이 할당한 영역들을 삭제한다. 이것이 영역 할당과 서버의 에러 처리 규율이 함께 설계되는 이유다. 예외 언와인드 경로가 해야 할 메모리 작업의 전부는 “이 컨텍스트들을 삭제하라”는 것이다.
이론 ↔ PostgreSQL 대응표
섹션 제목: “이론 ↔ PostgreSQL 대응표”| 이론 / 관례 | PostgreSQL 이름 |
|---|---|
| 영역 / 아레나 | MemoryContext (MemoryContextData 헤더 + 메서드 vtable) |
| 영역 트리(부모/자식) | MemoryContextData의 parent / firstchild / nextchild 링크 |
| 현재 영역(암묵적) | CurrentMemoryContext 전역; MemoryContextSwitchTo() |
| 현재 영역에서 단순 할당 | palloc() / palloc0() |
| 소유 영역으로 청크 해제 | pfree() → MCXT_METHOD(p, free_p) |
| 영역 전체 폐기 | MemoryContextReset() / MemoryContextDelete() |
| 청크 헤더 = 할당기 타입 태그 | uint64 MemoryChunk 헤더의 하위 4비트 = MemoryContextMethodID |
| 할당기 vtable | MemoryContextMethods; 메서드 id로 인덱싱되는 mcxt_methods[] 배열 |
| 범용 정책 | AllocSetContext (aset.c) |
| 고정 크기 정책 | SlabContext (slab.c) |
| FIFO / 수명 그룹 정책 | GenerationContext (generation.c) |
| 헤더 없는 쓰기 전용 정책 | BumpContext (bump.c) |
| 영역 해제로 처리하는 에러 정리 | abort 경로가 TopTransactionContext 등을 삭제 |
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”PostgreSQL은 백엔드 전용 메모리를 거의 전부 메모리 컨텍스트에서 할당한다. 컨텍스트는 추상 기반 구조체(MemoryContextData)로서 트리 링크와 메서드 테이블 포인터를 가진다. 구체적인 저장 정책은 이 기반 구조체를 첫 번째 필드로 포함하는 파생 구조체(AllocSetContext, SlabContext 등)에 구현된다. palloc()은 CurrentMemoryContext에서 할당하고, pfree()/repalloc()은 청크 헤더에서 소유 컨텍스트를 찾아내므로 어떤 컨텍스트가 현재 활성화되어 있든 상관없이 동작한다. 컨텍스트를 리셋하거나 삭제하면 그 안에 할당된 모든 것이 해제된다. 삭제의 경우 자손 컨텍스트까지 모두 해제된다. 에러 처리가 언와인드할 때 쓰는 메커니즘이 바로 이것이다.
범위 주석. 이 문서는 컨텍스트 트리, 네 가지 할당기, palloc/pfree API, 리셋/삭제를 다룬다. 에러 경로가 어떤 컨텍스트를 삭제할지 결정하는 방법은
postgres-error-handling.md(PG_TRY/sigsetjmp장치와ErrorContext)에 있다. 메모리와 함께 해제되는 비(非)메모리 자원(버퍼 핀, 릴레이션 락, 파일 디스크립터)은postgres-resource-owners.md에 있다. 이 문서는 메모리 쪽과 컨텍스트가 비메모리 자원을 해제하게 해 주는 리셋 콜백 훅만을 다룬다.
추상 컨텍스트와 vtable
섹션 제목: “추상 컨텍스트와 vtable”MemoryContextData는 모든 컨텍스트 타입이 앞에 두는 공통 헤더다. 트리 링크, 집계 카운터, 두 개의 불리언, 메서드 vtable 포인터를 담는다.
// MemoryContextData — src/include/nodes/memnodes.htypedef struct MemoryContextData{ pg_node_attr(abstract) /* there are no nodes of this type */ NodeTag type; /* identifies exact kind of context */ bool isReset; /* T = no space alloced since last reset */ bool allowInCritSection; /* allow palloc in critical section */ Size mem_allocated; /* track memory allocated for this context */ const MemoryContextMethods *methods; /* virtual function table */ MemoryContext parent; /* NULL if no parent (toplevel context) */ MemoryContext firstchild; /* head of linked list of children */ MemoryContext prevchild; /* previous child of same parent */ MemoryContext nextchild; /* next child of same parent */ const char *name; /* context name */ const char *ident; /* context ID if any */ MemoryContextCallback *reset_cbs; /* list of reset/delete callbacks */} MemoryContextData;methods 포인터는 C 수준의 가상 함수 테이블이다. README는 MemoryContextData를 “본질적으로 추상 슈퍼클래스”라고 부른다. vtable의 모양은 다음과 같다.
// MemoryContextMethods — src/include/nodes/memnodes.h (condensed)typedef struct MemoryContextMethods{ void *(*alloc) (MemoryContext context, Size size, int flags); void (*free_p) (void *pointer); void *(*realloc) (void *pointer, Size size, int flags); void (*reset) (MemoryContext context); void (*delete_context) (MemoryContext context); MemoryContext (*get_chunk_context) (void *pointer); Size (*get_chunk_space) (void *pointer); bool (*is_empty) (MemoryContext context); void (*stats) (MemoryContext context, MemoryStatsPrintFunc printfunc, ...); /* ... check() under MEMORY_CONTEXT_CHECKING ... */} MemoryContextMethods;할당기 타입 하나당 vtable 인스턴스가 정확히 하나씩 있다. 이 인스턴스들은 mcxt.c의 지정 이니셜라이저 배열 하나에 모여 있다. 배열은 작은 열거형인 MemoryContextMethodID로 인덱싱된다.
// mcxt_methods[] — src/backend/utils/mmgr/mcxt.c (condensed)static const MemoryContextMethods mcxt_methods[] = { [MCTX_ASET_ID].alloc = AllocSetAlloc, [MCTX_ASET_ID].free_p = AllocSetFree, [MCTX_ASET_ID].realloc = AllocSetRealloc, [MCTX_ASET_ID].reset = AllocSetReset, [MCTX_ASET_ID].delete_context = AllocSetDelete, /* ... get_chunk_context / get_chunk_space / is_empty / stats ... */
/* generation.c */ [MCTX_GENERATION_ID].alloc = GenerationAlloc, /* ... */ /* slab.c */ [MCTX_SLAB_ID].alloc = SlabAlloc, /* ... */ /* bump.c */ [MCTX_BUMP_ID].alloc = BumpAlloc, /* ... */
/* Reserved / unused IDs get BOGUS_MCTX dummy entries so a bad * pointer fails cleanly instead of jumping through garbage. */ BOGUS_MCTX(MCTX_1_RESERVED_GLIBC_ID), /* ... */};flowchart TB
subgraph ABS["추상 기반"]
MCD["MemoryContextData<br/>type, isReset, mem_allocated,<br/>parent/firstchild/nextchild,<br/>methods -> vtable"]
end
MCD --> VT["MemoryContextMethods vtable<br/>(할당기 타입당 하나)"]
VT --> ARR["mcxt_methods[ MemoryContextMethodID ]"]
ARR --> A1["MCTX_ASET_ID -> AllocSet* 함수들"]
ARR --> A2["MCTX_GENERATION_ID -> Generation* 함수들"]
ARR --> A3["MCTX_SLAB_ID -> Slab* 함수들"]
ARR --> A4["MCTX_BUMP_ID -> Bump* 함수들"]
subgraph DERIVED["구체적 컨텍스트 (첫 번째 필드가 MemoryContextData)"]
D1["AllocSetContext"]
D2["GenerationContext"]
D3["SlabContext"]
D4["BumpContext"]
end
D1 -. methods .-> A1
D2 -. methods .-> A2
D3 -. methods .-> A3
D4 -. methods .-> A4
그림 1 — 컨텍스트 타입 계층. MemoryContextData는 추상 헤더이고, 각 구체적 할당기는 그것을 첫 번째 필드로 내장한다. methods는 MemoryContextMethodID로 선택된 mcxt_methods[] vtable 배열의 한 행을 가리킨다. 손으로 구현한 C 스타일 단일 상속이다.
청크 헤더: pfree가 포인터만으로 할당기를 찾는 방법
섹션 제목: “청크 헤더: pfree가 포인터만으로 할당기를 찾는 방법”pfree(p)와 repalloc(p)가 컨텍스트 인자 없이도 동작하는 우아한 트릭이 있다. 모든 청크 앞에는, 패딩 없이 바로, 하위 4비트가 소유 컨텍스트의 MemoryContextMethodID인 uint64가 붙는다. 청크 포인터 하나를 받으면, pfree는 그 4비트를 읽고, mcxt_methods[]를 인덱싱하고, 올바른 free_p를 호출한다.
// GetMemoryChunkMethodID — src/backend/utils/mmgr/mcxt.c (condensed)static inline MemoryContextMethodIDGetMemoryChunkMethodID(const void *pointer){ uint64 header; /* a non-MAXALIGNED pointer can't be a real chunk */ Assert(pointer == (const void *) MAXALIGN(pointer)); header = *((const uint64 *) ((const char *) pointer - sizeof(uint64))); return (MemoryContextMethodID) (header & MEMORY_CONTEXT_METHODID_MASK);}
#define MCXT_METHOD(pointer, method) \ mcxt_methods[GetMemoryChunkMethodID(pointer)].method
voidpfree(void *pointer){ MCXT_METHOD(pointer, free_p) (pointer); /* dispatch to owner's free */}4비트 id는 진단 목적으로 예약값이 정해져 있다. 0000(MCTX_0_RESERVED_UNUSEDMEM_ID)은 한 번도 건드리지 않은 메모리가 읽히는 값이고, 1111(MCTX_15_RESERVED_WIPEDMEM_ID)은 wipe_mem이 남기는 값이다. 0001/0010은 glibc의 malloc이 남기는 패턴에 맞춘 값이다. 따라서 glibc 포인터나 이미 해제된 메모리를 pfree에 넘기면 BOGUS_MCTX 항목에 걸려 힙을 오염시키는 대신 깔끔하게 실패한다.
헤더의 나머지 60비트는 할당기가 자유롭게 쓴다. 인트리(in-tree) 할당기 넷은 모두 memutils_memorychunk.h의 MemoryChunk 헤더 타입을 공유한다. 이 타입은 60비트 안에 30비트 청크 크기(또는 프리리스트 인덱스)와 30비트 청크-에서-블록 오프셋을 채운다. 한 비트는 “외부”(오버사이즈, 블록 하나에 청크 하나) 청크 표시에 쓴다. 이 오프셋으로 할당기는 청크에서 블록 헤더를 찾고, 블록은 소유 컨텍스트를 가리킨다. 이것이 GetMemoryChunkContext가 “이 포인터를 소유한 컨텍스트는?”이라는 질문에 답하는 방식이다.
// GetMemoryChunkContext — src/backend/utils/mmgr/mcxt.cMemoryContextGetMemoryChunkContext(void *pointer){ return MCXT_METHOD(pointer, get_chunk_context) (pointer);}palloc과 현재 컨텍스트
섹션 제목: “palloc과 현재 컨텍스트”palloc은 CurrentMemoryContext를 대상으로 특화된 MemoryContextAlloc이다. 백엔드에서 가장 뜨거운 경로의 스택 프레임 하나를 줄이기 위해, 래퍼가 아닌 거의 중복된 별도 함수로 구현했다.
// palloc — src/backend/utils/mmgr/mcxt.c (condensed)void *palloc(Size size){ void *ret; MemoryContext context = CurrentMemoryContext;
Assert(MemoryContextIsValid(context)); AssertNotInCriticalSection(context);
context->isReset = false; ret = context->methods->alloc(context, size, 0); Assert(ret != NULL); /* OOM is handled inside alloc, via elog */ VALGRIND_MEMPOOL_ALLOC(context, ret, size); return ret;}README가 명시한 두 가지 API 계약은 엔진 나머지 부분에서 전제로 깔린다.
palloc은 절대 NULL을 반환하지 않는다. OOM 시 할당기 내부에서elog(ERROR)로 빠져나간다(MemoryContextAllocationFailure경유). 호출자는 null 체크를 하지 않는다. null이 필요한 드문 호출자는palloc_extended(..., MCXT_ALLOC_NO_OOM)으로 옵트아웃한다.pfree/repalloc은CurrentMemoryContext를 무시한다. 청크의 소유 컨텍스트로 라우팅되므로, 현재 활성 컨텍스트와 다른 컨텍스트 소속의 청크도 해제할 수 있다. (repalloc은 NULL을 받을 수 없다. 청크가 없으면 어느 컨텍스트에서 할당해야 할지 알 수 없기 때문이다.)
현재 컨텍스트 전환은 헤더에 인라인으로 정의된 간단한 저장/복원 연산이다. 어디서나 인라인된다.
// MemoryContextSwitchTo — src/include/utils/palloc.hstatic inline MemoryContextMemoryContextSwitchTo(MemoryContext context){ MemoryContext old = CurrentMemoryContext; CurrentMemoryContext = context; return old;}README의 경고를 반복할 가치가 있다. CurrentMemoryContext는 정상 작업 중에 수명이 짧은 컨텍스트(일반적으로 per-tuple 컨텍스트)를 가리켜야 한다. 실수로 해제하지 않은 palloc이 프로세스 수명 내내 누수되지 않고 곧 회수되도록 하기 위해서다.
컨텍스트 포리스트와 잘 알려진 루트들
섹션 제목: “컨텍스트 포리스트와 잘 알려진 루트들”컨텍스트들은 포리스트를 이룬다. 현재 실제로는 TopMemoryContext를 루트로 하는 단일 트리다. TopMemoryContext는 리셋되거나 삭제되지 않는 유일한 컨텍스트다. 잘 알려진 전역들(mcxt.c에 선언, README에 설명)은 수명 앵커 역할을 하며, 모든 수명 짧은 컨텍스트가 그 아래에 매달린다.
flowchart TB TOP["TopMemoryContext<br/>(리셋 없음; ~malloc과 유사)"] TOP --> ERR["ErrorContext<br/>(에러 복구용으로 예약)"] TOP --> CACHE["CacheMemoryContext<br/>(relcache/catcache; 리셋 없음)"] TOP --> MSG["MessageContext<br/>(현재 FE 메시지; 각 명령마다 리셋)"] TOP --> TTX["TopTransactionContext<br/>(최상위 트랜잭션 종료 시 리셋)"] TOP --> POSTM["PostmasterContext<br/>(fork 후 자식에서 해제)"] MSG --> PLAN["parse/plan temp<br/>(MessageContext의 자식)"] TTX --> CUR["CurTransactionContext<br/>(최상위에서는 TopTransaction과 동일;<br/>서브트랜잭션마다 자식)"] POR["PortalContext (활성 포털당 하나)"] --> EXEC["ExecutorState<br/>(ExecutorStart..ExecutorEnd)"] EXEC --> PT1["ExprContext per-tuple<br/>(튜플마다 리셋)"] EXEC --> PT2["ExprContext per-tuple<br/>(플랜 노드당 하나)"]
그림 2 — 잘 알려진 컨텍스트 루트들과 그 아래의 전형적인 일시적 서브트리. 각 루트는 수명에 대응한다. 프로세스(Top, Cache), 명령당(Message), 트랜잭션당(TopTransaction/CurTransaction), 포털당(Portal), 질의당(ExecutorState), 튜플당(ExprContext). 정리는 “방금 끝난 수명에 맞는 노드를 리셋/삭제”하는 것이다.
README에서 루트가 이렇게 많은 이유를 설명하는 주요 항목들이다.
- **
ErrorContext**는 항상 몇 KB의 여유 공간을 유지한다. 메모리 부족도 일반ERROR로 보고할 수 있게 하기 위해서다. 에러 경로 자체도 메모리가 필요하다. 각 복구 후에 리셋된다. TopTransactionContext는 에러 즉시 정리되지 않는다. COMMIT/ROLLBACK으로 트랜잭션 블록이 종료될 때까지 내용이 남는다.CurTransactionContext는 최상위에서는 동일하지만, 서브트랜잭션 안에서는 per-subxact 자식을 가리킨다. 중단된 서브트랜잭션은 자식을 버리고, 커밋된 서브트랜잭션의 자식은 최상위 커밋까지 유지된다.- **
CacheMemoryContext**는TopMemoryContext처럼 리셋되지 않는다. 구별이 존재하는 주된 이유는 디버깅 편의와, 부속 캐시 저장소가 그것보다 수명이 짧은 자식 컨텍스트에 살 수 있도록 하기 위해서다.
생성, 리셋, 삭제 — 생명주기
섹션 제목: “생성, 리셋, 삭제 — 생명주기”MemoryContextCreate는 추상 헤더를 초기화하고 노드를 부모의 자식 리스트에 연결한다. 실패해서는 안 되는 함수다(Assert만 사용하고 elog는 없다). 크리티컬 섹션 안에서는 실행을 거부한다.
// MemoryContextCreate — src/backend/utils/mmgr/mcxt.c (condensed)voidMemoryContextCreate(MemoryContext node, NodeTag tag, MemoryContextMethodID method_id, MemoryContext parent, const char *name){ Assert(CritSectionCount == 0); node->type = tag; node->isReset = true; node->methods = &mcxt_methods[method_id]; node->parent = parent; node->firstchild = NULL; /* ... */ if (parent) { node->nextchild = parent->firstchild; if (parent->firstchild != NULL) parent->firstchild->prevchild = node; parent->firstchild = node; node->allowInCritSection = parent->allowInCritSection; } /* ... */}호출자는 이 함수를 직접 호출하지 않는다. 할당기별 생성 헬퍼를 통한다. 대부분의 경우는 AllocSetContextCreate(parent, name, ALLOCSET_DEFAULT_SIZES)다. 이 헬퍼가 파생 구조체(+ 초기 “키퍼” 블록)를 malloc하고 내장 헤더로 MemoryContextCreate를 호출한다.
리셋은 컨텍스트 객체를 남기고 내용만 해제한다. 관례적으로 자식들은 리셋하지 않고 삭제한다.
// MemoryContextReset / MemoryContextResetOnly — src/backend/utils/mmgr/mcxt.cvoidMemoryContextReset(MemoryContext context){ /* save a call in the common no-children case */ if (context->firstchild != NULL) MemoryContextDeleteChildren(context); if (!context->isReset) MemoryContextResetOnly(context);}
voidMemoryContextResetOnly(MemoryContext context){ if (!context->isReset) { MemoryContextCallResetCallbacks(context); /* release non-mem resources */ context->methods->reset(context); /* allocator-specific */ context->isReset = true; }}isReset 플래그는 빠른 경로 가드다. 생성 또는 마지막 리셋 이후 palloc이 한 번도 없었던 컨텍스트는 할당기의 reset을 완전히 건너뛴다. per-tuple 컨텍스트는 아무것도 할당하지 않았어도 모든 튜플마다 리셋되기 때문에 이 최적화가 중요하다.
삭제는 컨텍스트와 전체 서브트리를 해체한다. 의도적으로 설계된 두 가지 세부사항이 있다.
// MemoryContextDelete — src/backend/utils/mmgr/mcxt.c (condensed)voidMemoryContextDelete(MemoryContext context){ MemoryContext curr = context; for (;;) { MemoryContext parent; /* descend to a leaf */ while (curr->firstchild != NULL) curr = curr->firstchild; parent = curr->parent; MemoryContextDeleteOnly(curr); /* delink + delete one leaf */ if (curr == context) break; curr = parent; }}- 재귀 없음. 삭제는 명시적인 리프-하강 루프다. 재귀를 쓰지 않은 이유는 삭제가 에러/abort 경로에서 일어나기 때문이다. 이미 abort 처리 중에 “스택 깊이 제한 초과” 에러가 발생하면 재앙이다.
- 해제 전에 연결 해제.
MemoryContextDeleteOnly는free하기 전에MemoryContextSetParent(context, NULL)을 호출해 노드를 트리에서 끊는다. 리셋 콜백이 삭제 도중 에러를 내더라도, 트리가 반쯤 해제된 컨텍스트를 가리키는 상태가 되지 않는다. “크래시보다는 누수가 낫다”는 원칙이다.
flowchart TD
A["MemoryContextDelete(ctx)"] --> B{"ctx에 자식 있음?"}
B -- "예" --> C["첫 번째 리프로 하강"]
C --> B
B -- "아니오(리프)" --> D["MemoryContextCallResetCallbacks"]
D --> E["MemoryContextSetParent(leaf, NULL)<br/>먼저 트리에서 연결 해제"]
E --> F["methods->delete_context<br/>(모든 블록 + 구조체 해제)"]
F --> G{"leaf == 원래 ctx?"}
G -- "아니오" --> H["저장된 parent로 상승"]
H --> B
G -- "예" --> I["완료"]
그림 3 — 반복적인 리프-하강, 삭제, 상승 루프로 구현된 MemoryContextDelete. 리셋 콜백이 먼저 실행되고, 저장소를 해제하기 전에 노드 연결이 해제된다. 재귀를 피해 abort 정리 중 스택 오버플로를 방지한다.
리셋/삭제 콜백 — 비메모리 자원 해제
섹션 제목: “리셋/삭제 콜백 — 비메모리 자원 해제”컨텍스트는 다음 리셋 또는 삭제 직전에 한 번 실행될 콜백 리스트를 가질 수 있다. 덕분에 컨텍스트는 palloc된 객체에 연관되어 있지만 그 자체는 메모리가 아닌 자원을 해제하는 훅이 된다. tuplesort 객체 뒤의 열린 파일, 캐시 엔트리의 참조 카운트 같은 것들이 그 예다.
// MemoryContextRegisterResetCallback — src/backend/utils/mmgr/mcxt.cvoidMemoryContextRegisterResetCallback(MemoryContext context, MemoryContextCallback *cb){ /* push onto head: newest-registered fires first */ cb->next = context->reset_cbs; context->reset_cbs = cb; context->isReset = false; /* ensure reset path runs the callbacks */}콜백은 등록 역순으로 실행된다. 서브트리 해체 중에는 자식의 콜백이 부모의 콜백보다 먼저 실행된다. 호출자는 MemoryContextCallback 구조체를 직접 제공한다. 대개 대상 컨텍스트 안에 할당되므로 별도 palloc 없이 함께 해제된다. 메모리 컨텍스트가 자원 관리와 만나는 좁은 솔기는 여기다. 핀, 락, 디스크립터를 포함한 더 넓은 이야기는 postgres-resource-owners.md에 있다.
에러 처리가 컨텍스트 삭제로 언와인드하는 방법
섹션 제목: “에러 처리가 컨텍스트 삭제로 언와인드하는 방법”결정적인 이득이다. 백엔드가 elog(ERROR)에 부딪히면, 제어 흐름은 감싸는 PG_TRY/sigsetjmp 배리어로 siglongjmp한다(배리어 소유권은 postgres-error-handling.md). 그 배리어에서의 메모리 정리는 데이터 구조를 돌며 객체를 해제하는 것이 아니다. 소수의 컨텍스트 리셋과 삭제다. 가장 바깥 수준에서, AbortTransaction → CleanupTransaction은 TopTransactionContext를 리셋/삭제한다. 그 트랜잭션 중에 만들어진 모든 per-statement, per-portal, per-tuple 자식 컨텍스트가 함께 사라진다. per-message 루프는 각 사이클 시작 시 MessageContext를 리셋한다. 모든 일시적 할당이 그 루트들 중 하나의 자손이었으므로, 단 한 번의 삭제가 모든 것을 회수한다. 성공 경로와 에러 경로 모두에서, 누수를 낼 수 있는 per-object 해제 없이 동작한다.
sequenceDiagram
participant Q as backend op
participant E as elog(ERROR)
participant LJ as sigsetjmp barrier<br/>(error-handling doc)
participant MC as memory contexts
Q->>E: error raised
E->>LJ: siglongjmp to enclosing PG_TRY
LJ->>MC: AbortCurrentTransaction()
MC->>MC: MemoryContextDelete(TopTransactionContext)
Note over MC: deletes whole subtree:<br/>per-portal, per-query,<br/>per-tuple contexts
MC->>MC: callbacks fire (files closed, pins... see resowner doc)
LJ->>MC: MemoryContextReset(MessageContext) next loop
Note over MC: every transient chunk reclaimed<br/>by region teardown, not per-object free
그림 4 — 영역 해제로서의 에러 언와인드. longjmp 장치는 postgres-error-handling.md가 소유한다. 이 다이어그램은 메모리 쪽만 보여준다. TopTransactionContext를 삭제하는 것만으로 전체 일시적 서브트리를 한 번에 해제한다. 이것이 엔진이 OOM과 임의의 문장 중간 에러를 모두 일반 복구 가능 조건으로 처리할 수 있는 이유다.
네 가지 할당기, 네 가지 할당 패턴
섹션 제목: “네 가지 할당기, 네 가지 할당 패턴”구체적 할당기 넷은 모두 동일한 외부 동작(palloc/pfree/reset/delete)을 제공하지만, 내부 정책은 근본적으로 다르다. README의 한 줄 요약을 확장하면 다음과 같다.
| 할당기 | 파일 | 최적 패턴 | 개별 pfree 가능? | 청크 헤더 |
|---|---|---|---|---|
| AllocSet | aset.c | 범용(기본값) | 가능, 크기 클래스 프리리스트로 재활용 | 전체 MemoryChunk |
| Slab | slab.c | 동일 크기 청크 다수 | 가능, 밀집 패킹, 빈 블록은 OS에 반환 | 전체 MemoryChunk |
| Generation | generation.c | 수명이 비슷한 그룹 / FIFO | 가능, 단 공간 재사용 없음; 블록이 비면 해제 | 전체 MemoryChunk |
| Bump | bump.c | 쓰기 전용, 개별 해제 없음 | 불가 (pfree/realloc 미지원) | 없음 (일반 빌드) |
AllocSet — 범용 기본 할당기
섹션 제목: “AllocSet — 범용 기본 할당기”aset.c가 AllocSetContextCreate로 만드는 것이다. 시스템의 거의 모든 컨텍스트가 이것을 쓴다. 정책은 다음과 같다. 각 요청을 2의 거듭제곱 크기 클래스로 올림하고 재활용된 청크의 per-class 프리리스트를 유지한다. malloc된 블록에서 청크를 잘라낸다. 블록 크기는 두 배씩 커진다(초기 8 KB → 최대 8 MB). allocChunkLimit(~8 KB)보다 큰 요청은 pfree 시 통째로 반환하는 전용 블록으로 간다.
// AllocSetContext — src/backend/utils/mmgr/aset.c (condensed)typedef struct AllocSetContext{ MemoryContextData header; /* Standard memory-context fields */ AllocBlock blocks; /* head of list of blocks in this set */ MemoryChunk *freelist[ALLOCSET_NUM_FREELISTS]; /* free chunk lists */ uint32 initBlockSize; /* initial block size */ uint32 maxBlockSize; /* maximum block size */ uint32 nextBlockSize; /* next block size to allocate */ uint32 allocChunkLimit; /* effective chunk size limit */ int freeListIndex; /* index in context_freelists[], or -1 */} AllocSetContext;ALLOC_MINBITS = 3, ALLOCSET_NUM_FREELISTS = 11이면 프리리스트 k는 크기 1 << (k+3)의 청크를 담는다. 8, 16, 32, … 8192바이트다. 할당 빠른 경로는 다음과 같다.
// AllocSetAlloc — src/backend/utils/mmgr/aset.c (condensed)void *AllocSetAlloc(MemoryContext context, Size size, int flags){ AllocSet set = (AllocSet) context;
if (size > set->allocChunkLimit) /* oversized: dedicated block */ return AllocSetAllocLarge(context, size, flags);
fidx = AllocSetFreeIndex(size); /* size class */ chunk = set->freelist[fidx]; if (chunk != NULL) /* reuse a recycled chunk */ { set->freelist[fidx] = GetFreeListLink(chunk)->next; return MemoryChunkGetPointer(chunk); } /* else carve from the current block, or start a new one */ chunk_size = GetChunkSizeFromFreeListIdx(fidx); block = set->blocks; if (unlikely((block->endptr - block->freeptr) < (chunk_size + ALLOC_CHUNKHDRSZ))) return AllocSetAllocFromNewBlock(context, size, flags, fidx); return AllocSetAllocChunkFromBlock(context, block, size, chunk_size, fidx);}pfree는 청크를 freelist[fidx]에 되돌린다(OS로 반환하지 않는다). 오버사이즈 청크의 전용 블록은 malloc에 반환된다. 리셋은 첫 번째(“키퍼”) 블록을 남기고 나머지를 해제한다. 반복 리셋 컨텍스트가 malloc을 thrash하지 않는다. 작은 컨텍스트 프리리스트(context_freelists[], 상한 MAX_FREE_CONTEXTS = 100)도 있다. 방금 삭제된 기본 크기 AllocSet을 통째로 캐싱해 두었다가 다음 생성 요청에 돌려준다. 컨텍스트 구조체 자체의 반복 malloc/free를 피한다.
flowchart LR
REQ["palloc(size) in an AllocSet"] --> Q1{"size > allocChunkLimit?"}
Q1 -- "예" --> LRG["AllocSetAllocLarge<br/>전용 블록, 외부 청크"]
Q1 -- "아니오" --> FIDX["fidx = AllocSetFreeIndex(size)<br/>(2의 거듭제곱 클래스)"]
FIDX --> Q2{"freelist[fidx] 비어 있지 않음?"}
Q2 -- "예" --> POP["재활용 청크 꺼냄<br/>O(1)"]
Q2 -- "아니오" --> Q3{"현재 블록에 공간 있음?"}
Q3 -- "예" --> CARVE["블록의 freeptr 전진"]
Q3 -- "아니오" --> NEW["새 블록 malloc<br/>(크기는 maxBlockSize까지 두 배씩)"]
그림 5 — AllocSetAlloc 결정 흐름. 작은 요청은 2의 거듭제곱 프리리스트(재활용 청크, 없으면 두 배씩 커지는 현재 블록에서 잘라냄)로 간다. allocChunkLimit(~8 KB) 초과 요청은 pfree 시 통째로 해제되는 전용 블록으로 간다.
Slab — 고정 크기 청크, 단편화 저항
섹션 제목: “Slab — 고정 크기 청크, 단편화 저항”slab.c는 동일 크기 객체 스트림을 위한 것이다(청크 크기는 컨텍스트 생성 시점에 고정). 블록을 정확한 크기의 청크로 나누고, 빈 청크 수를 기준으로 버킷화한 blocklist[] 배열에 블록을 보관한다. 새 할당은 가장 꽉 찬 비만 블록에서 서비스한다. 사용 청크가 밀집되어 비워진 블록이 OS에 반환될 수 있다. 수명이 긴 객체와 짧은 객체가 같은 크기로 섞일 때 발생하는 단편화를 직접 공략한다.
// SlabContext — src/backend/utils/mmgr/slab.c (condensed)typedef struct SlabContext{ MemoryContextData header; uint32 chunkSize; /* the requested (non-aligned) chunk size */ uint32 fullChunkSize; /* chunk size with header + alignment */ uint32 blockSize; /* size of each block of chunks */ int32 chunksPerBlock; int32 curBlocklistIndex; /* fullest blocks live here */ /* ... */ dlist_head blocklist[SLAB_BLOCKLIST_COUNT]; /* blocks bucketed by nfree */} SlabContext;인트리 사용처: reorderbuffer.c(논리 디코딩이 WAL 변경마다 균일한 ReorderBufferChange를 할당한다 — 교과서적인 고정 크기 스트림 패턴).
Generation — FIFO / 수명 유사 그룹
섹션 제목: “Generation — FIFO / 수명 유사 그룹”generation.c는 세대(generation) 단위로 함께 죽는 객체, 또는 대략 FIFO 순서로 죽는 객체에 적합하다. 블록 단위 범프 스타일 할당기인데 한 가지 특이점이 있다. 블록당 해제 카운트를 추적해 모든 청크가 pfree되면 블록을 OS에 반환한다. 단 블록 안에서 공간을 재사용하지 않는다(프리리스트 없음). freeblock 하나를 재활용용으로 보관해 malloc thrash를 방지한다.
// GenerationContext — src/backend/utils/mmgr/generation.c (condensed)typedef struct GenerationContext{ MemoryContextData header; uint32 initBlockSize, maxBlockSize, nextBlockSize, allocChunkLimit; GenerationBlock *block; /* current (most recently allocated) block */ GenerationBlock *freeblock; /* one empty block kept for recycling */ dlist_head blocks; /* list of blocks */} GenerationContext;인트리 사용처: reorderbuffer.c(튜플 데이터, 커밋 순서로 해제), tuplestore.c, gistvacuum.c — AllocSet의 프리리스트가 낭비일 뿐인 FIFO 유사 생산자/소비자 패턴들.
Bump — 헤더 없는 쓰기 전용 할당기
섹션 제목: “Bump — 헤더 없는 쓰기 전용 할당기”bump.c(PG17에 추가)는 가장 밀도가 높은 할당기다. 일반 빌드에서 청크에 헤더가 전혀 없다. 따라서 pfree, repalloc, GetMemoryChunkSpace, GetMemoryChunkContext는 미지원이며, 시도하면 에러가 발생한다. 많은 수의 작은 청크를 할당하고, 개별적으로는 해제하지 않으며, 컨텍스트 전체의 리셋/삭제로만 회수한다. 헤더를 없애면 블록당, 캐시 라인당 더 많은 청크가 들어간다.
// BumpContext — src/backend/utils/mmgr/bump.c (condensed)typedef struct BumpContext{ MemoryContextData header; uint32 initBlockSize, maxBlockSize, nextBlockSize, allocChunkLimit; dlist_head blocks; /* block being filled is at the head */} BumpContext;일반 빌드에서 Bump_CHUNKHDRSZ는 0이다. MEMORY_CONTEXT_CHECKING 아래에서만 bump가 MemoryChunk 헤더를 추가해 미지원 연산을 명확한 ERROR로 잡는다. 인트리 사용처: nodeAgg.c(해시 집계 그룹 상태), tuplesort.c, tidstore.c — 모두 “할당하고, 쓰고, 아레나 전체를 버린다” 패턴의 스크래치패드.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼 이름을 기준점으로 삼는다. 줄 번호는 사용하지 않는다. PostgreSQL 소스는 이동하고, 함수/구조체/매크로 이름은 대부분의 리팩터에서 살아남아 grep할 수 있다.
git grep -n '<symbol>' src/backend/utils/mmgr/으로 현재 위치를 찾는다. 위치 힌트 표의 줄 번호는 커밋273fe94(REL_18) 기준 관측값이며, 빠른 힌트일 뿐이다.
추상 계층과 디스패치 (src/include/nodes/memnodes.h, src/backend/utils/mmgr/mcxt.c)
섹션 제목: “추상 계층과 디스패치 (src/include/nodes/memnodes.h, src/backend/utils/mmgr/mcxt.c)”struct MemoryContextData(memnodes.h) — 추상 컨텍스트 헤더(트리 링크,methodsvtable 포인터,isReset,mem_allocated).struct MemoryContextMethods(memnodes.h) — 할당기 vtable.enum MemoryContextMethodID(memutils_internal.h) — 4비트 할당기 태그. 예약값(_RESERVED_GLIBC_,_RESERVED_UNUSEDMEM_,_RESERVED_WIPEDMEM_)은 잘못된 포인터가 깔끔하게 실패하게 한다.mcxt_methods[](mcxt.c) — 할당기당 한 행짜리 vtable 배열.GetMemoryChunkMethodID/MCXT_METHOD(mcxt.c) — 청크 헤더에서 4비트 id를 읽고 디스패치.MemoryContextCreate(mcxt.c) — 헤더 초기화, 부모에 연결.
생명주기 (src/backend/utils/mmgr/mcxt.c)
섹션 제목: “생명주기 (src/backend/utils/mmgr/mcxt.c)”MemoryContextReset/MemoryContextResetOnly/MemoryContextResetChildren— 내용 해제, 자식 삭제(또는 리셋);isReset빠른 경로.MemoryContextDelete/MemoryContextDeleteOnly/MemoryContextDeleteChildren— 반복적 리프-하강 해체, 해제 전 연결 해제.MemoryContextSetParent— 재부모화(fill 후 컨텍스트의 수명 변경에 사용).MemoryContextRegisterResetCallback/MemoryContextCallResetCallbacks— 비메모리 자원 훅.MemoryContextAllocationFailure/MemoryContextSizeFailure— OOM / 잘못된 크기elog(ERROR)경로.
할당 API (src/backend/utils/mmgr/mcxt.c, src/include/utils/palloc.h)
섹션 제목: “할당 API (src/backend/utils/mmgr/mcxt.c, src/include/utils/palloc.h)”palloc/palloc0/palloc_extended/palloc_aligned—CurrentMemoryContext에서 할당.MemoryContextAlloc/MemoryContextAllocZero/MemoryContextAllocExtended— 지정 컨텍스트에서 할당.pfree/repalloc/repalloc0/repalloc_extended— 청크 헤더에서 컨텍스트 복원;CurrentMemoryContext무시.GetMemoryChunkContext/GetMemoryChunkSpace— 포인터 → 컨텍스트 / 크기.MemoryContextSwitchTo(인라인,palloc.h) — 현재 컨텍스트 저장/복원.
할당기
섹션 제목: “할당기”AllocSetContext,AllocSetAlloc,AllocSetFree,AllocSetReset,AllocSetDelete,AllocSetFreeIndex(aset.c) — 범용; 2의 거듭제곱 프리리스트, 두 배씩 커지는 블록, 컨텍스트 프리리스트.SlabContext,SlabAlloc,SlabReset,SlabContextCreate(slab.c) — 고정 크기, 가장 꽉 찬 블록 우선.GenerationContext,GenerationAlloc,GenerationFree,GenerationContextCreate(generation.c) — FIFO/수명, 재사용 없음.BumpContext,BumpAlloc,BumpContextCreate(bump.c) — 헤더 없음, pfree 불가.AllocSetContextCreate매크로 +ALLOCSET_DEFAULT_SIZES/ALLOCSET_SMALL_SIZES(memutils.h) — 일반적인 생성 진입점.
위치 힌트 (2026-06-05 기준, REL_18 273fe94)
섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”| 심볼 | 파일 | 줄 |
|---|---|---|
struct MemoryContextData | src/include/nodes/memnodes.h | 117 |
struct MemoryContextMethods | src/include/nodes/memnodes.h | 58 |
MemoryContextIsValid | src/include/nodes/memnodes.h | 145 |
enum MemoryContextMethodID | src/include/utils/memutils_internal.h | 121 |
mcxt_methods[] | src/backend/utils/mmgr/mcxt.c | 47 |
MCXT_METHOD (매크로) | src/backend/utils/mmgr/mcxt.c | 188 |
GetMemoryChunkMethodID | src/backend/utils/mmgr/mcxt.c | 196 |
MemoryContextReset | src/backend/utils/mmgr/mcxt.c | 389 |
MemoryContextResetOnly | src/backend/utils/mmgr/mcxt.c | 408 |
MemoryContextDelete | src/backend/utils/mmgr/mcxt.c | 460 |
MemoryContextDeleteOnly | src/backend/utils/mmgr/mcxt.c | 502 |
MemoryContextRegisterResetCallback | src/backend/utils/mmgr/mcxt.c | 574 |
MemoryContextCallResetCallbacks | src/backend/utils/mmgr/mcxt.c | 591 |
MemoryContextSetParent | src/backend/utils/mmgr/mcxt.c | 643 |
GetMemoryChunkContext | src/backend/utils/mmgr/mcxt.c | 713 |
MemoryContextCreate | src/backend/utils/mmgr/mcxt.c | 1106 |
MemoryContextAlloc | src/backend/utils/mmgr/mcxt.c | 1191 |
MemoryContextAllocExtended | src/backend/utils/mmgr/mcxt.c | 1248 |
palloc | src/backend/utils/mmgr/mcxt.c | 1346 |
palloc0 | src/backend/utils/mmgr/mcxt.c | 1376 |
pfree | src/backend/utils/mmgr/mcxt.c | 1553 |
repalloc | src/backend/utils/mmgr/mcxt.c | 1573 |
MemoryContextSwitchTo (인라인) | src/include/utils/palloc.h | 138 |
struct AllocSetContext | src/backend/utils/mmgr/aset.c | 152 |
AllocSetContextCreateInternal | src/backend/utils/mmgr/aset.c | 347 |
AllocSetReset | src/backend/utils/mmgr/aset.c | 537 |
AllocSetDelete | src/backend/utils/mmgr/aset.c | 607 |
AllocSetAlloc | src/backend/utils/mmgr/aset.c | 967 |
AllocSetFree | src/backend/utils/mmgr/aset.c | 1062 |
struct GenerationContext | src/backend/utils/mmgr/generation.c | 59 |
GenerationContextCreate | src/backend/utils/mmgr/generation.c | 160 |
GenerationAlloc | src/backend/utils/mmgr/generation.c | 527 |
struct SlabContext | src/backend/utils/mmgr/slab.c | 103 |
SlabContextCreate | src/backend/utils/mmgr/slab.c | 322 |
SlabAlloc | src/backend/utils/mmgr/slab.c | 631 |
struct BumpContext | src/backend/utils/mmgr/bump.c | 66 |
BumpContextCreate | src/backend/utils/mmgr/bump.c | 131 |
BumpAlloc | src/backend/utils/mmgr/bump.c | 491 |
ALLOCSET_DEFAULT_SIZES (매크로) | src/include/utils/memutils.h | 160 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”각 항목은 커밋
273fe94(REL_18) 기준 현재 소스의 사실로 시작한다. 다른 자료 없이도 읽을 수 있어야 한다. 확인 방법과 이력적 맥락은 뒤에 붙인다. 미해결 질문은 아래에 별도 정리한다.
검증된 사실
섹션 제목: “검증된 사실”-
할당기 타입은 모든 청크 바로 앞에, 패딩 없이,
uint64헤더의 하위 4비트에 인코딩된다.GetMemoryChunkMethodID와MCXT_METHOD매크로(mcxt.c)에서 확인. 마스크는MEMORY_CONTEXT_METHODID_MASK이고, 예약 id 값들(0000,0001,0010,1111)은 미사용/glibc/지워진 메모리에 대응해 잘못된 포인터가 쓰레기 디스패치 대신 깔끔하게 실패하도록 한다. -
REL_18에는 구체적 할당기가 정확히 넷이다: AllocSet, Slab, Generation, Bump.
MemoryContextIsValid(memnodes.h, 네 가지 NodeTag를IsA-체크) 및mcxt_methods[]배열(mcxt.c)에서 확인. 다섯 번째 메서드 id인MCTX_ALIGNED_REDIRECT_ID가 있으나, 이는 독립적인 컨텍스트 타입이 아니다.palloc_aligned(alignedalloc.c)가 쓰는 리다이렉트이며,free_p/realloc/get_chunk_*만 채워져 있다. -
일반 빌드에서 Bump 청크는 헤더가 없다(
Bump_CHUNKHDRSZ == 0).bump.c에서 확인. Bump 청크에 대한pfree/repalloc/GetMemoryChunkSpace/GetMemoryChunkContext는 미지원이다.MEMORY_CONTEXT_CHECKING아래에서는MemoryChunk헤더가 추가되어 해당 호출이 메모리 오염 대신 명확한 ERROR를 낸다. -
palloc은 절대 NULL을 반환하지 않는다. OOM은 할당기의alloc메서드 내부에서elog(ERROR)로 종료된다.palloc(Assert(ret != NULL))과MemoryContextAllocationFailure(mcxt.c)에서 확인. NULL을 반환받을 수 있는 유일한 경로는palloc_extended/MemoryContextAllocExtended의MCXT_ALLOC_NO_OOM플래그다. -
MemoryContextDelete는 반복적이며 재귀하지 않는다. 각 노드를 해제하기 전에 연결을 끊는다.MemoryContextDelete(abort 정리 중 “스택 깊이 제한 초과” 위험을 명시적으로 언급하는 리프-하강 루프)와MemoryContextDeleteOnly(delete_context전에MemoryContextSetParent(context, NULL))에서 확인. README와 코드 내 주석 모두 동의한다. “크래시보다는 누수가 낫다.” -
AllocSet 소형 할당은 11개의 2의 거듭제곱 크기 클래스(8 B … 8 KB) 중 하나로 올림된다. 더 큰 요청은 전용 블록으로 간다.
ALLOC_MINBITS = 3,ALLOCSET_NUM_FREELISTS = 11,ALLOC_CHUNK_LIMIT,AllocSetAlloc/AllocSetFreeIndex(aset.c)에서 확인.ALLOCSET_DEFAULT_INITSIZE는 8 KB,ALLOCSET_DEFAULT_MAXSIZE는 8 MB(memutils.h)이며 그 사이에서 두 배씩 커진다. -
리셋/삭제 콜백은 최신 등록 순으로 실행된다. 서브트리 해체 중에는 자식 콜백이 부모 콜백보다 먼저 실행된다.
MemoryContextRegisterResetCallback(리스트 헤드에 push)과MemoryContextCallResetCallbacks(헤드에서 pop)에서 확인. README의 “등록 역순” 서술과 일치한다. 콜백 구조체는 호출자가 제공하며, 일반적으로 대상 컨텍스트 안에 할당된다. -
TopTransactionContext는 에러 즉시 정리되지 않는다. 트랜잭션 블록이 종료될 때까지 유지된다. README에 명시됨. 실제 abort 시점의 리셋/삭제는 트랜잭션 관리 계층에서 구동되며,mcxt.c에서 오지 않는다. 정확한 호출 지점 확인은 이 문서의 범위 밖이다(postgres-error-handling.md/ xact 참조).
미해결 질문
섹션 제목: “미해결 질문”-
Bump 할당기 채택 현황. Bump는 PG17에 추가됐고, REL_18은
nodeAgg.c,tuplesort.c,tidstore.c에서 쓴다. 과거 AllocSet 또는 Generation 기반 스크래치패드 중 어떤 것이 전환 후보이고, 실제 밀도/CPU 이득은 얼마인가? 조사 방법: REL_16→REL_18에 걸쳐BumpContextCreate호출 지점의 diff를 보고, 객체를 명시적으로 pfree하지 않는aset/generation컨텍스트를 찾는다. -
context_freelists상한 100.aset.c는 삭제된 기본 크기 컨텍스트를 최대MAX_FREE_CONTEXTS = 100개까지 캐싱하고, “목록이 넘치면 기존 항목을 모두 삭제”한다. 어떤 워크로드(단기 per-relation 컨텍스트 다수?)에서 이 초과가 문제가 되는가? 100이 병목이 된 사례가 있는가? 조사 방법: 고 DDL 또는 고 플랜-변동 워크로드에서context_freelists[].num_free를 계측한다. -
메모리 집계 비용.
mem_allocated는 블록 단위로 지연 업데이트되므로,MemoryContextMemAllocated/stats는 서브트리를 재귀적으로 탐색해야 한다(README “Memory Accounting”). 컨텍스트 포리스트가 매우 넓거나 깊은 백엔드(수천 개의 relcache 자식 컨텍스트)에서pg_log_backend_memory_contexts()덤프의 비용은 얼마인가? 조사 방법: 대형 relcache 아래에서ProcessLogMemoryContextInterrupt의 시간을 측정한다.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”포인터 목록이다. 분석이 아니다. 각 항목은 후속 문서를 시작하기 위한 진입점이다.
-
CUBRID의 프라이빗 힙 / 스레드별 할당기. CUBRID는 컨텍스트 포리스트를 쓰지 않는다. 프라이빗 힙(
db_private_alloc) 방식과 명시적 free, 스레드별 스크래치 영역을 혼합한다. 나란히 비교하면 PostgreSQL이 트리로 얻는 것(O(1) 에러 경로 회수)과 치르는 것(청크별 헤더 오버헤드, 압축 불가)이 드러난다. CUBRID 힙 / 메모리 분석 문서와 교차 참조한다. -
언어에서의 영역 추론 (Tofte & Talpin, “Region-Based Memory Management”, 1997). PostgreSQL은 영역을 수동으로 할당한다(어느 컨텍스트로 전환할지 직접 고른다). 이 연구 방향은 영역 수명을 정적으로 추론한다. 비교하면 PostgreSQL의 수동
MemoryContextSwitchTo방식이 추론 없는 기준선임이 드러나고, 버그(잘못된 컨텍스트 할당)가 어디서 오는지가 명확해진다. -
슬랩 할당 계보 (Bonwick, “The Slab Allocator”, USENIX 1994). PostgreSQL의
slab.c는 Solaris 커널 할당기와 같은 아이디어(단편화를 막는 고정 크기 객체 캐시)다. 가장 꽉 찬 블록 우선 개선은 PostgreSQL 고유의 것이다. 계보를 추적하는 노트는 슬랩 설계 선택들을 원래 근거에 닻을 내린다. -
다른 시스템의 아레나 할당기 (jemalloc/tcmalloc 아레나, Rust의
bumpalo, V8 존). Bump의 헤더 없는 쓰기 전용 아레나는 컴파일러/VM의 “존”과 같은 패턴이다. 헤더 없는 아레나가 프리리스트 할당기를 이기는 조건과, PG17의 bump 커밋이 인용한 밀도/캐시 숫자를 비교할 가치가 있다. -
메모리 집계와 질의당 메모리 한도. PostgreSQL의 지연 블록 단위
mem_allocated집계(그리고 REL_18 기준으로 백엔드별 하드 메모리 상한이 없다는 점)는 반복적인 제안 주제다.postgres-tuplesort.md/ hash-agg의work_mem스필 메커니즘과, 백엔드 메모리 한도에 관한 커뮤니티 논의와 연결 짓는다.
인트리 README
섹션 제목: “인트리 README”src/backend/utils/mmgr/README— “Memory Context System Design Overview”: 설계 근거(현재 컨텍스트, 부모/자식 트리, 전역적으로 알려진 컨텍스트, 리셋 콜백, 네 가지 할당기, 메모리 집계). 이 서브시스템의 주 설계 문서.
소스 파일 (/data/hgryoo/references/postgres/, REL_18 273fe94)
섹션 제목: “소스 파일 (/data/hgryoo/references/postgres/, REL_18 273fe94)”src/backend/utils/mmgr/mcxt.c— 추상 계층, 디스패치, 생명주기, palloc/pfree.src/backend/utils/mmgr/aset.c— AllocSet (기본 할당기).src/backend/utils/mmgr/generation.c— Generation 할당기.src/backend/utils/mmgr/slab.c— Slab 할당기.src/backend/utils/mmgr/bump.c— Bump 할당기.src/include/nodes/memnodes.h—MemoryContextData,MemoryContextMethods.src/include/utils/palloc.h— palloc/pfree API,MemoryContextSwitchTo.src/include/utils/memutils.h— 생성 매크로,ALLOCSET_*_SIZES.src/include/utils/memutils_internal.h—MemoryContextMethodID.
교과서 / 이론
섹션 제목: “교과서 / 이론”- Database Internals (Petrov), Part I 구현 장 — 메모리 대 디스크 기반 저장소, 할당/단편화 트레이드오프; §“이론적 배경”의 영역/아레나 프레임.
- 영역 기반 메모리 관리 배경: Tofte & Talpin 1997(영역), Bonwick 1994(슬랩) — 구체적 앵커는 §“PostgreSQL 너머” 참조.
교차 참조 (메커니즘 중복 없음)
섹션 제목: “교차 참조 (메커니즘 중복 없음)”postgres-error-handling.md— abort 경로에서 컨텍스트 삭제를 트리거하는PG_TRY/sigsetjmp/elog장치.postgres-resource-owners.md— 비메모리 자원(버퍼 핀, 락, 파일 디스크립터) 해제. 메모리 컨텍스트는 리셋 콜백으로만 이 부분과 접촉한다.postgres-architecture-overview.md— Axis 11 base-infra 위치 지정. “백엔드는 본질적으로 프라이빗 메모리 컨텍스트 트리를 가진 제어 흐름이다.”