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나 마이그레이션에서 흔한 흐름이다.

  1. publisher(구 primary)에서 subscriber(신 primary)로 logical replication을 건다.
  2. 테이블 데이터는 잘 넘어온다. row가 수백만 건 들어와 있고, id 컬럼은 publisher에서처럼 큰 값까지 차 있다.
  3. cutover 시점에 subscriber를 promote하고 애플리케이션을 붙인다.
  4. 첫 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 SEQUENCESALL 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

srsubstater 이면 그 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_countsync_table_error_count로 이름이 바뀌었다. sequence 오류가 별도로 집계되기 때문이다. 모니터링 대시보드가 sync_error_count를 직접 참조하고 있었다면 19 업그레이드 때 컬럼명을 손봐야 한다. (릴리스 노트)

PostgreSQL 18 이하 vs 19

PG18 이하PG19
logical replication의 sequence 처리복제 제외동기화 지원
failover 후 첫 INSERTduplicate 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_statssync_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가 더 이상 사고의 출처가 아니게 된다.

참고 자료