PostgreSQL 19부터 logical replication이 sequence 값을 subscriber로 동기화해요. 18까지는 테이블 데이터만 넘어가고 SERIAL/IDENTITY 뒤에 붙은 sequence는 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 새 기능 총정리 (이 블로그)