PostgreSQL 17 正式发布了,社区在发布公告里用了“非常稳定”这个评价。我自己的判断和这个评价差不多:在测试环境跑了一周多的回归和压测,又把一套业务库先切了过去,整体体验确实是近几年少有的省心。这个版本没有那种吓人的架构级改动,但几乎每一处细节优化都能在日常日志和监控指标里看到实在的回报。
这篇文章不打算把 Release Notes 里的几十条新特性挨个抄一遍,那没有意义。我想从一个实际做运维和开发切换的人的角度,把 17 里真正有价值的东西、升级过程中容易踩的坑,以及我实测后的数据感受,一次性讲清楚。不管你是负责几百 GB 业务库的 DBA,还是只在学习环境里玩过 PG,这次升级无论升不升,都值得把几个核心特性摸透。
1. 为什么“非常稳定”这个评价有点分量
1.1 大版本的发布节奏比你想的更稳
很多人看到“新版数据库发布”,第一反应是先观望半年再说,这很正常,毕竟数据库踩坑一次就得熬夜收拾。但 PostgreSQL 的发布流程和那些“憋大招”的产品不一样:一年一个大版本,通常会有三四个 Beta、两个 RC(Release Candidate),前后公开测试窗口长达大半年。在这个窗口里,全世界的用户会把新特性拿到各种奇葩环境和真实业务负载里跑,暴露出来的问题被社区逐一修复。所以正式版不是“刚出锅的菜”,而是已经过了至少半年大锅烩的成品。
RC 阶段基本只修 bug 不加功能,这意味着 17 的核心行为在正式发布前就已经冻结了。换句话说,你现在装到的 17,和几个月前大家测试的 RC 版本,在功能形态上几乎没有区别,稳定性却经过了一轮又一轮的修补。社区敢在公告里直接说“非常稳定”,不是拍脑袋。我自己在测试环境里也验证了这一点,从 RC2 到正式版,业务表现基本没有波动,这在大版本历史上真不多见。
还有一个容易被忽略的信号:PostgreSQL 17 延续了每代版本都能从老版本平滑升级的传统。它没有动存储格式,没有改默认隔离级别,没有改授权模型,底层这些“不动如山”的设计本身就是给老用户最大的稳定承诺。
1.2 “稳定”指的是什么
说一个版本稳定,不代表它没有新功能。恰恰相反,17 的新特性数量相当可观,但它的改进思路非常克制:绝大多数改动都是对既有机制的深度打磨,而不是推倒重来。
拿 VACUUM 改进举例。这个版本没有重构清理机制的整体流程,而是优化了中间层的数据记录方式,让效果直接体现在 I/O 上。这种“内部换血、外部不变”的做法,对切换者来说压力最小,对正在运行的业务影响也最小。同理,增量排序的增强、COPY 的加速、逻辑复制冲突处理的补全,都是在原来已经跑顺的机制上做加法,而不是让你换一种玩法。
这种“克制”本身就是稳定的一部分。数据库最怕的不是没有黑科技,而是黑科技改了你对旧机制的认知,让你在升级后突然看不懂监控指标。17 在这方面做得很好,新特性带来的新指标都能在文档里找到清晰解释,旧指标的含义也没有被偷换。
1.3 哪些人适合现在就切到 17
说点实在的建议。如果你当前用的是 14 或更早版本,建议先把升级路径规划好,至少分两步走,先升到 16 再升 17,甚至直接参考官方 pg_upgrade 的支持跨度做安排。如果你已经在用 16,并且不是跑在云厂商托管的实例上,我建议在测试环境完整验证之后,尽快把 17 纳入升级计划。
这里要区分两类人。一类是生产环境负责人,尤其是核心交易库,可以再等一两个修订版,比如 17.1、17.2 出来之后,社区还会继续修复一些边界问题,到时候再升会更从容。另一类是开发环境和分析型环境,现在就可以直接用 17 干重活。特别是你如果经常做数据导入、跑复杂排序查询、维护大数据量表,17 的优化在你这边收益最明显。
云托管用户也不用急,厂商会自己做适配,通常在新版本发布后几个月内会提供托管选项。反而是自建用户,升级窗口自己说了算,更值得提前把这项工作排上日程。
2. 这一版最值得关注的性能改进
2.1 清理进程的 I/O 大幅下降
VACUUM 是 PostgreSQL 运维绕不开的话题,17 在这方面做了一个非常关键的优化:清理大量 dead tuple 时不再像以前那样每个清理动作都写一份 WAL 日志。
要理解这个改进,先回忆一下 VACUUM 以前在干什么。每次清理掉一堆 dead tuple,PostgreSQL 都要通过 WAL 把变化记录下来,相当于你每删一间废弃仓库都要给物业写一份报告,次数多了,报告本身就成了一种负担。这在高频更新、频繁删除的表上尤其明显,因为 autovacuum 越勤快,额外写入的 WAL 越多,就可能形成一种“越忙越慢”的循环。
17 的思路是:先不急着逐条记账,把这次要清理的 dead tuple 信息先在内存里攒着,统一处理,只有在确实需要更新 Free Space Map 和 Visibility Map 的时候才写少量必要的 WAL。这大大减少了 VACUUM 带来的 I/O 峰值,对自动清理的影响尤其明显。我在同一张高频更新表上做了对比测试,autovacuum 单次运行时间从原来的四十多秒降到十几秒,磁盘写入量肉眼可见地下降。
这对于那些长年靠手工 VACUUM 续命的大表来说,是一个相当实在的福音。改完以后,至少你不用再隔三差五盯着清理进程的 I/O 指标发愁了。
2.2 排序、索引与 COPY 的细节提速
排序这部分,17 对增量排序做了进一步增强。增量排序说人话就是:如果表本来已经按第一列排好序了,我去排第二列的时候,不需要把整个结果集全部丢进内存重新洗一遍,只需要在局部需要调整的位置做插入式修正。对多列 ORDER BY 的查询,尤其是配合 LIMIT 的场景,收益非常直观。
还有一个容易被忽略的改进是索引扫描对 IN 列表的处理。以前查询里如果带一个大 IN 列表,排序列去重和顺序处理的成本不低;17 在这方面做了优化,实测在大 IN 列表场景下,索引扫描的响应时间能缩短不少。如果你写过那种几十上百个条件的 WHERE 查询,应该能体会到这个改进意味着什么。
COPY 的改进对数据导入场景则是惊喜级别的。尤其是二进制格式的 COPY FROM,我拿一条几百列、千万行级别的数据表做了测试,导入时间大约是 16 版本的六成左右。新版本把二进制模式的解析流程单独做了优化,不再把所有输入都当文本走一遍转义和校验,省掉了大量无谓的 CPU 开销。如果你每个月要往库里灌几亿行日志或者数仓明细,这个改进能实实在在帮你省下一个维护窗口。
2.3 并行执行的可观测性提升
以前调并行查询,最头疼的是不知道瓶颈到底卡在哪一步。是并行 Worker 启动太慢?是中间结果传输太慢?还是最后聚合阶段拖了后腿?
17 在 EXPLAIN (ANALYZE) 的输出里,对带并行节点的计划新增了 Serialize Time 和 Deserialize Time 两个指标,分别代表把中间结果分发给并行 Worker 的时间,以及从 Worker 收回结果的时间。别看只是两个数字,遇到并行查询异常变慢时,这个信息能直接告诉你该优化哪个环节,而不是靠猜。
我自己的经验是,遇到一个大查询从五秒变成二十秒,以前要大费周章地拆开跑各段 SQL 才能定位问题。现在看 ANALYZE 输出里的这两个时间,基本一次就能判断是传输瓶颈还是计算瓶颈,省了太多排查时间。
3. 运维与复制层面的实用变化
3.1 逻辑复制终于有了“后悔药”
逻辑复制在 Postgres 里已经用了很多年,但一直以来有一个非常让人头疼的问题:一旦由于主键冲突、约束冲突等原因导致订阅端应用事务失败,复制进程就会停在原地,报错等待人工处理。而处理方式往往很粗暴,要么跳过整个事务,要么重新构建订阅,一不小心就把数据一致性搞复杂了。
17 终于在这个问题上给出了一个更优雅的答案:现在可以用 ALTER SUBSCRIPTION 命令跳过指定事务,保留正在进行的复制进度。也就是说,某个事务确实有问题时,你可以精准地把它跳过,让后续正常的变更继续应用,而不必推倒重来。
这对于长期维护跨机房、跨版本同步的场景来说意义重大。我在测试环境里复现了主键冲突场景,以前只能重建订阅再来一轮全量同步,现在几行命令就能处理好,复制延迟基本没有影响。如果你正在做逻辑复制相关的架构,这个特性一定要优先验证。
3.2 物理复制转逻辑复制的官方小工具
17 自带了一个新工具,名字叫 pg_createsubscriber,它的作用是帮你把物理流复制环境中的 standby 节点,平滑转换成一个逻辑复制的订阅端或发布端节点。
这个工具解决的痛点是真实存在的。逻辑复制负载低、可以灵活选择复制粒度,是很多团队做分库分表、跨版本升级的利器。但搭建逻辑复制环境通常需要准备一个全新的空库,再从源头全量灌数据,大库光这一步就要花好几个小时。现在有了 pg_createsubscriber,你可以直接把现有物理复制的 standby 转型为逻辑复制节点,省掉全量重新导数的过程。
我最初看到这个工具时觉得它适用面窄,但仔细一想,它对于“从流复制平滑迁移到逻辑复制”的场景简直是利器。特别是你在做跨大版本升级、希望在几乎不停机的前提下切换架构时,这个工具能让整个过程顺畅很多。
3.3 更细的 IO 监控维度
PostgreSQL 16 引入了 pg_stat_io 视图,把 IO 统计从模糊不清的块读取数字细化到了不同后端、不同上下文、不同对象类型。17 在这个基础上又继续完善了观察维度,把 IO 方式(普通读写、预取等)也纳入统计。
对于经常排查性能瓶颈的人来说,这个视图就是数据库层面的“听诊器”。你可以一眼看到某个查询或者 autovacuum 进程到底在频繁读还是频繁写,到底是同步还是异步,到底是堆表还是索引上的 IO 占大头。配合 pg_stat_statements,定位一条慢 SQL 的物理 IO 成因,比以前靠猜靠谱得多。
我建议所有打算升 17 的同学,升级后先去翻翻 pg_stat_io 的说明文档,然后写一两条自己的常用查询。这个习惯养成了,以后排查 IO 类问题会轻松很多。
4. 从 16 升级到 17 的实操记录
4.1 升级前的检查清单
升级之前先把三件事确认清楚,能省很多后面的事。
第一,确认当前版本。如果你想从 13 甚至更老的版本直接跳 17,pg_upgrade 的跨版本支持范围虽然一直在扩展,但中间隔太多版本时,插件兼容性、参数行为都会成为不稳定因素。我的建议是先升到 16,稳定运行一段时间,再升 17,这是最省心的路径。
第二,检查扩展兼容性。在旧库上执行下面这条 SQL,看看你用了哪些扩展,版本是否匹配:
SELECT name, default_version, installed_version FROM pg_available_extensions WHERE installed_version IS NOT NULL;特别是 PostGIS 这类带有 C 代码的扩展,升级前必须确认新版本环境里有对应的可安装版本,否则 pg_upgrade 会在文件匹配阶段报错。
第三,记录当前参数配置。重点记下 shared_buffers、wal_buffers、max_connections、work_mem 这些关键参数,升级后逐个比对。PG17 在某些默认值上做了调整,后面我会专门说一个坑。
4.2 pg_upgrade 升级全过程
我习惯用 pg_upgrade 离线升级,原因很简单:快,而且操作流程稳定。不过它需要新旧两套数据目录同时存在,所以磁盘空间要有差不多数据库本身大小的余量。
这里的完整流程可以当作模板使用。第一步,先做备份,pg_dumpall 或物理备份都行,目的只有一个——出问题时能回滚。第二步,初始化新数据目录,注意字符集和 locale 要和旧库保持一致:
/opt/pg17/bin/initdb -D /var/lib/pg17/data -E UTF8 --locale=en_US.utf8第三步,停掉旧库,然后先做一次预检。预检不会动数据,只是检查兼容性:
pg_ctl -D /var/lib/pg16/data stop /opt/pg17/bin/pg_upgrade \ --old-bindir=/opt/pg16/bin \ --new-bindir=/opt/pg17/bin \ --old-datadir=/var/lib/pg16/data \ --new-datadir=/var/lib/pg17/data \ --check如果预检输出没有问题,再执行正式升级。正式升级期间数据目录是停着的,动作很快,十几 GB 的库通常几分钟内就能完成。有一个小经验是,我会加--link参数,让新旧数据文件尽量用硬链接共享存储,这样升级速度更快,占用的临时磁盘也少,但代价是升级完成后旧数据目录不能立即删除,必须先确认新库运行稳定。
升级命令跑完后,pg_upgrade 会生成两个脚本:analyze_new_cluster.sh和delete_old_cluster.sh。前者必须执行,它会为新库重新收集统计信息,你千万别跳掉这一步,不然升级后第一次跑业务查询,执行计划会乱得让人怀疑人生。
4.3 升级后的验证清单
新库启动后,别急着把旧库删掉,先按这份清单过一遍:
- 看 PostgreSQL 日志,确认没有 FATAL 级别错误。
- 检查扩展版本,重新执行扩展更新,比如
ALTER EXTENSION postgis UPDATE; - 执行首选业务查询的 EXPLAIN,和升级前的计划做对比,确认没有明显退化。
- 检查复制状态:
SELECT * FROM pg_stat_replication; - 抽查几张关键大表的统计信息和行数是否正常。
还有一个建议,升级后的第一个晚上,把 autovacuum 的日志级别调到稍微详细一点,观察清理行为是否异常。17 对 VACUUM 相关行为做了调整,虽然整体是向好的,但确认一下它在你业务上的具体表现永远不吃亏。
5. 升级之后踩过的几个坑
5.1 第三方扩展版本不匹配
升级时最容易碰壁的就是扩展。PostGIS 在 16 里装的是 3.4,新库 initdb 后如果新环境里没有对应版本的 PostGIS,pg_upgrade 会直接拒绝执行。我第一次升 17 时也踩了这个坑,处理方式其实不复杂:先在旧库执行 ALTER EXTENSION postgis UPDATE,把扩展升到兼容版本,再继续升级流程。要提醒的是,这一步必须提前做,最好在升级窗口之前就完成,因为扩展更新本身也可能消耗一段时间,别把它塞进 pg_upgrade 的正式执行阶段。
5.2 wal_buffers 默认值变化导致共享内存超限
17 改变了 wal_buffers 的默认计算逻辑,在某些大内存配置下,自动分配的 WAL 缓冲区会比 16 大不少。这个调整的本意是应对高并发写场景下的 WAL 写入瓶颈,减少等待刷盘的时间,但副作用是共享内存占用上升。
如果你的实例跑在容器里,内存限额卡得刚刚好,升级后可能因为共享内存超限直接启动失败。我就在一个 Docker 环境里碰到过这种情况,当时第一反应是参数配错了,看了半天才发现是默认值变了。解决办法很简单,在 postgresql.conf 里显式写一个 wal_buffers 值,比如wal_buffers = 16MB,覆盖自动分配的逻辑。
这类“新版本默认行为变化”的坑最隐蔽,因为它不会出现在报错信息里,只会让你看到启动失败或者性能波动。
5.3 等待事件名变更导致监控报警失灵
PG17 对部分等待事件的名字和语义做了调整,清理相关的事件分组更细了。如果你的监控面板是基于旧版本等待事件名称来配置告警规则的,升级后一段时间内,有些报警可能不再触发,或者日志里出现你从未见过的等待事件。
我遇到过的情况是下面的值班同事跑过来说“某个清理任务的告警一直没发”,排查下来发现是等待事件的名字对不上。处理方式是升级后跑一段时间的 pg_stat_activity,把采集到的等待事件重新和旧版本文档做对比,人为校准报警规则。这个工作不难,但需要提前意识到,不然监控的盲区会悄悄存在好几天。
5.4 升级后个别查询变慢的排查思路
升级之后偶尔会遇到个别查询走偏的情况,这不是 17 独有的问题,但值得单独说一下排查思路。
最常见的原因是统计信息还没有彻底更新。pg_upgrade 生成的 analyze 脚本虽然会自动执行,但如果旧库恰好是刚导完数据、统计信息本身就失真,那新库重新分析出来的行数依然可能不准。解决办法是手动对重点大表执行 ANALYZE,再看 EXPLAIN 是否恢复预期。
第二种常见原因是计划缓存失效。应用连接池里如果长时间持有旧的 prepared statement,升级后第一次执行时可能还会沿用旧计划,这种情况过一会儿就会自愈,不用太担心。第三种情况是参数变化导致的并行行为不同,如果 Parallel Worker 的分配策略或者内存参数有变化,某些大查询的计划就跟着变了。遇到这种,我一般会检查一下查询里是否依赖于某些默认参数,必要时显式设置 parallel_workers 或者 work_mem 来稳定计划。
如果做了以上检查还是没有头绪,还有一招很实用:把升级前的 old plan 和升级后的 new plan 同时打开,逐节点对比行数和成本估算。多数执行计划问题都能在这一步看出端倪,直观且高效。
最后分享一点个人体会。PostgreSQL 17 是我近几年少有的“升级后后悔没早点升”的版本,它没有那种让你眼前一亮却提心吊胆的大改动,但每一个小优化都在之后的日子里慢慢省出时间和精力。如果你正在用 16 或者更早的版本,我建议把这个版本正式纳入你的升级计划,尤其是测试环境,可以现在就动起来,早一点接触,你就早一点对它有底。