PostgreSQL 19부터 logical replication이 sequence 값을 subscriber로 동기화한다. 18까지는 테이블 데이터만 넘어가고 SERIAL·IDENTITY 뒤에 붙은 sequence는 subscriber에서 초기값에 그대로 멈춰 있었다. 그래서 subscriber를 promote한 직후 첫 INSERT가 duplicate key로 터지는 사고가 흔했다. 19는 publication에 ALL SEQUENCES 절을 들이고 ALTER SUBSCRIPTION ... REFRESH SEQUENCES 명령을 더해 이 오래된 구멍을 메웠다.
이 글은 pgEdge의 Shaun Thomas(숀 토머스)가 쓴 “Looking Forward to Postgres 19: Logically Sequenced”를 한국어로 풀고, PostgreSQL 19 릴리스 노트와 실제 동작 데모로 사실을 교차검증한다. DBA 관점에서 “failover 때 왜 사고가 났나”, “19에서 무엇이 정확히 달라지고 무엇은 여전히 그대로인가"를 함께 본다.
sequence는 왜 그동안 복제 대상이 아니었나
sequence가 logical replication에서 빠져 있던 건 게으름이 아니라 구조 문제였다. logical decoding은 WAL을 트랜잭션 단위로 재조립해 commit 순서대로 replay한다. 철저히 트랜잭션 기반이다.
그런데 sequence는 트랜잭션과 어울리지 않는다. nextval()로 뽑은 값은 트랜잭션을 rollback해도 되돌아오지 않는다. 한 번 소비된 번호는 영구히 사라진다. 이 비트랜잭션 동작과 commit 순서 기반 decoding을 깔끔하게 화해시키기가 어려웠다.
실제로 Tomas Vondra(토마스 본드라)가 만든 “logical decoding of sequences” 패치가 PostgreSQL 16에 한 번 들어갔다가, 트랜잭션과 비트랜잭션 동작을 조율하는 난점 때문에 되돌려진 적이 있다. 그만큼 묵은 숙제였다. (pgEdge)
결과적으로 18 이하의 logical replication에서 sequence는 공식 문서의 제약 사항에 명시된 복제 제외 대상이었다. 테이블 row는 넘어가지만, 그 row의 id를 발급하는 sequence는 subscriber에서 따로 놀았다.
failover 때 무슨 사고가 났나
운영 입장에서 이게 왜 문제였는지는 cutover 시나리오로 보면 분명하다. zero-downtime upgrade나 마이그레이션에서 흔한 흐름이다.
- publisher(구 primary)에서 subscriber(신 primary)로 logical replication을 건다.
- 테이블 데이터는 잘 넘어온다. row가 수백만 건 들어와 있고, id 컬럼은 publisher에서처럼 큰 값까지 차 있다.
- cutover 시점에 subscriber를 promote하고 애플리케이션을 붙인다.
- 첫 INSERT가 떨어지는 순간
duplicate key value violates unique constraint로 터진다.
원인은 단순하다. 테이블의 id는 큰 값까지 차 있는데, 그 id를 발급하는 sequence는 subscriber에서 초기값(보통 1)에 그대로 멈춰 있었다. sequence가 복제 대상이 아니었으니 당연한 결과다. nextval()이 1을 돌려주는데, 그 자리는 이미 넘어온 데이터가 차지하고 있다.
그동안의 회피책은 cutover 직전에 손으로 sequence를 끌어올리는 것이었다.
-- 옛날 방식: 각 sequence를 publisher 값보다 높게 수동으로 밀어 올림
SELECT setval('public.orders_id_seq', 5000000 + 1000);
+ 1000 같은 인위적인 여유분은 cutover 도중 publisher에 추가로 들어올 write를 흡수하려는 안전 마진이다. sequence가 수십·수백 개면 이걸 전부 스크립트로 긁어 돌려야 했고, 마진을 잘못 잡으면 번호가 비거나 충돌했다. failover runbook에서 늘 신경 쓰이던 자리다.
PostgreSQL 19가 푸는 방식
19는 이 작업을 logical replication 안으로 들였다. 릴리스 노트는 이렇게 적는다.
Allow sequence values stored in subscribers to match the publisher (Vignesh C) — PostgreSQL 19 Release Notes
핵심은 세 조각이다.
- publication에
ALL SEQUENCES절이 생겼다. - subscriber가 sequence 값을 publisher에서 당겨오는 시점이 세 개 정해졌다.
- 백그라운드에서 sequence를 배치로 당겨오는 sequencesync worker가 추가됐다.
publication — ALL SEQUENCES
publication이 sequence를 포함할 수 있다.
-- 테이블과 sequence를 함께 발행
CREATE PUBLICATION migration_pub FOR ALL TABLES, ALL SEQUENCES;
-- sequence만 발행도 가능
CREATE PUBLICATION pubseq FOR ALL SEQUENCES;
ALL SEQUENCES는 ALL TABLES와 함께 묶을 수 있다. 다만 TABLE 이나 TABLES IN SCHEMA 같은 세밀한 옵션과는 함께 쓸 수 없다. (dbi-services)
여기서 첫 제약이 나온다. sequence는 개별 선택(cherry-pick)이 안 된다. 테이블처럼 “이 sequence만 publication에 넣겠다"가 불가능하고, 전부(ALL SEQUENCES) 아니면 전무다.
subscription — 동기화 시점
subscription 쪽은 평소처럼 만든다.
CREATE SUBSCRIPTION migration_sub
CONNECTION 'host=oldprimary dbname=app user=repl'
PUBLICATION migration_pub;
sequence 값이 subscriber로 당겨지는 시점은 정확히 셋이다. (릴리스 노트)
-- (1) CREATE SUBSCRIPTION — 최초 1회 당겨옴
-- (2) sequence 존재 여부 + 값을 publication에 맞춰 재조정
ALTER SUBSCRIPTION migration_sub REFRESH PUBLICATION;
-- (3) 값만 갱신 (membership 은 건드리지 않음)
ALTER SUBSCRIPTION migration_sub REFRESH SEQUENCES;
REFRESH PUBLICATION은 publication에서 sequence가 추가·제거된 걸 반영하면서 값도 맞춘다. REFRESH SEQUENCES는 membership은 그대로 두고 값만 다시 당겨온다. 내부적으로는 pg_subscription_rel의 모든 sequence를 INIT 상태로 되돌린 뒤 sequencesync worker가 다시 채운다.
연속 동기화가 아니다 — 가장 중요한 함정
여기서 DBA가 반드시 머리에 둬야 할 한 가지가 있다. sequence 동기화는 연속이 아니라 시점 동기화다.
테이블 row처럼 publisher의 변경이 실시간으로 흐르지 않는다. 위의 세 시점에만 값을 당겨오고, 그 직후부터 publisher가 새 번호를 발급하는 순간 subscriber의 값은 곧바로 낡은(stale) 값이 된다.
dbi-services의 데모가 이걸 그대로 보여준다. publisher에서 sequence의 last_value가 3까지 올라가도 subscriber는 2에 머문다. ALTER SUBSCRIPTION ... REFRESH SEQUENCES를 실행해야 비로소 3으로 맞춰진다. 그 뒤 publisher에 다시 row를 넣어 sequence가 6까지 올라가도, subscriber는 또 멈춰 있고 다시 한번 refresh를 실행해야 따라온다. (dbi-services)
그래서 운영 원칙은 명확하다. REFRESH SEQUENCES는 promote 직전에 실행한다. 미리 돌려두면 그 사이 publisher가 발급한 번호만큼 다시 어긋난다. cutover runbook에서 sequence refresh는 publisher write를 멈추고 promote로 넘어가기 바로 전 칸에 들어가야 한다. (pgEdge)
동기화 흐름
cutover 시점의 sequence 동기화를 단계로 그리면 이렇다.
flowchart TD
A["publisher write 중단"]
B["REFRESH SEQUENCES 실행"]
C["sequencesync worker 기동"]
D["INIT 상태 sequence 수집"]
E["publisher 값·LSN 조회"]
F["subscriber sequence 갱신"]
G["READY 상태로 표시"]
H["subscriber promote"]
I["애플리케이션 연결"]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
G --> H
H --> I
worker는 INIT으로 표시된 sequence를 모아 publisher에서 현재 값과 page LSN을 가져와 subscriber에 쓰고, 끝나면 해당 항목을 READY로 바꾼다. 배치로 처리해서 빠르다 — depesz의 테스트에서는 sequence 1만 개 동기화가 약 1초, 100ms당 약 1,200개 속도였다. (depesz)
상태와 모니터링
sequence도 테이블처럼 pg_subscription_rel에서 상태를 가진다. 동기화 전에는 INIT(i), 끝나면 READY(r)다.
SELECT c.relname, r.srsubstate, r.srsublsn
FROM pg_subscription AS s
JOIN pg_subscription_rel AS r ON s.oid = r.srsubid
JOIN pg_class AS c ON r.srrelid = c.oid;
relname | srsubstate | srsublsn
--------------+------------+------------
orders_id_seq| r | 0/04004780
srsubstate 가 r 이면 그 sequence는 READY 상태로 한 번 동기화를 마쳤다는 뜻이다. (dbi-services)
값 자체를 확인할 때는 새로 들어온 pg_get_sequence_data() 함수를 쓴다.
SELECT last_value FROM pg_get_sequence_data('public.orders_id_seq');
오류 카운트도 새로 추적된다. pg_stat_subscription_stats 뷰에 sync_seq_error_count 컬럼이 추가됐고, 기존 sync_error_count는 sync_table_error_count로 이름이 바뀌었다. sequence 오류가 별도로 집계되기 때문이다. 모니터링 대시보드가 sync_error_count를 직접 참조하고 있었다면 19 업그레이드 때 컬럼명을 손봐야 한다. (릴리스 노트)
PostgreSQL 18 이하 vs 19
| PG18 이하 | PG19 | |
|---|---|---|
| logical replication의 sequence 처리 | 복제 제외 | 동기화 지원 |
| failover 후 첫 INSERT | duplicate key 위험 | refresh 후 정상 |
| sequence 값 맞추기 | setval 수동 스크립트 | REFRESH SEQUENCES 명령 |
| publication 포함 방법 | — | ALL SEQUENCES 절 |
| 동기화 방식 | — | 시점 동기화 (3개 시점) |
| 개별 sequence 선택 | — | 불가 (전체만) |
| 오류 추적 | — | sync_seq_error_count |
운영 점검 포인트
19로 올려 sequence 동기화를 쓰기 전에 한 번씩 확인할 자리들이다.
- cutover runbook 순서.
REFRESH SEQUENCES는 publisher write 중단 후, promote 직전 칸에 둔다. 미리 돌리면 그 사이 발급된 번호만큼 다시 어긋난다. - publication 설계. sequence는 개별 선택이 안 된다. 일부 sequence만 복제하려는 설계가 있었다면
ALL SEQUENCES전체 발행 전제로 다시 봐야 한다. - stale 값 인지. 동기화는 연속이 아니다. 평상시 replication이 도는 동안에도 subscriber의 sequence는 따라가지 않는다는 사실을 모니터링 기준에 반영한다.
- 모니터링 컬럼명.
pg_stat_subscription_stats의sync_error_count를 직접 참조하던 쿼리·알람은sync_table_error_count로 바꾸고,sync_seq_error_count도 함께 본다.
정리
PostgreSQL 19의 sequence 동기화는 화려한 신기능이라기보다 logical replication에 오래 남아 있던 구멍을 메운 변화다. failover 직후 첫 INSERT가 duplicate key로 터지던 사고, 그리고 그걸 막으려고 setval 마진 스크립트를 돌리던 수작업이 REFRESH SEQUENCES 한 줄로 정리됐다.
다만 연속 동기화가 아니라는 점이 이 기능의 성격을 결정한다. 평상시 흐르는 게 아니라 cutover 시점에 한 번 맞추는 도구다. 그래서 진짜 가치는 zero-downtime upgrade와 마이그레이션 cutover에서 나온다 — runbook의 정해진 칸에 한 줄을 넣고, promote 직전에 실행하면 sequence가 더 이상 사고의 출처가 아니게 된다.
참고 자료
- Looking Forward to Postgres 19: Logically Sequenced — pgEdge (Shaun Thomas)
- PostgreSQL 19 Release Notes
- PostgreSQL 19: Logical replication of sequences — dbi-services
- Waiting for PostgreSQL 19 – Sequence synchronization in logical replication — depesz
- Logical Replication Restrictions — PostgreSQL 18 Documentation
- PostgreSQL 19의 wal_level — 고정값에서 동적 floor 값으로 — 이 블로그
- PostgreSQL 19 새 기능 총정리 — 이 블로그