PGConf.dev 2026:Postgres 集群拓扑、物理读观测与排序的三道未解题

2026-05-26 23 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:11 分钟

刚从温哥华回来,PGConf.dev 2026 的技术讨论密度远超预期。Jeremy Schneider 在会后总结中梳理了几条真正触及 Postgres 设计短板的线索——不是"新功能很酷"那种,而是"我们一直假装没问题但其实有结构性缺口"那种。下面挑三条最值得生产环境用户关注的展开。

Postgres 缺一个"集群拓扑"的第一性概念

物理复制做高可用,已经是 Postgres 用户最主流的方案。但一旦你真正在多节点间做 failover,就会发现一堆没解的粗糙边缘:两次连续 failover 后逻辑复制槽丢失、从 standby 无法解码逻辑复制、WAL 备份在集群层面缺乏统一管理。

unconference 讨论的前半段被"HA 该不该进 core"吃掉了,但更有价值的方向是后半段——Postgres 缺一个关于集群拓扑的顶层概念。

目前你能做的,只是通过 synchronous_standby_names 声明哪些节点是同步备库。但这个参数既不描述完整拓扑,也不支持异步节点(你没法把数量设为 0 来声明"我有一个异步集群"),更难处理参数变更的一致性传播:你怎么确认每一个连接和 session 都已经感知到新的集群成员列表?

Schneider 提了一个思路:把 synchronous_standby_names 里的节点集合定义为"集群"(他原话叫 herd,因为 cluster 在 PG 里已有含义),然后在此基础上构建 pg_nodes 视图或函数。比如:

-- 现状:你只能这样查同步备库
SELECT * FROM pg_stat_replication;

-- 设想中的 pg_nodes:一次看清整个拓扑
-- 这是概念性示例,目前 PG 不存在此视图
SELECT node_name, node_type, upstream_node,
       replication_mode, sync_state
  FROM pg_nodes;

-- 设想中的 failover 咨询函数
-- 主库不可达时,在 standby 上调用
SELECT candidate_node, lag_seconds, priority
  FROM pg_failover_candidates();

更棘手的问题是参数变更的传播。当你加一个同步备库,需要所有连接都知道新成员——因为每个连接自己负责确认事务已复制到足够节点后才向客户端返回 commit。如果某个连接还在用旧的成员列表,它可能向客户端误报"已同步"。

# 当前变更 synchronous_standby_names 的粗暴方式
# 修改 postgresql.conf 后 reload,但无法保证所有 session 立即感知

psql -c "ALTER SYSTEM SET synchronous_standby_names = 'standby1, standby2';"
psql -c "SELECT pg_reload_conf();"

# 你无法精确知道每个连接何时采纳了新配置
# 只能靠应用层重连来"强制刷新"

逻辑复制槽的问题同样指向拓扑概念:连续两次 failover 后槽位丢失,本质上是因为槽位只绑定在单个节点上,没有集群级别的槽位协调。如果有一个集群概念,槽位可以随拓扑自动迁移。

物理读观测:pg_stat_kcache 的窗口即将被 PG 18 的 AIO 打破

pg_stat_statements 告诉你逻辑读(从 OS page cache 读的),但不告诉你真正的物理读(从磁盘读的)。生产环境里这个区别至关重要——一个查询可能"读了 1000 个 shared buffer page",但其中 990 个已经在 OS cache 里,只有 10 个真正触发了磁盘 IO。你优化这 10 个 page 才有收益。

目前要拿到物理读数据,只能靠 pg_stat_kcache,它调用 getrusage() 获取进程级 IO 统计:

-- 安装 pg_stat_kcache
CREATE EXTENSION pg_stat_kcache;

-- 查看每个查询的物理读
SELECT query,
       reads AS physical_reads,
       reads_blks AS physical_read_blocks,
       writes AS physical_writes
  FROM pg_stat_kcache_by_query
 ORDER BY physical_reads DESC
 LIMIT 10;

但 PG 18 引入 AIO(异步 IO)后台 worker 后,getrusage() 只统计调用进程自身的 IO——后台 worker 的 IO 不计入查询进程。这意味着 pg_stat_kcache 可能漏掉大量物理读。

Sami Imseih 正在推进 pg_stat_statements 的 LWLock 并发优化和查询文本文件改进,但物理读统计的 AIO 适配仍是开放问题。getrusage() 的调用频率开销也需要基准测试验证——如果每查询调用一次还行,但如果需要从每个 AIO worker 分别收集,开销可能不可接受。

# 检查你的容器环境是否支持 io_uring
# GKE 和 Docker 默认 seccomp profile 禁用 io_uring
# 这意味着部分用户可能被迫禁用 IO worker

# Docker 默认 seccomp 中 io_uring 相关 syscall 被禁用:
# io_uring_setup, io_uring_enter, io_uring_register

# 如果你在容器中运行 PG 18,可能需要:
docker run --security-opt seccomp=unconfined ... postgres:18

# 或者干脆禁用 AIO worker 以保留 kcache 统计
# (假设 PG 18 提供 GUC 参数,目前为假设性示例)
# postgresql.conf
# io_workers = 0

另一个可观测性缺口是 Wait Events。目前 pg_stat_activity 显示当前等待事件,但不累计历史。Schneider 建议给每个 wait event 加计数器和总时长。LWLock 的 wait event 只在真正让出 CPU 等信号量时才注册——快速获取锁的情况不注册——所以计数器开销可能可控:

-- 设想中的 wait event 累计统计(目前不存在)
SELECT wait_event_type, wait_event,
       count AS trigger_count,
       total_duration_ms
  FROM pg_stat_wait_events
 ORDER BY total_duration_ms DESC
 LIMIT 20;

-- 现有的 LWLOCK_STATS 是编译期选项,默认不启用
-- 如果你想在调试时启用,需要重新编译:
# CFLAGS="-DLWLOCK_STATS" ./configure --enable-debug ...

排序(Collation):ICU 升级会让你的索引失效

Jeff Davis 和 Schneider 的"退休后再次讨论排序"传统继续。核心问题是:语言排序规则会变,就像时区会变一样。ICU 每次升级可能调整语言学排序,而排序规则一变,所有使用该 collation 的索引就失效了——Postgres 目前没有 ICU 升级的平滑处理方案。

PG 已经有了 pg_c_utf8——一个内置的稳定 code-point 排序规则:

-- 创建数据库时用 pg_c_utf8 作为默认排序
CREATE DATABASE myapp
  LC_COLLATE = 'pg_c_utf8'
  LC_CTYPE = 'pg_c_utf8'
  ENCODING = 'UTF8';

-- 在需要语言学排序的列上显式指定
CREATE TABLE users (
  name TEXT COLLATE "fr_FR.utf8",  -- 法语排序的列
  email TEXT  -- 默认 pg_c_utf8,索引稳定
);

-- 索引用 pg_c_utf8,不受 ICU 升级影响
CREATE INDEX idx_users_email ON users (email);

但现实是:非英语用户不想在每个列和表达式上手动加 COLLATE "fr_FR.utf8"。Schneider 提了一个"client locale"概念——数据库和索引始终用 pg_c_utf8,但客户端连接可以声明自己的 locale,让 ORDER BY 默认按语言学排序,而比较运算符(<>)默认用 codepoint。Oracle 的做法是:部分运算符默认 binary,部分默认 client locale,索引始终 binary 且大多数查询仍能利用索引。

-- 设想中的 client locale 机制(概念性示例,PG 目前不支持)
-- 连接时声明客户端 locale
SET client_locale = 'fr_FR.utf8';

-- 此时 ORDER BY 默认按法语排序
SELECT * FROM users ORDER BY name;
-- 等效于: SELECT * FROM users ORDER BY name COLLATE "fr_FR.utf8";

-- 但 < > 比较默认仍用 pg_c_utf8(codepoint)
-- 这样 pg_c_utf8 索引仍可被查询计划利用

-- 索引始终基于 pg_c_utf8,ICU 升级不影响
CREATE INDEX idx_users_name ON users (name COLLATE "pg_c_utf8");

关键挑战在于:当用户请求语言学排序时,bttextcmp() 能否仍然使用 pg_c_utf8 索引?或者在执行计划层面,能否只在顶层节点应用语言学排序,底层扫描仍用 codepoint 索引?这些还在头脑风暴阶段,没有确定方案。

落地建议

这三个缺口不是"下一个版本就会解决"的——它们需要设计层面的共识和多年迭代。但生产环境用户现在就可以做几件事:

  1. 复制拓扑:用 synchronous_standby_names + pg_stat_replication 搭建自己的拓扑视图,不要假设 failover 是自动安全的。连续 failover 场景下,手动检查逻辑复制槽状态再决定是否 promote。

  2. 物理读观测:现在就部署 pg_stat_kcache,建立物理读基线。PG 18 上线前,在容器环境测试 AIO worker 是否可用,评估是否需要禁用 IO worker 以保留统计。

  3. 排序稳定性:新数据库一律用 pg_c_utf8 作为默认 collation。对已有数据库,至少确保索引列用 pg_c_utf8,语言学排序只在 ORDER BY 和显式比较时指定。ICU 升级前,检查依赖 ICU collation 的索引数量并做好重建预案。

# 检查你的数据库中有多少索引依赖 ICU collation
psql -c "
SELECT indexname, indexdef
  FROM pg_indexes
 WHERE indexdef LIKE '%icu%' OR indexdef NOT LIKE '%pg_c_utf8%';
"

# 检查当前默认 collation
psql -c "
SELECT datname, datcollate, datctype
  FROM pg_database;
"

Postgres 的成熟度毋庸置疑,但"能用"和"有清晰的第一性概念支撑"之间仍有距离。集群拓扑、IO 观测、排序稳定性——这三条线索指向的是同一个底层问题:Postgres 在分布式和国际化场景下,还缺少几个让复杂问题自然变简单的概念锚点。


相关推荐