콘텐츠로 이동

(KO) PostgreSQL 트랜잭션 관리 — 커밋 상태 머신, 서브트랜잭션, 2PC 연결 지점

목차

트랜잭션(transaction)은 데이터베이스에서 *원자성(atomicity)*과 *내구성(durability)*의 단위다. 여러 읽기와 쓰기가 완전히 반영되거나 전혀 반영되지 않는다는, 그리고 시스템이 “커밋됨”을 보고한 순간부터 그 결과가 장애를 넘어 살아남는다는 ACID 약속이다. Database System Concepts (Silberschatz, Korth, Sudarshan, 7판, 17장 “Transactions”)는 고전적인 트랜잭션 상태 다이어그램 — active → partially committed → committed, 또는 active → failed → aborted — 으로 이 개념을 정리하고, 내구성이 확정되는 순간을 커밋 로그 레코드가 안정 저장소에 도달하는 시점으로 못 박는다. 그 이전의 모든 것은 되돌릴 수 있고, 그 이후의 모든 것은 되돌릴 수 없다.

그 문장 하나 안에 이 모듈이 존재하는 설계 공간이 숨어 있다. 교과서 모델은 “커밋 레코드를 쓰고 내구성을 확보하라”고 말하지만, 정리 단계들의 순서 어디에 레코드를 써야 하는지, 엔진이 트랜잭션에 이름을 어떻게 붙이는지, 사용자가 전체 트랜잭션을 잃지 않고 중간 작업을 되돌리려 할 때 어떻게 처리하는지는 알려 주지 않는다. 세 가지 설계 선택이 뒤따르고, PostgreSQL의 xact.c는 각각을 특정 방향으로 결정한다.

  1. 트랜잭션에 이름을 언제 붙이는가? 교과서는 트랜잭션 식별자가 트랜잭션 전체 생명주기 동안 존재한다고 가정한다. 실제 엔진은 BEGIN 시점에 즉시(eagerly) 할당할지, 첫 번째 쓰기 시점에 지연(lazily) 할당할지 결정해야 한다. 읽기 전용 트랜잭션도 잠금과 스냅샷을 위한 핸들은 필요하지만, 영구 커밋 로그에 등장할 필요는 없다.

  2. 커밋의 연산 순서는 무엇인가? 커밋은 하나의 명령이 아니다. “지연된 트리거를 실행하고, 커서를 닫고, 프리-커밋 콜백을 실행하고, WAL 커밋 레코드를 쓰고, 플러시하고, 커밋 로그를 표시하고, 실행 중 트랜잭션 집합에서 빠져나오고, 잠금을 해제하고, 포스트-커밋 콜백을 실행하고, 메모리를 해제한다”는 순서 있는 파이프라인이다. 교과서는 내구성 확정 순간을 제시하고, 공학적 과제는 나머지 모든 것을 그 순간 주위에 어떻게 배열하느냐다.

  3. 트랜잭션의 일부를 어떻게 취소하는가? SQL 세이브포인트(savepoint)와 PL/pgSQL 예외 블록은 중첩되어 부분적으로 롤백 가능한 단위를 요구한다. 이것의 모델이 **중첩 트랜잭션(nested transaction)**이다. 자식의 결과가 영구적이 되려면 자식과 모든 조상이 모두 커밋해야 한다. 정확성의 틀은 Database System Concepts §17.3이 제시하고, 공학적 틀은 “트리를 어떻게 값싸게 표현하고 되감는가”다.

이 모든 것의 밑바닥에 있는 내구성 메커니즘은 **선행 기록 로깅(write-ahead logging, WAL)**이다. 그 표준 설명이 ARIES(Mohan et al., ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging, ACM TODS 1992; dbms-papers/aries.md)다. ARIES는 이 모듈이 의존하는 세 가지 원칙을 확립한다. WAL(변경이 데이터 페이지에 반영되기 전에 로그에 기록), redo 중 히스토리 반복, 그리고 undo 동작 로깅(세이브포인트나 서브트랜잭션 중단이 그 자체로 복구 가능한 연산이 되도록). PostgreSQL의 트랜잭션 매니저는 ARIES 방식의 복구가 나중에 재연하는 커밋/중단 레코드의 생산자다. 복구 쪽은 postgres-recovery-redo.md가, WAL 삽입 기계는 postgres-xlog-wal.md가 담당한다. xact.c가 소유하는 것은 결정의 순간이다. 트랜잭션이 커밋된다는 사실, 커밋 순서, 그리고 그 결정을 내구성 있게 만드는 레코드를 누가 쓰는가.

교과서가 예상하지 못한 PostgreSQL 고유의 미묘함이 하나 있다. PostgreSQL의 힙은 no-overwrite MVCC 방식이므로, 커밋 레코드는 데이터가 보이게 되는 지점이 아니라는 점이다. 가시성은 스냅샷 기계(postgres-mvcc-snapshots.md)가 커밋 로그(pg_xact / CLOG, postgres-clog-commit-ts.md)에서 XID의 커밋 상태를 읽어 결정한다. 따라서 xact.c의 역할은 인플레이스(in-place) 엔진보다 좁다. 새로운 값을 설치하지 않는다. 2비트 상태를 “진행 중”에서 “커밋됨”(또는 “중단됨”)으로 뒤집고, 실행 중 트랜잭션 집합에서 빠져나온다. 다른 모든 백엔드의 가시성 결정은 그 뒤집힘에 의존한다.

PostgreSQL, Oracle, SQL Server, MySQL/InnoDB, CUBRID에 걸쳐, 교과서 트랜잭션을 실현하는 반복적인 엔지니어링 관례가 있다. 이 관례들은 교과서에 없지만, 이름 붙이기·커밋 순서·중첩이라는 동일한 세 가지 압력이 구현자들을 같은 형태로 밀어 넣기 때문에 반복된다.

단순 증가 트랜잭션 ID 카운터, 짧은 락 아래 할당. 모든 엔진은 트랜잭션 식별자를 증가 순서로 내어 주는 전역 카운터를 갖고 있다. 이 순서는 가시성과 복구 모두가 “이 ID가 저 ID보다 오래됐다”고 추론하는 근거이므로 중요하다. 카운터는 몇 명령만 락을 잡는 좁은 임계 구간으로 보호된다. 시스템에서 가장 뜨거운 공유 자원 중 하나이기 때문이다. PostgreSQL은 XidGenLock 아래 nextXid라 부르고, InnoDB는 trx_sys->max_trx_id를 쓴다. 공통 요령은 락을 최대한 짧게 잡고, 새 ID를 공유 실행 중 트랜잭션 레지스트리에 락을 놓기 전에 게시하는 것이다. 동시에 실행 중인 스냅샷이 새 ID를 놓치지 않게 하기 위해서다.

읽기 전용 작업에 대한 지연 ID 할당. 성숙한 엔진은 쓰기가 없는 트랜잭션에 영구 ID를 소비하지 않는다. 읽기 전용 트랜잭션도 잠금을 잡고 스냅샷을 고정하기 위한 어떤 핸들은 필요하므로, 엔진들은 정체성을 둘로 나눈다. 시작 시 저렴하게 할당되는 백엔드 로컬 핸들과, 첫 번째 쓰기 때만 할당되는 비싼 전역 순서 ID다. PostgreSQL의 분리가 VXID(가상 트랜잭션 ID) vs XID다. VXID — (procNumber, localTransactionId) — 는 StartTransaction에서 공유 메모리 경쟁 없이 할당되고, 실제 XID는 AssignTransactionId까지 미뤄진다.

하나의 내구성 순간을 중심으로 한 고정 파이프라인으로서의 커밋. 모든 엔진은 “커밋”을 정확히 하나의 되돌릴 수 없는 지점 — 커밋 로그 레코드를 쓰고 플러시하는 단계 — 을 가진 순서 있는 시퀀스로 만든다. 그 이전 단계들은 중단 안전(에러가 중단 경로로 우회)이고, 그 이후 단계들은 트랜잭션을 실패시켜서는 안 되는 “비핵심 정리”다. 보편적 순서 규칙은 커밋이 내구성 있게 된 후에만 변경 사항을 다른 주체에게 보이게 하고(행/페이지 잠금 해제), 스냅샷 취득자와 인터록하는 방식으로 실행 집합을 벗어나는 것이다.

“중단은 무료” 경로의 별도 존재. 복구 규칙이 “커밋 레코드가 없는 트랜잭션은 중단된 것으로 간주”이므로, 엔진은 중단 레코드를 플러시할 필요가 없다. PostgreSQL은 중단 레코드를 쓰지만(핫 스탠바이와 잠금 대기자 깨우기에 유용), 의도적으로 XLogFlush를 호출하지 않는다. 커밋은 내구성 비용을 지불하고, 중단은 지불하지 않는다는 이 비대칭은 보편적이다.

스택과 상향 병합으로 구현하는 중첩 트랜잭션. 세이브포인트와 예외 블록은 레벨별 상태 레코드의 스택으로 구현된다. 커밋한 자식은 독립적으로 내구성 있게 커밋되지 않는다. 대신 그 정체성(과 자신의 커밋된 자식들)은 부모로 상향 병합되고, 최상위 커밋만이 한 레코드로 전체 트리를 내구성 있게 커밋한다. 중단한 자식은 즉시 중단 표시가 되어(잠금 대기자와 가시성 검사가 조기 종료) 스택에서 해제된다.

트랜잭션 범위에 묶인 리소스 소유권. 핀된 버퍼, 열린 파일, 카탈로그 캐시 항목, 보유 잠금 — 이 모두는 트랜잭션 종료 시 정의된 순서로 일괄 해제될 수 있도록 추적된다. PostgreSQL은 이를 ResourceOwner 트리(utils/resowner)에 중앙화한다. 커밋과 중단 경로는 단계적으로(잠금 전, 잠금, 잠금 후) 트리를 순회한다.

이론 / 관례PostgreSQL 이름 (이 모듈)
트랜잭션 상태 (active / committed / aborted)TransState 열거형 — TRANS_DEFAULT, TRANS_START, TRANS_INPROGRESS, TRANS_COMMIT, TRANS_ABORT, TRANS_PREPARE
클라이언트 블록 상태 (BEGIN/COMMIT/SAVEPOINT 블록)TBlockState 열거형 — TBLOCK_*
트랜잭션별 제어 블록TransactionStateData (스택 노드)
전역 단조 증가 ID 카운터TransamVariables->nextXid, XidGenLock 아래
시작 시 저렴한 핸들 (읽기 전용)VXID(procNumber, localTransactionId)
지연 실제 ID (첫 번째 쓰기 시)XIDAssignTransactionIdGetNewTransactionId
내구성 확정 레코드XactLogCommitRecord가 쓰는 XLOG_XACT_COMMIT
커밋 상태 저장소 (뒤집힘)TransactionIdCommitTree를 통한 pg_xact / CLOG (postgres-clog-commit-ts.md 참조)
실행 집합 탈퇴 (인터록)ProcArrayEndTransaction (postgres-mvcc-snapshots.md 참조)
중첩 트랜잭션 노드parent 링크를 가진 TransactionStateData 하나
서브커밋 시 자식→부모 병합AtSubCommit_childXids → 부모의 childXids[]
범위별 리소스 정리ResourceOwner 해제 단계 + AtEOXact_* / AtEOSubXact_* 콜백
부분 롤백을 위한 undo 로깅 (ARIES)중단 레코드 + 즉각적 CLOG 중단 표시; 물리적 undo는 MVCC, 로그 없음

다음 절에서 TransactionStateDataAssignTransactionId가 등장할 때, 독자는 각각이 어떤 종류의 것인지 이미 알고 있다.

두 겹의 상태 머신, 계층당 하나

섹션 제목: “두 겹의 상태 머신, 계층당 하나”

인트리(in-tree) README는 트랜잭션 시스템을 “3계층 시스템”이라 부른다. 최하위 계층은 저수준 루틴(StartTransaction, CommitTransaction, …)이고, 중간 계층은 postgres.c의 쿼리별 StartTransactionCommand / CommitTransactionCommand이며, 최상위 계층은 SQL 교통정리자(BeginTransactionBlock, EndTransactionBlock, …)다. 이 계층들을 관통하는 두 상태는 의도적으로 분리되어 있다.

// TransState — src/backend/access/transam/xact.c
typedef enum TransState
{
TRANS_DEFAULT, /* idle */
TRANS_START, /* transaction starting */
TRANS_INPROGRESS, /* inside a valid transaction */
TRANS_COMMIT, /* commit in progress */
TRANS_ABORT, /* abort in progress */
TRANS_PREPARE, /* prepare in progress */
} TransState;

TransState엔진의 관점이다. 이 백엔드가 지금 물리적으로 무엇을 하고 있는가. IsTransactionState()TRANS_INPROGRESS에서만 true를 반환한다. START/COMMIT/ABORT/PREPARE 상태는 명시적으로 “무언가 의미 있는 일을 하기엔 너무 이르거나 너무 늦은” 상태다. 데이터베이스 접근은 TRANS_INPROGRESS에서만 안전하다는 의미다.

두 번째 머신은 클라이언트의 관점이다. 사용자의 BEGIN/COMMIT 블록이 무엇을 원하는가. 여러 쿼리 사이클에 걸쳐 의도를 기억해야 하므로 상태가 훨씬 많다.

// TBlockState — src/backend/access/transam/xact.c (condensed)
typedef enum TBlockState
{
/* not-in-transaction-block states */
TBLOCK_DEFAULT, /* idle */
TBLOCK_STARTED, /* running single-query transaction */
/* transaction block states */
TBLOCK_BEGIN, /* starting transaction block */
TBLOCK_INPROGRESS, /* live transaction */
TBLOCK_IMPLICIT_INPROGRESS, /* live transaction after implicit BEGIN */
TBLOCK_PARALLEL_INPROGRESS, /* live transaction inside parallel worker */
TBLOCK_END, /* COMMIT received */
TBLOCK_ABORT, /* failed xact, awaiting ROLLBACK */
TBLOCK_ABORT_END, /* failed xact, ROLLBACK received */
TBLOCK_ABORT_PENDING, /* live xact, ROLLBACK received */
TBLOCK_PREPARE, /* live xact, PREPARE received */
/* subtransaction states */
TBLOCK_SUBBEGIN, TBLOCK_SUBINPROGRESS, TBLOCK_SUBRELEASE,
TBLOCK_SUBCOMMIT, TBLOCK_SUBABORT, TBLOCK_SUBABORT_END,
TBLOCK_SUBABORT_PENDING, TBLOCK_SUBRESTART, TBLOCK_SUBABORT_RESTART,
} TBlockState;

두 머신이 필요한 이유는 README가 한 페이지를 할애해 설명하는 미묘함에 있다. SQL COMMIT 하나가 트랜잭션을 즉시 닫지 않는다는 점이다. 사용자가 COMMIT을 입력하면, 교통정리자의 EndTransactionBlock은 블록 상태를 TBLOCK_END로 옮기기만 한다. 실제 CommitTransaction()은 나중에, 메인 루프가 다음 번에 CommitTransactionCommand를 호출할 때 실행된다. 이 분리 덕분에 트랜잭션이 아직 열린 채로 xact.c를 벗어나 메인 루프가 같은 트랜잭션 안에서 계속 처리할 수 있다. 저수준 TransState는 물리적 커밋을 추적하고, 상위 TBlockState는 커밋이 보류 중임을 기억한다.

TBLOCK_* 머신은 거의 전부 CommitTransactionCommandInternal(과 중단 쪽 짝)의 거대한 switch 하나로 구동된다. 주요 전이를 보면 다음과 같다.

stateDiagram-v2
    [*] --> TBLOCK_DEFAULT
    TBLOCK_DEFAULT --> TBLOCK_STARTED: StartTransactionCommand
    TBLOCK_STARTED --> TBLOCK_DEFAULT: CommitTransaction \n 단일 쿼리 트랜잭션 암묵 종료

    TBLOCK_STARTED --> TBLOCK_BEGIN: BEGIN
    TBLOCK_BEGIN --> TBLOCK_INPROGRESS: CommitTransactionCommand
    TBLOCK_INPROGRESS --> TBLOCK_INPROGRESS: CommandCounterIncrement \n 구문별 실행
    TBLOCK_INPROGRESS --> TBLOCK_END: COMMIT 수신
    TBLOCK_END --> TBLOCK_DEFAULT: CommitTransaction

    TBLOCK_INPROGRESS --> TBLOCK_ABORT_PENDING: ROLLBACK 수신
    TBLOCK_ABORT_PENDING --> TBLOCK_DEFAULT: AbortTransaction 후 CleanupTransaction

    TBLOCK_INPROGRESS --> TBLOCK_ABORT: 블록 내부 오류
    TBLOCK_ABORT --> TBLOCK_ABORT_END: ROLLBACK 수신
    TBLOCK_ABORT_END --> TBLOCK_DEFAULT: CleanupTransaction

    TBLOCK_INPROGRESS --> TBLOCK_PREPARE: PREPARE TRANSACTION
    TBLOCK_PREPARE --> TBLOCK_DEFAULT: PrepareTransaction

그림 1 — 최상위 트랜잭션에 대한 고수준 TBlockState 머신. 핵심 비대칭은 다음과 같다. 블록 내부 오류는 TBLOCK_ABORT에 착륙해 사용자의 ROLLBACK기다린다(그 사이 다른 명령은 무시된다). 반면 정상 블록에 대한 명시적 ROLLBACKTBLOCK_ABORT_PENDING을 거쳐 엔진이 중단을 실행해야 한다. 양쪽 모두 CleanupTransaction을 거쳐 유휴 상태로 돌아온다. COMMIT은 즉시 커밋되지 않는다. 다음 CommitTransactionCommand 때까지 TBLOCK_END에 대기한다.

README의 예시는 두 단계 특성을 구체적으로 보여 준다. BEGIN; SELECT; INSERT; COMMIT;에서 메인 루프는 모든 구문 주위에서 StartTransactionCommand / CommitTransactionCommand를 호출한다. BEGIN 구문만이 실제로 StartTransaction()을 실행하고, COMMIT 구문만이 실제로 CommitTransaction()을 실행한다. 중간의 CommitTransactionCommand들은 CommandCounterIncrement()만 호출해 이후 명령들이 앞선 명령의 결과를 볼 수 있게 한다.

XID 할당: 지연 방식, 가상 ID 우선, XidGenLock 아래

섹션 제목: “XID 할당: 지연 방식, 가상 ID 우선, XidGenLock 아래”

트랜잭션은 XID 없이 시작한다. StartTransaction은 공유 메모리 비용이 전혀 없는 VXID만 할당한다.

// StartTransaction — src/backend/access/transam/xact.c (condensed)
s->state = TRANS_START;
s->fullTransactionId = InvalidFullTransactionId; /* until assigned */
// ...
vxid.procNumber = MyProcNumber;
vxid.localTransactionId = GetNextLocalTransactionId();
VirtualXactLockTableInsert(vxid);
MyProc->vxid.lxid = vxid.localTransactionId;

실제 XID는 필요한 시점에 처음으로 할당된다. GetCurrentTransactionId()가 공개 진입점이며, 지연 할당을 수행한다.

// GetCurrentTransactionId — src/backend/access/transam/xact.c
TransactionId
GetCurrentTransactionId(void)
{
TransactionState s = CurrentTransactionState;
if (!FullTransactionIdIsValid(s->fullTransactionId))
AssignTransactionId(s);
return XidFromFullTransactionId(s->fullTransactionId);
}

AssignTransactionId는 인트리 README의 “Transaction and Subtransaction Numbering” 절이 설명하는 네 가지 작업을 수행한다. (1) 서브트랜잭션이라면, 아직 할당되지 않은 모든 부모에게 먼저 XID를 할당한다. 재귀가 아닌 반복으로, 자식 XID > 부모 XID 불변식을 보존하기 위해서다. (2) GetNewTransactionId를 호출한다. (3) 서브트랜잭션이라면 SubTransSetParent로 pg_subtrans에 부모 링크를 기록한다. (4) 트랜잭션 XID 잠금(XactLockTableInsert)을 이 레벨의 ResourceOwner에 귀속시키고, 최상위 트랜잭션이라면 서술 락(SSI) 시스템에 XID를 등록한다.

// AssignTransactionId — src/backend/access/transam/xact.c (condensed)
s->fullTransactionId = GetNewTransactionId(isSubXact);
if (!isSubXact)
XactTopFullTransactionId = s->fullTransactionId;
if (isSubXact)
SubTransSetParent(XidFromFullTransactionId(s->fullTransactionId),
XidFromFullTransactionId(s->parent->fullTransactionId));
if (!isSubXact)
RegisterPredicateLockingXid(XidFromFullTransactionId(s->fullTransactionId));
// take XID lock charged to this level's ResourceOwner
currentOwner = CurrentResourceOwner;
CurrentResourceOwner = s->curTransactionOwner;
XactLockTableInsert(XidFromFullTransactionId(s->fullTransactionId));
CurrentResourceOwner = currentOwner;

실제 카운터 증가는 varsup.c에 있다. GetNewTransactionId만이 nextXid를 전진시킨다. XidGenLock을 독점으로 잡고, 래핑(wraparound) 한계(xidVacLimit / xidWarnLimit / xidStopLimit — 경고, 알림, XID 발급 거부로 이어지는 단계적 방어)를 확인하고, 새 페이지용 SLRU를 확장하고, 카운터를 전진시키고, 핵심적으로 — 락을 놓기 전에 — ProcArray에 XID를 게시한다.

// GetNewTransactionId — src/backend/access/transam/varsup.c (condensed)
LWLockAcquire(XidGenLock, LW_EXCLUSIVE);
full_xid = TransamVariables->nextXid;
xid = XidFromFullTransactionId(full_xid);
// ... wraparound-limit checks (xidStopLimit -> ERROR, xidWarnLimit -> WARNING) ...
ExtendCLOG(xid); /* zero a new pg_xact page if needed */
ExtendCommitTs(xid);
ExtendSUBTRANS(xid);
FullTransactionIdAdvance(&TransamVariables->nextXid);
if (!isSubXact)
{
/* LWLockRelease acts as barrier */
MyProc->xid = xid;
ProcGlobal->xids[MyProc->pgxactoff] = xid;
}
else
{
/* store into the PGPROC subxid cache, or set overflowed */
if (nxids < PGPROC_MAX_CACHED_SUBXIDS) { ... MyProc->subxids.xids[nxids] = xid; ... }
else MyProc->subxidStatus.overflowed = substat->overflowed = true;
}
LWLockRelease(XidGenLock);
flowchart TD
    NEED["쓰기가 XID 필요<br/>GetCurrentTransactionId"] --> HAVE{fullTransactionId<br/>이미 유효?}
    HAVE -- 예 --> RET["기존 XID 반환"]
    HAVE -- 아니오 --> SUB{미할당 부모가 있는<br/>서브트랜잭션?}
    SUB -- 예 --> PAR["부모 먼저 할당<br/>반복적으로, 자식 XID > 부모 XID"]
    SUB -- 아니오 --> GNT
    PAR --> GNT["XidGenLock 아래 GetNewTransactionId"]
    GNT --> EXT["ExtendCLOG / CommitTs / SUBTRANS<br/>이후 nextXid 전진"]
    EXT --> PUB["XID를 ProcArray에 게시<br/>(XidGenLock 해제 전)"]
    PUB --> POST["SubTransSetParent (서브트랜잭션)<br/>RegisterPredicateLockingXid (최상위)<br/>XactLockTableInsert"]
    POST --> RET

그림 2 — 지연 XID 할당. README의 “Interlocking” 절에서 나오는 순서 규칙이 핵심이다. GetNewTransactionIdXidGenLock을 해제하기 전에 새 XID를 ProcArray에 저장해야 한다. 그래야만 latestCompletedXid 이하의 모든 최상위 XID가 ProcArray에 존재하거나(또는 더 이상 실행 중이지 않거나) 보장된다. 이 순서가 없다면, 동시 백엔드가 나중 XID를 할당하고 커밋해 이 XID가 보이기 전에 latestCompletedXid를 앞지를 수 있다. 그렇게 되면 vacuum이 의존하는 oldest-xmin 계산이 깨진다.

CommitTransaction()은 길고 의도적으로 순서가 정해진 루틴이다. 형태는 세 단계다. 프리-커밋(사용자 코드가 실행될 수 있고, 오류가 중단으로 우회될 수 있는), 내구성 확정 순간(RecordTransactionCommit, 이후 실행 집합 탈퇴), 포스트-커밋 정리(트랜잭션을 실패시켜서는 안 됨). 요약된 순서는 다음과 같다.

// CommitTransaction — src/backend/access/transam/xact.c (heavily condensed)
/* --- pre-commit: user code may run, errors reroute to abort --- */
for (;;) { AfterTriggerFireDeferred(); if (!PreCommit_Portals(false)) break; }
CallXactCallbacks(XACT_EVENT_PRE_COMMIT);
AtEOXact_Parallel(true);
AfterTriggerEndXact(true);
PreCommit_on_commit_actions();
smgrDoPendingSyncs(true, is_parallel_worker);
AtEOXact_LargeObject(true);
PreCommit_Notify();
if (!is_parallel_worker)
PreCommit_CheckForSerializationFailure(); /* SSI may abort here */
HOLD_INTERRUPTS();
s->state = TRANS_COMMIT;
/* --- the durable instant --- */
if (!is_parallel_worker)
latestXid = RecordTransactionCommit(); /* writes + flushes + CLOG */
ProcArrayEndTransaction(MyProc, latestXid); /* leave running set */
/* --- post-commit: noncritical cleanup, must not fail the xact --- */
CallXactCallbacks(XACT_EVENT_COMMIT);
ResourceOwnerRelease(TopTransactionResourceOwner, RESOURCE_RELEASE_BEFORE_LOCKS, true, true);
AtEOXact_Buffers(true); AtEOXact_RelationCache(true);
AtEOXact_Inval(true); /* publish catalog invals */
ResourceOwnerRelease(TopTransactionResourceOwner, RESOURCE_RELEASE_LOCKS, true, true);
ResourceOwnerRelease(TopTransactionResourceOwner, RESOURCE_RELEASE_AFTER_LOCKS, true, true);
smgrDoPendingDeletes(true); /* drop files of dropped rels */
AtCommit_Notify();
/* ... AtEOXact_GUC / SPI / Namespace / PgStat / Snapshot ... */
AtCommit_Memory(); /* free TopTransactionContext */
s->state = TRANS_DEFAULT;
RESUME_INTERRUPTS();

이 순서에는 README에서 직접 나온 두 가지 순서 규칙이 있다. 다른 모든 DBMS도 같은 문제를 풀어야 하므로 이름을 붙여 둘 만하다.

  • 실행 집합 탈퇴(ProcArrayEndTransaction)는 RecordTransactionCommit 이후, 잠금 해제 이전에만 한다. 이 XID를 더 이상 실행 중으로 보지 않는 동시 스냅샷 취득자는 이미 CLOG에서 커밋된 것으로 볼 수 있어야 한다. 잠금 대기자는 트랜잭션이 그들의 관점에서 완전히 정리될 때까지 깨워서는 안 된다.
  • 카탈로그 무효화 게시(AtEOXact_Inval)는 relcache 참조를 드롭한 후, 잠금을 해제하기 전에 한다. 그래야 이 트랜잭션이 수정한 릴레이션의 잠금을 기다리는 백엔드가, 릴레이션을 사용하기 전에 카탈로그 변경 사항을 알 수 있다.

RecordTransactionCommit: 내구성이 일어나는 곳

섹션 제목: “RecordTransactionCommit: 내구성이 일어나는 곳”

이 모듈에서 되돌릴 수 없는 지점을 담고 있는 유일한 함수다. 구조는 다음과 같다. 커밋 레코드에 필요한 것(드롭된 파일, 커밋된 자식들, 드롭된 통계, 무효화 메시지)을 모으고, 커밋할 XID가 있는지 결정하고, 있다면 임계 구간에 진입해 레코드를 쓰고, synchronous_commit 설정에 따라 플러시하거나 하지 않고, CLOG를 업데이트한다.

// RecordTransactionCommit — src/backend/access/transam/xact.c (condensed)
TransactionId xid = GetTopTransactionIdIfAny();
bool markXidCommitted = TransactionIdIsValid(xid);
// ... gather nrels, nchildren, ndroppedstats, invalMessages ...
if (!markXidCommitted) {
/* no XID: nothing to commit. Only flush if we wrote WAL (e.g. HOT pruning) */
if (!wrote_xlog) goto cleanup;
} else {
/* force concurrent checkpoint to wait until pg_xact is updated */
START_CRIT_SECTION();
MyProc->delayChkptFlags |= DELAY_CHKPT_START;
XactLogCommitRecord(GetCurrentTransactionStopTimestamp(),
nchildren, children, nrels, rels,
ndroppedstats, droppedstats,
nmsgs, invalMessages, RelcacheInitFileInval,
MyXactFlags,
InvalidTransactionId, NULL /* plain commit */);
TransactionTreeSetCommitTsData(xid, nchildren, children, ...);
}
if ((wrote_xlog && markXidCommitted && synchronous_commit > SYNCHRONOUS_COMMIT_OFF)
|| forceSyncCommit || nrels > 0)
{
XLogFlush(XactLastRecEnd); /* SYNCHRONOUS path */
if (markXidCommitted)
TransactionIdCommitTree(xid, nchildren, children); /* CLOG = COMMITTED now */
}
else
{
XLogSetAsyncXactLSN(XactLastRecEnd); /* ASYNCHRONOUS path */
if (markXidCommitted)
TransactionIdAsyncCommitTree(xid, nchildren, children, XactLastRecEnd);
}
if (markXidCommitted) {
MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
END_CRIT_SECTION();
}
if (wrote_xlog && markXidCommitted)
SyncRepWaitForLSN(XactLastRecEnd, true); /* wait for sync standby */

세 가지를 읽어 낼 수 있다.

  1. XID 없음, 커밋 레코드 없음. 읽기 전용 트랜잭션(또는 임시 테이블만 건드린 트랜잭션)은 XID를 받지 못했으므로 markXidCommitted가 false다. 커밋할 것이 말 그대로 없다. 정리로 건너뛴다. 이런 트랜잭션이 플러시를 하는 유일한 이유는 어떤 부수적 이유(HOT pruning)로 WAL을 썼을 경우뿐이다. 그 플러시는 커밋을 위한 것이 아니라 그 작업의 내구성을 위한 것이다.

  2. 커밋 임계 구간 + DELAY_CHKPT_START. WAL 레코드를 쓰는 것과 CLOG를 업데이트하는 것 사이에, 백엔드는 DELAY_CHKPT_START를 설정한다. 동시 체크포인트가 CLOG 업데이트를 플러시하지 않은 채 커밋 레코드를 지나쳐 redo 포인터를 옮기지 못하게 하기 위해서다. 이 인터록 없이는 체크포인트 직후 장애가 발생하면 WAL이 이미 기록한 커밋을 잃을 수 있다.

  3. 동기 vs 비동기 커밋. synchronous_commit이 켜져 있으면 WAL이 플러시(XLogFlush)되고 CLOG가 동기적으로 업데이트(TransactionIdCommitTree)된다. synchronous_commit=off면 WAL이 플러시되지 않는다. 대신 커밋 LSN이 기록(XLogSetAsyncXactLSN)되어 walwriter가 곧 플러시하게 되고, CLOG는 비동기로 업데이트된다. 해당 LSN까지 WAL이 플러시됐음이 알려질 때까지 실제 CLOG 쓰기가 미뤄지는 것이다(TransactionIdAsyncCommitTree 경로; README의 “Asynchronous Commit” 절과 postgres-clog-commit-ts.md 참조). 레코드, 이후 플러시, 이후 CLOG라는 순서는 불변이다. 비동기 커밋이 완화하는 것은 플러시가 언제 완료되는지이며, 장애 시 소량의 커밋 손실 가능성과 지연 감소를 맞바꾼다.

레코드 자체는 XactLogCommitRecord가 조립한다. 선택적 서브레코드들의 집합체다. 베어 xl_xact_commit은 타임스탬프뿐이고, 추가 페이로드(서브트랜잭션들, 드롭된 relfilelocator들, 드롭된 통계, 무효화 메시지, 2단계 xid/gid, 복제 출처)는 xinfo 비트마스크로 존재 여부를 표시해 존재할 때만 덧붙인다. 같은 함수가 평범한 커밋과 COMMIT PREPARED 양쪽에 쓰이는 이유가 여기 있다. twophase_xid가 유효한지 여부만이 달라지며, 그것이 opcode를 XLOG_XACT_COMMIT에서 XLOG_XACT_COMMIT_PREPARED로 뒤집는다.

AbortTransaction: 거울 이미지, 하지만 무료

섹션 제목: “AbortTransaction: 거울 이미지, 하지만 무료”

중단은 구조적으로 유사하지만 정신은 반대다. 먼저 경량 리소스를 최대한 빠르게 해제하고(README: “다른 백엔드를 지연시키지 않도록 모든 공유 리소스를 해제”), 중단을 기록하고, 실행 집합을 떠나고, 더 무거운 정리를 한다.

// AbortTransaction — src/backend/access/transam/xact.c (condensed)
HOLD_INTERRUPTS();
AtAbort_Memory(); AtAbort_ResourceOwner();
LWLockReleaseAll(); /* drop LWLocks immediately */
UnlockBuffers();
XLogResetInsertion(); /* discard half-built WAL record */
LockErrorCleanup();
s->state = TRANS_ABORT;
SetUserIdAndSecContext(s->prevUser, s->prevSecContext); /* undo SECURITY DEFINER */
// ... AtEOXact_Parallel(false), AfterTriggerEndXact(false), AtAbort_Portals() ...
if (!is_parallel_worker)
latestXid = RecordTransactionAbort(false); /* writes record, NO flush */
ProcArrayEndTransaction(MyProc, latestXid);
if (TopTransactionResourceOwner != NULL) {
CallXactCallbacks(XACT_EVENT_ABORT);
ResourceOwnerRelease(..., RESOURCE_RELEASE_BEFORE_LOCKS, false, true);
// ... buffers, relcache, inval, multixact ...
ResourceOwnerRelease(..., RESOURCE_RELEASE_LOCKS, false, true);
ResourceOwnerRelease(..., RESOURCE_RELEASE_AFTER_LOCKS, false, true);
smgrDoPendingDeletes(false);
// ... GUC / SPI / PgStat ...
}
/* State remains TRANS_ABORT until CleanupTransaction(). */
RESUME_INTERRUPTS();

핵심 차이는 RecordTransactionAbort에 있다. 임계 구간 안에서 중단 레코드를 쓰고 TransactionIdAbortTree로 CLOG를 즉시 중단 표시하지만, 플러시는 결코 하지 않으며 DELAY_CHKPT_START도 설정하지 않는다.

// RecordTransactionAbort — src/backend/access/transam/xact.c (condensed)
TransactionId xid = GetCurrentTransactionIdIfAny();
if (!TransactionIdIsValid(xid)) { /* no XID -> nobody cares we aborted */
if (!isSubXact) XactLastRecEnd = 0;
return InvalidTransactionId;
}
if (TransactionIdDidCommit(xid)) /* sanity: didn't half-commit */
elog(PANIC, "cannot abort transaction %u, it was already committed", xid);
START_CRIT_SECTION();
XactLogAbortRecord(xact_time, nchildren, children, nrels, rels,
ndroppedstats, droppedstats, MyXactFlags,
InvalidTransactionId, NULL);
if (!isSubXact)
XLogSetAsyncXactLSN(XactLastRecEnd); /* nudge walwriter, but do not block */
TransactionIdAbortTree(xid, nchildren, children); /* CLOG = ABORTED */
END_CRIT_SECTION();

소스의 주석은 명시적이다. “장애 이후 기본 가정이 어차피 중단이므로 여기서 XLOG를 디스크에 플러시하지 않는다.” 보편적인 “중단은 무료” 비대칭이 구체화된 것이다. README가 설명하는 2단계 중단 처리도 짚어 둘 지점이다. AbortTransaction은 공유 리소스를 즉시 해제하지만(다른 백엔드가 지연되지 않도록), CleanupTransaction — 최종적으로 TopTransactionContext를 해제하고 TRANS_DEFAULT로 돌아오는 — 은 사용자가 실제로 ROLLBACK을 발행하기 전까지 실행되지 않는다. 실패한 트랜잭션 블록이 TBLOCK_ABORT 상태에서 종료 명령을 볼 때까지 모든 것을 무시하는 이유가 바로 그것이다.

서브트랜잭션: 스택과 상향 병합

섹션 제목: “서브트랜잭션: 스택과 상향 병합”

세이브포인트, PL/pgSQL 예외 블록, 내부 서브트랜잭션은 모두 같은 기본 요소 위에 구축된다. parent로 연결된 TransactionStateData 노드의 스택이다. 이 구조체는 레벨이 독립적으로 되감기는 데 필요한 모든 것을 담고 있다.

// TransactionStateData — src/backend/access/transam/xact.c (condensed)
typedef struct TransactionStateData
{
FullTransactionId fullTransactionId; /* my XID (lazy; may be invalid) */
SubTransactionId subTransactionId; /* my subxact ID */
char *name; /* savepoint name, if any */
int savepointLevel;
TransState state; /* low-level state */
TBlockState blockState; /* high-level state */
int nestingLevel; /* transaction nesting depth */
int gucNestLevel;
MemoryContext curTransactionContext; /* my xact-lifetime context */
ResourceOwner curTransactionOwner; /* my query resources */
TransactionId *childXids; /* subcommitted child XIDs, in XID order */
int nChildXids;
int maxChildXids;
Oid prevUser;
int prevSecContext;
bool prevXactReadOnly;
bool startedInRecovery;
bool didLogXid;
// ... parallel-mode bookkeeping, chain flag ...
struct TransactionStateData *parent; /* back link to parent */
} TransactionStateData;

PushTransactionTopTransactionContext에 새 노드를 할당하고, 백엔드 로컬 currentSubTransactionId 카운터를 증가시키고(최상위 레벨은 SubTransactionId 1이고, 서브트랜잭션은 2 이상이다. 카운터는 최상위 트랜잭션마다 초기화되며 XID가 아니다), 현재 노드에 연결하고, blockState = TBLOCK_SUBBEGIN으로 설정한다. StartSubTransaction은 그 후 서브시스템(AtSubStart_Memory, AtSubStart_ResourceOwner)을 초기화하고 노드를 TRANS_INPROGRESS로 옮긴다. 서브트랜잭션 노드는 XID 없이 시작한다는 점에 주목하라. 최상위 트랜잭션과 마찬가지로, 읽기만 하는 세이브포인트는 XID를 소비하지 않는다.

flowchart TB
    TOP["최상위 TransactionStateData<br/>subxid 1, XID 100 (지연)<br/>childXids: [101, 102]"]
    S1["서브트랜잭션 노드<br/>subxid 2, XID 103<br/>parent ->"]
    S2["서브트랜잭션 노드 (현재)<br/>subxid 3, XID (미할당)<br/>parent ->"]
    S2 --> S1 --> TOP
    note["PushTransaction: 할당 + 연결 + TBLOCK_SUBBEGIN<br/>PopTransaction: CurrentTransactionState를 부모로 재연결, 노드 해제"]

그림 3 — 서브트랜잭션 스택. 각 노드는 독립적인 되감기 단위로, 자체 ResourceOwner와 메모리 컨텍스트를 갖는다. XID는 각 노드에서 여전히 지연 방식이다. AssignTransactionId의 불변식은 스택 아래로 갈수록 XID가 증가함을 보장한다(부모가 자식보다 먼저 XID를 얻는다). 이것이 childXids[]가 정렬 상태를 유지하는 이유다.

흥미로운 순간은 서브커밋이다. 커밋하는 서브트랜잭션은 자체 커밋 레코드를 쓰지 않고 CLOG를 커밋 표시하지도 않는다. 대신 CommitSubTransaction이 부모로 정체성을 상향 병합하고 자신의 XID 잠금만 해제한다. 다른 모든 잠금은 부모의 ResourceOwner로 이전된다.

// CommitSubTransaction — src/backend/access/transam/xact.c (condensed)
s->state = TRANS_COMMIT;
CommandCounterIncrement(); /* make subxact's commands visible */
/* Prior to 8.4 we marked subcommit in clog here; now deferred to top-level. */
if (FullTransactionIdIsValid(s->fullTransactionId))
AtSubCommit_childXids(); /* merge my XID + my children into parent */
// ... AfterTriggerEndSubXact(true), AtSubCommit_Portals(...), callbacks ...
CurrentResourceOwner = s->curTransactionOwner;
if (FullTransactionIdIsValid(s->fullTransactionId))
XactLockTableDelete(XidFromFullTransactionId(s->fullTransactionId));
/* Other locks transfer to parent */
ResourceOwnerRelease(s->curTransactionOwner, RESOURCE_RELEASE_LOCKS, true, false);
// ...
CurrentResourceOwner = s->parent->curTransactionOwner;
PopTransaction();

AtSubCommit_childXids가 병합이다. 부모의 childXids[] 배열을 키우고(상각을 위해 두 배씩, MaxAllocSize 상한), “자식 XID > 부모 XID” 불변식에 의존해 서브트랜잭션 자신의 XID와 그 자식들을 정렬된 채로 복사한다. 최상위 트랜잭션이 최종적으로 커밋하면, RecordTransactionCommit은 이 전체 children 배열을 XactLogCommitRecordTransactionIdCommitTree에 넘긴다. 전체 트리가 CLOG에서 원자적으로, 하나의 레코드로 커밋 표시된다. README의 “pg_xact and pg_subtrans” 절은 경계 사례를 설명한다. 트리의 상태가 여러 CLOG 페이지에 걸치면 중간 “서브커밋됨” 상태가 다중 페이지 업데이트를 원자적으로 유지하는 데 쓰이지만, 단일 페이지 안에서는 모두 한 번에 커밋됨으로 뒤집힌다.

**서브중단(subabort)**은 서브커밋이 지연된 것과 달리 즉각적이다. AbortSubTransaction은 즉시 RecordTransactionAbort(true)를 호출하고(중단 레코드를 쓰고, CLOG를 중단 표시하고, XidCacheRemoveRunningXids로 실패한 XID들을 PGPROC 실행 중 자식 캐시에서 바로 제거), CleanupSubTransaction이 노드를 팝한다. 비대칭의 이유는 최상위 레벨과 같다. 중단된 서브트랜잭션의 XID는 지금 당장 중단됨으로 관찰 가능해야 XactLockTableWait와 가시성 검사가 조기 종료할 수 있다. 반면 커밋된 서브트랜잭션의 결말은 조상에게 달려 있으므로, CLOG 표시는 최상위를 기다린다.

ROLLBACK TO <savepoint>는 이 기본 요소들로 만든다. 엔진이 이름 붙여진 레벨까지 서브트랜잭션들을 중단시키고, 그 레벨을 같은 이름으로 재생성한다. 내부적으로는 “완전히 새로운 서브트랜잭션”이다. RELEASE는 이름 붙여진 레벨과 그 위 모든 것을 단순히 커밋(상향 병합)한다.

트랜잭션 종료 작업은 두 가지 병렬 콜백 패밀리와 ResourceOwner 단계들로 분배된다. AtEOXact_* 함수들(버퍼, relcache, inval, GUC, SPI, 네임스페이스, pgstat, 스냅샷, …)은 최상위 커밋/중단 시 isCommit 불리언과 함께 실행된다. AtEOSubXact_* 패밀리는 서브트랜잭션 경계에서 같은 일을 하되, 상태를 버리는 대신 재부모화할 수 있도록 서브/부모 subxid를 추가로 받는다. 익스텐션들도 등록된 콜백으로 같은 연결 지점에 플러그인된다.

// RegisterXactCallback / the event enum — src/include/access/xact.h
typedef enum
{
XACT_EVENT_COMMIT, XACT_EVENT_PARALLEL_COMMIT,
XACT_EVENT_ABORT, XACT_EVENT_PARALLEL_ABORT,
XACT_EVENT_PREPARE,
XACT_EVENT_PRE_COMMIT, XACT_EVENT_PARALLEL_PRE_COMMIT,
XACT_EVENT_PRE_PREPARE,
} XactEvent;
typedef void (*XactCallback) (XactEvent event, void *arg);
extern void RegisterXactCallback(XactCallback callback, void *arg);

PRE_COMMIT 이벤트는 사용자 코드가 실행될 수 있고 오류가 중단으로 우회될 수 있는 동안 발생한다. 반면 평범한 COMMIT/ABORT 이벤트는 실패가 더 이상 회복 불가능한 포스트-커밋 단계에서 발생한다. 실패 가능성이 있는 콜백이 COMMIT이 아닌 PRE_COMMIT에 속해야 하는 이유가 바로 그것이다.

ResourceOwner 해제는 세 단계로 순서 있게 진행된다. RESOURCE_RELEASE_BEFORE_LOCKS(버퍼 핀, 파일 — 다른 백엔드에게 보이는 것들), RESOURCE_RELEASE_LOCKS(헤비웨이트 잠금 자체), RESOURCE_RELEASE_AFTER_LOCKS(백엔드 로컬 잡동사니). 대기 중인 백엔드가 이 트랜잭션을 완전히 정리된 것으로 볼 정확한 지점에서 잠금이 해제되도록 하기 위해서다. 메커니즘 자체는 postgres-resource-owners.md(계획됨, base-infra)에 문서화되어 있다. xact.c는 그것의 주요 호출자다.

xact.c에는 PrepareTransaction이 있지만, 2단계 커밋의 본체 — PREPARE TRANSACTION, 온디스크 twophase 상태 파일, COMMIT/ROLLBACK PREPARED, 준비된 트랜잭션의 복구 — 는 twophase.c에 있으며 postgres-two-phase-commit.md의 주제다. 이 문서가 소유하는 연결 지점은 좁고, 경계를 명확히 할 만한 가치가 있다.

  • TBLOCK_PREPARE 블록 상태가 PrepareTransaction()으로 라우팅된다. 이것은 CommitTransaction()과 거의 동일한 쌍둥이이지만, RecordTransactionCommit 대신 트랜잭션의 상태를 twophase 기계(StartPrepare / EndPrepare)에 넘긴다. 이것이 prepare 레코드와 상태 파일을 내구성 있게 쓴다.
  • XactLogCommitRecordXactLogAbortRecord는 평범한 경로와 준비된 경로 양쪽에서 공유된다. 유효한 twophase_xid를 전달하면 opcode가 XLOG_XACT_COMMIT_PREPARED / XLOG_XACT_ABORT_PREPARED로 뒤집히고 xl_xact_twophase(및 선택적으로 GID) 서브레코드가 덧붙는다. WAL 레코드 형식은 여기서 정의되고, 준비된 변형을 사용하는 프로토콜은 저기서 정의된다는 의미다.

2PC의 나머지 — 해소자, GID 네임스페이스, 준비된 트랜잭션이 보류 중인 채로 재시작을 살아남기 — 는 의도적으로 이 문서의 범위 밖이다.

서브시스템별로 묶었다. 심볼이 안정적인 기준점이다. 끝의 위치 힌트 표가 줄 번호를 커밋 273fe94에 고정한다.

  • TransState, TBlockState (열거형, xact.c) — 두 상태 머신.
  • TransactionStateData / TransactionState (xact.c) — 스택 노드. TopTransactionStateData는 정적 최상위 노드이고, CurrentTransactionState는 스택 안의 커서다.
  • StartTransaction (xact.c) — VXID 할당, 트랜잭션별 카운터 초기화, 메모리/리소스 오너 초기화. 상태를 TRANS_INPROGRESS로 남긴다.
  • CommitTransaction (xact.c) — 순서 있는 커밋 파이프라인.
  • AbortTransaction (xact.c) — 빠른 리소스 해제, 중단 기록, 정리.
  • CleanupTransaction (xact.c) — 중단 후 최종 해제. TopTransactionContext를 해제하고 TRANS_DEFAULT로 돌아온다.
  • PrepareTransaction (xact.c) — CommitTransaction의 2PC 쌍둥이(연결 지점).
  • CommitTransactionCommandInternal / AbortCurrentTransactionInternal (xact.c) — 블록 상태 전이를 구동하는 TBLOCK_* switch.
  • IsTransactionState, IsAbortedTransactionBlockState (xact.c) — 백엔드의 나머지 부분이 광범위하게 사용하는 상태 조건자.
  • GetCurrentTransactionId, GetTopTransactionId (xact.c) — AssignTransactionId를 지연 호출하는 공개 진입점.
  • GetCurrentTransactionIdIfAny, GetTopTransactionIdIfAny (xact.c) — 할당하지 않는 변형(아직 XID가 없으면 Invalid 반환).
  • AssignTransactionId (xact.c) — 부모 우선 할당, pg_subtrans 링크, XID 잠금, SSI 등록, 핫스탠바이 XLOG_XACT_ASSIGNMENT 일괄 처리.
  • GetNewTransactionId (varsup.c) — nextXid가 전진하는 유일한 곳. 래핑 한계, SLRU 확장, XidGenLock 아래 ProcArray 게시.
  • ReadNextFullTransactionId, AdvanceNextFullTransactionIdPastXid (varsup.c) — 읽기 / 복구 시점 전진.
  • GetCurrentSubTransactionId, GetCurrentCommandId (xact.c) — 백엔드 로컬 서브/명령 카운터.
  • RecordTransactionCommit (xact.c) — 되돌릴 수 없는 지점. 임계 구간 + DELAY_CHKPT_START, 쓰기, 플러시 또는 비동기, CLOG 업데이트, 동기 복제 대기.
  • RecordTransactionAbort (xact.c) — 중단 레코드 쓰기, CLOG 중단 표시, 플러시 없음.
  • XactLogCommitRecord, XactLogAbortRecord (xact.c) — xinfo 마스크로 키된 선택적 서브레코드들로 가변 WAL 레코드 조립. 2PC 경로와 공유.
  • xl_xact_commit, xl_xact_abort, xl_xact_xinfo, xl_xact_subxacts, xl_xact_assignment, XACT_XINFO_HAS_* 플래그, XLOG_XACT_* opcode (xact.h) — 온디스크 레코드 어휘.
  • xact_redo (xact.c, xact.h에 선언) — 이 레코드들에 대한 rmgr redo 진입점(복구 쪽; postgres-recovery-redo.md에 상세).
  • PushTransaction, PopTransaction (xact.c) — 스택 push/pop. subxid 카운터 증가와 래핑 방어.
  • StartSubTransaction, CommitSubTransaction, AbortSubTransaction, CleanupSubTransaction (xact.c) — 서브트랜잭션 생명주기.
  • AtSubCommit_childXids (xact.c) — 부모 childXids[]로의 상향 병합.
  • xactGetCommittedChildren (xact.c) — 커밋된 자식 배열을 레코드 작성자에게 넘김.
  • DefineSavepoint, ReleaseSavepoint, RollbackToSavepoint, BeginInternalSubTransaction, ReleaseCurrentSubTransaction, RollbackAndReleaseCurrentSubTransaction (xact.c, xact.h) — SQL과 내부 진입점.
  • RegisterXactCallback / UnregisterXactCallback, RegisterSubXactCallback / UnregisterSubXactCallback (xact.c) — 익스텐션 훅 등록. CallXactCallbacks / CallSubXactCallbacks가 실행.
  • AtEOXact_*AtEOSubXact_* 패밀리 (xact.c에서 호출, 여러 모듈에서 정의) — 서브시스템별 트랜잭션/서브트랜잭션 종료 정리.
  • XactEvent, SubXactEvent (xact.h) — 이벤트 열거형.

위치 힌트 (2026-06-05 기준, REL_18 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심볼파일
TransState (열거형)src/backend/access/transam/xact.c141
TBlockState (열거형)src/backend/access/transam/xact.c157
TransactionStateData (구조체)src/backend/access/transam/xact.c193
TopTransactionStateDatasrc/backend/access/transam/xact.c247
IsTransactionStatesrc/backend/access/transam/xact.c386
GetTopTransactionIdsrc/backend/access/transam/xact.c426
GetCurrentTransactionIdsrc/backend/access/transam/xact.c454
AssignTransactionIdsrc/backend/access/transam/xact.c635
RecordTransactionCommitsrc/backend/access/transam/xact.c1315
AtSubCommit_childXidssrc/backend/access/transam/xact.c1664
RecordTransactionAbortsrc/backend/access/transam/xact.c1754
StartTransactionsrc/backend/access/transam/xact.c2064
CommitTransactionsrc/backend/access/transam/xact.c2228
PrepareTransactionsrc/backend/access/transam/xact.c2514
AbortTransactionsrc/backend/access/transam/xact.c2809
CleanupTransactionsrc/backend/access/transam/xact.c3008
CommitTransactionCommandInternalsrc/backend/access/transam/xact.c3175
StartSubTransactionsrc/backend/access/transam/xact.c5067
CommitSubTransactionsrc/backend/access/transam/xact.c5104
AbortSubTransactionsrc/backend/access/transam/xact.c5219
CleanupSubTransactionsrc/backend/access/transam/xact.c5383
PushTransactionsrc/backend/access/transam/xact.c5416
PopTransactionsrc/backend/access/transam/xact.c5478
xactGetCommittedChildrensrc/backend/access/transam/xact.c5790
XactLogCommitRecordsrc/backend/access/transam/xact.c5814
XactLogAbortRecordsrc/backend/access/transam/xact.c5986
GetNewTransactionIdsrc/backend/access/transam/varsup.c77
ReadNextFullTransactionIdsrc/backend/access/transam/varsup.c288
TransState / TBlockState 소비자grep 참조
XLOG_XACT_COMMIT … opcodesrc/include/access/xact.h169
XACT_XINFO_HAS_* 플래그src/include/access/xact.h188
XactEvent / XactCallbacksrc/include/access/xact.h126
  • 트랜잭션은 독립적인 두 상태 열거형 TransStateTBlockState를 갖고, 데이터베이스 접근은 TRANS_INPROGRESS에서만 합법이다. xact.c의 열거형 정의와 IsTransactionState를 2026-06-05(커밋 273fe94) 기준으로 읽어 검증. IsTransactionStates->state == TRANS_INPROGRESS만을 반환한다.

  • XID 할당은 지연 방식이다. StartTransaction은 VXID만 할당하고, 실제 XID는 GetCurrentTransactionId / GetTopTransactionId 첫 번째 호출 시 할당된다. StartTransaction(fullTransactionId = InvalidFullTransactionId 설정)과 두 getter(id가 유효하지 않을 때만 AssignTransactionId 호출)를 직접 읽어 검증.

  • GetNewTransactionIdXidGenLock을 해제하기 전에 새 XID를 ProcGlobal->xids[]에 게시한다. varsup.c 검증: MyProc->xid = xid; ProcGlobal->xids[...] = xid; 저장이 XidGenLock 보유 구간 안에 있고, 주석이 인터록 근거로 access/transam/README를 인용한다. README의 “Interlocking” 절이 올바른 ComputeXidHorizons / oldest-xmin 추적에 필요하다고 확인.

  • 커밋의 내구성 확정 순간은 RecordTransactionCommit이다. 커밋 레코드 하나를 쓰고, (동기적으로) XLogFlush + TransactionIdCommitTree를 하며, DELAY_CHKPT_START가 있는 임계 구간으로 보호된다. 2026-06-05 해당 함수 직접 읽기로 검증. 비동기 경로(synchronous_commit=off)는 XLogFlushXLogSetAsyncXactLSN으로, TransactionIdCommitTreeTransactionIdAsyncCommitTree로 대체한다.

  • 중단은 WAL을 플러시하지 않는다. RecordTransactionAbort 검증: XLogFlush 호출이 없고, 소스 주석은 장애 후 가정이 “어차피 중단”이라고 명시. CLOG는 중단 표시(TransactionIdAbortTree)를 임계 구간 안에서 하고, walwriter를 XLogSetAsyncXactLSN으로 촉구한다.

  • 서브커밋은 WAL 레코드를 쓰거나 CLOG를 표시하지 않는다. 자식들을 부모로 상향 병합하고 CLOG 표시를 최상위 커밋에 미룬다. CommitSubTransaction(레코드 작성 호출 없음; AtSubCommit_childXids 호출)으로 검증. 소스 주석 “Prior to 8.4 we marked subcommit in clog at this point. We now only perform that step … as part of the atomic update of the whole transaction tree at top level commit or abort”와 README의 “pg_xact and pg_subtrans” 절이 확인.

  • 서브중단은 즉각적이다. AbortSubTransaction은 즉시 RecordTransactionAbort(true)XidCacheRemoveRunningXids를 호출한다. AbortSubTransactionRecordTransactionAbort(isSubXact 분기가 실패한 XID들을 PGPROC 실행 중 자식 캐시에서 즉시 제거)로 검증.

  • XactLogCommitRecord / XactLogAbortRecord는 2단계 커밋과 공유된다. 유효한 twophase_xid가 opcode를 *_PREPARED 변형으로 뒤집는다. XactLogCommitRecord 검증: info = !TransactionIdIsValid(twophase_xid) ? XLOG_XACT_COMMIT : XLOG_XACT_COMMIT_PREPARED. XACT_XINFO_HAS_TWOPHASE 서브레코드는 xid가 유효할 때만 덧붙는다.

  • PGPROC의 subxid 캐시는 PGPROC_MAX_CACHED_SUBXIDS로 한계가 있고, 초과하면 오버플로 플래그를 설정해 독자가 pg_subtrans를 참조해야 한다. GetNewTransactionId(varsup.c)의 isSubXact 분기와 README의 “pg_xact and pg_subtrans” 절로 검증.

  1. 비동기 커밋 CLOG 힌트 비트 지연 정확한 방식. 이 문서는 synchronous_commit=off일 때 CLOG 표시가 TransactionIdAsyncCommitTree를 거치고 heap 페이지의 실제 힌트 비트 설정이 관련 LSN까지 WAL이 플러시될 때까지 미뤄진다고 명시한다. 페이지당 LSN 캐시(그룹 크기 32, GROUP_LSNS 기계)의 역학은 SLRU/clog 계층에 있으며 xact.c에 없다. 조사 경로: postgres-clog-commit-ts.mdpostgres-slru.md를 README의 “Asynchronous Commit” 절과 교차 읽기해 그룹 LSN 크기가 REL_18에서 바뀌지 않았는지 확인.

  2. AbortCurrentTransactionInternal 블록 상태 커버리지. 이 문서는 커밋 쪽 TBLOCK_* switch를 전부 읽었지만 중단 쪽 switch는 요약만 한다. 모든 중단 쪽 블록 상태가 완전히 대칭적인 전이를 갖는지(특히 TBLOCK_SUBABORT_RESTART / TBLOCK_SUBRESTART 세이브포인트 재생성 경로)는 완전히 추적하지 않았다. 조사 경로: AbortCurrentTransactionInternal을 케이스별로 읽고 서브트랜잭션 중단/재시작 전이를 다이어그램으로 그리기.

  3. 커밋 레코드 xinfo 플래그의 버전 간 변화. XACT_XINFO_HAS_DROPPED_STATS 플래그(PG15 누적 통계)와 XACT_XINFO_HAS_* 집합은 REL_18에서 읽었다. 독자가 비교할 수 있는 버전(PG14↔PG18)에서 비트 할당이 안정적인지는 여기서 검증하지 않았다. 조사 경로: 관련 REL_*_STABLE 태그에 걸쳐 xact.h 플래그 정의를 diff.

PostgreSQL 너머 — 비교 설계와 연구 프론티어

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”
  • CUBRID의 트랜잭션 디스크립터(TDES) vs PostgreSQL의 TransactionState 스택. CUBRID는 트랜잭션별 상태를 LOG_TDES에 중앙화하고, 세이브포인트를 독립적인 제어 블록 스택이 아닌 undo 로그의 저장 LSN 마커로 추적한다(cubrid-transaction.md 참조). PostgreSQL의 레벨별 ResourceOwner + 메모리 컨텍스트는 더 무겁지만 더 균일한 되감기 단위다. 두 모델 간 “세이브포인트 롤백 비용” 비교는 PostgreSQL이 그 균일성을 위해 지불하는 것을 수치화할 것이다.

  • 물리적 undo 로깅(ARIES CLR) vs MVCC-as-undo. ARIES(dbms-papers/aries.md)는 undo 동작을 로깅하고 보상 로그 레코드(compensation log record)를 써서 부분 롤백이 그 자체로 복구 가능하게 한다. PostgreSQL은 대부분의 물리적 undo를 우회한다. 롤백된 튜플 버전은 단순히 보이지 않게 되고(중단이 CLOG를 뒤집고, vacuum이 나중에 죽은 버전을 회수), xact.c는 거의 undo를 쓰지 않는다. zheap 프로젝트(undo 기반 PostgreSQL 스토리지 AM)가 자연스러운 비교 대상이다. PostgreSQL에 ARIES 방식 undo를 재도입해 이 모듈의 중단 경로를 상당히 바꿀 것이다.

  • 즉시 vs 지연 XID와 64비트 XID 논쟁. PostgreSQL의 에포크 확장 FullTransactionId를 가진 지연 32비트 XID는 래핑에 대한 직접적인 응답이다. 네이티브 64비트 트랜잭션 ID를 가진 엔진(InnoDB의 더 큰 trx id 포함)은 xidStopLimit 방어를 완전히 피한다. 온디스크 XID를 64비트로 만들자는 반복되는 커뮤니티 제안은 varsup.c의 래핑 기계를 삭제할 것이다. 이 모듈을 기준으로 그 제안을 추적하는 것은 유용한 프론티어 메모다.

  • 그룹 커밋과 커밋 지연. PostgreSQL의 synchronous_commit 비동기 경로에 walwriter가 더해지면 그룹 커밋에 근접한다. 전용 그룹 커밋 설계(고전적인 Helland/DeWitt 그룹 커밋 작업과 현대의 로그 파이프라이닝)는 여러 백엔드의 플러시를 더 공격적으로 합친다. PostgreSQL의 백엔드별 DELAY_CHKPT_START + SyncRepWaitForLSN이 더 무거운 그룹 커밋 방식과 어떻게 상호작용하는지는 열린 비교다. postgres-xlog-wal.md가 이것이 기반으로 삼을 플러시 역학을 소유한다.

인트리 설계 문서:

  • src/backend/access/transam/README — “The Transaction System” (3계층 모델, BEGIN/SELECT/INSERT/COMMIT 예시), “Subtransaction Handling”, “Transaction and Subtransaction Numbering”, “Interlocking Transaction Begin, Transaction End, and Snapshots”, “pg_xact and pg_subtrans”, “Asynchronous Commit”, “Transaction Emulation during Recovery”.

소스 파일 (REL_18_STABLE, 커밋 273fe94):

  • src/backend/access/transam/xact.c — 트랜잭션 매니저: 상태 머신, 커밋/중단 파이프라인, 서브트랜잭션, 레코드 작성자, 콜백.
  • src/include/access/xact.h — 레코드 형식, xinfo 플래그, opcode, 공개 API, 콜백 이벤트 열거형.
  • src/backend/access/transam/varsup.cGetNewTransactionId, nextXid, 래핑 한계.

교과서 / 논문 기준점:

  • Database System Concepts, Silberschatz, Korth, Sudarshan, 7판 — 17장 (Transactions: ACID, 상태 다이어그램, 복구 가능/연쇄 없는 스케줄, 중첩 트랜잭션). 캡처: knowledge/research/dbms-general/database-system-concepts.md.
  • ARIES: A Transaction Recovery Method…, Mohan et al., ACM TODS 1992 — WAL, 히스토리 반복, 부분 롤백을 위한 undo 로깅. 캡처: knowledge/research/dbms-papers/aries.md.

교차 참조 (메커니즘 중복 없음):

  • postgres-xlog-wal.md — WAL 삽입/플러시, XLogInsert, LSN 게이트.
  • postgres-clog-commit-ts.md — pg_xact 커밋 상태 저장소와 커밋 타임스탬프; 비동기 커밋 CLOG 힌트 지연.
  • postgres-two-phase-commit.mdPREPARE TRANSACTION, twophase 상태 파일, COMMIT/ROLLBACK PREPARED, 준비된 트랜잭션 복구.
  • postgres-mvcc-snapshots.mdProcArrayEndTransaction, 스냅샷, latestCompletedXid, CLOG 뒤집힘에 의존하는 가시성 결정.
  • postgres-recovery-redo.mdxact_redo와 커밋/중단 레코드 재연.