본문으로 건너뛰기

12.3 Hot standby와 query conflict

Hot standby는 standby에서도 SELECT를 받을 수 있게 하는 기능. 같은 클러스터에서 read 부하를 분산하거나 분석용 standby를 두는 표준 방법입니다. 단, standby에서 query를 돌리는 동안 primary가 WAL을 적용하려 들면 conflict가 생깁니다. 이 동작을 이해해야 운영이 안정됩니다.

활성​

# standby의 postgresql.conf
hot_standby = on # 기본 on (PG 10+)

hot_standby = on인 standby는 recovery 중에도 SELECT를 받습니다.

사용​

psql -h standby.example.com -d app_main
SELECT count(*) FROM orders; # OK
INSERT INTO orders ...; # ERROR — read-only

standby에서 가능한 작업:

작업가능
SELECT (read-only, read-write 트랜잭션 제외)가능
SET TRANSACTION READ ONLY 명시가능
temporary table 생성불가
LISTEN/NOTIFY불가
sequence nextval불가
모든 쓰기 SQL불가
CREATE/DROP/ALTER불가

Query conflict 유형​

standby에서 SELECT 중 primary의 WAL이 도착해 그 SELECT가 보고 있는 row를 건드리려 할 때 충돌이 발생합니다.

유형원인
Snapshotvacuum이 dead tuple을 정리 — standby의 옛 스냅샷 무효화
Tablespacetablespace drop
LockDDL이 강한 락 잡음
Buffer pinbuffer cleanup 충돌
DatabaseDB drop
Deadlockstartup process의 buffer pin 대기와 쿼리 락 대기가 교착
Logical slot (PG 16+)standby의 logical slot이 필요한 catalog row가 제거됨

가장 자주 만나는 건 Snapshot conflict — primary의 VACUUM이 dead tuple을 cleanup하면 standby의 옛 스냅샷이 깨집니다.

충돌 해결 — max_standby_*​

standby가 충돌 시 얼마나 기다릴지 결정합니다.

max_standby_archive_delay = 30s # archive로 받은 WAL
max_standby_streaming_delay = 30s # 스트리밍 WAL
값동작
0즉시 query 취소 → recovery 진행
30s30초 기다린 후에도 안 끝나면 취소
-1무한 — WAL 재생 멈춤 (lag 폭증 위험)

운영 트레이드오프:

  • 짧은 값: query 자주 cancel, 하지만 standby가 primary와 거의 동기
  • 긴 값: query 성공률 ↑, standby lag 커질 수 있음

보고서·분석 standby는 5min~10min, 읽기 부하 분산 standby는 30s 정도가 보통.

query cancel 에러​

ERROR: canceling statement due to conflict with recovery
DETAIL: User query might have needed to see row versions that must be removed.

애플리케이션이 이걸 잡아 재시도해야 합니다. 처음 standby에 read를 보내는 워크로드면 이 에러 핸들링이 필수입니다.

hot_standby_feedback​

standby가 자기 트랜잭션의 xmin을 primary에 알려 vacuum이 그 dead tuple을 정리하지 않게 막습니다.

# standby
hot_standby_feedback = on
효과메모
query cancel ↓conflict 안 일어남
primary BLOAT ↑standby의 긴 트랜잭션이 primary vacuum 막음
pg_replication_slots의 xmin 잡힘catalog_xmin도

트레이드오프: 표준 OLTP는 off, 분석 standby는 on + 별도 vacuum 튜닝.

시각화​

hot_standby_feedback 대안 — replication slot의 xmin​

standby가 primary_slot_name으로 slot을 쓰면 hot_standby_feedback의 xmin이 slot에 기록되어, standby가 재연결하는 사이에도 유지됩니다.

일반적으로는 hot_standby_feedback = on 또는 max_standby_*_delay를 큰 값으로 두어 해결합니다.

실시간 read 일관성​

standby의 lag로 인해 방금 primary에 INSERT한 row를 standby에서 SELECT하면 안 보임. 비동기 복제의 자연 결과입니다.

-- primary
INSERT INTO orders ... ;
-- (밀리초 후)

-- standby
SELECT * FROM orders WHERE ...; -- 안 보일 수 있음

해결:

  • synchronous_commit = remote_apply + 동기 복제 (비싼)
  • 애플리케이션이 쓰기 직후는 primary에서 read
  • 애플리케이션이 primary의 commit LSN을 받아 standby의 pg_last_wal_replay_lsn()을 폴링

PostgreSQL 18까지는 standby에서 LSN 도달을 기다리는 내장 기능이 없어, read after write는 위 폴링 방식으로 구현합니다.

standby promote​

primary 사고 시 standby를 read-write로 승격:

# standby에서
pg_ctl -D /var/lib/pgsql/17/data promote

또는 SQL (PG 12+):

SELECT pg_promote();

promote 후 standby는 새 timeline의 primary가 됩니다. 다른 standby들을 새 primary로 다시 연결 — 또는 자동화 도구(Patroni 등)에 위임.

운영 사례​

분석용 standby​

hot_standby = on
hot_standby_feedback = on
max_standby_streaming_delay = 10min

긴 분석 쿼리에 친화적입니다. primary BLOAT 모니터링이 필수입니다.

읽기 부하 분산​

hot_standby = on
hot_standby_feedback = off
max_standby_streaming_delay = 30s

쿼리가 짧으므로 cancel이 거의 없고 lag도 작습니다. application은 cancel 에러를 재시도합니다.

운영 안티패턴​

안티패턴위험
분석용 + hot_standby_feedback = off + 짧은 delayquery cancel 폭주
부하 분산 + hot_standby_feedback = onprimary BLOAT 폭증
max_standby_streaming_delay = -1standby lag 무한 — 사실상 비활성
application이 cancel 에러 안 잡음사용자에게 에러 노출
한 standby로 분석·OLTP 모두두 워크로드 충돌

정리​

  • hot standby = standby에서 read 가능
  • 충돌 유형 중 가장 흔한 것은 snapshot conflict (vacuum이 dead tuple 정리 시)
  • max_standby_*_delay로 query vs lag 트레이드오프 조절
  • read-after-write는 pg_last_wal_replay_lsn() 폴링 또는 primary로

hot_standby_feedback은 query cancel을 줄이는 대신 primary BLOAT을 키우므로, 분석 standby와 OLTP read standby는 서로 다른 값으로 튜닝합니다.