2.1 Compute와 Storage 분리
일반 PostgreSQL에서는 하나의 인스턴스가 backend, checkpointer, WAL writer 같은 여러 프로세스로 역할을 나눕니다. WAL을 쓰고 데이터 파일을 갱신하며 checkpoint를 수행하고 백업까지 담당합니다. Neon은 이 중에서 "질의를 실행하고 WAL을 생성하는 일"만 compute에 남기고, 나머지는 모두 storage 서비스로 옮겼습니다. Compute가 사라져도 데이터는 남습니다. 데이터가 있는 곳에 새 compute를 연결하면 곧바로 같은 데이터베이스로 동작합니다.
요약
- Compute는 패치된 PostgreSQL 바이너리와
neonextension으로 구성됩니다. 로컬에는 영속(logged) relation의 데이터 파일이 없습니다. - 쓰기: WAL 레코드가 Safekeeper quorum에 도착하면 commit입니다. 로컬 디스크 fsync는 commit의 기준이 아닙니다.
- 읽기: shared_buffers에 없으면 LFC(Local File Cache)를 확인하고, 그다음 Pageserver에
GetPage@LSN으로 요청합니다. - Pageserver는 요청받은 LSN 시점의 page를 base image와 WAL redo로 재구성해 반환합니다.
- Object storage는 질의 경로에 직접 관여하지 않습니다. Pageserver가 layer 파일을 올리고 내려받는 저장 계층입니다.
Compute는 무엇으로 이루어지는가
Compute 노드 안에는 세 가지가 있습니다.
| 구성 요소 | 역할 |
|---|---|
| 패치된 PostgreSQL | 질의 실행, WAL 생성. 패치 목록은 2.6 |
neon extension (pgxn/neon) | smgr(storage manager) 인터페이스에 연결되어 page 읽기를 Pageserver 요청으로 바꿈 |
| WAL proposer | extension 안의 background worker. WAL을 Safekeeper들에 push |
PostgreSQL의 smgr는 relation 파일을 읽고 쓰는 추상 계층입니다. 원래 구현체는 md.c(magnetic disk) 하나였으며, 8 kB page를 로컬 파일에서 읽습니다. Neon은 extension이 이 인터페이스를 교체할 수 있도록 core를 수정했습니다. 그런 다음 smgrread() 호출을 Pageserver로 보내는 네트워크 요청으로 전환했습니다. 따라서 heap, index, visibility map 같은 영속(logged) relation 데이터는 compute의 디스크에 존재하지 않습니다. unlogged relation은 예외입니다. WAL을 남기지 않으므로 compute 로컬 디스크에만 존재하며, compute가 재시작되면 사라집니다(2.6).
docker-compose 예제의 compute 설정(config.json)이 이 구조를 그대로 보여 줍니다. 아래는 spec.cluster.settings 배열에서 관련 항목만 추린 것입니다.
{
"settings": [
{ "name": "shared_preload_libraries", "value": "neon,pg_cron,timescaledb,pg_stat_statements" },
{ "name": "neon.safekeepers", "value": "safekeeper1:5454,safekeeper2:5454,safekeeper3:5454" },
{ "name": "neon.pageserver_connstring", "value": "host=pageserver port=6400" },
{ "name": "neon.tenant_id", "value": "TENANT_ID" },
{ "name": "neon.timeline_id", "value": "TIMELINE_ID" },
{ "name": "synchronous_standby_names", "value": "walproposer" },
{ "name": "fsync", "value": "off" }
]
}
fsync=off가 눈에 띕니다. 일반 PostgreSQL에서는 위험한 설정이지만, Neon compute의 로컬 디스크에는 내구성을 책임질 데이터가 없습니다. 내구성은 Safekeeper가 담당하므로 compute 쪽 fsync는 의미가 없습니다. synchronous_standby_names=walproposer는 WAL proposer를 동기 standby처럼 취급합니다. quorum 확인 전에는 commit 응답을 클라이언트에 보내지 않도록 하는 장치입니다.
기동: basebackup tarball
데이터 파일이 없는 PostgreSQL은 어떻게 시작할까요. Pageserver는 compute 기동 시점에 basebackup이라는 tarball을 생성합니다. 이름은 같지만 pg_basebackup과는 무관합니다. 이 tarball에는 relation 데이터가 없으며, 다음 항목만 담깁니다.
pg_control, 설정 파일 같은 control/config 파일- SLRU(commit log, multixact 등) 일부. 큰 것은 이후 on-demand로 내려받습니다
- 필요한 디렉터리 골격
- WAL 쓰기를 시작하기 위한 빈 WAL segment
- 시작 LSN을 알려 주는
neon.signal파일
일반 PostgreSQL은 마지막 checkpoint 레코드를 WAL에서 읽어 recovery를 시작합니다. Neon compute에는 해당 WAL segment가 없습니다. 따라서 neon.signal에 기록된 LSN에서 "이미 일관된 상태"로 기동하도록 core가 수정되어 있습니다. Recovery 없이 바로 그 LSN부터 새 WAL을 쓰기 시작합니다.
쓰기 경로
Commit의 정의가 바뀌었다는 점이 중요합니다. 일반 PostgreSQL에서 commit은 WAL이 로컬 디스크에 fsync된 순간입니다. Neon에서는 WAL 레코드가 Safekeeper 과반에 도착하고 각 디스크에 flush한 순간입니다. Pageserver가 해당 WAL을 page에 반영하는 작업은 commit 경로 밖에서 비동기로 진행됩니다. Pageserver가 잠시 느려져도 commit 지연은 증가하지 않습니다. 대신 지나치게 뒤처지면 backpressure가 작동합니다. 자세한 내용은 2.2에서 다룹니다.
읽기 경로
세 단계의 캐시가 있습니다.
- shared_buffers: PostgreSQL 고유의 buffer pool. Compute 메모리 내부에 있습니다.
- LFC(Local File Cache): compute VM의 로컬 NVMe에 두는 page 캐시. shared_buffers보다 훨씬 크게 구성할 수 있어 네트워크 왕복을 줄이는 두 번째 캐시 계층입니다. Autoscaling 알고리즘은 이 캐시에 working set이 들어가는지를 기준 중 하나로 씁니다(2.5).
- Pageserver 자체 캐시: 최근 재구성한 page를 메모리에 유지합니다.
세 캐시에서 찾지 못하면 Pageserver가 layer 파일을 읽어 page를 재구성합니다. Object storage는 이 단계에서 layer 파일이 로컬에 없을 때만 사용됩니다. compute가 S3를 직접 읽는 일은 없습니다.
GetPage@LSN의 LSN은 어디서 오는가
요청에 붙는 LSN은 "이 LSN 시점의 page 버전을 달라"는 뜻입니다. Pageserver는 요청 LSN 이하의 image와 WAL 레코드로 해당 시점의 page를 재구성합니다. Compute는 page를 buffer에서 evict할 때 해당 page의 LSN을 기억합니다(last-written LSN 추적). 나중에 같은 page를 다시 읽을 때 그 LSN을 포함해 요청합니다. Pageserver는 해당 LSN까지의 WAL이 도착한 뒤에만 응답합니다.
가장 최근 insert LSN을 항상 사용하면 정확하지만 느립니다. Pageserver가 아직 받지 못한 최신 WAL을 기다려야 하기 때문입니다. 따라서 page 단위로 보수적인 값을 기억하는 편이 유리합니다. 이 추적은 대부분 smgrwrite() 안에서 core 수정 없이 이루어집니다. CREATE DATABASE처럼 예외적인 경로에만 명시적 호출이 추가되어 있습니다.
Prefetch가 중요한 이유
로컬 디스크 PostgreSQL은 sequential scan 성능의 상당 부분을 OS readahead에 의존합니다. Neon compute에는 읽을 로컬 파일이 없으므로 readahead도 없습니다. Pageserver 왕복 지연은 로컬 디스크보다 큽니다. 따라서 sequential scan과 index scan 여러 곳에 prefetch 패치가 적용되어 있습니다. 여러 page 요청을 미리 보내고 응답을 순서대로 처리하는 방식입니다. Neon의 EXPLAIN에는 prefetch와 LFC 적중 정보를 보여 주는 확장이 있습니다. 이 부분은 upstream에 별도 패치로 제안되어 있습니다.
이 구조가 만드는 성질
- Compute는 자유롭게 교체합니다. 한 timeline에 연결된 compute를 끄고 다른 크기의 compute를 연결해도 데이터는 그대로입니다. 이 구조가 Autoscaling과 scale to zero를 가능하게 합니다.
- 같은 timeline에 read-only compute를 여러 개 연결하면 read replica가 됩니다. 데이터 복사는 없습니다.
- Branch는 storage 안의 timeline 하나입니다. Compute 입장에서는 접속할 timeline_id가 다를 뿐입니다.
- 반대로 모든 page miss에는 네트워크 왕복 비용이 발생합니다. Working set이 shared_buffers와 LFC에 들어가는지가 성능을 좌우합니다.
실패 사례: 로컬 PostgreSQL 습관이 어긋나는 곳
pg_basebackup으로 compute를 백업하려는 시도는 의미가 없습니다. Replication 권한 문제로 거절될 수 있습니다. 성공하더라도 데이터 디렉터리에 영속 relation 데이터가 없으므로 독립 복구에 사용할 수 없는 물리 백업이 됩니다. 백업과 PITR은 storage 계층(history window, snapshot, branch)의 기능으로 다룹니다. 마찬가지로 pg_database_size() 같은 크기 함수는 데이터 디렉터리를 직접 읽지 않습니다. 대신 smgr API로 계산하도록 패치되어 있습니다.
연습 문제
SELECT pg_current_wal_flush_lsn();를 compute에서 실행해 값을 기록한 뒤, Pageserver의 timeline 정보 API가 보여 주는last_record_lsn과 비교합니다. 두 값의 차이가 무엇을 의미하는지 설명합니다. 실습 절차는 3.4 Storage 관찰에 있습니다.- compute 컨테이너 안의
/var/db/postgres/compute디렉터리에서base/아래 파일 크기를 확인합니다. 일반 PostgreSQL과의 차이점을 기록합니다. fsync=off가 compute에서 안전한 이유와, 같은 설정을 일반 PostgreSQL에 적용하면 위험한 이유를 각각 한 문장으로 정리합니다.
심화 체크
- Commit 응답 시점을 결정하는 것은 Safekeeper quorum인가, Pageserver 반영인가?
- 한 page를 두 번 읽을 때 두 번째 요청에 붙는 LSN은 어떤 값이어야 정확하면서도 빠른가?
- shared_buffers, LFC, Pageserver 캐시 중 어느 계층이 비었을 때 지연이 가장 크게 늘어나는가?