본문으로 건너뛰기

"checkpoint" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

모든 태그 보기

PGSimCity 3D 도시

· 약 7분

DBA가 아닌 개발자에게 checkpoint 스파이크를 설명해 본 적 있다면 그 난감함을 알아요. WAL이 뭔지부터 시작해야 하고, dirty page가 왜 쌓이는지, max_wal_size가 왜 그 시점을 정하는지 순서대로 쌓아야 해요. 그림을 그려도 정적이라 "시간에 따라 이게 몰린다"는 감각이 전달되지 않아요.

PGSimCity는 그 문제를 정면으로 노립니다. PostgreSQL 내부를 탐험 가능한 3D 도시로 만들어 놓고, 시간을 흘려보내며 그 안에서 무슨 일이 벌어지는지 보여줍니다. 만든 사람은 postgres.ai의 Nikolay Samokhvalov(니콜라이 사모흐발로프)입니다.

브라우저에서 바로 열립니다. nikolays.github.io/PGSimCity에 접속하면 설치 없이 도시가 뜹니다. WebGL2를 지원하는 브라우저가 필요합니다.

저장소를 직접 빌드해 봤습니다

소개 글을 쓰려면 실제로 돌려 보는 게 맞습니다. 클론해서 테스트와 빌드를 통과시켜 보고 확인한 사실을 먼저 적습니다.

git clone https://github.com/NikolayS/PGSimCity.git
cd PGSimCity
npm install
npm run typecheck # 통과, 오류 없음
npm test # 48개 파일, 360개 테스트 통과 (10.4초)
npm run build # 2.69초
npm run dev # localhost:5173
항목확인값
버전0.12.0
라이선스Apache-2.0
TypeScript 소스107개 파일, 60,517줄
테스트48개 파일, 360개 통과
런타임 의존성three 0.185, @electric-sql/pglite 0.5.4
빌드 결과 최대 청크city 1.17MB (gzip 392KB)

README에는 테스트가 234개로 적혀 있는데 실측은 360개였습니다. 최근 커밋이 확인 시점과 같은 날짜였으니 문서가 코드를 못 따라가는 중입니다. 그만큼 활발하다는 뜻으로 읽으면 됩니다.

의존성이 얇은 게 눈에 띕니다. 번들 런타임 의존성이 Three.js 하나뿐이고, 시뮬레이션 코드는 Three.js를 import하지 않습니다. 시뮬레이션과 렌더링이 SimState에서만 만나는 구조라, 도식 없이도 시뮬레이션 로직만 따로 테스트할 수 있습니다. 360개 테스트가 붙어 있는 이유가 여기에 있습니다.

도시가 표현하는 것

지구가 PostgreSQL의 구성요소에 대응합니다.

  • 클라이언트 영역: 들어오는 connection
  • backend 구역: connection 하나당 프로세스 하나, 각자의 private memory
  • buffer pool: shared_buffers를 샘플링한 프레임 격자
  • 스토리지: heap 파일, B-tree, TOAST
  • WAL 지구: 쓰기가 디스크에 먼저 도착하는 경로
  • 유지보수 구역: checkpointer, autovacuum launcher와 worker, background writer
  • replication 구역: standby와 WAL receiver

색이 상태를 나타냅니다. WAL은 호박색, dirty page는 빨강, 청소 작업은 보라색입니다. 도시를 내려다보다가 빨간 블록이 넓어지면 dirty page가 쌓이는 중이라는 걸 눈으로 압니다.

소스를 검사하다 예상 밖의 지구를 하나 발견했습니다. src/world/continuity.ts에 백업과 복구 쪽 오브젝트가 따로 들어 있습니다.

archive.gate archive 소유권 경계
timeline.yard timeline 전환 조차장
object.store 오브젝트 스토리지
backup.vault 백업 금고
recovery.clock recovery_target_time
restore.winch restore_command

PITR을 도시 구조물로 옮겨 놓은 것입니다. recovery_target_time이 시계로, restore_command가 권양기로 서 있습니다. Barman이나 pgBackRest로 PITR을 설명할 때 "timeline이 갈라진다"는 말이 잘 안 통했다면 이 조차장을 보여주는 편이 빠르겠습니다.

투어 14장

T를 누르면 안내 투어가 시작됩니다. 순서가 곧 커리큘럼입니다.

  1. 클라이언트가 접속한다
  2. connection 하나에 프로세스 하나
  3. 쿼리가 plan이 된다
  4. 페이지 읽기, 캐시
  5. 페이지란 실제로 무엇인가
  6. 쓰기: 페이지보다 WAL이 먼저 디스크로
  7. commit, 그리고 fsync의 값
  8. checkpoint, 그리고 스파이크
  9. MVCC: update는 시체를 남긴다
  10. autovacuum이 치운다
  11. vacuum이 못 치울 때: horizon
  12. standby로 streaming
  13. lag, 그리고 네 개의 LSN
  14. 도시 전체를 다시

저는 9장에서 11장까지가 한 묶음으로 붙어 있는 게 좋아요. update가 옛 버전을 남기고, autovacuum이 그걸 치우고, 그런데 horizon 때문에 못 치우는 경우까지 세 장으로 이어집니다. 이 순서를 말로 설명하면 늘 세 번째에서 막히는데, 도시에서는 vacuum worker가 왔다 갔는데 빨간 블록이 그대로 남아 있는 장면으로 보여줍니다.

13장의 "네 개의 LSN"은 sent_lsn, write_lsn, flush_lsn, replay_lsn입니다. replication lag을 볼 때 어느 단계에서 밀리는지 구분해야 하는데, 이 넷의 간격을 도시 안 거리로 보여주는 방식입니다.

시나리오 13종

투어와 별도로 상황을 직접 재현하는 시나리오가 있습니다. 소스에서 확인한 목록입니다.

시나리오재현하는 상황
steady-state평상시 부하
checkpoint-stormcheckpoint 몰림
cache-thrashbuffer pool 히트율 붕괴
bloat-and-vacuumdead tuple 누적과 회수
xmin-horizonidle 트랜잭션이 vacuum을 막음
lock-pileupACCESS EXCLUSIVE 락 뒤에 줄이 쌓임
replication-lagstandby 지연
wal-floodWAL 급증
index-vs-seqscan인덱스 스캔과 순차 스캔
no-bgwriterbackground writer 없는 상태
connection-stormconnection 폭주
logical-replicationlogical replication 흐름
full-page-writesfull page write 부하

각 시나리오는 knob 값 묶음과 시간축 해설(beats)로 되어 있습니다. xmin-horizon 하나를 열어 보면 이런 식입니다.

knobs: tps 900, writeRatio 0.8, updateRatio 0.9,
longRunningXact: true, autovacuum: true, ...

beats:
0초 누군가 BEGIN을 입력했다
14초 horizon이 얼어붙었다
30초 autovacuum은 여전히 돈다
48초 worker가 헛도는 것을 본다
66초 브레이크 없는 bloat
86초 어디를 봐야 하나
108초 풀어 준다
126초 그리고 예방한다

30초 지점 해설이 정확합니다. launcher는 여전히 worker를 보내고, worker는 테이블까지 가서 heap 전체를 스캔하고 I/O를 태우는데 회수하는 건 거의 없습니다. 모니터링에는 vacuum이 도는 것으로 보이고 테이블은 반대를 말합니다. 86초 지점에서는 pg_stat_activitystate = 'idle in transaction'으로 걸러 xact_start 순으로 정렬하라고 알려주고, 버려진 replication slot과 hot_standby_feedback이 켜진 standby의 장기 쿼리도 같은 메커니즘이라고 덧붙입니다.

이 대목은 어제 쓴 MVCC 비용 비교 글에서 직접 실험으로 확인한 내용과 그대로 겹칩니다. VACUUM VERBOSE0 removed, 400000 are dead but not yet removable을 뱉는 장면, 범인이 pg_stat_activity에만 있는 게 아니라 replication slot과 prepared transaction에도 있다는 지점까지 같습니다. 실측으로 확인한 것을 시각 자료로 다시 설명할 수 있게 됐습니다.

모델인가 에뮬레이터인가

저자는 이게 PostgreSQL 모델이고 에뮬레이터가 아니라고 밝힙니다. PostgreSQL 소스가 돌아가지 않고, 숫자는 사람이 눈으로 따라갈 수 있게 축척을 조정했습니다. buffer pool도 shared_buffers 전체가 아니라 1,024개 프레임 샘플입니다.

다만 빌드 결과를 보다가 청크 하나가 눈에 걸렸습니다.

dist/assets/real-postgres-runtime-BHGmqr-J.js 539.32 kB

소스를 열어 보니 @electric-sql/pglite를 import하고 pglite.wasm, initdb.wasm을 함께 번들합니다. PGlite는 PostgreSQL을 WASM으로 컴파일한 것입니다.

import { PGlite } from '@electric-sql/pglite'
import initdbWasmUrl from '.../pglite/dist/initdb.wasm?url'
import pgliteWasmUrl from '.../pglite/dist/pglite.wasm?url'

즉 구분이 필요합니다. 도시 시뮬레이션은 모델이지만, Query Lab 쪽은 브라우저 안에서 진짜 PostgreSQL을 띄워 실제 실행 계획을 받아 옵니다. accounts, orders, events, sessions 테이블에 인덱스까지 붙인 시드 스키마가 코드에 들어 있습니다. 도시의 움직임은 모형이고, 쿼리 실습은 실물입니다.

이 조합이 영리합니다. 실제 PostgreSQL을 8KB 페이지 단위로 시각화하면 사람 눈에는 아무것도 안 보입니다. 그래서 보여주는 층은 축척을 조정한 모델로 두고, 정확성이 중요한 실행 계획은 실물 엔진에 맡겼습니다.

한계와 쓸 자리

버전이 0.12.0입니다. 저자도 0.x 초기 단계임을 명시하고, PostgreSQL 정확성은 문서와 소스로 3라운드 검토했다고 밝힙니다. 기억에 의존해 만들지 않았다는 뜻이고, 그래도 프로덕션 판단 근거로 쓸 물건은 아닙니다. 축척을 조정한 숫자를 실제 튜닝 값으로 옮기면 안 됩니다.

쓸 자리는 분명합니다. 신입 교육 첫 주에 개념 지도를 잡아 주는 용도, 장애 회고에서 "이때 이런 일이 있었다"를 비개발자에게 설명하는 용도, DB를 운영해 본 적 없는 개발자에게 idle_in_transaction_session_timeout이 왜 필수인지 납득시키는 용도입니다. 마지막 항목은 특히 말로 하면 잔소리로 들리는데, worker가 헛도는 장면을 2분간 같이 보면 설명이 끝납니다.

라이선스는 Apache-2.0이라 사내 교육 자료로 가져다 쓰기에도 걸림이 없습니다. 정적 번들이니 npm run build 결과를 사내 어디든 올려 둘 수 있습니다. 다만 PostgreSQL은 PostgreSQL Community Association of Canada의 상표이고 이 프로젝트는 공식 지원을 받지 않는 독립 교육 프로젝트라는 점, 그리고 이름과 달리 Electronic Arts와 무관하다는 점은 저자가 직접 밝혀 둔 대로 함께 알려 주는 게 좋겠습니다.

참고

이 블로그의 관련 글로는 MVCC 비용은 어디에 숨었나PostgreSQL 18의 autovacuum_worker_slots가 있어요.

PG19 동적 wal_level

· 약 7분

PostgreSQL 19에서 wal_level이 의미를 바꿨어요: 더 이상 "이 서버가 항상 쓰는 WAL 레벨"이 아니라 하한값이며, 실제 effective level은 그 위에서 slot 상태에 따라 자동으로 움직여요. logical replication slot이 하나 생기면 effective level이 logical로 올라가고, 마지막 slot이 사라지면 다음 checkpoint에서 내려가요.

이 글에서는 The Build의 Christophe Pettus가 다룬 "The wal_level You Set Is Not the wal_level You Get"을 한국어로 풀고, cascading standby와 archived WAL에서 운영자가 한 번은 부딪힐 자리들을 함께 살펴봅니다.

보험성 wal_level=logical 의 비용

기존 PostgreSQL에서 wal_level은 셋 중 하나로 못 박혀 있었습니다 — minimal, replica, logical. 변경하려면 서버를 재시작해야 했습니다.

운영 패턴이 자연스럽게 굳어졌습니다. 지금 당장 logical replication을 쓰지 않더라도, 나중에 쓸 가능성이 조금이라도 있으면 일단 logical로 잡아둡니다. 그래야 그날 새벽에 슬롯 하나 만들면서 재시작을 잡을 일이 없습니다. "혹시 모르니까" 라는 안전망입니다.

이 안전망에는 비용이 붙습니다.

  • wal_level = logical은 모든 변경에 대해 추가 메타데이터를 WAL에 적습니다. row의 이전 이미지, replica identity 정보, 다중행 변경의 stream 표식 등입니다.
  • 실제로 logical replication slot이 단 하나도 없어도 적힙니다. "지금 안 쓰는데 적어두는 값"이 분 단위로 디스크에 흐릅니다.
  • 운영 환경에 따라 다르지만, replica 대비 WAL 볼륨이 10~30% 더 늘어나는 케이스가 흔히 보고됩니다.
  • WAL 볼륨이 늘면 archive 비용, streaming replication 대역폭, PITR 복구 시간이 같이 늘어납니다.

"The right thing has corners." — Christophe Pettus, The Build

PostgreSQL 19는 이 안전망의 비용을 덜기 위해 wal_level을 "고정 레벨"에서 "최소 보장"으로 바꿨습니다.

PG19의 configured wal_level과 effective wal_level

PostgreSQL 19에서 서버는 두 개의 WAL level을 가집니다.

configured wal_levelpostgresql.conf에 적힌 값입니다. 여전히 재시작 파라미터이며, "이 서버가 어떤 경우에도 떨어지지 않을 하한값"을 뜻합니다. effective wal_level은 서버가 실제로 지금 WAL에 쓰는 레벨이며, configured 값보다 위로 올라갈 수 있습니다.

규칙은 단순합니다.

상태effective wal_level
logical replication slot이 하나 이상 살아있음logical
streaming replication 연결만 있음replica 또는 configured 중 높은 쪽
아무것도 없음configured 값 그대로

즉, configured를 replica로 잡아두고도 logical replication slot을 만드는 순간 서버가 알아서 logical로 올라갑니다. 마지막 slot이 사라지면 다음 checkpoint에서 다시 내려갑니다.

상태 전이도

중요한 지점은 두 군데입니다 — logical 진입 시점은 forced checkpoint 직후이고, 다시 내려가는 시점은 그다음 일반 checkpoint입니다. 두 시점은 비대칭입니다.

동작 단계

올라갈 때와 내려갈 때를 따로 봅니다.

올라갈 때: logical로의 전이

  1. 운영자(또는 publication 생성 시 PostgreSQL)가 첫 logical replication slot 생성을 요청합니다.
  2. 서버는 effective level이 logical 미만이면 즉시 forced checkpoint를 돕니다.
  3. 이 checkpoint 직후의 LSN을 "guaranteed LSN"으로 기록하고, slot은 그 지점부터 시작합니다.
  4. 그 LSN 이후의 WAL은 logical 레벨 메타데이터를 함께 적기 시작합니다.

내려갈 때: replica로의 하강

  1. 마지막 logical slot이 사라집니다.
  2. 서버는 다음 일반 checkpoint까지 그대로 logical을 유지합니다. 즉시 내리지 않습니다.
  3. 그 checkpoint가 돌고 나면 effective level이 configured 값으로 복귀합니다.
  4. 이후 WAL은 다시 replica 메타데이터로 가벼워집니다.

이 비대칭은 의도된 설계입니다. 올라갈 때는 slot이 막 만들어진 WAL을 읽지 못하면 안 되므로 checkpoint를 즉시 실행합니다. 내려갈 때는 별도 checkpoint를 잡아 운영 영향을 만들 이유가 없습니다.

운영 영향

가장 큰 효과는 WAL 볼륨 절감입니다.

  • "혹시 모르니까 logical"로 잡아둔 클러스터는, configured를 replica로 내리고 PG19로 올리면 그대로 WAL이 줄어듭니다. 실제로 logical을 쓸 일이 생기면 그 순간 자동으로 올라가니 운영 부담은 늘지 않습니다.
  • archive 비용, streaming replication 대역폭, PITR 복구 시간이 모두 따라 줄어듭니다.
  • 클라우드 관리형 PostgreSQL에서는 archive 저장 비용이 매월 청구되는 항목이라 체감이 큽니다.

부차 효과는 logical replication을 사용 중인 동안에만 그 비용을 낸다는 점입니다. 일회성 마이그레이션을 위해 logical을 켰다가 작업을 마치면 비용이 자동으로 사라집니다.

함정과 주의사항

네 가지를 짚습니다.

첫째, cascading standby의 비대칭입니다. primary와 standby는 각자 effective level을 따로 가집니다. primary의 마지막 logical slot이 사라져도 standby는 자신의 슬롯이 살아있는 한 effective logical을 유지합니다. primary가 보내는 WAL은 그새 replica로 내려갈 수 있지만, standby는 자기 슬롯을 위해 그 WAL을 logical로 받아야 합니다. 이 경계는 운영자에게 보이지 않는 곳에서 정렬됩니다. PG19가 알아서 잘 처리하지만, replication lag를 분석할 때 헷갈리지 않으려면 "두 노드의 effective level은 따로 움직인다"는 사실을 머리에 둬야 합니다.

둘째, archive에 mixed-level WAL이 섞입니다. 같은 archive 디렉토리에 replica 시기의 WAL과 logical 시기의 WAL이 함께 쌓입니다. PostgreSQL은 segment마다 metadata로 어떤 레벨인지 표시해두지만, "이 archive는 통째로 logical이다" 같은 가정을 두고 작성한 PITR script가 있다면 다시 봐야 합니다.

셋째, prepared transaction입니다. 2-phase commit 자체의 동작은 변하지 않습니다. 다만 prepared transaction이 많은 환경에서는 logical 전이 시점의 forced checkpoint가 의외로 오래 걸릴 수 있습니다. 테스트 환경에서 한 번은 의도적으로 재현해 보고 timeout 설정을 검토하는 편이 좋습니다.

넷째, 첫 slot 생성 시 checkpoint 대기입니다. 새로 만든 publication이 logical slot을 생성하면 그 호출이 forced checkpoint를 기다립니다. 평소 checkpoint 부담이 큰 시스템에서는 수십 초가 걸릴 수도 있습니다. 자동화 script가 CREATE SUBSCRIPTION 호출에 짧은 timeout을 걸어뒀다면 PG19 업그레이드 직후 한 번 흔들릴 가능성이 있습니다.

PITR / archive script 점검 포인트

PostgreSQL 19로 올라가기 전에 한 번씩 확인할 자리들입니다.

archive level과 관련해서는 script가 "이 archive는 logical 레벨"임을 전제로 메타데이터를 파싱하지 않는지 확인합니다. slot 생성 timeout은 자동화 도구의 CREATE SUBSCRIPTION / pg_create_logical_replication_slot 호출에 30초 미만으로 설정되어 있지 않은지 살펴봅니다.

WAL 볼륨 모니터링에서는 PG19로 올린 직후 WAL 생성률이 줄어드는 변화를 정상으로 인식하는지 확인합니다. 감소를 장애로 판단해 알람을 보내지 않도록 해야 합니다. cascading 구성에서는 primary, intermediate, leaf의 effective level이 각각 어떻게 정렬되는지 그림으로 한 번 그려두는 편이 좋습니다.

"혹시 모르니까"라는 비용

logical replication을 본격적으로 쓰기 시작한 계기는 zero-downtime upgrade와 CDC 파이프라인입니다. 한 번 켜놓으면 끝나는 것이 아니라 "혹시 다음 마이그레이션 때 또 쓸지 모르니까" wal_level = logical 그대로 두는 패턴이 굳어졌습니다. 그 사이 WAL 볼륨이 10~30% 더 흐르고, archive 비용과 streaming replication 대역폭이 같이 늘어납니다 — 평소에는 의식하지 못하지만 PITR 복구 시간을 재 보면 한 번씩 보여요. PostgreSQL 19의 동적 wal_level은 이 "보험 비용"을 시점성 비용으로 바꿉니다. 일회성 마이그레이션이 끝나면 slot만 정리해도 effective level이 자동으로 내려가고 WAL이 가벼워집니다. 운영자가 wal_level을 내릴지 말지 회의에 올릴 일이 없어진다는 점이 PG19가 일상에 주는 가장 큰 차이입니다.

정리

PG18 이하PG19
wal_level 의 의미고정값하한값 (floor)
logical 전환 비용항상 부담slot 있는 동안만
WAL 볼륨 (logical 미사용 시)풀 부담replica 수준
첫 logical slot 생성즉시forced checkpoint 후
마지막 slot 제거 후 하강n/a다음 checkpoint

PostgreSQL 19는 운영자가 "혹시 모르니까"라는 이유로 영구히 짊어졌던 비용을 시점성 비용으로 바꿔, logical replication이 필요한 그 순간에만 그 비용을 내게 해요. 기본 동작이 더 똑똑해진 변화지만, cascading standby와 archive처럼 effective level이 노드별로 따로 움직이는 곳에서는 모서리가 한 번씩 보여요.

참고 자료