본문으로 건너뛰기

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

모든 태그 보기

PG19 WAIT FOR 읽기 일관성

· 약 17분

PostgreSQL 19에는 standby가 지정한 WAL 위치까지 replay할 때까지 기다리는 WAIT FOR LSN 명령이 들어가요. primary에 쓴 직후 비동기 standby에서 읽으면 방금 쓴 값이 보이지 않을 수 있어요. 이 명령은 그 문제를 읽는 쪽에서만 해결하게 해 줘요. 이 글에서는 공식 문서와 commit을 바탕으로 문법과 제약을 확인했어요. primary 읽기, remote_apply, WAIT FOR LSN도 19beta4 primary 1대, standby 1대 구성에서 비교해 봤어요.

소재는 ClickHouse 블로그의 Read your writes: WAIT FOR in PostgreSQL 19(Gülçin Yıldırım Jelínek(귈친 이을드름 옐리넥), 2026-08-25)입니다. stale read 문제에서 시작해 문법을 설명하고, top-level 명령이어야 하는 이유로 이어지는 구성을 이 글에 활용했습니다. 원문에는 측정값이 없어 지연과 stale read 비율은 직접 측정했습니다. 원문 발행 뒤 9월에 추가된 잠금 제한과 LSN 선택 문제는 commit과 측정 결과를 바탕으로 따로 정리했습니다.

비동기 standby의 stale read​

streaming replication은 기본적으로 비동기입니다. primary는 커밋 WAL을 로컬에 flush하면 클라이언트에 성공을 반환합니다. standby가 그 WAL을 받아 replay하는 작업은 이후에 진행됩니다. 쓰기는 primary로 보내고 읽기는 standby로 보내는 구성에서는 사용자가 방금 저장한 값을 다음 화면에서 보지 못하는 일이 생깁니다. 공식 문서의 Read-Your-Writes Consistency 절도 이 구간을 "a short window where changes on the primary may not yet be visible on the standby"로 설명합니다.

PostgreSQL 18까지 사용하던 해법은 크게 두 가지였습니다. 하나는 방금 쓴 사용자의 읽기를 primary로 보내는 것입니다. 다른 하나는 synchronous_commit = remote_apply를 설정해 커밋이 synchronous standby의 replay까지 기다리게 하는 것입니다. synchronous_commit 문서는 이 설정을 "commits will wait until replies from the current synchronous standby(s) indicate they have received the commit record of the transaction and applied it"라고 설명합니다. 이어서 "This will cause much larger commit delays than previous settings since it waits for WAL replay"라고 적습니다. 첫 번째 방법은 읽기 부하를 standby로 분산한다는 목적을 일부 포기해야 합니다. 두 번째 방법은 쓰는 쪽에서 지연을 감수해야 합니다.

WAIT FOR LSN은 세 번째 선택지입니다. 쓰기는 비동기로 유지하고, 일관성이 필요한 읽기만 standby에서 해당 WAL 위치까지 기다립니다.

WAIT 문법​

공식 문서(19)의 synopsis는 다음과 같습니다.

WAIT FOR LSN 'lsn'
[ WITH ( option [, ...] ) ]

-- option:
-- MODE 'mode'
-- TIMEOUT 'timeout'
-- NO_THROW
-- mode:
-- standby_replay | standby_write | standby_flush | primary_flush

명령 이름은 개발 도중 한 번 정리됐습니다. 원래 문서 제목과 오류 메시지는 "WAIT FOR"였지만, commit 1a562a266(2026-09-14)에서 명령 이름을 WAIT로 통일했습니다. command tag가 이미 WAIT였고, FOR는 INSERT INTO의 INTO처럼 문장을 자연스러운 영어로 만드는 noise word라는 이유입니다. 문법은 그대로 WAIT FOR LSN이고, 문서 URL은 sql-wait-for.html에서 sql-wait.html로 바뀌었습니다. 이 글에서도 명령은 WAIT, 문법을 나타낼 때는 WAIT FOR LSN으로 씁니다.

MODE는 대기할 대상을 정합니다. 기본값은 standby_replay입니다.

mode기다리는 대상실행 위치
standby_replay (기본)standby에서 해당 LSN까지 replay 완료standby
standby_writestandby가 해당 LSN까지 WAL을 디스크에 write (flush 전)standby
standby_flushstandby가 해당 LSN까지 WAL을 flushstandby
primary_flushprimary가 해당 LSN까지 WAL을 flushprimary

read-your-writes에 사용하는 모드는 standby_replay입니다. write와 flush는 WAL이 standby에 도착했다는 뜻일 뿐이며, 쿼리에 보인다는 보장은 없습니다. 이 두 모드는 commit cba67b5b8부터 walreceiver 위치와 replay 위치 중 큰 값을 기준으로 사용합니다. base backup이나 archive에서 이미 replay한 WAL을 walreceiver가 다시 받을 때까지 기다리는 일을 방지하기 위한 수정입니다.

TIMEOUT은 밀리초 단위 숫자 또는 '100ms', '5s' 같은 단위 문자열을 받습니다. 지정하지 않거나 0이면 무기한 기다립니다. 문서에 따르면 0.5ms 이하 값은 반올림하면 0이 되므로 무기한 대기합니다.

반환값은 status 컬럼 하나이며, 값은 success, timeout, not in recovery 중 하나입니다. NO_THROW가 없으면 timeout과 not in recovery는 오류가 됩니다. 19beta4 standby에서 확인한 두 경우의 출력은 다음과 같습니다.

lab=# WAIT FOR LSN '0/062759E0' WITH (TIMEOUT '100ms');
ERROR: timed out while waiting for target LSN 0/062759E0 to be replayed; current standby_replay LSN 0/052759E0

lab=# WAIT FOR LSN '0/062759E0' WITH (TIMEOUT '100ms', NO_THROW);
status
---------
timeout

문서는 NO_THROW를 사용하는 경우도 설명합니다. 애플리케이션에서 timeout을 처리할 예정이면 NO_THROW로 상태 문자열을 받습니다. 다시 기다리거나 다른 서버로 읽기를 보내는 경우가 그렇습니다. 목표 LSN에 도달하지 못한 상태에서 진행하면 안 되는 흐름이라면 옵션을 빼고 오류로 중단합니다. NO_THROW로 받은 상태는 명시적 트랜잭션을 abort 상태로 만들지 않는다는 차이도 있습니다.

기다릴 LSN 고르기​

애플리케이션은 쓰기 커밋 직후 primary에서 LSN을 받아 보관했다가 standby에 전달합니다. 문서는 이 LSN을 클라이언트나 connection pooler가 보관하라고 안내합니다.

어떤 LSN을 받을지가 중요합니다. commit a203d2019(2026-09-17)에서 문서에 추가된 조건은 "the target LSN is at or after the end of the relevant write transaction's COMMIT record on the primary"입니다. standby가 비교하는 위치는 마지막으로 replay한 레코드의 끝입니다. 그래서 COMMIT 레코드 내부를 가리키는 LSN은 COMMIT이 아직 적용되지 않았는데도 조건을 만족할 수 있다는 설명입니다. 따라서 트랜잭션 안에서 RETURNING pg_current_wal_insert_lsn()처럼 커밋 전에 받은 값은 이 조건을 만족하지 않습니다. autocommit에서는 쓰기 문장이 끝난 뒤 별도 문장으로 LSN을 조회해야 합니다.

문서 예시는 이 LSN을 pg_current_wal_insert_lsn()으로 받습니다. synchronous_commit이 off여도 커밋 레코드 뒤를 가리키도록 insert 위치를 사용한다고 적혀 있습니다. 하지만 이 함수에는 페이지 경계 문제가 있습니다. 마지막 레코드가 8kB WAL 페이지 끝에서 정확히 끝나면 다음 insert 위치는 다음 페이지 헤더(24바이트) 뒤를 가리킵니다. standby의 replay 위치는 페이지 경계에서 멈춰 있으므로, 다른 WAL 레코드가 하나 더 생길 때까지 이 LSN에 도달하지 않습니다. 이 현상은 2026-09-22 pgsql-hackers의 049_wait_for_lsn 테스트 스레드에 보고됐습니다. 내부 함수 GetXLogInsertEndRecPtr()을 SQL 함수로 노출하자는 패치가 제안된 상태이며, REL_19_BETA4에는 반영되지 않았습니다.

아래 측정에서 이 현상을 확인했습니다. 뒤의 "단점과 제약" 절에 결과를 정리했습니다.

측정 환경​

  • macOS Docker Desktop VM (2 CPU, 약 12GB, Linux 6.8.0), 다른 작업과 공유하는 VM
  • postgres:19beta4 이미지 (PostgreSQL 19beta4, aarch64), docker-compose로 primary 1대, physical streaming standby 1대, 측정용 client 1대 구성
  • 컨테이너마다 CPU 1개, primary와 standby는 메모리 768MB
  • client는 Python 3.13과 psycopg 3.2.6 사용, 같은 Docker 네트워크에서 두 서버에 각각 연결
  • 행 하나(id = 1)를 UPDATE ... RETURNING v로 증가시키고 바로 읽어, 읽은 값이 방금 쓴 값보다 작으면 stale로 셉니다

지연 조건은 두 가지입니다. 하나는 지연을 주입하지 않은 상태입니다. 다른 하나는 primary에서 standby로 가는 replication 경로에만 tc netem delay 20ms를 적용한 상태입니다. client와 서버 사이 경로에는 지연이 없습니다. 20ms 조건에서 pg_stat_replication의 replay_lag는 약 20.6ms였습니다.

비동기 시나리오는 synchronous_standby_names를 비우고 세션에서 synchronous_commit = local로 실행했습니다. remote_apply 시나리오에서만 synchronous_standby_names = 'standby1'로 바꾸고 sync_state = sync를 확인한 뒤 실행했습니다.

세 가지 해법 비교​

지연을 주입하지 않은 상태에서 1,000회씩 반복한 결과입니다. 시간은 쓰기부터 읽기까지 한 번 왕복하는 데 걸린 전체 시간이고 단위는 ms입니다.

시나리오stalep50p95p99
A. primary 쓰기, standby 바로 읽기999 / 10000.290.741.77
B. primary 쓰기, primary 읽기0 / 10000.250.490.80
C. remote_apply, standby 읽기0 / 10000.440.611.79
D. 커밋 후 insert LSN으로 WAIT0 / 10000.420.601.12
E. 커밋 전 insert LSN으로 WAIT0 / 10000.532.285.22

같은 VM 안에서 지연을 전혀 주지 않았는데도 A는 1,000회 중 999회가 stale이었습니다. 앞선 500회 측정에서도 489회가 stale이었습니다. 커밋 응답 직후 standby에 보낸 SELECT가 0.1ms 안팎에 끝나므로, 그 안에 WAL 전송과 replay가 완료되지 않은 것으로 보입니다. 복제 지연이 거의 없는 환경에서도 "바로 읽기"에서는 stale이 발생하는 것이 일반적이라고 보는 편이 맞습니다.

D의 WAIT 단계 자체는 p50 0.13ms였습니다. 다만 D의 최댓값은 2,004ms였습니다. 1,000회 중 1회가 2초 timeout에 걸렸기 때문입니다. 해당 회차의 목표 LSN은 0/03132018로 페이지 시작에서 24바이트 뒤였습니다. standby replay 위치와 primary의 write, flush 위치는 모두 0/03132000이었습니다. 앞 절에서 설명한 페이지 경계 현상입니다.

E는 커밋 전에 받은 LSN이므로 문서의 조건을 만족하지 않습니다. 이번 측정에서는 stale이 한 번도 나오지 않았습니다. 제 추정으로는 UPDATE 레코드와 COMMIT 레코드가 짧은 간격으로 함께 전송되고 replay되어 그사이에 읽기가 실행되지 못한 것 같습니다. 다만 이 부분은 확인하지 못했습니다. 측정에서 나오지 않았다고 해서 안전한 방법이 되는 것은 아닙니다.

replication 경로에 20ms를 적용한 상태에서 300회씩 반복한 결과입니다. F는 insert 위치 대신 pg_current_wal_lsn()(write 위치)을 받은 경우입니다.

시나리오stale쓰기 p50전체 p50전체 p95전체 p99
A. standby 바로 읽기300 / 3000.180.230.360.73
B. primary 읽기0 / 3000.160.210.350.50
C. remote_apply0 / 30022.9023.2625.5627.48
D. insert LSN으로 WAIT0 / 3000.4523.1825.6232.38
F. write LSN으로 WAIT0 / 3000.4823.2925.4726.71

C와 D의 전체 시간은 거의 같습니다. 둘 다 결국 standby가 replay할 때까지 기다리기 때문입니다. 차이는 대기하는 주체에 있습니다. C는 커밋에 22.9ms가 걸렸습니다. D와 F는 커밋이 0.5ms 안에 끝난 뒤 읽는 쪽에서 22ms를 기다렸습니다.

이 차이가 처리량에 어떻게 나타나는지 같은 20ms 조건에서 pgbench(-N, 클라이언트 4개, 15초)로 확인했습니다.

synchronous_committps평균 지연 (ms)
local4,9190.81
remote_apply17123.43

remote_apply는 세션이나 트랜잭션 단위로도 지정됩니다. 일관성이 필요한 쓰기에만 켜면 이 비용이 줄어듭니다. 그래도 이를 켠 트랜잭션은 replication 경로의 왕복 시간만큼 커밋이 늦어지고, 그동안 행 잠금도 유지됩니다.

장점​

WAIT의 가장 큰 장점은 비용을 읽는 쪽으로 옮긴다는 점입니다. 쓰기 경로는 비동기 상태를 유지하므로 쓰기 처리량과 커밋 지연이 standby 상태에 영향을 받지 않습니다. standby가 느려지거나 멈춰도 해당 standby에서 기다리는 읽기만 영향을 받습니다. TIMEOUT과 NO_THROW를 사용하면 해당 읽기를 primary로 돌릴지도 애플리케이션이 판단합니다.

remote_apply는 standby가 응답하지 않으면 커밋이 멈춥니다. 확인을 위해 synchronous_standby_names = 'standby1' 상태에서 standby 컨테이너를 docker pause로 멈췄습니다. 그리고 statement_timeout = '3s'를 설정한 세션에서 remote_apply UPDATE를 실행했습니다. 3초가 지나도 끝나지 않았습니다. 68초 시점에 pg_stat_activity의 wait_event는 SyncRep였습니다. pg_cancel_backend()로 취소하자 다음 경고와 함께 반환됐습니다.

WARNING: canceling wait for synchronous replication due to user request
DETAIL: The transaction has already committed locally, but might not have been replicated to the standby.

autocommit 문장에서는 statement_timeout이 이 대기에 적용되지 않았습니다. finish_xact_command()가 CommitTransactionCommand() 전에 disable_statement_timeout()을 호출하기 때문으로 보입니다(src/backend/tcop/postgres.c, REL_19_BETA4). 코드를 읽고 추정한 내용이며 다른 경로까지 확인하지는 않았습니다.

remote_apply가 보장하는 대상은 "current synchronous standby(s)"뿐이라는 점도 다릅니다. 읽기 standby가 여러 대이고 그중 일부만 synchronous라면 나머지 standby에서는 여전히 stale read가 발생합니다. WAIT는 읽기를 받은 standby에서 직접 기다리므로 standby 수와 관계없이 같은 방식으로 동작합니다.

단점과 제약​

애플리케이션의 LSN 전달​

WAIT는 SQL 한 줄이지만, 그 전후 과정은 애플리케이션이 처리해야 합니다. 쓰기 뒤에 LSN을 조회하고 세션이나 요청 컨텍스트에 보관한 다음 읽기 standby에 넘겨야 합니다. 읽기 경로와 쓰기 경로가 서로 다른 서비스로 분리되어 있다면 LSN을 전달할 통로도 만들어야 합니다. primary 읽기나 remote_apply는 애플리케이션 코드 변경이 거의 없다는 점에서 여전히 단순합니다.

페이지 경계의 insert LSN​

pg_current_wal_insert_lsn()의 페이지 경계 현상을 별도로 재현했습니다. primary에 다른 부하가 없는 상태에서 pg_logical_emit_message()로 WAL을 채워 마지막 레코드가 8kB 경계에서 끝나도록 만들었습니다. 그런 다음 두 LSN으로 각각 WAIT를 실행했습니다.

시도pg_current_wal_lsn() 대기pg_current_wal_insert_lsn() 대기대기를 끝낸 다음 레코드
10.3ms12,345ms다른 트랜잭션의 HOT_UPDATE, COMMIT
20.4ms6,690msRUNNING_XACTS
30.4ms19,432msCHECKPOINT_REDO, RUNNING_XACTS

write 위치를 기준으로 기다리면 즉시 끝났습니다. insert 위치를 기준으로 기다리면 전혀 관계없는 WAL 레코드가 생길 때까지 6초에서 19초를 기다렸습니다. 쓰기가 계속 발생하는 서버라면 다음 레코드가 금방 생성되어 이 현상이 드러나지 않을 것입니다. 그러나 쓰기가 드문 서버에서는 TIMEOUT이 짧으면 timeout이 발생하고, TIMEOUT이 없으면 그만큼 읽기가 늦어집니다. 앞의 1,000회 측정에서 발생한 2초 timeout 1건도 같은 원인이었습니다.

pg_current_wal_lsn()은 페이지 헤더를 건너뛰지 않습니다. 하지만 synchronous_commit = off이면 커밋이 반환된 시점에 write 위치가 아직 COMMIT 레코드 끝에 도달하지 않았을 가능성이 있습니다. 따라서 어떤 함수를 사용할지는 synchronous_commit 설정에 따라 달라집니다. on이나 local처럼 커밋이 로컬 flush를 기다리는 설정에서는 pg_current_wal_lsn()이 조건을 만족합니다. off를 사용하는 트랜잭션에서는 insert 위치를 사용하되 TIMEOUT을 반드시 지정하는 편이 안전합니다. 이 판단은 문서와 제 측정을 종합한 것이며, 공식 문서가 직접 권고하는 내용은 아닙니다.

snapshot 보유 시 실행 불가​

WAIT가 함수가 아닌 명령인 이유가 여기에 있습니다. 최초 구현 commit 447aae13b는 다음과 같이 설명합니다.

WAIT FOR needs to wait without any snapshot held. Otherwise, the snapshot could prevent the replay of WAL records, implying a kind of self-deadlock.

snapshot을 가진 세션이 replay를 기다리는 동안 replay가 해당 snapshot 때문에 멈추면 서로 기다리는 상태가 됩니다. 함수와 프로시저는 snapshot을 가진 상태에서 실행되므로 이 기능을 포함할 수 없다는 것이 commit의 설명입니다. 이후 commit a8b61c23c(2026-04-13)는 함수, 프로시저, DO 블록 안에서의 실행을 명시적으로 막았습니다.

이 제약에 관한 문서 표현은 원문 발행 뒤 바뀌었습니다. 원문은 "READ COMMITTED보다 높은 격리 수준의 트랜잭션에서는 실행할 수 없다"는 취지로 설명합니다. 그러나 commit 6fe990f34(2026-09-18)는 이것이 격리 수준 전체에 대한 금지가 아니라고 정정했습니다. 코드가 검사하는 조건은 "현재 트랜잭션이 snapshot을 갖고 있는가"입니다. REPEATABLE READ 트랜잭션은 첫 쿼리 전까지 snapshot이 없으므로 그 전에는 WAIT를 실행해도 됩니다. 반대로 READ COMMITTED여도 cursor가 열려 있으면 실행할 수 없습니다. 19beta4 standby에서 확인한 결과는 다음과 같습니다.

상황결과
REPEATABLE READ, WAIT 후 SELECTsuccess
REPEATABLE READ, SELECT 후 WAIT오류 (snapshot 보유, 격리 수준 DETAIL 포함)
READ COMMITTED, cursor 연 뒤 WAIT오류 (snapshot 보유, DETAIL 없음)
DO 블록 안 EXECUTE 'WAIT ...'오류 (top-level 전용)
ERROR: WAIT cannot be executed while the current transaction holds a snapshot
DETAIL: This transaction runs at an isolation level higher than READ COMMITTED, so it holds a snapshot from its first query until it ends.

REPEATABLE READ에서 WAIT를 먼저 실행하고 첫 SELECT에서 snapshot을 얻으면 해당 트랜잭션 전체가 목표 LSN 이후의 데이터를 봅니다. 여러 쿼리에서 같은 시점의 데이터를 읽어야 하는 화면이라면 이 순서가 적합합니다.

잠금 보유 시 대기 거부​

snapshot이 없어도 잠금이 남아 있으면 같은 순환이 발생합니다. commit f7c092917(2026-09-12)의 설명은 다음과 같습니다. standby의 startup process가 replay 중에 이 세션이 가진 잠금을 필요로 하는 경우가 있습니다. 이때 startup은 세션의 잠금 해제를 기다리고 세션은 startup의 replay를 기다립니다. WAIT의 대기는 잠금 대기로 등록되지 않아 deadlock detector가 이 순환을 감지하지 못합니다. standby conflict 지연이 무제한이면 이 상태가 무기한 지속될 수 있습니다. 이 commit 이후에는 목표 LSN에 아직 도달하지 않았고 세션이 heavyweight lock을 하나라도 가지고 있으면 WAIT를 거부합니다. 19beta4 발표문의 "Several fixes for the new WAIT FOR command, including a deadlock and clearer reporting of isolation-level errors"에서 deadlock 부분은 날짜로 보아 이 수정을 가리키는 것으로 보입니다.

READ COMMITTED에서도 트랜잭션 안에서 앞선 SELECT가 획득한 AccessShareLock은 트랜잭션이 끝날 때까지 남습니다. 따라서 다음 순서는 거부됩니다.

lab=# BEGIN;
lab=*# SELECT v FROM t WHERE id = 1;
lab=*# WAIT FOR LSN '0/062759E0' WITH (TIMEOUT '100ms', NO_THROW);
ERROR: cannot wait for a standby LSN while holding locks
DETAIL: This session holds a lock on relation 16389 of database 16384, which could make recovery wait for this session while this session waits for recovery.
HINT: Release the locks, or execute WAIT before acquiring them.

NO_THROW를 지정해도 이 오류는 그대로 발생합니다. 문서는 NO_THROW가 timeout과 not in recovery의 보고 방식만 바꾼다고 명시합니다. 세션 단위 advisory lock(pg_advisory_lock(42))을 획득한 상태에서도 같은 오류가 발생했습니다. 목표 LSN에 이미 도달한 경우에는 잠금이 있어도 즉시 success가 반환됐습니다. 문서에서 권하는 순서는 트랜잭션 밖에서, 또는 트랜잭션의 첫 문장으로 WAIT를 실행한 뒤 읽기 쿼리를 실행하는 것입니다.

timeline과 recovery conflict​

WAIT는 LSN 숫자만 비교합니다. 문서는 cascading standby의 upstream이 promote되어 timeline이 바뀐 경우에도 숫자만 넘으면 success를 반환한다고 명시합니다. timeline 확인이 필요하면 애플리케이션에서 직접 확인하라고 안내합니다. standby 자신이 기다리는 도중 promote되면 not in recovery가 반환되거나 오류가 발생합니다. 이 두 경우는 이번에 재현하지 않았습니다.

standby의 recovery conflict 때문에 WAIT 세션이 종료될 수도 있습니다. 문서는 tablespace drop replay처럼 모든 backend를 종료하는 경우를 예로 들며, 재시도나 대체 경로를 준비하라고 안내합니다.

점검 쿼리​

WAIT를 도입하기 전후에 확인할 쿼리입니다. 앞의 두 개는 이번 환경에서 실행해 확인했습니다. 세 번째의 wait event 이름은 소스의 wait_event_names.txt(REL_19_BETA4)를 기준으로 적었으며, 실제 대기 중인 세션에서 조회해 보지는 않았습니다.

primary에서 standby별 지연을 확인합니다. 20ms 조건에서 replay_lag는 약 20.6ms였고, WAIT 대기 시간 p50은 22.0ms로 서로 비슷했습니다.

SELECT application_name, sync_state,
write_lag, flush_lag, replay_lag,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_behind_bytes
FROM pg_stat_replication;

standby에서 현재 replay 위치를 확인합니다. 문서에 따르면 standby_replay 모드가 성공하면 pg_last_wal_replay_lsn()이 목표 LSN 이상이 됩니다.

SELECT pg_last_wal_receive_lsn(),
pg_last_wal_replay_lsn(),
now() - pg_last_xact_replay_timestamp() AS since_last_replayed_xact;

standby에서 WAIT로 대기하는 세션을 찾습니다. 소스에 따르면 wait event는 WaitForWalReplay, WaitForWalWrite, WaitForWalFlush입니다.

SELECT pid, application_name, wait_event, now() - query_start AS waiting, query
FROM pg_stat_activity
WHERE wait_event IN ('WaitForWalReplay', 'WaitForWalWrite', 'WaitForWalFlush');

버전별 차이​

WAIT는 PostgreSQL 19에서 처음 추가된 명령입니다. remote_apply는 9.6부터 있었습니다. 18 이하에서 standby read-your-writes를 보장하려면 primary 읽기, remote_apply, 또는 애플리케이션에서 standby의 pg_last_wal_replay_lsn()을 반복 조회하는 방법을 사용해야 했습니다. 반복 조회 방식은 이번에 측정하지 않았습니다. 2020년 PostgreSQL 13 개발 주기에도 트랜잭션 시작 시 LSN을 기다리는 기능이 commit(0f5ca02f5)됐지만, 다음 날 revert(1aac32df8)된 적이 있습니다.

19 개발 주기의 주요 commit은 다음과 같습니다. 모두 REL_19_BETA4 태그(2026-09-21 stamp)에 포함돼 있습니다.

날짜(UTC)commit내용
2025-11-05447aae13bWAIT FOR 명령 추가
2026-01-0549a181b5dMODE 옵션 추가 (write, flush, primary_flush)
2026-04-0610484c2ccrecovery conflict로 중단될 수 있음을 문서화
2026-04-13a8b61c23c함수, 프로시저, DO 블록 안 실행 금지
2026-05-03cba67b5b8write, flush 모드의 기준에 replay 위치 반영
2026-05-0321d290161timeline을 구분하지 않는다는 점 문서화
2026-09-12f7c092917잠금 보유 시 standby 모드 대기 거부
2026-09-141a562a266문서와 오류 메시지의 명령 이름을 WAIT로 변경
2026-09-14f260c03bctimeout overflow 수정
2026-09-17a203d2019read-your-writes에 쓸 LSN 조건 문서화
2026-09-186fe990f34snapshot 제약 설명과 오류 메시지 정정

베타 기간에 문서 표현과 제약이 여러 번 바뀌었습니다. 페이지 경계 문제처럼 논의 중인 사안도 있습니다. 따라서 도입 시점에는 GA 버전의 문서와 릴리스 노트를 다시 확인하는 것이 좋습니다.

측정 결과 remote_apply와 WAIT의 전체 지연은 거의 같았어요. 차이는 그 지연을 쓰기와 읽기 중 어디에 두느냐였어요. 기다릴 LSN을 어떤 함수로 받을지는 GA 전까지 계속 지켜볼 생각이에요.

참고​