PostgreSQL 升级里最容易漏掉的序列同步问题

2026-06-30 24 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:6 分钟

pg_upgradepg_createsubscriber 组合做 PostgreSQL 19 集群升级,思路很诱人:先把物理只读副本转换成逻辑订阅端,让新集群追上旧集群,再在很短的窗口里切流量。这个流程把停机时间压得很低,但也暴露了一个长期存在的坑:逻辑复制不会复制 sequence 的当前状态。

这不是一个边角问题。只要表里有 serialbigserialGENERATED ... AS IDENTITY,背后通常都有 sequence。数据行复制过去了,如果 sequence 没跟上,应用切到新集群后,下一次插入可能生成已经存在的主键。

为什么表数据同步了,主键还会撞

PostgreSQL 的逻辑复制主要关注表数据变化:INSERTUPDATEDELETE。sequence 的 nextval() 状态并不是普通表行变化,它维护的是一个独立对象的计数状态。

因此在近零停机升级流程中,可能出现这样的时间线:

  • 旧集群继续服务写入请求,表数据通过逻辑复制进入新集群。
  • 业务不断调用 nextval(),旧集群 sequence 持续前进。
  • 新集群收到了新行,但本地 sequence 仍停在较旧的位置。
  • 切流量后,新集群执行 nextval(),生成一个已经存在的 ID。

这个问题在低写入量系统里可能很久才暴露;在高并发写入系统里,可能切换后第一批请求就报错。

升级前后应该把 sequence 当成独立资产

做这类升级时,不要只问“表追平了吗”,还要问“sequence 也追平了吗”。尤其是以下对象:

  • 使用 serialbigserial 的主键列。
  • 使用 GENERATED BY DEFAULT AS IDENTITY 的列。
  • 应用直接调用 nextval('some_sequence') 的业务编号。
  • 多租户、分片或批量导入任务中自定义维护的 sequence。

更稳妥的做法是:在最终切流量窗口里,暂停旧集群写入,确认逻辑复制延迟归零,然后从旧集群抽取 sequence 状态,并在新集群执行 setval()。这样不会依赖逻辑复制去做它本来不做的事情。

可以这样实践:导出并同步所有 sequence

下面示例假设你有两个连接串:

  • OLD_DSN 指向升级前仍在服务的源集群。
  • NEW_DSN 指向升级后的目标集群或逻辑订阅端。

切换前先暂停应用写入,然后运行:

#!/usr/bin/env bash
set -euo pipefail

: "${OLD_DSN:?set OLD_DSN first}"
: "${NEW_DSN:?set NEW_DSN first}"

TMP_SQL="/tmp/sync_sequences.sql"

psql "$OLD_DSN" -At <<'SQL' > "$TMP_SQL"
SELECT format(
  'SELECT setval(%L, %s, %s);',
  quote_ident(sequence_schema) || '.' || quote_ident(sequence_name),
  last_value,
  is_called
)
FROM information_schema.sequences s
JOIN LATERAL (
  SELECT last_value, is_called
  FROM pg_catalog.pg_sequences
  WHERE schemaname = s.sequence_schema
    AND sequencename = s.sequence_name
) g ON true
WHERE sequence_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY sequence_schema, sequence_name;
SQL

psql "$NEW_DSN" -v ON_ERROR_STOP=1 -f "$TMP_SQL"

运行前需要根据你的权限模型确认两点:执行用户能读取源端 sequence 状态,也能在目标端执行 setval()。如果环境里有非常老的 PostgreSQL 版本,pg_sequences 视图的可用性也需要单独确认;可以改用逐个 SELECT last_value, is_called FROM schema.seq 的方式生成 SQL。

同步后,可以用下面的查询快速检查目标端 sequence 是否小于表内最大 ID。这里以 public.orders(id) 为例:

SELECT
  max(id) AS max_table_id,
  last_value AS sequence_last_value,
  last_value >= max(id) AS sequence_is_safe
FROM public.orders,
     public.orders_id_seq;

如果 sequence_is_safefalse,就应该在目标端修正:

SELECT setval('public.orders_id_seq', (SELECT max(id) FROM public.orders), true);

把检查放进切换清单,而不是靠记忆

真正危险的不是 sequence 不复制,而是团队以为“逻辑复制已经覆盖所有状态”。建议把下面几项写进升级 runbook:

  • 切换前冻结旧集群写入,避免同步过程中又产生新的 nextval()
  • 等待逻辑订阅追平,确认表数据没有复制延迟。
  • 从源集群导出所有业务 schema 下的 sequence 状态。
  • 在目标集群执行 setval(),并抽样验证关键表的 max(id)
  • 切流量后监控唯一键冲突、主键冲突和应用插入失败。

近零停机升级的关键,不只是让数据行过去,还要让数据库对象的“隐含状态”过去。sequence 正是这种隐含状态里最容易被忽略、也最容易造成生产事故的一类。


相关推荐