콘텐츠로 이동

(KO) PostgreSQL 데이터 체크섬 — 페이지 무결성 검증

목차

관계형 엔진의 가장 기본적인 약속은 어제 쓴 행이 오늘도 그대로 읽힌다는 것이다. INSERTSELECT 사이에서 그 행을 담은 8 KB 페이지는 DBMS가 통제하지 못하는 긴 경로를 거친다. OS 페이지 캐시, 파일시스템, 블록 레이어, HBA 또는 NVMe 컨트롤러, 드라이브 펌웨어와 DRAM 캐시, 네트워크 스토리지라면 SAN 패브릭까지다. 이 경로의 어느 고리에서도 비트가 뒤집히거나, 쓰기가 엉뚱한 섹터로 향하거나, 낡은 블록 사본이 반환되거나, 전원 손실 후 페이지가 절반만 기록된 채 남을 수 있다. Database Internals(Petrov 2019, ch. 3 “File Formats” §“Checksumming”; research/dbms-general/database-internals.md 수록)는 이 문제를 간결하게 정리한다. “디스크의 파일은 소프트웨어 버그와 하드웨어 오류로 손상될 수 있다. 이를 사전에 파악하고 손상된 데이터가 다른 서브시스템이나 노드로 전파되는 것을 막으려면 체크섬과 CRC를 활용할 수 있다.”

핵심 형용사는 *무증상(silent)*이다. 명시적 읽기 오류를 반환하는 드라이브는 처리하기 쉽다. DBMS가 EIO를 받아 보고하면 된다. 위험한 경우는 **무증상 데이터 손상(silent data corruption)**이다. 스토리지 스택이 성공을 반환하면서 잘못된 바이트를 건네는 경우다. 종단 간 무결성 검사가 없으면 엔진은 손상된 페이지와 정상 페이지를 구분하지 못한다. 쓰레기가 된 라인 포인터를 충실히 따라가고, 트랜잭션 ID를 잘못 읽고, 실제 오류와 한참 떨어진 지점에서 크래시하거나 — 더 나쁜 경우 — 잘못된 답을 반환하면서 손상을 백업·복제본·덤프까지 전파시킨다. 데이터와 함께 저장된 체크섬은 무증상 손상을 유증상 손상으로 바꾼다. 읽기 시점에 불일치가 발생하면 그것은 감지·귀속 가능한 이벤트가 된다.

Database Internals는 세 가지 무결성 코드 계열의 보증 강도를 구분하는데, 이 선택이 실제 공학적 트레이드오프이기 때문이다.

  • **체크섬(Checksums)**은 “가장 약한 형태의 보증을 제공하며 다중 비트 손상을 감지하지 못할 수 있다. 보통 XOR 패리티 검사나 합산으로 계산한다.”
  • CRC는 “연속된 여러 비트가 손상되는 버스트 오류를 감지하는 데 도움이 된다.” 이런 멀티비트 감지가 중요한 이유는 “통신 네트워크와 저장 장치 오류의 상당 비율이 이 방식으로 발현되기 때문이다.”
  • 암호화 해시의도적 변조에 저항한다. 책의 경고는 명확하다. “비암호화 해시와 CRC는 데이터가 변조됐는지 검증하는 데 사용해서는 안 된다… CRC의 주요 목적은 의도하지 않은 우발적 데이터 변경이 없음을 확인하는 것이다.”

마지막 요점이 위협 모델을 확정한다. 데이터 페이지 체크섬은 변조 감지기가 아닌 사고 감지기다. 페이지를 덮어쓸 수 있는 공격자는 엔진과 똑같이 쉽게 체크섬을 재계산할 수 있다. 설계 목표는 무작위 손상(비트 부식, 잘못된 방향의 쓰기, 상주하는 클린 페이지에서의 RAM 비트 플립)이고, 설계 목표는 암호화 강도가 아니라 CPU 사이클당 감지 확률을 극대화하는 것이다.

책은 또한 PostgreSQL이 선택한 페이지 단위 적용이 왜 자연스러운지를 설명한다. “전체 파일에 대한 체크섬 계산은 종종 비실용적이므로… 페이지 체크섬은 보통 페이지 단위로 계산되어 페이지 헤더에 배치된다. 이 방식에서 체크섬은 더 견고할 수 있고(작은 데이터 부분집합에서 수행되므로), 손상이 단일 페이지에 국한되어 있으면 전체 파일을 버릴 필요가 없다.” 페이지 단위 코드는 버퍼 매니저가 이미 읽고 쓰는 I/O 단위에서 정확히 검증되고, 찾는 비용이 없으며(페이지 헤더에 있으므로), 책임을 단일 8 KB 블록으로 한정한다.

두 가지 설계 변수가 이 틀에서 도출되어 나머지 문서를 구성한다.

  1. 어떤 알고리즘으로, 얼마나 빠르게? 체크섬은 모든 페이지 읽기와 쓰기에서 재계산되므로 엔진에서 가장 뜨거운 I/O 경로 위에 있다. 작업 집합이 OS 캐시에는 맞지만 공유 버퍼에는 맞지 않는 워크로드에서 페이지는 메모리 대역폭 속도로 들어오고 체크섬이 그 자체로 병목이 될 수 있다. 따라서 알고리즘은 단지 올바른 것에 그치지 않고 벡터화 가능해야 한다.

  2. 언제 계산·검증하고 불일치 시 무슨 일이 일어나는가? 체크섬은 집행 시점만큼만 유용하다. 엔진은 바이트가 디스크로 떠나기 직전 — 비WAL 기록 힌트 비트를 포함한 모든 인메모리 변경 이후 — 에 스탬프를 찍어야 하고, 바이트가 도착하는 즉시 검증하며, 검증 실패 시 크게 실패·이벤트 계수·또는 운영자 재정의 아래 계속 진행이라는 명확한 정책을 갖추어야 한다.

페이지 레벨 무결성 검사를 제공하는 엔진들은 반복적인 설계 선택들로 수렴한다. 이것들을 먼저 이름 붙이면 다음 절의 PostgreSQL 심볼들이 공유된 설계 공간 내의 좌표로 읽힌다.

체크섬 필드는 페이지 헤더에 있고 자기 자신을 제외한다

섹션 제목: “체크섬 필드는 페이지 헤더에 있고 자기 자신을 제외한다”

이 방식을 채택한 모든 엔진은 페이지 헤더의 작은 고정 필드에 무결성 코드를 예약하고, 나머지 페이지를 대상으로 코드를 계산한다. 자기 참조 문제 — 체크섬을 담는 필드 자체에 체크섬을 계산할 수 없는 문제 — 는 어디서나 같은 방식으로 해결된다. 계산 중에 필드를 0으로 처리하거나 건너뛰는 것이다. 검증 시에는 필드를 다시 0으로 만들고(또는 0으로 만든 상태에서 예상값을 재계산하여) 비교한다. 필드는 작다(페이지 단위 코드로는 16~32비트가 일반적). 헤더 공간은 귀하고, 16비트 코드도 이미 ~1/65,000 위음성율을 가지며, 여기에 독립적인 헤더 일관성 검사를 조합하면 사고 감지에 충분하다.

쓰기 시 계산, 읽기 시 검증 — 버퍼/스토리지 경계에서

섹션 제목: “쓰기 시 계산, 읽기 시 검증 — 버퍼/스토리지 경계에서”

자연스러운 집행 지점은 버퍼 매니저 I/O의 양 끝이다.

  • 쓰기 경로. 더티 페이지가 스토리지 레이어(write()/pwrite())에 넘겨지기 직전, 엔진은 곧 기록될 이미지에 체크섬을 계산하여 헤더에 스탬프한다. 결정적으로 이것은 WAL 기록되지 않은 것들을 포함한 모든 인메모리 변경 이후에 이루어져야 한다.
  • 읽기 경로. 스토리지 레이어가 페이지 이미지를 반환한 직후, 엔진이 그 안의 어떤 필드도 신뢰하기 전에 체크섬을 재계산하여 비교한다.

이 배치는 체크섬이 정지 상태 표현만 보호함을 의미한다. 체크섬은 WAL이 보호하는 페이지 이미지의 일부가 아니고 플러시마다 새로 계산되므로, 체크섬 스탬프 이전 인메모리 페이지 손상을 감지하지 못한다. 올바른 스탬프와 이후 읽기 사이 — 즉 스토리지 스택 내부 — 에서 발생하는 손상을 잡도록 목적에 맞게 설계된 것이다.

힌트 비트 위험: 안정된 이미지에 체크섬 계산하기

섹션 제목: “힌트 비트 위험: 안정된 이미지에 체크섬 계산하기”

비덮어쓰기 MVCC 엔진은 힌트 비트(캐시된 가시성 결정)를 페이지에 기회적으로 기록한다. 종종 공유 락/래치보다 강하지 않은 상태에서, 종종 WAL 없이다. 이는 체크섬과 미묘한 경쟁을 만든다. 엔진이 다른 백엔드가 힌트 비트를 뒤집는 도중에 공유 페이지의 체크섬을 계산한다면, 스탬프된 체크섬이 실제로 디스크에 도달하는 바이트와 일치하지 않는다. 엔진들은 (a) 페이지의 개인 복사본을 만들어 복사본을 체크섬하거나, (b) 힌트 비트 쓰기 자체를 플러시와 직렬화함으로써 이를 해결한다. (a)가 일반적인 선택인데, 뜨거운 변경 경로를 락-프리로 유지하기 때문이다.

클러스터 전체 온/오프, 제어 구조에 기록

섹션 제목: “클러스터 전체 온/오프, 제어 구조에 기록”

체크섬 활성화는 클러스터 내 모든 페이지의 온디스크 포맷 계약을 변경하므로, 플래그는 테이블이나 세션 단위가 아니라 제어 파일에 한 번 기록되는 클러스터 전체 속성이다. 활성화 이후 변경에는 기존의 모든 페이지를 다시 쓰거나 최소한 재스탬프해야 하는데, 이것이 역사적으로 엔진들이 클러스터 초기화 시에 이를 설정하고 나중에 변경하려면 별도의 오프라인 도구를 제공하는 이유다.

운영자 탈출구가 있는 명확한 실패 정책

섹션 제목: “운영자 탈출구가 있는 명확한 실패 정책”

기본적으로 감지된 불일치는 읽기를 중단시킨다. 엔진은 손상된 것을 알면서 페이지를 반환하기를 거부하고, 명확히 코드화된 오류를 발생시키며 표시 카운터를 올려 모니터링이 경보를 울릴 수 있게 한다. 그러나 손상된 클러스터를 복구하는 운영자는 때로 손상을 통과하여 읽어 구제 가능한 것을 구해야 하므로, 엔진들은 복구 세션 동안 오류를 경고로 낮추거나 페이지를 0으로 만드는 명시적이고 위험한 재정의를 제공한다.

이론 / 관례PostgreSQL 이름
헤더의 페이지 단위 무결성 코드PageHeaderData.pd_checksum (16비트)
페이지 단위, 자기 제외 계산pg_checksum_pagepd_checksum을 일시적으로 0으로 처리
빠른, 벡터화 가능 알고리즘FNV-1a 기반 pg_checksum_block, N_SUMS = 32 병렬 합산
물리적 위치 혼합checksum ^= blkno (전치된 페이지 감지)
0 체크섬 방지(checksum % 65535) + 1 (범위 1..65535)
쓰기 시 계산 (공유 버퍼)PageSetChecksumCopypg_checksum_page, FlushBuffer에서 호출
쓰기 시 계산 (개인 메모리)PageSetChecksumInplace (localbuf, bulk_write, 해시 오버플로우)
힌트 비트 위험 완화PageSetChecksumCopy가 개인 pageCopy를 취함
읽기 시 검증PageIsVerifiedpg_checksum_page, 버퍼 읽기 완료에서 호출
클러스터 전체 활성화 플래그ControlFileData.data_checksum_version (0 = 비활성, PG_DATA_CHECKSUM_VERSION = 1)
런타임 “체크섬 활성?” 조건자DataChecksumsEnabled()
initdb 시 활성화bootstrap.c -kbootstrap_data_checksum_version
오프라인 활성화/비활성화/검증 도구pg_checksums (프런트엔드, pg_checksum_page 호출)
실패 정책 + 탈출구ignore_checksum_failure, zero_damaged_pages GUC; PIV_* 플래그
가시적 실패 카운터pg_stat_database.checksum_failures (pgstat_report_checksum_failures_in_db 경유)
범용 다중 알고리즘 헬퍼 (CRC32C/SHA)checksum_helper.c pg_checksum_* (페이지 체크섬과 별개)

PostgreSQL 데이터 체크섬은 클러스터 전체에 적용되는 선택적 기능이다. initdb -k (--data-checksums)로 클러스터를 초기화하거나 이후 오프라인 pg_checksums 도구로 활성화하면, 버퍼 매니저가 쓰는 모든 페이지는 페이지 헤더의 pd_checksum 필드에 16비트 체크섬을 기록하고, 버퍼 매니저가 읽는 모든 페이지는 체크섬 검증을 거친다. 기능이 꺼져 있으면(PG 17까지 역사적 기본값; PG 18의 initdb-k를 기본적으로 켜지만 여전히 없이 초기화 가능), pd_checksum 필드는 단순히 0으로 남고 참조되지 않는다.

이 기능은 세 개의 작동 부분으로 구성된다. (1) 알고리즘 — 페이지 이미지와 블록 번호를 uint16으로 변환하는 단일 함수 pg_checksum_page(); (2) 집행 지점 — 쓰기 경로의 PageSetChecksumCopy / PageSetChecksumInplace와 읽기 경로의 PageIsVerified, 모두 DataChecksumsEnabled()로 게이트됨; (3) 클러스터 플래그 — 부트스트랩 시 설정된 pg_controldata_checksum_version.

체크섬은 모든 페이지 헤더의 앞부분, 페이지 LSN 바로 뒤의 16비트 슬롯에 위치한다.

// PageHeaderData — src/include/storage/bufpage.h
typedef struct PageHeaderData
{
PageXLogRecPtr pd_lsn; /* LSN: next byte after last byte of xlog
* record for last change to this page */
uint16 pd_checksum; /* checksum */
uint16 pd_flags; /* flag bits, see below */
LocationIndex pd_lower; /* offset to start of free space */
LocationIndex pd_upper; /* offset to end of free space */
LocationIndex pd_special; /* offset to start of special space */
uint16 pd_pagesize_version;
TransactionId pd_prune_xid; /* oldest prunable XID, or zero if none */
ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER]; /* line pointer array */
} PageHeaderData;

pd_checksum이 체크섬 영역 안에 있는 것(바이트 오프셋 8, pd_lower 앞)은 앞서 설명한 자기 참조 문제 그대로다. pg_checksum_page는 필드를 일시적으로 0으로 만들어 이를 해결한다 — 아래에서 자세히 설명한다. “이 클러스터는 체크섬을 사용한다”는 온디스크 포맷 버전은 제어 파일에 한 번 기록되는 별도의 상수다.

// version constants — src/include/storage/bufpage.h
#define PG_PAGE_LAYOUT_VERSION 4
#define PG_DATA_CHECKSUM_VERSION 1

알고리즘: FNV-1a, 32-폭, SIMD 친화적

섹션 제목: “알고리즘: FNV-1a, 32-폭, SIMD 친화적”

실제 코드는 checksum.c(단일 #include 라인)가 아닌 storage/checksum_impl.h에 있다. 외부 프로그램들 — pg_checksums, pg_upgrade, 서드파티 블록 검사 도구 — 이 백엔드에 링크하지 않고도 동일한 알고리즘을 #include할 수 있게 하기 위해서다. checksum.c는 서버에 끌어들이기 위한 목적만으로 존재한다.

// checksum.c — src/backend/storage/page/checksum.c
#include "storage/checksum.h"
/*
* The actual code is in storage/checksum_impl.h. This is done so that
* external programs can incorporate the checksum code by #include'ing
* that file from the exported Postgres headers. (Compare our CRC code.)
*/
#include "storage/checksum_impl.h" /* IWYU pragma: keep */

알고리즘 헤더 주석은 성능 동기를 직접 명시한다. “페이지 체크섬에 사용되는 알고리즘은 매우 빠른 계산을 위해 선택됐다. 데이터베이스 작업 집합이 OS 파일 캐시에는 맞지만 공유 버퍼에는 맞지 않는 워크로드는 매우 빠른 속도로 페이지를 읽어들일 수 있고 체크섬 알고리즘 자체가 가장 큰 병목이 될 수 있다.” 알고리즘은 hash = (hash ^ value) * FNV_PRIME으로 데이터를 혼합하는 기본 연산을 가진 FNV-1a(Fowler/Noll/Vo) 해시를 기반으로 하되 두 가지 의도적인 변형을 가한다.

첫째, 순수 FNV-1a는 “하이 비트 혼합이 나쁘다 — 입력 데이터의 상위 비트가 출력 데이터의 상위 비트에만 영향을 미친다.” PostgreSQL은 값을 17비트 오른쪽으로 시프트하여 다시 XOR하고 4바이트씩 처리함으로써 아발란체(avalanche)를 고친다.

// CHECKSUM_COMP, FNV_PRIME — src/include/storage/checksum_impl.h
/* prime multiplier of FNV-1a hash */
#define FNV_PRIME 16777619
/*
* Calculate one round of the checksum.
*/
#define CHECKSUM_COMP(checksum, value) \
do { \
uint32 __tmp = (checksum) ^ (value); \
(checksum) = __tmp * FNV_PRIME ^ (__tmp >> 17); \
} while (0)

둘째 — 이것이 SIMD 트릭이다 — 페이지는 단일 직렬 스트림으로 해시되지 않는다. 8 KB 페이지는 BLCKSZ / (4 * 32) 행 × 32 열의 uint32 배열로 재해석되고, 32개의 독립 부분 합산이 열당 하나씩 단계적으로 진행된다. 32번의 곱셈이 서로 데이터 의존성이 없으므로 벡터화 컴파일러가 SIMD 레인(SSE4.1 pmulld, ARM NEON vmul.i32)에 매핑하여 곱셈 지연을 숨긴다. union은 strict aliasing을 위반하지 않고 페이지를 재해석하며, 각 부분 합산은 서로 다른 무작위 “오프셋 기준”에서 시작하므로 동일한 열이 동일한 부분으로 붕괴되지 않는다.

// PGChecksummablePage, N_SUMS, checksumBaseOffsets — src/include/storage/checksum_impl.h
/* number of checksums to calculate in parallel */
#define N_SUMS 32
/* Use a union so that this code is valid under strict aliasing */
typedef union
{
PageHeaderData phdr;
uint32 data[BLCKSZ / (sizeof(uint32) * N_SUMS)][N_SUMS];
} PGChecksummablePage;
/*
* Base offsets to initialize each of the parallel FNV hashes into a
* different initial state.
*/
static const uint32 checksumBaseOffsets[N_SUMS] = {
0x5B1F36E9, 0xB8525960, 0x02AB50AA, 0x1DE66D2A,
/* ... 28 more randomly-chosen 32-bit constants ... */
0x9FBF8C76, 0x15CA20BE, 0xF2CA9FD3, 0x959BD756
};

pg_checksum_block이 실제 작업을 한다. 오프셋 테이블에서 32개의 부분을 시드하고, 모든 행을 순회하며 메인 패스를 실행하고, “마지막으로 추가된 값의 비트를 혼합하기 위해” 0의 두 번의 추가 라운드를 추가한 다음, 32개의 부분을 하나의 uint32로 XOR 폴드한다.

// pg_checksum_block — src/include/storage/checksum_impl.h
static uint32
pg_checksum_block(const PGChecksummablePage *page)
{
uint32 sums[N_SUMS];
uint32 result = 0;
uint32 i, j;
/* ensure that the size is compatible with the algorithm */
Assert(sizeof(PGChecksummablePage) == BLCKSZ);
/* initialize partial checksums to their corresponding offsets */
memcpy(sums, checksumBaseOffsets, sizeof(checksumBaseOffsets));
/* main checksum calculation */
for (i = 0; i < (uint32) (BLCKSZ / (sizeof(uint32) * N_SUMS)); i++)
for (j = 0; j < N_SUMS; j++)
CHECKSUM_COMP(sums[j], page->data[i][j]);
/* finally add in two rounds of zeroes for additional mixing */
for (i = 0; i < 2; i++)
for (j = 0; j < N_SUMS; j++)
CHECKSUM_COMP(sums[j], 0);
/* xor fold partial checksums together */
for (i = 0; i < N_SUMS; i++)
result ^= sums[i];
return result;
}

래퍼 pg_checksum_page는 페이지 체크섬을 범용 블록 해시와 구분하는 세 가지 나머지 단계를 추가한다. pd_checksum을 일시적으로 0으로 만들어 필드가 자기 자신을 제외하게 하고, 블록 번호를 혼합하여(엉뚱한 위치에 기록된 페이지를 바이트가 개별적으로 손상되지 않았어도 감지), 0이 아닌 uint16으로 축소한다.

// pg_checksum_page — src/include/storage/checksum_impl.h
uint16
pg_checksum_page(char *page, BlockNumber blkno)
{
PGChecksummablePage *cpage = (PGChecksummablePage *) page;
uint16 save_checksum;
uint32 checksum;
/* We only calculate the checksum for properly-initialized pages */
Assert(!PageIsNew((Page) page));
/*
* Save pd_checksum and temporarily set it to zero, so that the checksum
* calculation isn't affected by the old checksum stored on the page.
* Restore it after, because actually updating the checksum is NOT part of
* the API of this function.
*/
save_checksum = cpage->phdr.pd_checksum;
cpage->phdr.pd_checksum = 0;
checksum = pg_checksum_block(cpage);
cpage->phdr.pd_checksum = save_checksum;
/* Mix in the block number to detect transposed pages */
checksum ^= blkno;
/*
* Reduce to a uint16 (to fit in the pd_checksum field) with an offset of
* one. That avoids checksums of zero, which seems like a good idea.
*/
return (uint16) ((checksum % 65535) + 1);
}

주목할 세 가지 미묘한 점이 있다. (1) 함수는 기존 pd_checksum복원한다. 값을 반환하지만 저장하지 않는다 — 스탬프는 호출자(PageSetChecksum*)의 역할이다. (2) N_SUMS = 32는 “병렬성을 변경하면 체크섬 결과가 달라지기 때문에” 알고리즘의 고정 부분이다 — 온디스크 포맷 불변이며 조정 가능한 값이 아니다. (3) 축소 (checksum % 65535) + 1은 모든 결과를 1..65535로 매핑하여 “0”을 “체크섬 없음”으로 예약하고, “더 낮은 값으로 매우 약한 편향”을 도입하는데 이는 무시할 수 있는 수준으로 판단됐다.

flowchart TD
  P["8 KB 페이지 이미지<br/>(pd_checksum 포함)"] --> Z["pd_checksum 저장,<br/>필드 = 0 설정"]
  Z --> R["32열 uint32 배열로<br/>재해석"]
  R --> S["checksumBaseOffsets에서<br/>32개 부분 합산 시드"]
  S --> M["메인 패스:<br/>열당 CHECKSUM_COMP,<br/>32개 레인 병렬"]
  M --> ZR["추가 0 라운드 2회<br/>(아발란체 완료)"]
  ZR --> F["32개 부분을<br/>uint32 하나로 XOR 폴드"]
  F --> B["checksum ^= blkno<br/>(전치 페이지 감지)"]
  B --> RED["(checksum % 65535) + 1<br/>-> 1..65535 범위 uint16"]
  Z -.->|복원| P
  RED --> OUT["uint16 반환"]

그림 1 — pg_checksum_page 데이터 흐름. 헤더 필드를 일시적으로 0으로 만들어 자기 자신을 제외하고, 페이지를 32개의 병렬 FNV 스트림으로 해시하여 SIMD 처리량을 확보하고, 블록 번호를 혼합하여 잘못 위치된 페이지를 감지하고, 32비트 결과를 1..65535 범위 uint16으로 축소한다. 반환된 값을 pd_checksum에 스탬프하는 것은 호출자의 역할이다.

계산 위치 (쓰기)와 검증 위치 (읽기)

섹션 제목: “계산 위치 (쓰기)와 검증 위치 (읽기)”

알고리즘은 위치 무관이다. 언제 적용할지의 정책은 버퍼 매니저와 페이지 지원 레이어에 있으며, 모든 호출은 DataChecksumsEnabled()로 게이트된다. 패턴은 이렇다. smgrwrite 직전 PageSetChecksum{Copy,Inplace}에서 스탬프, 읽기 완료 직후 PageIsVerified에서 검증.

코드는 네 가지 호출 흐름으로 명확히 분리된다. 알고리즘 (checksum.cchecksum_impl.h), 쓰기 경로 (버퍼 플러시 사이트에서 PageSetChecksum*), 읽기 경로 (버퍼 읽기 완료에서 PageIsVerified와 실패 보고 기계), 그리고 활성화/제어 경로 (bootstrap.c, xlog.c, pg_control.h)다. 종종 혼동되는 별도의 형제 — 범용 checksum_helper.c — 가 절을 마무리한다.

단일 exported 진입점은 storage/checksum.h에 선언되어 있다. 의도적으로 작게 유지되어 외부 프로그램이 프로토타입만 가져와 구현 헤더를 포함할 수 있게 한다.

// checksum.h — src/include/storage/checksum.h
#include "storage/block.h"
/*
* Compute the checksum for a Postgres page. The page must be aligned on a
* 4-byte boundary.
*/
extern uint16 pg_checksum_page(char *page, BlockNumber blkno);

구현(checksum_impl.h)은 매크로 CHECKSUM_COMP, N_SUMS = 32 / FNV_PRIME 상수, checksumBaseOffsets[32] 시드 테이블, PGChecksummablePage aliasing union, 내부 pg_checksum_block, 래퍼 pg_checksum_page로 분리된다 — 모두 앞 절에서 인용했다. 4바이트 정렬 요건은 실제다. union이 char * 페이지를 uint32 배열로 캐스트하므로, 호출자는 적절히 정렬된 버퍼를 전달해야 한다(공유 버퍼와 MemoryContextAllocAligned 복사본 모두 이를 만족한다).

쓰기 경로: PageSetChecksumCopyPageSetChecksumInplace

섹션 제목: “쓰기 경로: PageSetChecksumCopy와 PageSetChecksumInplace”

두 스탬퍼 모두 bufpage.c에 있으며, 페이지가 새(초기화되지 않은, 전체 0) 상태이거나 체크섬이 꺼져 있으면 단락된다 — 체크섬 비활성 클러스터는 비용이 0이다. 차이는 다른 누가 동시에 페이지를 건드릴 수 있느냐다.

PageSetChecksumCopy는 공유 콘텐츠 락 아래 플러시되는 공유 버퍼에 사용된다 — 지금 다른 백엔드가 힌트 비트를 설정 중일 수 있다. 따라서 개인 복사본을 체크섬하여 동시 힌트 비트 쓰기가 스탬프된 값을 무효화하지 못하게 한다.

// PageSetChecksumCopy — src/backend/storage/page/bufpage.c
char *
PageSetChecksumCopy(Page page, BlockNumber blkno)
{
static char *pageCopy = NULL;
/* If we don't need a checksum, just return the passed-in data */
if (PageIsNew(page) || !DataChecksumsEnabled())
return page;
/* ... palloc the aligned copy buffer once, reuse thereafter ... */
if (pageCopy == NULL)
pageCopy = MemoryContextAllocAligned(TopMemoryContext,
BLCKSZ, PG_IO_ALIGN_SIZE, 0);
memcpy(pageCopy, page, BLCKSZ);
((PageHeader) pageCopy)->pd_checksum = pg_checksum_page(pageCopy, blkno);
return pageCopy;
}

정적 pageCopy는 백엔드당 한 번 할당되어(정렬됨, 체크섬의 uint32 캐스트를 위해) 재사용된다. 함수는 호출자가 즉시 써야 하는 버퍼를 반환한다. 유일한 호출자는 FlushBuffer이며 smgrwrite 직전에 있고, 그 곳의 주석이 복사가 회피하는 위험을 명시한다.

// FlushBuffer — src/backend/storage/buffer/bufmgr.c
/*
* Update page checksum if desired. Since we have only shared lock on the
* buffer, other processes might be updating hint bits in it, so we must
* copy the page to private storage if we do checksumming.
*/
bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);
/* ... */
smgrwrite(reln, BufTagGetForkNum(&buf->tag), buf->tag.blockNum,
bufToWrite, false);

PageSetChecksumInplace는 호출자가 다른 어느 누구도 버퍼를 변경할 수 없음을 알 때 사용하는 더 저렴한 변형이다. 복사 없이 pd_checksum을 직접 스탬프한다.

// PageSetChecksumInplace — src/backend/storage/page/bufpage.c
void
PageSetChecksumInplace(Page page, BlockNumber blkno)
{
/* If we don't need a checksum, just return */
if (PageIsNew(page) || !DataChecksumsEnabled())
return;
((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
}

이 함수의 호출자는 페이지가 쓰기 담당자에게 개인 소유인 세 가지 컨텍스트다. localbuf.cFlushLocalBuffer(임시 테이블 버퍼는 공유되지 않음), bulk_write.csmgr_bulk_flush(인덱스 빌드와 COPY 스타일 릴레이션 확장에 사용되는 대량 쓰기 설비는 페이지를 완전히 소유), hashpage.c_hash_alloc_buckets(마지막 오버플로우 페이지를 사전 포맷하여 직접 쓴다).

// FlushLocalBuffer — src/backend/storage/buffer/localbuf.c
PageSetChecksumInplace(localpage, bufHdr->tag.blockNum);
/* ... */
smgrwrite(reln, BufTagGetForkNum(&bufHdr->tag), bufHdr->tag.blockNum,
localpage, false);
// smgr_bulk_flush -> PageSetChecksumInplace — src/backend/storage/smgr/bulk_write.c
for (int i = 0; i < npending; i++)
{
BlockNumber blkno = pending_writes[i].blkno;
Page page = pending_writes[i].buf->data;
PageSetChecksumInplace(page, blkno);
/* ... then smgrwrite / smgrextend the run ... */
}
flowchart TD
  subgraph WRITE["쓰기 경로 (pd_checksum 스탬프)"]
    FB["FlushBuffer<br/>(공유 버퍼,<br/>공유 콘텐츠 락)"] --> PSCC["PageSetChecksumCopy<br/>(개인 복사본,<br/>힌트 비트 경쟁 회피)"]
    LB["FlushLocalBuffer<br/>(임시 버퍼)"] --> PSCI["PageSetChecksumInplace"]
    BW["smgr_bulk_flush<br/>(대량 쓰기)"] --> PSCI
    HP["_hash_alloc_buckets"] --> PSCI
    PSCC --> PCP["pg_checksum_page"]
    PSCI --> PCP
    PCP --> SW["smgrwrite -> 디스크"]
  end
  GATE{"DataChecksumsEnabled()<br/>and not PageIsNew?"}
  PSCC -.->|"no -> 페이지 그대로 반환"| GATE
  PSCI -.->|"no -> return"| GATE

그림 2 — 쓰기 경로 스탬핑. 공유 버퍼는 PageSetChecksumCopy를 거친다(공유 락 아래 동시 힌트 비트 쓰기를 막기 위한 개인 복사본). 개인 페이지(임시 버퍼, 대량 쓰기, 해시 오버플로우 할당)는 저렴한 인플레이스 스탬프를 사용한다. 모든 사이트는 DataChecksumsEnabled()로 게이트되고 새/영(zero) 페이지를 건너뛴다.

읽기 경로: PageIsVerified와 실패 보고

섹션 제목: “읽기 경로: PageIsVerified와 실패 보고”

검증은 PageIsVerified에서 발생하며, 페이지 이미지가 smgr에서 도착하는 순간 버퍼 읽기 완료 콜백에서 호출된다. 이 함수는 체크섬 비교를 먼저 수행하고(비새 페이지에서만, 활성화 시만), 독립적인 헤더 일관성 검사 집합을 수행한 다음, 전체 0 페이지를 허용 가능으로 처리한다(크래시가 디스크에 0으로 된 언로그드 확장 페이지를 남길 수 있다).

// PageIsVerified — src/backend/storage/page/bufpage.c
bool
PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_failure_p)
{
const PageHeaderData *p = (const PageHeaderData *) page;
bool checksum_failure = false;
bool header_sane = false;
uint16 checksum = 0;
if (checksum_failure_p)
*checksum_failure_p = false;
if (!PageIsNew(page))
{
if (DataChecksumsEnabled())
{
checksum = pg_checksum_page(page, blkno);
if (checksum != p->pd_checksum)
{
checksum_failure = true;
if (checksum_failure_p)
*checksum_failure_p = true;
}
}
/* independent header sanity (offsets nested correctly, MAXALIGNed) */
if ((p->pd_flags & ~PD_VALID_FLAG_BITS) == 0 &&
p->pd_lower <= p->pd_upper &&
p->pd_upper <= p->pd_special &&
p->pd_special <= BLCKSZ &&
p->pd_special == MAXALIGN(p->pd_special))
header_sane = true;
if (header_sane && !checksum_failure)
return true;
}
/* ... all-zero page is OK; else fall through to the failure report ... */

두 레이어는 의도적이다. 체크섬은 높은 확률로 무작위 손상을 감지하고, 헤더 검사는 16비트 체크섬이 놓칠 수 있는 구조적 손상(불가능한 오프셋)을 감지한다. 신뢰받으려면 페이지는 두 가지 모두를 통과해야 한다. 실패 후처리는 진단을 로그하고 페이지가 운영자 재정의 아래 구제 가능한지 결정한다.

// PageIsVerified (failure tail) — src/backend/storage/page/bufpage.c
if (checksum_failure)
{
if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("page verification failed, calculated checksum %u but expected %u",
checksum, p->pd_checksum)));
if (header_sane && (flags & PIV_IGNORE_CHECKSUM_FAILURE))
return true;
}
return false;
}

PIV_* 플래그(bufpage.h에 있음)는 정책을 매개변수화한다. PIV_LOG_WARNING / PIV_LOG_LOG는 로그 레벨을 선택하고, PIV_IGNORE_CHECKSUM_FAILURE는 잘못된 체크섬에도 헤더가 정상인 페이지를 통과시킨다.

// PIV flags — src/include/storage/bufpage.h
#define PIV_LOG_WARNING (1 << 0)
#define PIV_LOG_LOG (1 << 1)
#define PIV_IGNORE_CHECKSUM_FAILURE (1 << 2)

버퍼 읽기 완료(bufmgr.cbuffer_readv_complete_one)는 GUC를 해당 플래그에 연결한다. 완료는 I/O 워커에서 실행될 수 있으므로 항상 PageIsVerified로그(오류가 아닌)를 요청하며, 사용자 가시적 WARNING/ERRORbuffer_readv_report로 지연된다. 세션의 ignore_checksum_failurePIV_IGNORE_CHECKSUM_FAILURE를 추가하고, zero_damaged_pages(READ_BUFFERS_ZERO_ON_ERROR로 일찍 변환됨)는 검증 불가 페이지를 0으로 대체한다.

// buffer read completion -> PageIsVerified — src/backend/storage/buffer/bufmgr.c
piv_flags = PIV_LOG_LOG;
/* the local zero_damaged_pages may differ from the definer's */
if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
if (!PageIsVerified((Page) bufdata, tag.blockNum, piv_flags, failed_checksum))
{
if (flags & READ_BUFFERS_ZERO_ON_ERROR)
{
memset(bufdata, 0, BLCKSZ);
*zeroed_buffer = true;
}
else
{
*buffer_invalid = true;
failed = true;
}
}
else if (*failed_checksum)
*ignored_checksum = true;

두 세션 GUC는 WaitReadBuffers에서 읽어져 읽기 플래그에 포함된다. 완료 콜백이 다른 프로세스에서 실행될 수 있으므로 일관된 결정을 보게 된다.

// WaitReadBuffers -> read flags — src/backend/storage/buffer/bufmgr.c
if (zero_damaged_pages)
flags |= READ_BUFFERS_ZERO_ON_ERROR;
/* For the same reason ... we need to use this backend's value. */
if (ignore_checksum_failure)
flags |= READ_BUFFERS_IGNORE_CHECKSUM_FAILURES;

감지된 모든 불일치(무시된 것도 포함)는 누적 통계에 계산되어 pg_stat_database.checksum_failures...checksum_last_failure로 노출된다. 증가는 크리티컬 섹션 내부에서 실행될 수 있도록 공유 메모리에 직접 기록된다.

// pgstat_report_checksum_failures_in_db — src/backend/utils/activity/pgstat_database.c
void
pgstat_report_checksum_failures_in_db(Oid dboid, int failurecount)
{
/* ... fetch the shared DB entry (create=false, no allocation) ... */
sharedent = (PgStatShared_Database *) entry_ref->shared_stats;
sharedent->stats.checksum_failures += failurecount;
sharedent->stats.last_checksum_failure = GetCurrentTimestamp();
pgstat_unlock_entry(entry_ref);
}

SQL 접근자는 체크섬이 꺼져 있으면 NULL을 반환하여 모니터링이 “실패 없음”과 “기능 비활성”을 구별할 수 있게 한다.

// pg_stat_get_db_checksum_failures — src/backend/utils/adt/pgstatfuncs.c
if (!DataChecksumsEnabled())
PG_RETURN_NULL();
/* ... else return dbentry->checksum_failures ... */
flowchart TD
  RD["smgr 읽기 완료<br/>(buffer_readv_complete_one)"] --> PIV["PageIsVerified"]
  PIV --> CK{"DataChecksumsEnabled<br/>and pd_checksum<br/>불일치?"}
  CK -->|no| HDR{"헤더 정상?"}
  CK -->|yes| LOG["ereport LOG/WARNING<br/>ERRCODE_DATA_CORRUPTED<br/>page verification failed"]
  LOG --> CNT["pg_stat_database<br/>.checksum_failures 증가"]
  CNT --> IGN{"PIV_IGNORE_CHECKSUM<br/>and 헤더 정상?"}
  IGN -->|yes| OKI["페이지 수락<br/>(무시됨)"]
  IGN -->|no| ZERO{"ZERO_ON_ERROR<br/>zero_damaged_pages?"}
  ZERO -->|yes| ZP["페이지 memset 0<br/>수락"]
  ZERO -->|no| BAD["버퍼 무효<br/>-> 클라이언트에 ERROR"]
  HDR -->|yes| OK["페이지 수락"]
  HDR -->|no| BAD

그림 3 — 읽기 경로 검증과 실패 정책. 체크섬 불일치는 무조건 로그되고 계산된다. 치명적인지 여부는 ignore_checksum_failurezero_damaged_pages GUC에 따라 결정되며 PIV_* / READ_BUFFERS_* 플래그로 변환된다. 독립적인 헤더 일관성 검사는 활성화된 체크섬도 대체할 수 없는 두 번째 구조적 레이어다.

활성화/제어 경로: pg_controldata_checksum_version

섹션 제목: “활성화/제어 경로: pg_control의 data_checksum_version”

전체 기계가 활성 상태인지는 제어 파일에 기록된 단일 클러스터 속성이다.

// ControlFileData (excerpt) — src/include/catalog/pg_control.h
uint32 data_checksum_version;

정확히 한 번, 부트스트랩 시에 설정된다. initdb -k-k를 부트스트랩 백엔드에 전달하여 버전을 0에서 PG_DATA_CHECKSUM_VERSION으로 올린다.

// BootstrapModeMain option handling — src/backend/bootstrap/bootstrap.c
case 'k':
bootstrap_data_checksum_version = PG_DATA_CHECKSUM_VERSION;
break;
/* ... later ... */
BootStrapXLOG(bootstrap_data_checksum_version);

BootStrapXLOG는 값을 InitControlFile에 전달하여 이후 모든 백엔드가 시작 시 읽는 제어 파일에 저장한다.

// InitControlFile — src/backend/access/transam/xlog.c
ControlFile->data_checksum_version = data_checksum_version;

런타임에서 DataChecksumsEnabled() 조건자는 모든 스탬프와 검증 사이트가 참조하는 단일 진실 공급원이다. 캐시된 제어 파일의 단순 읽기다.

// DataChecksumsEnabled — src/backend/access/transam/xlog.c
bool
DataChecksumsEnabled(void)
{
Assert(ControlFile != NULL);
return (ControlFile->data_checksum_version > 0);
}

이 플래그에서 두 가지 관련 사실이 도출된다. 첫째, xlog.c는 읽기 전용 data_checksums GUC를 게시하여 클라이언트가 클러스터 상태를 조회할 수 있게 한다. 둘째, 같은 플래그가 힌트 비트 쓰기를 WAL 로그하도록 강제한다. XLogHintBitIsNeeded()(DataChecksumsEnabled() || wal_log_hints)다. 이것은 torn-page 안전성에 필요하다 — 없으면 힌트 비트만의 변경이 페이지 바이트(따라서 체크섬)를 WAL 레코드 없이 변경하여, torn write 이후 크래시 복구에서 거짓 체크섬 실패를 낼 수 있다. 오프라인 pg_checksums 도구는 정지된 클러스터에서 모든 페이지를 재스탬프(또는 지우기)하며 data_checksum_version을 0과 1 사이에서 변경한다 — initdb 이후 설정을 변경하는 지원되는 방법이다.

혼동해서는 안 되는 형제: checksum_helper.c

섹션 제목: “혼동해서는 안 되는 형제: checksum_helper.c”

src/common/checksum_helper.c는 “checksum”이라는 단어 아래 존재하는 다른 설비로, grep 기반 독자를 자주 혼란스럽게 한다. 이것은 백업 매니페스트와 pg_verifybackup 같은 기능에서 사용하는 범용, 알고리즘 플러그인 가능 다이제스트다 — 페이지 체크섬이 아니다. pg_checksum_type enum은 CRC32C와 SHA-2 계열에 걸쳐 있고, init/update/final 스트리밍 API를 제공한다.

// pg_checksum_init (excerpt) — src/common/checksum_helper.c
int
pg_checksum_init(pg_checksum_context *context, pg_checksum_type type)
{
context->type = type;
switch (type)
{
case CHECKSUM_TYPE_NONE:
break;
case CHECKSUM_TYPE_CRC32C:
INIT_CRC32C(context->raw_context.c_crc32c);
break;
case CHECKSUM_TYPE_SHA224:
context->raw_context.c_sha2 = pg_cryptohash_create(PG_SHA224);
/* ... */
}
return 0;
}

명명 중복은 순전히 어휘적이다. pg_checksum_page(페이지 무결성, FNV, 고정 16비트)와 pg_checksum_init/_update/_final(매니페스트 다이제스트, 다중 알고리즘, 가변 길이)은 코드를 공유하지 않는다. basebackup.cverify_page_checksum 경로는 실제 페이지 체크섬(pg_checksum_page)을 사용한다 — 베이스 백업 스트리밍 중 페이지를 재검증하지만, 백업 시작 LSN 이후 수정된 페이지는 건너뛴다. 해당 페이지는 torn일 수 있고 “WAL 재생이 올바른 페이지를 복원”하기 때문이다.

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

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심볼파일라인
pg_checksum_pagesrc/include/storage/checksum_impl.h187
pg_checksum_blocksrc/include/storage/checksum_impl.h145
CHECKSUM_COMPsrc/include/storage/checksum_impl.h135
N_SUMS, FNV_PRIMEsrc/include/storage/checksum_impl.h106, 108
checksumBaseOffsetssrc/include/storage/checksum_impl.h121
PGChecksummablePagesrc/include/storage/checksum_impl.h111
pg_checksum_page (선언)src/include/storage/checksum.h22
#include checksum_impl.hsrc/backend/storage/page/checksum.c22
PageHeaderData.pd_checksumsrc/include/storage/bufpage.h164
PG_DATA_CHECKSUM_VERSIONsrc/include/storage/bufpage.h208
PIV_LOG_WARNING / PIV_LOG_LOG / PIV_IGNORE_CHECKSUM_FAILUREsrc/include/storage/bufpage.h469–471
PageIsVerifiedsrc/backend/storage/page/bufpage.c94
PageSetChecksumCopysrc/backend/storage/page/bufpage.c1509
PageSetChecksumInplacesrc/backend/storage/page/bufpage.c1541
ignore_checksum_failure (GUC var)src/backend/storage/page/bufpage.c27
FlushBufferPageSetChecksumCopysrc/backend/storage/buffer/bufmgr.c4372
읽기 완료 → PageIsVerifiedsrc/backend/storage/buffer/bufmgr.c7100
WaitReadBuffers GUC→플래그src/backend/storage/buffer/bufmgr.c1821, 1828
buffer_readv_reportsrc/backend/storage/buffer/bufmgr.c7286
FlushLocalBufferPageSetChecksumInplacesrc/backend/storage/buffer/localbuf.c201
smgr_bulk_flushPageSetChecksumInplacesrc/backend/storage/smgr/bulk_write.c282
_hash_alloc_bucketsPageSetChecksumInplacesrc/backend/access/hash/hashpage.c1032
DataChecksumsEnabledsrc/backend/access/transam/xlog.c4611
InitControlFile (버전 설정)src/backend/access/transam/xlog.c4200, 4231
bootstrap_data_checksum_version (-k)src/backend/bootstrap/bootstrap.c204, 287
ControlFileData.data_checksum_versionsrc/include/catalog/pg_control.h222
XLogHintBitIsNeededsrc/include/access/xlog.h120
verify_page_checksumsrc/backend/backup/basebackup.c1993
pgstat_report_checksum_failures_in_dbsrc/backend/utils/activity/pgstat_database.c166
pg_stat_get_db_checksum_failuressrc/backend/utils/adt/pgstatfuncs.c1154
pg_checksum_init (헬퍼, 별개)src/common/checksum_helper.c83

모든 발췌문은 커밋 273fe94의 REL_18_STABLE 작업 트리에서 읽었다. 검증 노트:

  • pg_checksum_pagechecksum.c가 아닌 헤더에 있다. checksum.c는 두 개의 #include 파일이다. 본체는 src/include/storage/checksum_impl.h에 위치하므로 프런트엔드 도구들이 동일한 알고리즘을 컴파일할 수 있다. checksum.c에서 FNV 루프를 grep하면 아무것도 나오지 않는다 — 두 파일을 직접 읽어 확인했다.
  • **N_SUMS = 32, FNV_PRIME = 16777619**는 checksum_impl.h에서 그대로 검증됐다. “병렬성을 변경하면 체크섬 결과가 달라진다”는 주석이 상수가 온디스크 포맷 불변임을 확인한다.
  • 축소는 (checksum % 65535) + 1이다 — 모듈러스 65535(65536이 아님)에 +1 오프셋으로 1..65535를 산출하며 0을 예약한다. pg_checksum_page에서 직접 읽었다.
  • pd_checksumPageHeaderData의 세 번째 필드의 uint16이다(오프셋 8, 8바이트 pd_lsn 뒤), bufpage.h에서 확인했다. 필드는 체크섬 영역 안에 있다. pg_checksum_page는 일시적으로 0으로 만든다.
  • 쓰기 사이트 확인: FlushBuffer(공유, PageSetChecksumCopy 경유), FlushLocalBuffer, smgr_bulk_flush, _hash_alloc_buckets(모두 PageSetChecksumInplace 경유). 공유 대 개인 구분과 힌트 비트 근거는 트리 내 주석에서 인용했다.
  • 읽기 사이트 확인: PageIsVerified가 백엔드 읽기 경로의 유일한 체크섬 검증 호출이다. bufmgr.c의 AIO 버퍼 읽기 완료에서 도달한다. 사용자 가시적 메시지는 "page verification failed, calculated checksum %u but expected %u" with ERRCODE_DATA_CORRUPTED다.
  • 두 개의 탈출구 GUC ignore_checksum_failurezero_damaged_pages는 모두 세션 설정으로 WaitReadBuffers에서 READ_BUFFERS_* 플래그로 변환된다. 완료 콜백이 정의자와 다른 프로세스에서 실행될 수 있기 때문이다. 1821/1828번 라인에서 확인했다.
  • 클러스터 활성화pg_controldata_checksum_version으로, 부트스트랩 시 -k로 한 번 설정된다. DataChecksumsEnabled()version > 0이다. xlog.c, bootstrap.c, pg_control.h에서 확인했다.
  • PG19 전용 내용은 어설트하지 않는다. 온라인 체크섬 활성화 워커/백그라운드 프로세스(PG18 이후)는 의도적으로 다루지 않는다. 여기서 설명하는 유일한 클러스터 내 방법은 생성 시 initdb -k 또는 정지된 클러스터에서 오프라인 pg_checksums 도구 — 둘 다 REL_18 사실이다.
  • checksum_helper.c는 별개의 설비다(백업 매니페스트용 CRC32C / SHA-2 다이제스트). enum과 init/update/final API를 직접 읽어 확인했다. pg_checksum_page를 호출하지 않는다.

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

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프런티어”

PostgreSQL 페이지 체크섬은 의도적으로 좁은 단일 도구다. 8 KB 블록에 대한 빠른 비암호화 오류 감지 코드로, 모든 플러시에서 재계산되고 모든 읽기에서 검증된다. 더 넓은 설계 공간에 배치하면 무엇을 얻고 무엇을 남기는지가 명확해진다.

  • 속도 우선 감지 대 암호화 무결성. 페이지 체크섬이 FNV-1a 기반인 이유는 헤더 주석이 경고하듯 “체크섬 알고리즘 자체가 가장 큰 병목이 될 수 있기” 때문이다. 선택은 오류 감지이지 변조 저항이 아니다. 명시된 2e-16 위음성율을 가진 uint16은 스토리지 비트 부식을 잡지만 쉽게 위조 가능하다. PostgreSQL은 암호화 계열을 완전히 분리 유지한다 — checksum_helper.c백업 매니페스트용으로 CRC32C와 SHA-2 제품군을 노출하며, 여기서는 적대자-또는-버그 모델과 페이지당이 아닌 파일당 세분성이 224512비트 다이제스트를 감당할 수 있게 한다.

    // pg_checksum_init — src/common/checksum_helper.c
    switch (type)
    {
    case CHECKSUM_TYPE_NONE:
    break;
    case CHECKSUM_TYPE_CRC32C:
    INIT_CRC32C(context->raw_context.c_crc32c);
    break;
    case CHECKSUM_TYPE_SHA256:
    context->raw_context.c_sha2 = pg_cryptohash_create(PG_SHA256);
    /* ... SHA224/384/512 are parallel cases ... */
    break;
    }

    두 설비는 만나지 않는다. 페이지 경로는 뜨거운 읽기/쓰기 루프에서의 처리량을 최적화하고, 매니페스트 경로는 차가운 파일당 일회성 패스에서의 다이제스트 강도를 위해 최적화된다.

  • 다른 엔진의 하드웨어/포맷 레벨 CRC. 많은 엔진이 무결성을 더 넓거나 다른 코드로 밀어 넣는다. InnoDB는 역사적으로 페이지에 CRC32C를 스탬프했고(레거시 “innodb” 체크섬과 crc32 모드 포함), PostgreSQL처럼 기록된 체크섬이 알려진 오프셋에 있도록 페이지를 브라켓한다. SQL Server의 PAGE_VERIFY CHECKSUM도 마찬가지로 헤더에 저장된 단일 페이지 전체 값을 계산한다. 공통 형태 — 하나의 코드, 페이지 세분성, 읽기 시 검증 — 는 같다. 다항식/기본 연산과 너비가 다르다. PostgreSQL의 독특한 점은 블록 번호를 혼합하여(checksum ^= blkno) 바이트가 개별적으로 손상되지 않았어도 잘못된 물리적 위치에 기록된 페이지를 감지한다는 것이다 — 콘텐츠만의 CRC가 놓치는 전치 오류 모드다.

  • 체크섬 대 종단 간 무결성(스토리지 스택 인수). 고전적인 시스템 결과 — Stone & Partridge의 TCP/이더넷 체크섬 갭 분석, 더 넓게는 종단 간 인수(Saltzer, Reed & Clark) — 는 한 레이어에서 적용된 체크섬이 해당 레이어의 관리를 떠난 데이터를 보호하지 못함을 보여준다. PostgreSQL 체크섬은 PageSetChecksum*에서 smgrwrite 직전에 계산되고 읽기 완료 직후 PageIsVerified에서 검증된다. 따라서 커널, 파일시스템, 블록 레이어, 매체를 통한 여정을 정확히 커버한다 — PostgreSQL이 바이트를 넘겨준 세그먼트다. 버퍼의 인메모리 수명은 의도적으로 커버하지 않는다(공유 버퍼의 더티 페이지에서 RAM 비트 플립은 플러시 시 “유효한” 페이지로 재체크섬됨) — 그 갭은 ECC 메모리의 영역이지 pd_checksum의 영역이 아니다.

  • 체크섬이 볼 수 없는 것. WAL은 자체 CRC와 전체 페이지 이미지를 포함하며 pd_checksum은 로그되지 않고 매 플러시마다 재계산되므로, 페이지 체크섬은 PostgreSQL 아래에서 발생한 손상을 감지하지만 의 로직 버그에는 맹목적이다. 의미상 잘못되었지만 구조적으로 유효한 튜플을 쓰는 백엔드는 완벽히 유효한 체크섬을 생성한다. 이것은 파일시스템 체크섬(ZFS, Btrfs)이 그리는 동일한 경계다 — 매체의 거짓말은 잡지만 애플리케이션의 거짓말은 잡지 못한다. 무증상 데이터 손상 연구 프런티어(Bairavasundaram et al.의 프로덕션 플릿에서 잠재 섹터 오류와 디스크 손상의 대규모 연구)가 정확히 pd_checksum이 감지하도록 만들어진 오류 클래스다. read()가 I/O 오류 없이 잘못된 바이트로 성공적으로 반환하는 손상이다.

  • 세분성과 틀릴 때의 비용. 16비트 페이지 코드는 의식적인 트레이드다. 8 KB당 2바이트(0.024%)와 몇 사이클 비용이다. 행 또는 셀 단위 체크섬(더 세밀한 지역화, 훨씬 높은 오버헤드) 또는 전체 세그먼트 다이제스트(비용 상각 저렴, 그러나 페이지 단위 격리 없고 온라인 검증 불가)와 비교된다. PostgreSQL의 페이지당 선택은 무결성 단위를 I/O 단위와 복구 단위에 맞춘다 — smgrread/smgrwrite의 원자, 버퍼 교체의 원자, WAL 전체 페이지 이미지의 원자 — 바로 그것이 실패를 단일 불량 블록으로 보고하고 선택적으로 0으로 만들거나(zero_damaged_pages) 허용할(ignore_checksum_failure) 수 있는 이유다.

  • 온라인 활성화 (범위 외, 독자를 위한 메모). 이 문서는 REL_18 현실을 설명한다. 체크섬은 initdb -k로 고정된 클러스터 전체 속성이며, 이후에는 정지된 클러스터에서 오프라인 pg_checksums 도구로만 변경 가능하다. 실행 중인 클러스터에서 백그라운드 워커로 온라인 체크섬 활성화를 지원하는 기능은 이후 개발 사항이며 여기서는 의도적으로 어설트하지 않는다. 이후 브랜치의 독자는 섹션 4에서 설명한 활성화 경로(data_checksum_version 부트스트랩 쓰기)를 바닥으로 봐야 하며 천장으로 보아서는 안 된다.

인트리 소스 파일 (REL_18_STABLE, 커밋 273fe94, 2026-06-06 기준)

섹션 제목: “인트리 소스 파일 (REL_18_STABLE, 커밋 273fe94, 2026-06-06 기준)”
  • src/backend/storage/page/checksum.c — 프런트엔드 도구들이 동일한 코드를 컴파일할 수 있도록 알고리즘 본체를 exported 헤더에서 가져오는 두 개의 #include shim.
  • src/include/storage/checksum_impl.h — 알고리즘 자체: PGChecksummablePage union, checksumBaseOffsets[N_SUMS], CHECKSUM_COMP 혼합 매크로, pg_checksum_block, 공개 pg_checksum_page(FNV-1a 병렬 합산, 두 번의 0 라운드, xor 폴드, 블록 번호 혼합, (checksum % 65535) + 1 축소). 긴 설계 근거 주석(SIMD 병렬성, 32를 선택한 이유, 병렬성이 포맷 불변인 이유)도 포함.
  • src/include/storage/checksum.hpg_checksum_page의 단일 라인 공개 프로토타입.
  • src/include/storage/bufpage.hPageHeaderData / pd_checksum 필드 레이아웃, PageSetChecksumInplace/PageSetChecksumCopy 선언, PageIsVerifiedExtendedPIV_* 플래그 비트.
  • src/backend/storage/page/bufpage.cPageIsVerifiedExtended, PageSetChecksumCopy(공유 락 아래 개인 복사본 경로), PageSetChecksumInplace(인플레이스 경로), 체크섬 실패 WARNING/ERROR 텍스트.
  • src/backend/storage/buffer/bufmgr.c — 읽기 완료 검증 경로(AIO 버퍼 읽기 콜백을 통한 PageIsVerified), FlushBuffer 쓰기 사이트, WaitReadBuffers에서 ignore_checksum_failure / zero_damaged_pagesREAD_BUFFERS_* 변환.
  • src/backend/storage/buffer/localbuf.cFlushLocalBuffer, 로컬 버퍼 쓰기 사이트.
  • src/backend/storage/smgr/bulk_write.csmgr_bulk_flush, 각 블록을 smgrextend/smgrwrite 전에 체크섬하는 대량 로드 쓰기 경로.
  • src/backend/access/transam/xlog.cDataChecksumsEnabledControlFiledata_checksum_version 배관.
  • src/backend/bootstrap/bootstrap.cinitdb 시 클러스터 체크섬 버전을 설정하는 부트스트랩 -k 처리.
  • src/backend/backup/basebackup.c — 베이스 백업 스트리밍 시 데이터 페이지 체크섬 검증.
  • src/backend/utils/activity/pgstat_database.cpgstat_report_checksum_failure*, pg_stat_database.checksum_failures / checksum_last_failure 공급.
  • src/include/catalog/pg_control.hControlFileDatadata_checksum_version, 클러스터 전체 플래그의 온디스크 위치.
  • src/common/checksum_helper.c별개 설비: 백업 매니페스트용 CRC32C / SHA-2 다이제스트 컨텍스트(pg_checksum_init/update/final, pg_checksum_type). pg_checksum_ 접두사만 공유. pg_checksum_page를 호출하지 않는다. 경계를 문서화하기 위해 포함됨.
  • FNV-1a 해시 — Fowler/Noll/Vo, 페이지 알고리즘이 기반하는 비암호화 해시 계열. 인트리 주석에 인용된 URL(isthe.com/chongo/tech/comp/fnv)에 설명됨. PostgreSQL 변형은 ^ ((hash ^ value) >> 17) 하이 비트 혼합 단계와 32-방향 병렬성을 추가.
  • 종단 간 인수 / 체크섬 커버리지 갭 — Saltzer, Reed & Clark, End-to-End Arguments in System Design (1984); Stone & Partridge, When the CRC and TCP Checksum Disagree (SIGCOMM 2000). 정확히 smgrwrite/읽기 경계에서 체크섬을 계산/검증하는 근거.
  • 현장의 무증상 데이터 손상 — Bairavasundaram et al., An Analysis of Latent Sector Errors / Data Corruption in the Storage Stack (FAST ‘07/’08). read()가 잘못된 바이트로 성공할 수 있다는 경험적 사례 — pd_checksum이 감지하도록 존재하는 오류 클래스.
  • DBMS 신뢰성 프레이밍knowledge/research/dbms-general/의 일반 DBMS 신뢰성/복구 자료(페이지 레벨 무결성은 WAL 및 버퍼 매니저와 함께 온디스크 지속성의 세 번째 다리로 위치)와 프로젝트 참고문헌에 따른 dbms-papers의 적절한 항목들.

관련 KB 문서 (교차 참조, 중복 없음)

섹션 제목: “관련 KB 문서 (교차 참조, 중복 없음)”
  • postgres-page-layout.mdPageHeaderData 필드별 레이아웃. 이 문서는 전체 헤더 해부를 거기로 미루고 pd_checksum의 오프셋만 인용한다.
  • postgres-buffer-manager.md — 버퍼 제거, FlushBuffer/WaitReadBuffers 메커니즘, AIO 읽기 완료 기계. 이 문서는 쓰기/읽기 호출 사이트를 이름 짓지만 주변 플러시/제거 수명 주기는 거기로 미룬다.
  • postgres-smgr-md.md — 체크섬이 브라켓하는 smgrwrite/smgrread 레이어.
  • postgres-xlog-wal.md — WAL CRC와 전체 페이지 이미지, 힙/인덱스 페이지가 아닌 로그를 보호하는 다른 무결성 코드.
  • postgres-backup-basebackup.md / postgres-incremental-backup.md — 별개의 checksum_helper.c 매니페스트 다이제스트가 실제로 소비되는 곳.