(KO) PostgreSQL 체크포인트 — 내구성 앵커: redo 포인터 고정, 버퍼 플러시, WAL 세그먼트 재활용
목차
- 학술적 배경
- DBMS 공통 설계 패턴
- PostgreSQL의 구현
- 소스 코드 가이드
- 소스 검증 (2026-06-05 기준)
- PostgreSQL 너머 — 비교 설계와 연구 프론티어
- 출처
학술적 배경
섹션 제목: “학술적 배경”WAL(Write-Ahead Logging, 미리-쓰기 로깅) 시스템에서 **체크포인트(checkpoint)**란 복구 관리자가 그 이전에 기록된 WAL 레코드를 버릴 수 있게 해 주는 내구성 마커다. 체크포인트가 없으면 크래시 복구는 처음부터 전체 WAL 이력을 재실행해야 한다. 이는 전체 WAL 크기에 비례해 복구 시간이 늘어나는 구조적 문제다. 체크포인트는 이 꼬리를 잘라 낸다. LSN L에서 체크포인트가 성공하면 복구는 L부터만 재실행하면 된다. L 이전의 모든 변경은 이미 안정 저장소에 기록되어 있기 때문이다.
이 메커니즘의 이론적 기반은 ARIES 알고리즘이다(Mohan et al., ARIES: A
Transaction Recovery Method Supporting Fine-Granularity Locking and Partial
Rollbacks Using Write-Ahead Logging, ACM TODS 1992;
knowledge/research/dbms-papers/aries.md에 정리).
ARIES는 체크포인트 의무를 명확히 규정한다. LSN L에 체크포인트 레코드를
쓰기 전에 PageLSN ≤ L인 모든 데이터 페이지가 안정 저장소에 플러시되어야
한다. 이를 체크포인트에 대한 미리-쓰기 규칙이라 한다. 체크포인트 레코드가
내구성을 확보하면 그 안에 박힌 redo 포인터가 복구가 시작해야 할
가장 이른 LSN이 된다.
ARIES는 두 가지 체크포인트 방식을 구분한다.
-
일관 체크포인트(consistent/quiesce checkpoint). 스냅샷을 찍는 동안 모든 활성 트랜잭션의 쓰기를 멈춘다. redo 포인터가 체크포인트 레코드 LSN과 일치한다. 추론이 단순하지만 쓰기 기아(write starvation)를 유발해 고동시성 시스템에는 적합하지 않다.
-
퍼지 체크포인트(fuzzy/online checkpoint). 버퍼 플러시가 진행되는 동안 쓰기가 계속된다. redo 포인터는 플러시가 시작되기 전에 고정된다. 체크포인트 윈도우 동안 더티 페이지를 생성할 수 있는 가장 이른 LSN 값이다. 트랜잭션들이 버퍼를 쓰는 동안 WAL 레코드를 계속 삽입하기 때문에, 체크포인트 레코드는 플러시 시작이 아닌 끝에 기록된다. 복구는 체크포인트 레코드 위치가 아닌 redo 포인터부터 재실행을 시작한다. 두 LSN은 퍼지 체크포인트에서 최대
max_wal_size바이트만큼 떨어질 수 있다.
PostgreSQL은 온라인 체크포인트에 퍼지 모델을 적용한다. redo 포인터를 먼저 고정하고, 플러시를 동시에 진행하며, 체크포인트 레코드를 마지막에 쓴다. 셧다운 체크포인트만 일관 방식에 가까운 변형을 쓰는데, 셧다운 시에는 동시 WAL 삽입이 불가능하기 때문이다.
Database Internals(Petrov, 5장 “Transaction Processing and Recovery”)는 모든 체크포인트 설계가 풀어야 할 두 가지 하위 문제를 제시한다.
-
I/O 증폭. 체크포인트 시점에 모든 더티 버퍼를 한꺼번에 쓰면 갑작스러운 I/O 급증이 발생해 전경 쿼리 I/O와 경합하고 지연 SLA를 위반할 수 있다. 해결책은 **체크포인트 페이싱(checkpoint pacing)**이다. 더티 버퍼 쓰기를 인터체크포인트 구간의 일정 비율 안에 분산시키는 방식이다.
-
WAL 보존 대 공간 소비. 가장 오래된 활성 redo 포인터 이전의 WAL 세그먼트는 재활용할 수 있다. 스탠바이 레플리카나 복제 슬롯이 redo 포인터보다 앞선 위치를 요구하면 세그먼트를 더 오래 보존해야 한다. 이 계산을 잘못하면 디스크 낭비나 레플리카 단절 중 하나로 이어진다.
두 문제 모두 PostgreSQL에 직접 구현되어 있다. 아래 § PostgreSQL의 구현에서 살핀다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”ARIES 이론은 모든 프로덕션 체크포인트 구현을 알아볼 수 있는 패턴으로 수렴시킨다. 이 절은 PostgreSQL, Oracle, InnoDB, DB2, SQL Server 등이 공유하는 공학적 관례를 정리한다.
전담 체크포인트 프로세스
섹션 제목: “전담 체크포인트 프로세스”거의 모든 엔진은 체크포인트 I/O를 별도의 백그라운드 프로세스(또는 스레드)로 분리한다. 덕분에 체크포인트 지연이 사용자 쿼리 응답 시간에 스며들지 않고, 체크포인트 엔진이 전경 스레드와 스케줄러 타임슬라이스를 놓고 경합하지 않으면서 자체 I/O 우선순위를 설정할 수 있다.
시작 시 redo 포인터 고정, 끝에 체크포인트 레코드 기록
섹션 제목: “시작 시 redo 포인터 고정, 끝에 체크포인트 레코드 기록”퍼지 체크포인트의 순서는 언제나 같다. (1) WAL 삽입 상태에 대한 짧은 배타
접근을 확보해 redo 포인터를 고정한다. (2) PageLSN ≤ redo pointer인 모든
페이지를 플러시한다. (3) redo 포인터 값을 담은 체크포인트 레코드를 쓰고
플러시한다. 복구는 체크포인트 레코드 위치로 이동해 재실행 시작점인 redo
포인터를 찾는다. 퍼지 체크포인트에서 이 두 LSN은 최대 max_wal_size만큼
떨어질 수 있다.
pg_control / 컨트롤 파일: 복구 앵커
섹션 제목: “pg_control / 컨트롤 파일: 복구 앵커”모든 ARIES 구현은 최신 체크포인트 레코드의 위치를 기록하는 작고 자주 갱신되는 컨트롤 파일을 유지한다. 시작 시 복구 관리자는 이 파일을 먼저 읽어 체크포인트 레코드를 찾고, redo 포인터를 추출해 재실행을 시작한다. 컨트롤 파일은 부분 쓰기를 견디도록 단일 페이지 원자적 갱신(또는 섀도 복사) 방식을 쓴다.
I/O 급증을 완화하는 페이싱
섹션 제목: “I/O 급증을 완화하는 페이싱”엔진들은 더티 버퍼 플러시를 인터체크포인트 구간 전체에 분산시키기 위해 버퍼 라이터를 스로틀링한다. 조정 손잡이는 보통 체크포인트 구간의 분수로 표현된다. “목표 분수 × 체크포인트 구간이 지날 때까지 플러시를 마쳐라”는 방식이다. 페이지 쓰기 배치 사이에 짧게 sleep하는 방식이 일반적이며, 뒤처진다고 판단되면 sleep 없이 쓰기를 가속한다.
fsync 통합
섹션 제목: “fsync 통합”페이지 쓰기와 fsync는 별개의 시스템 콜이다. 바쁜 시스템에서는 여러 백엔드가 체크포인트 사이에 같은 페이지(또는 같은 파일의 페이지)를 여러 번 쓴다. 효율적인 구현은 매 쓰기마다 fsync하지 않고 체크포인트 시점에 파일 단위로 fsync를 일괄 처리한다. O(쓰기 횟수) fsync가 O(건드린 파일 수) fsync로 줄어드는 것이다. 체크포인트 프로세스가 fsync 큐를 소유하고, 파일을 직접 쓰는 백엔드는 체크포인트 프로세스에 해당 파일을 등록하고, 통합은 checkpointer 프로세스가 중앙에서 처리한다.
복구 중 재시작 지점(restartpoint)
섹션 제목: “복구 중 재시작 지점(restartpoint)”아카이브 또는 스트리밍 복구 중에 스탠바이는 완전한 체크포인트를 찍을 수 없다. 프라이머리 스트림 이외의 새 WAL을 쓸 수 없기 때문이다. 대신 **재시작 지점(restartpoint)**을 찍는다. “이전에 기록된 체크포인트 레코드까지 안전하게 재실행했고, 그 시점까지 참조된 모든 페이지를 플러시했다”는 기록이다. 재시작 지점 덕분에 프라이머리가 새 체크포인트를 찍지 않아도 스탠바이에서 해당 체크포인트의 redo 포인터 이전 WAL 세그먼트를 재활용할 수 있다.
PostgreSQL의 구현
섹션 제목: “PostgreSQL의 구현”프로세스 모델: checkpointer
섹션 제목: “프로세스 모델: checkpointer”PostgreSQL은 체크포인트 작업 전체를 하나의 보조 프로세스에 맡긴다.
CheckpointerMain(in postmaster/checkpointer.c)은 AuxiliaryProcessMain이
MyBackendType = B_CHECKPOINTER로 실행한다. 이 프로세스가 담당하는 것들이다.
checkpoint_timeout초마다 발동하는 타이머 루프,- WAL 사용량이
max_wal_size를 초과할 때 발생하는CHECKPOINT_CAUSE_XLOG신호 처리, - postmaster의
SIGINT를 받으면 수행하는 셧다운 체크포인트, - 백엔드가
ForwardSyncRequest로 전달하는 fsync 요청 큐.
PostgreSQL 9.2 이전에는 bgwriter 프로세스가 백그라운드 버퍼 쓰기와 체크포인트를 모두 담당했다. 두 프로세스로 분리한 덕분에 bgwriter는 체크포인트 페이싱과 독립적으로 꾸준한 사전 쓰기를 수행할 수 있다.
CheckpointerShmemStruct: 요청과 완료 프로토콜
섹션 제목: “CheckpointerShmemStruct: 요청과 완료 프로토콜”백엔드와 checkpointer는 작은 공유 메모리 구조를 매개로 메시지를 주고받는다.
// CheckpointerShmemStruct — postmaster/checkpointer.ctypedef struct{ pid_t checkpointer_pid; /* PID (0 if not started) */ slock_t ckpt_lck; /* protects ckpt_* fields */ int ckpt_started; /* increments when a checkpoint starts */ int ckpt_done; /* set to ckpt_started on completion */ int ckpt_failed; /* increments on failure */ int ckpt_flags; /* OR of pending request flags */ ConditionVariable start_cv; /* signaled when ckpt_started advances */ ConditionVariable done_cv; /* signaled when ckpt_done advances */ int num_requests; int max_requests; CheckpointerRequest requests[FLEXIBLE_ARRAY_MEMBER]; /* fsync queue */} CheckpointerShmemStruct;RequestCheckpoint는 여섯 단계 핸드셰이크를 사용한다.
ckpt_lck를 잡고ckpt_failed와ckpt_started의 현재 값을 읽은 뒤 요청 플래그를 OR로 합친다.SetLatch로 checkpointer의 latch를 설정한다.start_cv에서ckpt_started가 증가할 때까지 잠든다.- 새
ckpt_started값을 기록한다. done_cv에서ckpt_done ≥ new_started(모듈 산술)가 될 때까지 잠든다.ckpt_failed가 달라졌으면 실패, 아니면 성공이다.
이 프로토콜 덕분에 여러 백엔드가 동시에 체크포인트를 요청해도 어느 쪽도
직접 체크포인트를 실행할 필요가 없다. ckpt_flags의 OR 의미론 덕분에
여러 요청은 하나로 합쳐진다. 어떤 백엔드가 CHECKPOINT_IMMEDIATE 플래그를
세우면 다음 체크포인트는 다른 동시 요청과 무관하게 즉시 모드로 실행된다.
CheckPoint 레코드 구조체
섹션 제목: “CheckPoint 레코드 구조체”XLOG_CHECKPOINT_ONLINE과 XLOG_CHECKPOINT_SHUTDOWN WAL 레코드의 본문은
src/include/catalog/pg_control.h의 CheckPoint 구조체다.
// CheckPoint — src/include/catalog/pg_control.htypedef struct CheckPoint{ XLogRecPtr redo; /* REDO start point */ TimeLineID ThisTimeLineID; TimeLineID PrevTimeLineID; bool fullPageWrites; int wal_level; FullTransactionId nextXid; /* next free XID */ Oid nextOid; MultiXactId nextMulti; MultiXactOffset nextMultiOffset; TransactionId oldestXid; /* cluster-wide datfrozenxid */ Oid oldestXidDB; MultiXactId oldestMulti; Oid oldestMultiDB; pg_time_t time; TransactionId oldestCommitTsXid; TransactionId newestCommitTsXid; TransactionId oldestActiveXid; /* oldest XID still running (online ckpt) */} CheckPoint;redo 필드가 redo 포인터다. ControlFileData.checkPointCopy는 최신
CheckPoint의 복사본을 담고, ControlFileData.checkPoint는 체크포인트
레코드 자체의 LSN을 담는다. 복구는 checkPoint로 WAL 레코드를 찾고,
checkPointCopy.redo로 재실행 시작점을 알아낸다.
ControlFileData.state(DBState enum, 값으로 DB_STARTUP,
DB_SHUTDOWNING, DB_SHUTDOWNED, DB_IN_PRODUCTION 등)는 체크포인트
전후로 원자적으로 갱신된다. 부분 쓰기가 발생해도 다음 시작 시 감지할 수 있다.
CreateCheckPoint: 온라인 체크포인트 흐름
섹션 제목: “CreateCheckPoint: 온라인 체크포인트 흐름”xlog.c의 CreateCheckPoint가 전체 체크포인트 순서를 실행한다.
온라인(비셧다운) 체크포인트 기준으로 살핀다.
// CreateCheckPoint — src/backend/access/transam/xlog.cboolCreateCheckPoint(int flags){ /* 1. 복구 중이면 거절(end-of-recovery 예외). */ if (RecoveryInProgress() && (flags & CHECKPOINT_END_OF_RECOVERY) == 0) elog(ERROR, "can't create a checkpoint during recovery");
/* 2. smgr 사전 훅 (크리티컬 섹션 바깥). */ SyncPreCheckpoint();
START_CRIT_SECTION();
/* 3. XID/OID/MultiXact 수위 값을 checkPoint 구조체에 수집. */ checkPoint.nextXid = TransamVariables->nextXid; /* XidGenLock 하에서 */ checkPoint.nextOid = TransamVariables->nextOid; /* OidGenLock 하에서 */ /* ... MultiXact, CommitTs 필드 ... */
/* 4. 마지막 체크포인트 이후 변경이 없으면 건너뜀. */ if (last_important_lsn == ControlFile->checkPoint) { END_CRIT_SECTION(); return false; }
/* 5. redo 포인터 고정: XLOG_CHECKPOINT_REDO 삽입. */ XLogBeginInsert(); XLogRegisterData(&wal_level, sizeof(wal_level)); (void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO); checkPoint.redo = RedoRecPtr; /* 이 시점에 고정됨 */
END_CRIT_SECTION();
/* 6. 커밋 크리티컬 섹션 중인 트랜잭션이 빠져나올 때까지 대기. */ vxids = GetVirtualXIDsDelayingChkpt(&nvxids, DELAY_CHKPT_START); while (HaveVirtualXIDsDelayingChkpt(...)) { AbsorbSyncRequests(); ... }
/* 7. 모든 더티 버퍼와 SLRU 플러시 (페이싱 적용). */ CheckPointGuts(checkPoint.redo, flags);
/* 8. 커밋을 완료 중인 트랜잭션이 빠져나올 때까지 대기. */ vxids = GetVirtualXIDsDelayingChkpt(&nvxids, DELAY_CHKPT_COMPLETE); while (HaveVirtualXIDsDelayingChkpt(...)) { AbsorbSyncRequests(); ... }
START_CRIT_SECTION();
/* 9. XLOG_CHECKPOINT_ONLINE 레코드 쓰기 및 플러시. */ XLogBeginInsert(); XLogRegisterData(&checkPoint, sizeof(checkPoint)); recptr = XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_ONLINE); XLogFlush(recptr);
/* 10. pg_control 원자적 갱신. */ ControlFile->checkPoint = ProcLastRecPtr; /* ckpt 레코드 LSN */ ControlFile->checkPointCopy = checkPoint; UpdateControlFile();
END_CRIT_SECTION();
/* 11. WAL 요약기 깨우기; 오래된 WAL 세그먼트 재활용. */ WakeupWalSummarizer(); RemoveOldXlogFiles(...); UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr); return true;}5단계가 퍼지 체크포인트의 핵심이다. XLOG_CHECKPOINT_REDO는 그 LSN이
checkPoint.redo가 되는 최소 WAL 레코드다. 이 LSN 이후에 쓰인 페이지는
PageLSN > redo를 갖게 되므로, 복구는 redo부터 재실행해야 해당 페이지를
복원할 수 있다. 이후 XLOG_CHECKPOINT_ONLINE 레코드에도 같은 checkPoint.redo
값이 담기므로, redo 레코드 자체의 페이로드 없이도 복구는 재실행 시작점을 알 수 있다.
셧다운 체크포인트(flags & CHECKPOINT_IS_SHUTDOWN)의 순서는 더 단순하다.
동시 WAL 삽입이 없으므로 redo 포인터는 Insert->CurrBytePos에서 직접 계산된다.
XLOG_CHECKPOINT_REDO 레코드는 생략되고 XLOG_CHECKPOINT_SHUTDOWN 하나만 기록된다.
flowchart TD
A["CreateCheckPoint(flags)"] --> B["SyncPreCheckpoint<br/>(smgr 사전 훅, 크리티컬 섹션 바깥)"]
B --> C["START_CRIT_SECTION<br/>nextXid / nextOid / MultiXact 수집<br/>CheckPoint 구조체에 기록"]
C --> D{"last_important_lsn ==<br/>ControlFile->checkPoint ?<br/>(변경 없음, 비강제)"}
D -->|yes| E["END_CRIT_SECTION<br/>return false (건너뜀)"]
D -->|no| F{"셧다운 ?"}
F -->|"온라인"| G["XLogInsert(XLOG_CHECKPOINT_REDO)<br/>checkPoint.redo = RedoRecPtr 고정"]
F -->|"셧다운"| H["redo = XLogBytePosToRecPtr(CurrBytePos)<br/>별도 REDO 레코드 없음"]
G --> I["GetVirtualXIDsDelayingChkpt(DELAY_CHKPT_START)<br/>커밋 크리티컬 트랜잭션 대기"]
H --> I
I --> J["CheckPointGuts(checkPoint.redo, flags)<br/>SLRU 플러시 + CheckPointBuffers (페이싱)<br/>ProcessSyncRequests (fsync)"]
J --> K["GetVirtualXIDsDelayingChkpt(DELAY_CHKPT_COMPLETE)<br/>커밋 완료 트랜잭션 대기"]
K --> L["XLogInsert(XLOG_CHECKPOINT_ONLINE / SHUTDOWN)<br/>XLogFlush(recptr)"]
L --> M["LWLock(ControlFileLock):<br/>ControlFile->checkPoint = ProcLastRecPtr<br/>checkPointCopy = checkPoint<br/>UpdateControlFile"]
M --> N["END_CRIT_SECTION<br/>WakeupWalSummarizer<br/>SyncPostCheckpoint"]
N --> O["UpdateCheckPointDistanceEstimate<br/>KeepLogSeg + RemoveOldXlogFiles<br/>(WAL 재활용 / 삭제)"]
O --> P["PreallocXlogFiles<br/>return true"]
그림 1 — CreateCheckPoint(xlog.c) 내부의 온라인 체크포인트 순서.
핵심 순서 불변식이 퍼지 체크포인트 보장을 만든다.
XLOG_CHECKPOINT_REDO가 checkPoint.redo를 고정하는 것은 CheckPointGuts가
더티 버퍼를 플러시하기 전이고, 같은 redo 값을 담은 XLOG_CHECKPOINT_ONLINE은
플러시가 완료된 후에 기록된다. ControlFileLock 아래의 컨트롤 파일 갱신이
새 체크포인트를 복구 앵커로 만드는 단 하나의 원자적 지점이다. 이 갱신 이후에야
KeepLogSeg + RemoveOldXlogFiles로 오래된 WAL 세그먼트가 재활용된다.
셧다운 경로에서는 동시 WAL 삽입이 불가능하므로 두 레코드 방식이 레코드 하나로 축소된다.
redo 포인터 고정: 두 가지 계산 경로
섹션 제목: “redo 포인터 고정: 두 가지 계산 경로”redo 포인터는 체크포인트가 생산하는 가장 중요한 값이다. CreateCheckPoint는
클러스터가 정지 중인지 여부에 따라 두 가지 다른 경로로 redo 포인터를 계산한다.
두 경로 모두 WAL 삽입 락을 잡은 상태에서 실행되므로 값이 WAL 끝과 일관성을 유지한다.
// CreateCheckPoint — src/backend/access/transam/xlog.cif (shutdown){ XLogRecPtr curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
/* * Compute new REDO record ptr = location of next XLOG record. * Since this is a shutdown checkpoint, there can't be any concurrent * WAL insertion. */ freespace = INSERT_FREESPACE(curInsert); if (freespace == 0) { if (XLogSegmentOffset(curInsert, wal_segment_size) == 0) curInsert += SizeOfXLogLongPHD; else curInsert += SizeOfXLogShortPHD; } checkPoint.redo = curInsert;
/* update shared RedoRecPtr while holding all insertion locks */ RedoRecPtr = XLogCtl->Insert.RedoRecPtr = checkPoint.redo;}
WALInsertLockRelease(); /* let other xacts proceed during the flush */
if (!shutdown){ /* Include WAL level in record for WAL summarizer's benefit. */ XLogBeginInsert(); XLogRegisterData(&wal_level, sizeof(wal_level)); (void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO);
/* XLogInsertRecord already advanced Insert.RedoRecPtr + RedoRecPtr; */ /* copy that LSN into the checkpoint record we will write at the end. */ checkPoint.redo = RedoRecPtr;}셧다운 경로는 빈 WAL 페이지 헤더를 건너뛰는 처리가 필요하다.
INSERT_FREESPACE(curInsert) == 0이면 다음 레코드는 curInsert(페이지
경계)에 시작할 수 없으므로, redo 포인터를 SizeOfXLogLongPHD(세그먼트
경계) 또는 SizeOfXLogShortPHD(세그먼트 중간) 만큼 앞으로 밀어준다.
온라인 경로는 이 산술을 완전히 피한다. XLOG_CHECKPOINT_REDO 레코드를
직접 삽입하면 일반 삽입 경로가 RedoRecPtr에 LSN을 기록해 돌려준다.
소스의 핵심 주석 — “RedoRecPtr 전진을 미룰 수 없다. 버퍼 덤프 중 발생하는
XLogInsert는 자신의 버퍼 변경이 이 체크포인트에 포함되지 않는다고 가정해야
한다” — 은 퍼지 체크포인트 불변식을 역방향으로 표현한 것이다.
RedoRecPtr 이후에 더티해진 버퍼는 이 체크포인트 밖에 있으므로
RedoRecPtr부터 재실행해야 복원된다.
flowchart TD
A["CreateCheckPoint(flags)"] --> B["SyncPreCheckpoint<br/>(smgr 사전 훅, 크리티컬 섹션 바깥)"]
B --> C["START_CRIT_SECTION<br/>nextXid / nextOid / MultiXact 수집<br/>CheckPoint 구조체에 기록"]
C --> D{"last_important_lsn ==<br/>ControlFile->checkPoint ?<br/>(변경 없음, 비강제)"}
D -->|yes| E["END_CRIT_SECTION<br/>return false (건너뜀)"]
D -->|no| F{"shutdown ?"}
F -->|"online"| G["XLogInsert(XLOG_CHECKPOINT_REDO)<br/>pin checkPoint.redo = RedoRecPtr"]
F -->|"shutdown"| H["redo = XLogBytePosToRecPtr(CurrBytePos)<br/>no separate REDO record"]
G --> I["GetVirtualXIDsDelayingChkpt(DELAY_CHKPT_START)<br/>wait for commit-critical xacts"]
H --> I
I --> J["CheckPointGuts(checkPoint.redo, flags)<br/>flush SLRUs + CheckPointBuffers (paced)<br/>ProcessSyncRequests (fsync)"]
J --> K["GetVirtualXIDsDelayingChkpt(DELAY_CHKPT_COMPLETE)<br/>wait for completing commits"]
K --> L["XLogInsert(XLOG_CHECKPOINT_ONLINE / SHUTDOWN)<br/>XLogFlush(recptr)"]
L --> M["LWLock(ControlFileLock):<br/>ControlFile->checkPoint = ProcLastRecPtr<br/>checkPointCopy = checkPoint<br/>UpdateControlFile"]
M --> N["END_CRIT_SECTION<br/>WakeupWalSummarizer<br/>SyncPostCheckpoint"]
N --> O["UpdateCheckPointDistanceEstimate<br/>KeepLogSeg + RemoveOldXlogFiles<br/>(recycle / remove WAL)"]
O --> P["PreallocXlogFiles<br/>return true"]
그림 1 — CreateCheckPoint(xlog.c) 내부의 온라인 체크포인트 순서.
핵심 순서 불변식이 퍼지 체크포인트 보장을 만든다.
XLOG_CHECKPOINT_REDO가 checkPoint.redo를 고정하는 것은 CheckPointGuts가
더티 버퍼를 플러시하기 전이고, 같은 redo 값을 담은 XLOG_CHECKPOINT_ONLINE은
플러시가 완료된 후에 기록된다. ControlFileLock 아래의 컨트롤 파일 갱신이
새 체크포인트를 복구 앵커로 만드는 단 하나의 원자적 지점이다. 이 갱신 이후에야
KeepLogSeg + RemoveOldXlogFiles로 오래된 WAL 세그먼트가 재활용된다.
셧다운 경로에서는 동시 WAL 삽입이 불가능하므로 두 레코드 방식이 레코드 하나로 축소된다.
CheckPointGuts: 모든 더티 상태 플러시
섹션 제목: “CheckPointGuts: 모든 더티 상태 플러시”CheckPointGuts(checkPointRedo, flags)가 I/O 집약 단계를 수행한다.
// CheckPointGuts — src/backend/access/transam/xlog.cstatic voidCheckPointGuts(XLogRecPtr checkPointRedo, int flags){ CheckPointRelationMap(); CheckPointReplicationSlots(flags & CHECKPOINT_IS_SHUTDOWN); CheckPointSnapBuild(); CheckPointLogicalRewriteHeap(); CheckPointReplicationOrigin();
/* Write out all dirty data in SLRUs and main buffer pool */ CheckPointCLOG(); CheckPointCommitTs(); CheckPointSUBTRANS(); CheckPointMultiXact(); CheckPointPredicate(); CheckPointBuffers(flags); /* <-- main shared_buffers flush */
/* Fsync everything */ ProcessSyncRequests();
/* 2PC state last */ CheckPointTwoPhase(checkPointRedo);}CheckPointBuffers는 storage/buffer/bufmgr.c의 BufferSync를 호출한다.
BufferSync는 버퍼 풀을 두 패스로 순회한다. 태그 패스에서는 체크포인트
시작 시점에 더티 상태였던 모든 버퍼에 BM_CHECKPOINT_NEEDED를 설정하고
CkptBufferIds[]에 기록한다. 이 시점 이후에 더티해진 페이지는 이 체크포인트
범위 밖으로 처리된다(다음 체크포인트에 해당한다).
// BufferSync — src/backend/storage/buffer/bufmgr.cnum_to_scan = 0;for (buf_id = 0; buf_id < NBuffers; buf_id++){ BufferDesc *bufHdr = GetBufferDescriptor(buf_id); buf_state = LockBufHdr(bufHdr);
if ((buf_state & mask) == mask) /* BM_DIRTY [| BM_PERMANENT] */ { CkptSortItem *item; buf_state |= BM_CHECKPOINT_NEEDED; item = &CkptBufferIds[num_to_scan++]; item->buf_id = buf_id; item->tsId = bufHdr->tag.spcOid; /* sort key: tablespace */ /* ... relNumber / forkNum / blockNum ... */ } UnlockBufHdr(bufHdr, buf_state);}쓰기 패스에서는 테이블스페이스별 최소 힙을 사용해 쓰기를 테이블스페이스
간에 균형있게 분산시킨다. 테이블스페이스 순서대로만 쓰면 다른 테이블스페이스의
스토리지가 굶주리기 때문이다. 버퍼를 하나 쓸 때마다 CheckpointWriteDelay를
호출해 페이싱을 적용한다.
// BufferSync — src/backend/storage/buffer/bufmgr.cnum_processed = 0;while (!binaryheap_empty(ts_heap)){ CkptTsStatus *ts_stat = (CkptTsStatus *) DatumGetPointer(binaryheap_first(ts_heap)); buf_id = CkptBufferIds[ts_stat->index].buf_id; bufHdr = GetBufferDescriptor(buf_id); num_processed++;
/* Flag may have been cleared by a backend/bgwriter writing it first. */ if (pg_atomic_read_u32(&bufHdr->state) & BM_CHECKPOINT_NEEDED) if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN) num_written++;
/* Advance per-tablespace progress, re-balance the heap. */ ts_stat->progress += ts_stat->progress_slice; /* ... binaryheap_remove_first / binaryheap_replace_first ... */
/* Sleep to throttle our I/O rate. */ CheckpointWriteDelay(flags, (double) num_processed / num_to_scan);}progress = num_processed / num_to_scan이 CheckpointWriteDelay에서
경과 시간 및 경과 WAL 분수와 비교되는 진행도 추정값이다. 이 단일 비율이
bufmgr.c의 버퍼 플러시 루프와 checkpointer.c의 checkpoint_completion_target
페이싱 로직을 연결하는 다리 역할을 한다. 루프 안의 BM_CHECKPOINT_NEEDED
재확인도 중요하다. 일반 백엔드나 bgwriter가 이미 해당 버퍼를 쓰고 플래그를
지웠을 수 있으므로, checkpointer는 플래그가 남아 있는 버퍼에만 SyncOneBuffer를
호출해 중복 I/O를 방지한다.
flowchart TD
A["CheckPointBuffers(flags)<br/>-> BufferSync(flags)"] --> B["태그 패스:<br/>모든 NBuffers를 LockBufHdr로 스캔"]
B --> C{"buf_state & mask == mask<br/>(BM_DIRTY [| BM_PERMANENT]) ?"}
C -->|no| B
C -->|yes| D["BM_CHECKPOINT_NEEDED 설정<br/>CkptBufferIds[num_to_scan++]에 추가<br/>(buf_id, tsId, relNumber, forkNum, blockNum)"]
D --> B
B --> E["CkptBufferIds를 테이블스페이스로 정렬<br/>테이블스페이스별 최소 힙 구성<br/>progress_slice = num_to_scan / ts.num_to_scan"]
E --> F{"binaryheap_empty(ts_heap) ?"}
F -->|yes| K["IssuePendingWritebacks<br/>CheckpointStats.ckpt_bufs_written += num_written"]
F -->|no| G["힙 최상위 테이블스페이스 선택<br/>buf_id = CkptBufferIds[ts.index]<br/>num_processed++"]
G --> H{"state & BM_CHECKPOINT_NEEDED<br/>여전히 설정되어 있나 ?"}
H -->|"아니오 (백엔드/bgwriter가 먼저 씀)"| I["쓰기 건너뜀"]
H -->|yes| J["SyncOneBuffer -> FlushBuffer<br/>num_written++"]
I --> L["ts.progress += progress_slice<br/>힙에서 제거 또는 교체"]
J --> L
L --> M["CheckpointWriteDelay(flags,<br/>num_processed / num_to_scan)"]
M --> N{"!CHECKPOINT_IMMEDIATE &&<br/>IsCheckpointOnSchedule(progress) ?"}
N -->|"yes (앞서 있음)"| O["AbsorbSyncRequests<br/>WaitLatch(100 ms)"]
N -->|"no (뒤처짐)"| P["즉시 반환<br/>(sleep 없음)"]
O --> F
P --> F
그림 3 — BufferSync(bufmgr.c)와 체크포인트 페이싱의 연결.
태그 패스는 작업 집합(BM_CHECKPOINT_NEEDED)을 고정해 동시 쓰기가 이 체크포인트의
의무를 늘리지 못하게 한다. 쓰기 패스는 테이블스페이스별 최소 힙으로 드레인해
모든 테이블스페이스의 스토리지가 바쁘게 유지되도록 한다. 루프에서 나오는
핵심 수치는 num_processed / num_to_scan이다. 이 값이 CheckpointWriteDelay에
전달되고, IsCheckpointOnSchedule이 플러시가 경과 시간과 경과 WAL 예산 모두에서
앞서 있다고 판단할 때만 100 ms 잠든다. CHECKPOINT_IMMEDIATE 아래서는 절대 잠들지
않는다.
ProcessSyncRequests(storage/sync/sync.c)는 모든 fsync 요청을 파일 하나당
fsync 호출 하나로 통합한다. 이 호출 직전에 AbsorbSyncRequests가
CheckpointerShmem->requests[]에 백엔드들이 ForwardSyncRequest로 추가한
항목들을 싱크 모듈 내부의 보류 테이블로 옮기면서 파일 태그 기준으로 중복을 제거한다.
체크포인트 페이싱: CheckpointWriteDelay와 IsCheckpointOnSchedule
섹션 제목: “체크포인트 페이싱: CheckpointWriteDelay와 IsCheckpointOnSchedule”// CheckpointWriteDelay — postmaster/checkpointer.cvoidCheckpointWriteDelay(int flags, double progress){ if (!(flags & CHECKPOINT_IMMEDIATE) && !ShutdownXLOGPending && IsCheckpointOnSchedule(progress)) { AbsorbSyncRequests(); CheckArchiveTimeout(); WaitLatch(MyLatch, WL_LATCH_SET | WL_TIMEOUT, 100, ...); ResetLatch(MyLatch); }}IsCheckpointOnSchedule(progress)는 두 가지 경과 분수(elapsed-fraction)를 계산한다.
- WAL 기반:
(현재 삽입 LSN - ckpt_start_recptr) / (wal_segment_size × CheckPointSegments) - 시간 기반:
경과 초 / checkpoint_timeout
progress × checkpoint_completion_target이 두 값 중 어느 것보다 크면 체크포인트가
일정보다 앞서 있다고 판단해 100 ms 동안 잠든다. 어느 쪽에도 뒤처지면
sleep 없이 쓰기를 계속한다. 이 적응형 스로틀은 정상적인 WAL 부하 아래서 플러시가
checkpoint_completion_target × checkpoint_timeout 시점 근방에서 완료되도록
유지한다. 남은 구간은 동기적 ProcessSyncRequests 단계에 사용된다.
CreateRestartPoint: 복구 중 체크포인트
섹션 제목: “CreateRestartPoint: 복구 중 체크포인트”아카이브 또는 스트리밍 복구 중에는 스타트업 프로세스가 WAL 레코드를 재실행하고,
체크포인트 레코드를 만날 때마다 RecoveryRestartPoint를 호출해 레코드를
XLogCtl->lastCheckPoint에 저장한다. checkpointer는 주기적으로
CreateRestartPoint를 호출한다.
// CreateRestartPoint — src/backend/access/transam/xlog.cboolCreateRestartPoint(int flags){ /* 1. 공유 메모리에서 가장 최근 안전 체크포인트를 가져온다. */ SpinLockAcquire(&XLogCtl->info_lck); lastCheckPoint = XLogCtl->lastCheckPoint; SpinLockRelease(&XLogCtl->info_lck);
/* 2. 이 체크포인트에서 이미 재시작 지점을 만들었다면 건너뜀. */ if (lastCheckPoint.redo <= ControlFile->checkPointCopy.redo) return false;
/* 3. redo 포인터 고정. */ WALInsertLockAcquireExclusive(); RedoRecPtr = XLogCtl->Insert.RedoRecPtr = lastCheckPoint.redo; WALInsertLockRelease();
/* 4. 버퍼와 SLRU 플러시. */ CheckPointGuts(lastCheckPoint.redo, flags);
/* 5. pg_control 갱신. */ LWLockAcquire(ControlFileLock, LW_EXCLUSIVE); ControlFile->checkPoint = lastCheckPointRecPtr; ControlFile->checkPointCopy = lastCheckPoint; UpdateControlFile(); LWLockRelease(ControlFileLock);
/* 6. 오래된 WAL 세그먼트 재활용. */ RemoveOldXlogFiles(...); return true;}CreateRestartPoint는 CreateCheckPoint와 세 가지가 다르다. (a) 새 WAL
레코드를 쓰지 않는다. 체크포인트 레코드는 이미 프라이머리가 기록했다.
(b) 스타트업 프로세스가 이미 재실행한 체크포인트까지만 전진할 수 있다.
(c) PerformWalRecovery(스타트업 프로세스가 병렬로 실행 중)와 동시에
실행되므로 lastCheckPoint를 읽을 때 info_lck를 잡는다.
flowchart TD
A["CheckpointerMain<br/>MyBackendType = B_CHECKPOINTER"] --> B["for (;;) 루프:<br/>ResetLatch · AbsorbSyncRequests<br/>ProcessCheckpointerInterrupts"]
B --> C{"ShutdownXLOGPending ||<br/>ShutdownRequestPending ?"}
C -->|yes| Z["루프 탈출 -><br/>ShutdownXLOG(0,0)<br/>(최종 셧다운 체크포인트/재시작지점)"]
C -->|no| D{"CheckpointerShmem->ckpt_flags != 0 ?"}
D -->|yes| G["do_checkpoint = true"]
D -->|no| E{"elapsed_secs >=<br/>CheckPointTimeout ?"}
E -->|yes| F["do_checkpoint = true<br/>flags |= CHECKPOINT_CAUSE_TIME"]
E -->|no| W["WaitLatch(cur_timeout)<br/>= min(CheckPointTimeout,<br/>XLogArchiveTimeout)"]
W --> B
F --> H
G --> H["SpinLock(ckpt_lck):<br/>flags |= ckpt_flags · ckpt_flags = 0<br/>ckpt_started++"]
H --> I["ConditionVariableBroadcast(start_cv)<br/>ckpt_start_recptr = GetInsertRecPtr /<br/>GetXLogReplayRecPtr"]
I --> J{"do_restartpoint =<br/>RecoveryInProgress() ?<br/>(END_OF_RECOVERY 예외)"}
J -->|"false (프라이머리)"| K["CreateCheckPoint(flags)"]
J -->|"true (스탠바이)"| L["CreateRestartPoint(flags)"]
L --> L1{"lastCheckPoint.redo <=<br/>checkPointCopy.redo ?"}
L1 -->|yes| L2["return false<br/>(재실행된 새 체크포인트 없음;<br/>15초 후 재시도)"]
L1 -->|no| L3["RedoRecPtr = lastCheckPoint.redo 고정<br/>CheckPointGuts(redo, flags)<br/>ControlFile 갱신 · RemoveOldXlogFiles"]
K --> M["smgrdestroyall"]
L2 --> M
L3 --> M
M --> N["SpinLock(ckpt_lck):<br/>ckpt_done = ckpt_started<br/>ConditionVariableBroadcast(done_cv)"]
N --> B
그림 2 — CheckpointerMain 이벤트 루프(checkpointer.c)와 재시작 지점 분기.
각 반복은 먼저 fsync 큐를 비우고(AbsorbSyncRequests), 요청된 체크포인트
(ckpt_flags 비제로)와 타이머 체크포인트(elapsed_secs >= CheckPointTimeout) 중
어느 쪽에 응답할지 결정한다. RecoveryInProgress()에 따라 프라이머리
(CreateCheckPoint)와 스탠바이(CreateRestartPoint) 경로로 갈라진다.
CHECKPOINT_END_OF_RECOVERY 플래그는 복구 중에도 전체 체크포인트 경로를
강제한다. start_cv / done_cv 브로드캐스트가 작업을 괄호처럼 감싸
RequestCheckpoint에서 대기 중인 백엔드가 시작과 완료를 관측할 수 있게 한다.
재시작 지점 조기 탈출에 주목하라. 마지막 재시작 지점 이후 체크포인트 WAL
레코드가 재실행되지 않았다면(lastCheckPoint.redo <= checkPointCopy.redo)
false를 반환하고 루프는 15초 후 다시 시도한다.
WAL 세그먼트 재활용과 max_wal_size
섹션 제목: “WAL 세그먼트 재활용과 max_wal_size”pg_control을 갱신한 뒤 CreateCheckPoint와 CreateRestartPoint 모두
새 redo 포인터에서 계산한 기준으로 RemoveOldXlogFiles를 호출한다.
알고리즘의 순서다.
RedoRecPtr을 세그먼트 번호_logSegNo로 변환한다.KeepLogSeg(recptr, &_logSegNo)로wal_keep_size와 오래된 LSN이 필요한 복제 슬롯을 고려해 기준을 연장한다.- 필요시
InvalidateObsoleteReplicationSlots를 호출해 재활용을 막는 슬롯을 무효화한다. _logSegNo를 하나 줄이고RemoveOldXlogFiles에 전달한다. 이 함수는 기준보다 오래된 세그먼트를 재활용(더 높은 세그먼트 번호로 이름 변경)하거나 삭제한다.
CheckPointDistanceEstimate는 인터체크포인트 구간당 생성된 WAL 바이트의
지수 이동 평균을 유지한다. PreallocXlogFiles는 이 추정값으로 WAL 세그먼트
파일을 미리 생성해 fallocate / 제로 채우기 비용을 구간 전체에 분산시킨다.
전체 페이지 쓰기와 체크포인트의 관계
섹션 제목: “전체 페이지 쓰기와 체크포인트의 관계”fullPageWrites(GUC이고 CheckPoint 구조체와 XLogCtl->Insert.fullPageWrites에
저장)는 모든 체크포인트 시작 시 자동으로 활성화된다. 체크포인트 이후 새
체크포인트 윈도우 안에서 버퍼 페이지를 처음 수정할 때는 반드시 **전체 페이지
이미지(FPI, full-page image)**로 기록해야 한다. 크래시로 인해 부분적으로
쓰인 페이지를 복구가 델타 적용이 아닌 FPI로 통째 교체할 수 있게 하기 위해서다.
FPI 가드는 체크포인트마다 리셋된다. 체크포인트가 모든 페이지가 디스크 위에서
일관된 상태임을 이미 보장했기 때문이다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”postmaster/checkpointer.c
섹션 제목: “postmaster/checkpointer.c”**CheckpointerMain**이 프로세스 진입점이다. MyBackendType = B_CHECKPOINTER를
설정하고 시그널 핸들러(SIGINT → ReqShutdownXLOG, SIGUSR2 →
SignalHandlerForShutdownRequest)를 등록한 뒤 메인 이벤트 루프를 실행한다.
루프는 (1) AbsorbSyncRequests로 fsync 요청을 흡수하고, (2) ckpt_flags에서
보류 요청을 확인하고, (3) elapsed_secs ≥ CheckPointTimeout이면 타이머 체크포인트를
발동하고, (4) CreateCheckPoint 또는 CreateRestartPoint를 적절히 호출하고,
(5) done_cv로 대기 중인 백엔드에 신호를 보내고, (6) 다음 체크포인트 타임아웃과
XLogArchiveTimeout 중 더 이른 시각까지 잠든다.
**CheckpointerShmemInit**이 공유 메모리에 CheckpointerShmemStruct를 할당하고
초기화한다. requests[] 배열 크기는 min(NBuffers, MAX_CHECKPOINT_REQUESTS)다.
**RequestCheckpoint**는 백엔드가 호출한다. ckpt_flags에 플래그를 OR로 합치고,
checkpointer의 latch를 설정한 뒤, 필요시 start_cv + done_cv 조건 변수로
체크포인트 완료와 성공을 기다린다.
**ForwardSyncRequest**는 CheckpointerCommLock 아래 requests[]에
(FileTag, SyncRequestType) 쌍을 삽입한다. 큐가 절반 이상 차면 checkpointer
latch를 누른다. 큐가 완전히 차면 CompactCheckpointerRequestQueue(인메모리
해시 테이블을 이용한 중복 제거 패스)를 시도한다. 압축도 실패하면 false를
반환해 호출자가 직접 fsync하게 한다.
**CheckpointWriteDelay**는 BufferSync가 페이지를 쓸 때마다 호출된다.
IsCheckpointOnSchedule이 플러시가 앞서 있다고 판단하면 100 ms 잠든다.
그렇지 않으면 WRITES_PER_ABSORB = 1000번 쓰기마다 fsync 요청을 흡수하고,
CHECKPOINT_IMMEDIATE 플래그가 설정된 경우 sleep을 건너뛴다.
**AbsorbSyncRequests**는 CheckpointerCommLock 아래 requests[]를 복사해
빼낸 뒤 num_requests를 초기화한다. 그런 다음 크리티컬 섹션 안에서 각 항목마다
RememberSyncRequest를 호출한다. 큐를 비운 뒤 실패하면 fsync 의무가
사라지므로 크리티컬 섹션이 필요하다.
access/transam/xlog.c — 체크포인트 경로
섹션 제목: “access/transam/xlog.c — 체크포인트 경로”CreateCheckPoint (6927번 줄): 완전한 온라인/셧다운 체크포인트 구현.
핵심 하위 호출: SyncPreCheckpoint, START_CRIT_SECTION,
WALInsertLockAcquireExclusive(셧다운 redo 포인터 계산 용도),
WALInsertLockRelease, 온라인 체크포인트에서는
XLogInsert(XLOG_CHECKPOINT_REDO), CheckPointGuts,
XLogInsert(XLOG_CHECKPOINT_ONLINE / XLOG_CHECKPOINT_SHUTDOWN),
XLogFlush, UpdateControlFile, WakeupWalSummarizer, RemoveOldXlogFiles,
PreallocXlogFiles.
CheckPointGuts (7550번 줄): 서브시스템별 체크포인트 루틴과
CheckPointBuffers / ProcessSyncRequests로 디스패치한다.
RecoveryRestartPoint (7590번 줄): 스타트업 프로세스가 체크포인트 WAL
레코드를 재실행할 때마다 호출한다. info_lck 아래 레코드를
XLogCtl->lastCheckPoint에 저장한다.
CreateRestartPoint (7631번 줄): 복구 중 checkpointer가 호출한다.
lastCheckPoint를 읽고, RedoRecPtr을 고정하고, CheckPointGuts를 호출하고,
ControlFile을 갱신하고, WAL을 재활용한다.
UpdateCheckPointDistanceEstimate (6824번 줄): 인터체크포인트 구간당
WAL 바이트의 느린 감쇠 이동 평균. 증가 시 즉시 반영, 감소 시 구간당 10%씩
낮춘다(공식: 0.9 × old + 0.1 × new).
IsCheckpointOnSchedule (checkpointer.c 841번 줄): elapsed_xlogs와
elapsed_time 분수를 계산해, 둘 다 progress × CheckPointCompletionTarget보다
작을 때만 true(일정 준수)를 반환한다.
include/catalog/pg_control.h
섹션 제목: “include/catalog/pg_control.h”CheckPoint 구조체 (35번 줄): 체크포인트 WAL 레코드의 본문.
ControlFileData.checkPointCopy에도 복사된다.
ControlFileData 구조체 (104번 줄): pg_control 파일 레이아웃.
state(DBState enum), checkPoint(마지막 체크포인트 레코드의 LSN),
checkPointCopy(CheckPoint 본문 복사), minRecoveryPoint(스탠바이가
최소한 여기까지 재실행해야 함).
DBState enum: DB_STARTUP, DB_SHUTDOWNED, DB_SHUTDOWNED_IN_RECOVERY,
DB_SHUTDOWNING, DB_IN_CRASH_RECOVERY, DB_IN_ARCHIVE_RECOVERY,
DB_IN_PRODUCTION.
위치 힌트 (커밋 273fe94, 2026-06-05)
섹션 제목: “위치 힌트 (커밋 273fe94, 2026-06-05)”| 심볼 | 파일 | 대략적 줄 번호 |
|---|---|---|
CheckpointerMain | src/backend/postmaster/checkpointer.c | 182 |
CheckpointerShmemStruct | src/backend/postmaster/checkpointer.c | 107 |
CheckpointerShmemInit | src/backend/postmaster/checkpointer.c | 959 |
RequestCheckpoint | src/backend/postmaster/checkpointer.c | 1003 |
ForwardSyncRequest | src/backend/postmaster/checkpointer.c | 1153 |
AbsorbSyncRequests | src/backend/postmaster/checkpointer.c | 1329 |
CompactCheckpointerRequestQueue | src/backend/postmaster/checkpointer.c | 1219 |
CheckpointWriteDelay | src/backend/postmaster/checkpointer.c | 772 |
IsCheckpointOnSchedule | src/backend/postmaster/checkpointer.c | 841 |
CreateCheckPoint | src/backend/access/transam/xlog.c | 6927 |
CheckPointGuts | src/backend/access/transam/xlog.c | 7550 |
RecoveryRestartPoint | src/backend/access/transam/xlog.c | 7590 |
CreateRestartPoint | src/backend/access/transam/xlog.c | 7631 |
UpdateCheckPointDistanceEstimate | src/backend/access/transam/xlog.c | 6824 |
XLogBytePosToRecPtr | src/backend/access/transam/xlog.c | 1861 |
INSERT_FREESPACE 매크로 | src/backend/access/transam/xlog.c | 581 |
BufferSync | src/backend/storage/buffer/bufmgr.c | 3353 |
CheckPointBuffers | src/backend/storage/buffer/bufmgr.c | 4219 |
CkptTsStatus 구조체 | src/backend/storage/buffer/bufmgr.c | 106 |
SyncOneBuffer | src/backend/storage/buffer/bufmgr.c | 521 |
CheckPoint 구조체 | src/include/catalog/pg_control.h | 35 |
ControlFileData 구조체 | src/include/catalog/pg_control.h | 104 |
DBState enum | src/include/catalog/pg_control.h | 89 |
XLOG_CHECKPOINT_REDO | src/include/catalog/pg_control.h | 82 |
XLOG_CHECKPOINT_ONLINE | src/include/catalog/pg_control.h | 69 |
XLOG_CHECKPOINT_SHUTDOWN | src/include/catalog/pg_control.h | 68 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”커밋 273fe94 (REL_18_STABLE, PostgreSQL 18.x),
/data/hgryoo/references/postgres 기준으로 검증.
확인된 사항:
CheckpointerMain의 메인 이벤트 루프와RecoveryInProgress()에 따른CreateCheckPoint/CreateRestartPoint분기,postmaster/checkpointer.c349–546번 줄에서 확인.CheckpointerShmemStruct레이아웃(start_cv,done_cv,FLEXIBLE_ARRAY_MEMBERrequests 포함), 107–131번 줄에서 확인.RequestCheckpoint여섯 단계 핸드셰이크, 1003–1130번 줄에서 확인.ForwardSyncRequest/CompactCheckpointerRequestQueue/AbsorbSyncRequests, 1153–1371번 줄에서 확인.CreateCheckPoint의 두 레코드 방식(XLOG_CHECKPOINT_REDO→XLOG_CHECKPOINT_ONLINE), xlog.c 7086–7109번 줄과 7250–7256번 줄에서 확인.CheckPointGuts호출 체인, 7550–7577번 줄에서 확인.CreateRestartPoint, 7631–7810번 줄에서 확인.CheckPoint구조체,pg_control.h35–65번 줄에서 확인.ControlFileData, 104–239번 줄에서 확인.DBStateenum, 89–98번 줄에서 확인.XLOG_CHECKPOINT_REDO = 0xE0, 82번 줄에서 확인.UpdateCheckPointDistanceEstimate이동 평균 공식(0.9/0.1 감쇠), 6848–6853번 줄에서 확인.WakeupWalSummarizer()체크포인트 레코드 플러시 이후 호출(PG18 WAL 요약화), 7337번 줄에서 확인.- redo 포인터 고정: 셧다운 경로(
XLogBytePosToRecPtr(CurrBytePos)+INSERT_FREESPACE페이지 헤더 건너뛰기), 7044–7077번 줄에서 확인. 온라인 경로(XLOG_CHECKPOINT_REDO삽입 후checkPoint.redo = RedoRecPtr), 7094–7108번 줄에서 확인. BufferSync두 패스 구조(태그 패스에서BM_CHECKPOINT_NEEDED를CkptBufferIds[]에 설정, 이후 테이블스페이스별 최소 힙 쓰기 패스에서CheckpointWriteDelay(flags, num_processed / num_to_scan)호출),storage/buffer/bufmgr.c3353–3615번 줄에서 확인.CkptTsStatus구조체는 106–128번 줄에서 확인.
미검증 / 범위 외:
ProcessSyncRequests내부(storage/sync/sync.c): smgr/md 문서 범위.pg_stat_checkpointer뷰:PendingCheckpointerStats와pgstat_report_checkpointer가 구동하고, 세부 내용은utils/activity/pgstat_checkpointer.c에 있다(이 문서 범위 밖).
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”비교: Oracle과 SQL Server
섹션 제목: “비교: Oracle과 SQL Server”Oracle은 전담 체크포인트 스레드가 아닌 데이터베이스 라이터(DBWn) 프로세스들이
주도하는 점진적 플러시를 포함한 퍼지 체크포인트를 사용한다. redo 포인터에
해당하는 개념은 컨트롤 파일에 저장되는 **SCN(System Change Number)**이다.
Oracle의 증분 체크포인트는 dirty_since 시간 순서로 정렬된 쓰기 큐를 기준으로
더티 버퍼 쓰기를 지속적으로 수행하는 방식이 특징이다. PostgreSQL의 bgwriter가
유사한 역할을 하지만 checkpointer와 완전히 분리되어 있다.
SQL Server는 버전 2012+에 도입된 **간접 체크포인트(indirect checkpoint)**를
사용한다. 시간 또는 WAL 볼륨 구간 대신 설정 가능한 더티 페이지 버퍼 나이를
목표로 삼아 더 부드러운 I/O를 달성한다. 더 잦은 백그라운드 플러시가 그 비용이다.
PostgreSQL의 checkpoint_completion_target은 체크포인트 수준에서 유사한
평활화를 달성한다.
비교: InnoDB (MySQL)
섹션 제목: “비교: InnoDB (MySQL)”InnoDB의 퍼지 체크포인트 역시 전담 스레드(마스터 스레드 또는 전용 I/O
스레드)가 주도한다. redo 로그는 순환형이라 로그가 꽉 차면 공간을 확보하려고
강제 체크포인트 대상 페이지를 플러시한다. PostgreSQL의 CHECKPOINT_CAUSE_XLOG
트리거, 즉 WAL 사용량이 max_wal_size에 가까워질 때 체크포인트가 요청되는
것과 유사한 메커니즘이다. InnoDB의 I/O 속도 조절 손잡이는 innodb_io_capacity이며,
PostgreSQL에서 이에 대응하는 것은 CheckpointWriteDelay의 암묵적 100 ms sleep이다.
비교: CUBRID
섹션 제목: “비교: CUBRID”CUBRID(knowledge/code-analysis/cubrid/cubrid-checkpoint.md 참조)도 전담
checkpointer 스레드가 주도하는 유사한 퍼지 체크포인트를 사용한다. redo 포인터
개념은 동일하지만 CUBRID는 WAL 레코드 안에 묻는 대신 전용 로그 앵커 파일에
저장한다. PostgreSQL의 XLOG_CHECKPOINT_REDO 두 레코드 방식에 해당하는 것이
CUBRID에는 없다. CUBRID에서는 체크포인트 레코드 자체가 redo 포인터 앵커를
겸한다. PostgreSQL이 redo 레코드와 체크포인트 레코드를 분리한 것은 플러시 중
동시 WAL 삽입을 지원하기 위한 직접적인 결과다.
연구 프론티어
섹션 제목: “연구 프론티어”체크포인트 급증 완전 제거. LeanStore와 Umbra 저장 엔진(Leis et al.)은
더티 페이지를 인플레이스 덮어쓰기 전에 섀도 위치에 먼저 쓰는 지속적 퇴거 루프를
사용해 별도의 체크포인트 단계를 없앤다. PostgreSQL의 checkpoint_completion_target이
이를 근사하지만 여전히 동기적 ProcessSyncRequests 단계가 남아 있다.
그룹 커밋과 체크포인트의 상호작용. 체크포인트 빈도가 높으면 커밋 시 WAL 플러시 비용이 줄어든다. 최근 커밋이 이미 체크포인트의 일부로 플러시되었기 때문이다. PostgreSQL은 이 결합을 명시적으로 활용하지 않는다. InnoDB의 그룹 커밋 문서는 유사한 상호작용을 언급한다.
WAL 요약화와 증분 백업 (PG18). PostgreSQL 18은 CreateCheckPoint 안에
WakeupWalSummarizer()를 포함한다. WAL 요약기(postmaster/walsummarizer.c)는
연속된 체크포인트 사이에 수정된 블록을 추적해 .walsummary 파일을 작성한다.
pg_basebackup --incremental이 이 파일을 소비해 수정되지 않은 블록을 건너뛴다.
체크포인트는 요약화의 자연스러운 에포크 경계다. 두 체크포인트 사이에 수정된
모든 페이지에는 알려진 WAL 레코드 집합이 대응하기 때문이다. 전체 설계는
postgres-archiving-walsummary.md를 참조한다.
src/backend/postmaster/checkpointer.c— checkpointer 프로세스, 요청 큐, 페이싱 로직.src/backend/access/transam/xlog.c—CreateCheckPoint,CreateRestartPoint,CheckPointGuts, redo 포인터 관리, WAL 세그먼트 재활용.src/include/catalog/pg_control.h—CheckPoint,ControlFileData,DBState.knowledge/research/dbms-papers/aries.md— ARIES 이론 (Mohan et al. 1992); redo 포인터와 퍼지 체크포인트 형식화.knowledge/research/dbms-general/database-internals.md— Petrov 5장 “Transaction Processing and Recovery”; 체크포인트 페이싱과 I/O 증폭 프레임.knowledge/research/dbms-general/database-system-concepts.md— Silberschatz 19장 “Recovery System”; WAL, 안정 저장소, 섀도 페이징.knowledge/code-analysis/postgres/postgres-xlog-wal.md— WAL 버퍼 관리,XLogInsert,XLogFlush, LSN 메커니즘.knowledge/code-analysis/postgres/postgres-recovery-redo.md— 복구 모드,PerformWalRecovery, 타임라인 관리.knowledge/code-analysis/postgres/postgres-buffer-manager.md—BufferSync,FlushBuffer, 더티 버퍼 추적.