Postgres 激进特性为何应该先在分支版本中经受真实负载

2026-07-26 38 预计阅读时间: 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.

预计阅读时间:12 分钟

Postgres 的竞争力不只来自某项查询优化技术。对大量生产用户而言,更重要的是开源、没有厂商锁定、维护路径清晰,以及出现问题时总能找到理解内核的人。它未必在每种负载下都最聪明,但通常足够可靠、足够可预测。

这也解释了为什么一个看起来很有价值的新特性,很难直接进入 Postgres 核心。核心代码承载着关键业务数据,每增加一条执行路径,社区就要长期承担兼容性、回归风险和维护成本。一个先锋特性首先要证明安全,其次要补齐测试和文档,最后才轮到讨论性能收益与设计巧思。

核心代码的门槛不是性能,而是长期责任

数据库研究通常关心一个算法能把查询加速多少,但 Postgres 社区必须追问更多问题:

  • 崩溃恢复后,数据是否仍然一致?
  • 并发执行会不会引入过去不存在的竞态条件?
  • 新代码是否改变了已有查询计划,导致另一类负载退化?
  • 主版本升级、复制、监控和故障诊断是否仍然可用?
  • 五年后修改缓冲区、执行器或存储层时,谁来维护这条特殊路径?

因此,单纯提供一组漂亮的 benchmark 通常不够。提案还需要展示真实用户正在遭遇的问题,并证明该问题值得整个社区永久增加复杂度。

分支版本的价值正在这里:它把“理论上可行”变成“已经在完整数据库中运行过”。开发者可以让真实查询、真实数据分布和真实并发暴露设计缺陷,而不是在核心补丁审查阶段才发现基本假设不成立。

临时表并行扫描暴露出的层层风险

临时表常用于 CRM 等系统,在服务端保存尚未提交的中间状态。但在查询计划中加入临时表,可能使原本可并行执行的计划退回串行执行。

一种直观设计是:临时表的目录信息和磁盘文件对其他后端可见,真正私有的是所有者后端本地缓冲区中的脏页。因此,可以先刷新相关本地缓冲区,再让并行工作进程读取文件。

第一次在分支版本中实现后,预生产压测很快暴露了遗漏:需要处理的不只是临时表本身,还可能包括 TOAST 表、索引和其他关联对象;临时表也不一定出现在简单的扫描节点中,它可能藏在表达式、子计划或 LIMIT 等位置。只在执行器的 Gather 节点看到临时表时刷新部分页面,并不能覆盖完整依赖关系。

后续设计选择一次刷新全部脏的本地缓冲区,并扩展并行安全分类,表达“刷新本地缓冲区后安全”。优化器据此识别真正使用临时表的位置,把刷新成本加入 Gather 成本估算,再决定并行计划是否值得执行。首次运行的 Gather 延迟执行刷新,从而只在确实需要时付出成本。

真实负载又揭示了更深的问题:即便页面已经刷新,只读扫描仍可能更新 hint bit,或者清理页面上的死亡元组,使页面再次变脏。禁用并行临时表扫描期间的剪枝和 hint bit 更新可以构成安全网,但也让实现不断积累特殊规则。

这类“补丁上的补丁”是重要信号:方案可能已经可用,却未必适合进入核心。更彻底的方向是让临时表页面进入共享缓冲区,从根源上消除并行工作进程访问本地缓冲区的矛盾。不过,这会扩大存储、生命周期和资源隔离方面的设计范围,同样需要通过分支和真实负载验证。

可以这样搭建可重复的分支验证环境

下面是一套可改造的最小实验流程。假设你已经把待测试的 Postgres 分支源码检出到本机,并安装了编译依赖。将 SRC 改成源码目录,将 PREFIX 改成独立安装目录;不要复用生产数据目录。

set -euo pipefail

SRC="$HOME/src/postgres-feature-fork"
PREFIX="$HOME/opt/postgres-feature"
PGDATA="$HOME/tmp/pg-feature-data"
PORT=55432

cd "$SRC"
./configure --prefix="$PREFIX" --enable-debug --enable-cassert
make -j"$(getconf _NPROCESSORS_ONLN)"
make install

rm -rf "$PGDATA"
"$PREFIX/bin/initdb" -D "$PGDATA"
{
  echo "port = $PORT"
  echo "max_parallel_workers = 8"
  echo "max_parallel_workers_per_gather = 4"
  echo "log_min_duration_statement = 0"
} >> "$PGDATA/postgresql.conf"

"$PREFIX/bin/pg_ctl" -D "$PGDATA" -l "$PGDATA/server.log" start
"$PREFIX/bin/createdb" -p "$PORT" feature_test

随后用一个包含临时表的查询检查计划与执行行为。下面的参数用于提高测试环境产生并行计划的概率,不能直接作为生产配置:

"$PREFIX/bin/psql" -X -v ON_ERROR_STOP=1 -p "$PORT" feature_test <<'SQL'
SET max_parallel_workers_per_gather = 4;
SET min_parallel_table_scan_size = 0;
SET parallel_setup_cost = 0;
SET parallel_tuple_cost = 0;

CREATE TABLE public.orders AS
SELECT i AS id, i % 1000 AS customer_id, (i % 100)::numeric AS amount
FROM generate_series(1, 1000000) AS g(i);
ANALYZE public.orders;

CREATE TEMP TABLE changed_customers AS
SELECT i AS customer_id
FROM generate_series(1, 500) AS g(i);
ANALYZE changed_customers;

EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT sum(o.amount)
FROM public.orders AS o
JOIN changed_customers AS c USING (customer_id);
SQL

这段 SQL 不是用来证明某个补丁正确,而是提供可重复的观测入口。应在官方版本、功能分支和关闭该功能的分支版本中分别运行,并保存以下结果:

  • 是否生成 GatherGather Merge
  • 执行时间、缓冲区读写量以及临时文件使用量;
  • 并行工作进程启动数与实际参与数;
  • 服务器日志中是否出现断言失败、警告或异常退出;
  • 高并发和重复执行后,结果是否始终一致。

还需要主动构造容易遗漏的场景,例如带索引的临时表、产生 TOAST 数据的大字段、子查询、表达式引用、提前退出的 LIMIT、事务回滚,以及并行扫描与临时表更新交替发生的工作负载。性能测试必须与正确性测试并行推进,否则更快的错误结果没有意义。

分支、扩展与核心补丁各自解决什么问题

纯扩展通常是最轻量的发布方式,但涉及优化器、执行器、缓冲区管理或存储布局的特性,很难只依靠稳定扩展接口完成。即使技术上可以实现,用户也可能因为升级、复制、崩溃恢复和云平台支持的不确定性而拒绝部署一个缺乏生产记录的扩展。

分支适合承担中间阶段:企业或研究团队可以控制升级节奏,补充诊断工具,并让功能在真实系统中失败。这里的失败不是浪费,它能区分“实现有 bug”和“整个抽象方向不对”。临时表并行扫描的经验表明,后者往往只有在完整系统中才会显现。

核心补丁则需要更强的证据:稳定的用户需求、可以解释的设计边界、持续维护者,以及足以覆盖回归面的测试。分支中的成功部署不是自动进入核心的通行证,但它能把讨论从设想推进到证据。

让验证结果重新进入社区视野

技术验证只是第一步。Postgres 的协作重心仍在邮件列表,主题数量、历史讨论和内部术语都提高了参与成本。远程开发者很可能发布补丁后得不到深入反馈。

更可执行的做法是尽早公开提案,即使第一版还不完整。帖子应明确描述用户问题、当前核心行为、分支实现、已知失败案例,以及接下来需要社区判断的设计问题。之后持续在同一讨论线程中报告测试结果和设计变化,使搜索引擎、工具和未来遇到相同问题的开发者能够找到完整上下文。

准备推进一个先锋 Postgres 特性时,可以使用这份检查清单:

  • 先用实际查询证明用户问题,而不只是展示算法优势;
  • 在独立分支中完成端到端实现,避免只验证孤立模块;
  • 同时记录正确性、回归、运维成本和性能数据;
  • 把真实负载暴露的失败案例写进测试与设计文档;
  • 当特殊规则不断增加时,重新检查底层抽象是否选错;
  • 尽早发布到社区渠道,并持续更新同一讨论上下文;
  • 在进入核心前明确长期维护者和升级策略。

Postgres 核心并不是展示新算法的低阻力舞台。它更像一份长期公共承诺:代码一旦进入,整个社区就要一起维护。先在分支里运行、失败、修正并积累用户证据,能让先锋想法以更成熟的形式接受这项承诺。


相关推荐