2.6 Postgres 패치와 제약
Neon의 compute는 "PostgreSQL"이지만 upstream 바이너리 자체는 아닙니다. Storage를 네트워크 너머로 옮기기 위해 core 소스에 여러 패치를 적용했습니다. Neon 로직의 대부분은 pgxn/neon extension에 있습니다. 개발 문서 core_changes.md는 이 패치들을 하나씩 나열합니다. 각 패치에 "왜 필요한가"와 "어떻게 없앨 것인가"도 함께 적어 두었습니다. 장기 목표는 패치를 전부 upstream에 넣거나 extension으로 옮기는 것입니다. 이를 통해 수정 없는 PostgreSQL이 Neon storage 위에서 실행되게 합니다. 이 장에서는 그 목록을 읽으며 기존 PostgreSQL 운영 지식이 어디서 어긋나는지 정리합니다.
요약
- Neon 로직의 대부분은 extension(
neon,neon_rmgr)에 있고 core 패치는 이를 가능하게 하는 최소 변경입니다. - 일부 패치는 WAL 포맷을 바꿉니다. Neon이 만든 WAL은 vanilla PostgreSQL이 재생할 수 없습니다.
- 패치 대부분은 "compute용"이고, 일부는 Pageserver 내부에서 실행되는 WAL redo 프로세스용입니다.
- 서비스 차원의 제약도 따릅니다. In-place major upgrade가 없고, 지원 버전은 최신 다섯 개(14~18)입니다.
Compute용 패치
Prefetch
Neon compute에는 OS readahead가 없습니다. Pageserver 왕복 지연도 로컬 디스크보다 큽니다. 따라서 sequential scan을 포함한 여러 경로에 prefetch가 추가되었습니다. 문서는 PostgreSQL 17의 streaming read와 18의 async I/O 작업으로 이 패치가 줄어들 것으로 기대합니다.
Heap WAL 레코드에 t_cid 추가
영향이 가장 큰 패치입니다. heapam.c와 heapam_xlog.h를 수정해 heap insert, update, delete WAL 레코드에 command id(t_cid)를 넣었습니다.
일반 PostgreSQL에서 command id는 해당 transaction 안에서만 의미가 있습니다. Commit이나 abort 뒤에는 아무도 command id를 확인하지 않습니다. 따라서 WAL에 기록하지 않고 replica는 항상 1로 채웁니다. 하지만 Neon은 transaction이 진행 중일 때도 WAL을 재생해 page를 재구성합니다. Buffer에서 evict된 page를 다시 읽을 때가 이에 해당합니다. 이때 command id가 없으면 같은 transaction 안에서 "내가 방금 갱신한 행"을 올바르게 판별하지 못합니다.
이 변경은 WAL 레코드 포맷을 바꿉니다. 따라서 Neon WAL은 vanilla PostgreSQL과 호환되지 않습니다. 문서에는 Heikki Linnakangas(헤이키 린나캉가스)가 2024년에 이 필드를 logical decoding에 활용하는 upstream 패치를 시도했다고 적혀 있습니다. 하지만 작업이 생각만큼 단순하지 않았다고 기록되어 있습니다.
Unlogged index build 표시
GIN, GiST, SP-GiST index는 빌드 중 buffer manager를 통해 page를 만듭니다. 이때 WAL을 남기지 않고, 빌드가 끝난 뒤 모든 page를 한 번에 WAL에 씁니다. 로컬 디스크에서는 문제가 없습니다. 하지만 Neon에서는 WAL 없이 evict된 page가 그대로 사라집니다. 그래서 smgr_start_unlogged_build, smgr_finish_unlogged_build_phase_1, smgr_end_unlogged_build 함수를 추가했습니다. 이 함수로 해당 구간을 명시적으로 알립니다. pgvector 0.6.0에도 같은 변경이 필요했다고 적혀 있습니다.
Last-written page LSN 추적
2.1에서 다룬 내용입니다. Evict된 page의 LSN을 기억해 다음 GetPage@LSN 요청에 포함합니다. 대부분 smgrwrite() 안에서 처리됩니다. 하지만 CREATE DATABASE 경로(dbcommands.c) 등 몇 곳에는 SetLastWrittenPageLSN() 호출을 추가했습니다.
Checkpoint 레코드 없이 기동
Compute는 stateless이므로 기동할 때 Pageserver가 만든 basebackup tarball을 받습니다. 여기에는 빈 WAL segment만 있고 checkpoint 레코드는 없습니다. 그래서 xlog.c를 수정해 neon.signal 파일에 적힌 LSN에서 recovery 없이 시작하게 했습니다. 문서는 "없앨 방법"에 ???를 적어 두었습니다. 가짜 checkpoint 레코드를 tarball에 넣는 대안도 검토했습니다. 하지만 그 레코드가 Safekeeper로 전송될 위험이 있어 채택하지 않았습니다.
Sequence caching 비활성
/* Neon XXX: to ensure sequence order of sequence in Zenith we need to WAL log each sequence update. */
/* #define SEQ_LOG_VALS 32 */
#define SEQ_LOG_VALS 0
PostgreSQL은 sequence 값을 32개씩 미리 WAL에 기록합니다. crash가 발생하면 미리 기록한 범위만큼 건너뜁니다. Neon에서는 sequence page가 buffer에서 evict됩니다. 이 때문에 crash가 없어도 값이 건너뛰어질 수 있으므로 갱신할 때마다 기록합니다. 그만큼 sequence 갱신마다 WAL이 늘어납니다. 문서는 "그냥 gap을 허용하자"는 안과 "evict 직전에만 기록하자"는 안을 함께 적었습니다. PITR 시 sequence가 뒤로 가는 문제도 언급합니다.
smgr 인터페이스 공개
smgr.c와 smgr.h를 수정해 extension이 storage manager 구현을 등록할 수 있게 했습니다. Neon 로직의 대부분은 이 인터페이스를 기반으로 동작합니다. Upstream commitfest에 제출되어 있으나 문서는 진행이 매우 느리다고 적었습니다. smgropen()에 relpersistence 인자를 추가한 것도 관련 변경입니다. Unlogged relation을 다르게 다루려면 smgr 구현이 그 정보를 알아야 하기 때문입니다.
그 밖의 compute 패치
| 패치 | 이유 |
|---|---|
dbsize.c에서 smgr 사용 | pg_database_size() 등이 데이터 디렉터리를 직접 읽는데 compute에는 파일이 없음 |
ProcessInterrupts() hook | Backpressure 구현. Pageserver가 뒤처지면 쓰기 backend 일시 중지 |
| SLRU on-demand 다운로드 | SLRU 전체를 basebackup에 넣으면 수 GB가 되어 기동이 느려짐 |
| All-zeros page를 hole로 기록 | 16 이후 relation extend 시 8 kB의 all-zeros page를 WAL에 통째로 넣지 않도록 함. 이 패치도 WAL 호환성을 깨는 항목 |
| walproposer 종료 순서 | Shutdown checkpoint 레코드가 Safekeeper에 도달하도록 walproposer를 마지막에 종료 |
| EXPLAIN 확장 | Prefetch와 LFC 통계 표시. upstream commitfest 제출 |
| Extension on-demand 다운로드 | 필요한 extension 파일만 내려받기 |
| Publication 권한 | neon_superuser가 publication을 만들 수 있게 함 |
WAL redo 프로세스용 패치
Pageserver는 WAL 재생을 PostgreSQL 바이너리에 맡깁니다(--wal-redo 모드). 악의적인 WAL 레코드에 대비해 이 프로세스를 seccomp로 격리합니다. 관련 패치는 세 가지입니다.
XLogReadBufferForRedo()가 대상 이외의 page에는BLK_DONE을 반환해 불필요한 재생을 건너뜁니다. GIN split처럼 "full-page image가 반드시 있어야 한다"고 단정하던 코드는 이 반환값을 처리하도록 수정했습니다.initdb에predefined_sysidentifier플래그를 추가했습니다. Backup은 없고 WAL만 전부 있을 때 같은 system identifier로 initdb를 다시 실행합니다. 이를 통해 base를 복원하는 재해 복구용 장치입니다.pg_waldump에 오류 무시 플래그를 추가했습니다. Neon의 첫 timeline은 WAL segment 중간에서 시작할 수 있으므로 기본pg_waldump이 실패합니다.
문서는 redo 함수를 Rust로 다시 쓰는 방안이 현실적이지 않다고 판단했습니다. 모든 redo 함수를 sandbox 없이 안전하게 만드는 방안도 마찬가지입니다. 새 PostgreSQL 버전마다 동기화해야 해 유지 비용이 크기 때문입니다.
운영 관점의 시사점
WAL은 외부 PostgreSQL과 호환되지 않는다
t_cid와 all-zeros hole 패치 때문에 Neon compute가 만든 WAL은 vanilla PostgreSQL standby에서 재생할 수 없습니다. Neon에서 다른 PostgreSQL로 데이터를 옮기려면 logical replication이나 dump/restore를 써야 합니다. 즉, "physical replication으로 Neon 밖에 standby를 두는" 구성은 불가능합니다.
Extension은 검증된 것만
Storage manager가 바뀌었으므로 extension이 relation 파일을 직접 다루면 동작이 어긋납니다. WAL 없이 buffer를 갱신해도 마찬가지입니다. GIN 빌드 문제가 이런 경로에 해당합니다. 데이터 디렉터리를 스캔하는 경우에도 동작이 어긋납니다. Neon은 이런 이유로 지원 extension 목록을 관리합니다. 자체 호스팅 실습 환경의 compute 이미지에 포함된 extension은 3.6에서 확인합니다.
Major upgrade는 새 project로
플랜 문서에 따르면 in-place major version upgrade는 제공되지 않습니다. 새 project를 만들어 데이터를 옮겨야 합니다. 그 이유는 문서에 명시되어 있지 않습니다. 다만 storage 계층은 WAL 포맷과 page 포맷을 해석합니다. 이런 구조에서 한 timeline 안에 두 major 버전의 이력을 섞는 일은 단순하지 않습니다. 이는 앞의 패치 목록에서 짐작할 수 있습니다.
지원 버전
Neon은 PostgreSQL 14, 15, 16, 17, 18을 지원합니다. PostgreSQL 공식 정책에 맞춰 최신 다섯 개 major 버전을 유지합니다. Upstream EOL이 되면 Neon에서도 지원이 끝납니다. Docker 이미지도 버전별로 나뉘어 있습니다(compute-node-v17 등). 따라서 실습에서도 버전을 명시해야 합니다.
Unlogged table은 compute 재시작 시 사라진다
문서에 따르면 unlogged table은 compute의 로컬 디스크에 있습니다. compute를 재시작하면 지워집니다. Scale to zero가 발생하면 unlogged table 내용도 함께 사라진다는 뜻입니다. 일반 PostgreSQL에서 unlogged table이 crash 때만 비워지는 것과 다릅니다.
실패 사례
pg_waldump으로 Neon compute의 pg_wal을 읽으려는 시도는 두 가지 이유로 실패하거나 빈 결과가 나옵니다. Compute는 WAL을 오래 보관하지 않습니다(wal_keep_size=0). 첫 segment도 중간에서 시작합니다. WAL 분석은 Safekeeper가 보관한 WAL을 대상으로 해야 합니다. 이때 Neon WAL 포맷(t_cid, hole 레코드)을 해석하는 패치된 pg_waldump를 사용해야 합니다. 패치된 바이너리는 compute 이미지에 포함되어 있습니다. PostgreSQL major 버전도 맞춰야 합니다. Vanilla pg_waldump은 레코드 해석 단계에서 실패합니다.
Sequence 값이 재시작 없이도 크게 건너뛰면 버그로 오해하기도 합니다. Neon에서는 SEQ_LOG_VALS=0이므로 일반 PostgreSQL보다 gap이 덜 생깁니다. 하지만 PITR로 과거 branch를 만들면 그 시점의 sequence 값으로 돌아갑니다. 부모에서 이미 소비한 값이 자식에서 다시 나올 수 있습니다. 따라서 branch 간 데이터를 합칠 때 주의가 필요합니다.
연습 문제
core_changes.md의 패치 중 WAL 포맷을 바꾸는 항목 두 개를 찾습니다. 각각 어떤 상황에서 vanilla PostgreSQL이 실패하는지 적습니다.- GIN index 빌드를 Neon에서 그대로 두면 왜 데이터 손실로 이어지는지 evict 관점에서 설명합니다.
- Neon에서 다른 PostgreSQL로 마이그레이션할 때 쓸 수 있는 방법과 쓸 수 없는 방법을 각각 하나씩 적고 이유를 붙입니다.
심화 체크
- 패치를 extension으로 옮기는 것과 upstream에 넣는 것 중 어느 쪽이 각 항목에 적합한지 구분할 수 있는가?
- WAL redo 프로세스를 seccomp로 격리해야 하는 위협 모델은 무엇인가?
- Unlogged table의 수명이 일반 PostgreSQL과 다른 이유를 storage 구조로 설명할 수 있는가?