PostgreSQL Field Guide

OS 升级与索引的静默损坏

glibc 或 ICU 升级改变文本排序规则、悄悄破坏 B-tree 索引的机理,以及如何排查、验证和重建

text 列上的 B-tree 索引按插入那一刻生效的排序规则(collation)排列键值。libc collation 的排序规则来自操作系统的 C 库。一次 OS 升级如果带了新的 glibc(或新的 ICU 库),规则就可能变化——而 PostgreSQL 不会拿新规则重新校验既有索引。查询照常使用这些索引,于是悄悄返回错误结果:范围扫描和前缀匹配丢行、ORDER BY ... LIMIT 输出不对、唯一性检查漏掉重复值。全程没有任何报错。

OS 升级如何损坏索引

libc collation 的字符串比较走操作系统的接口(strcoll_l 等)。glibc 改变某个 locale 的排序规则后,升级之后插入的键按新规则放置,升级前写入的键仍停在旧规则的位置上。索引在结构上完全正常——页面链接、校验和、元组格式都没问题——但它不再服从一套一致的顺序。任何依赖顺序的索引扫描都可能提前停止、跳过条目或按错误顺序返回。

最著名的触发点是 glibc 2.28(2018 年),它把大量 locale 对齐到了新的通用排序模板。跨代发行版升级——例如 RHEL/CentOS 7 → 8、Debian 9 → 10——是经典的踩坑场景,但任何发行版升级都可能携带变化的 locale 数据。ICU collation 暴露的是同一个问题,只是触发源换成了 ICU 库升级,与 glibc 无关。

哪些索引有风险

  • textvarcharchar 及其 domain 上的 B-tree 索引,当列 collation 由 libc 提供且不是 C/POSIX 时——包括数据库本身使用 libc provider 时、使用数据库默认 collation 的索引。
  • 这些列上的唯一约束和主键,因为底层就是这样的索引。
  • ICU provider 的 collation 受 ICU 库版本变化影响,与 glibc 无关。

不受影响的:CPOSIX collation(按字节序比较,到处稳定)、builtin provider 的 locale 如 C.UTF-8(设计上不可变,PostgreSQL 17+)、hash 索引(无顺序概念),以及 integerbigintuuidtimestamptz 等不可排序类型上的索引。

provider 语义见 PostgreSQL 18 官方文档 Collation Support

升级前先做索引盘点

任何 OS 升级之前,先列出所有依赖 libc 排序的索引并存档——这是事后比对的基线:

SELECT DISTINCT
  i.indexrelid::regclass AS index_name,
  i.indrelid::regclass   AS table_name,
  c.collname             AS collation,
  c.collprovider         AS provider   -- c = libc, d = default, i = icu, b = builtin
FROM pg_index i
JOIN LATERAL unnest(i.indcollation) AS u(coll_oid) ON true
JOIN pg_collation c ON c.oid = u.coll_oid
WHERE c.collprovider IN ('c', 'd')
  AND c.collname NOT IN ('C', 'POSIX')
ORDER BY 2, 1;

default collation(provider 为 d)继承数据库 locale,因此还要确认每个数据库实际使用什么:

SELECT datname, datcollate, datlocprovider, datcollversion
FROM pg_database;

如果 datlocprovidercdatcollate 不是 C/POSIX,该库所有默认 collation 的文本索引都依赖 libc。上面的查询覆盖索引键列;表达式索引的表达式里可能嵌入额外 collation,需要人工过一遍定义。

升级后的检测

比对已记录的排序规则版本

从 PostgreSQL 10 起,系统目录在 pg_collation.collversion 里记录每个 collation 的 provider 版本;PostgreSQL 15 又扩展到数据库默认 collation(pg_database.datcollversion,以及配套的 ALTER DATABASE ... REFRESH COLLATION VERSION)。当使用到的对象其记录版本与 OS 报告的版本不一致时,会话会对每个 collation 发出一次版本不匹配的警告。也可以不依赖警告、直接比对:

SELECT collname, collprovider, collversion,
       pg_collation_actual_version(oid) AS os_version
FROM pg_collation
WHERE collversion IS DISTINCT FROM pg_collation_actual_version(oid);

pg_collation_actual_version 向操作系统查询当前安装的版本。这条查询返回的每一行,都是一个行为可能已经在你脚下变过的 collation。PostgreSQL 9.6 及更早版本没有这套版本追踪设施——没有警告、无可比对,检测完全靠下面的结构检查,或者干脆按计划重建。

用 amcheck 验证索引结构

amcheck 扩展用当前生效的比较规则重新评估 B-tree 的顺序,恰好对准这种故障模式:一个在旧规则下内部一致的索引,升级后可能通不过验证,因为验证期待的是新顺序。

CREATE EXTENSION IF NOT EXISTS amcheck;

SELECT bt_index_parent_check('app.orders_customer_name_idx', heapallindexed => true);

索引正常时函数不返回行,发现不一致则抛错。bt_index_parent_check 是更严格的变体(额外检查父子页关系);bt_index_check 更轻。bt_index_parent_check 在索引及其表上持有 ShareLock,阻塞并发的 INSERT/UPDATE/DELETE,安排在维护窗口执行;bt_index_check 只持有 AccessShareLock——与普通 SELECT 同级——不阻塞写入。

两个局限要记牢:amcheck 只在“存储的键序与当前规则冲突”时才报告——如果排序规则的变化恰好没有重排任何既有键,检查会静默通过,所以通过不等于安全。反过来,在一次动过 glibc 或 ICU 的 OS 升级之后,文本索引上的 amcheck 报错几乎必然意味着“重建”,而不是“硬件故障”。

重建受影响的索引

不阻塞应用的重建方式:

REINDEX INDEX CONCURRENTLY app.orders_customer_name_idx;

-- 或者一次重建整张表的所有索引:
REINDEX TABLE CONCURRENTLY app.orders;

REINDEX CONCURRENTLY 期间表保持可读写,代价是比普通的 REINDEX 慢,且不能在事务块内执行。失败或中断的运行可能留下 INVALID 索引——在 pg_index 里找(NOT indisvalid),重试前先删掉。全实例级的事故按表逐个重建,并按索引大小排序,把大表的窗口期安排得有计划。

重建完成后,刷新记录的版本号以消除不匹配警告:

ALTER COLLATION "de_DE" REFRESH VERSION;
ALTER DATABASE app REFRESH COLLATION VERSION;

只能在依赖索引重建之后刷新版本——先刷新会把警告消掉,而损坏还留在原地。

这种故障在设计上就是静默的

没有报错,校验和不会触发,常规监控什么也看不到。症状是 OS 升级几天甚至几周之后,用户开始报"查不到数据"。把盘点查询和 amcheck 检查写进 OS 升级的 runbook,而不是留到事后复盘。

预防

  • 新数据库优先用 ICU 或 builtin collation。 CREATE DATABASE ... LOCALE_PROVIDER = icu ICU_LOCALE = 'de-DE' 把排序规则钉在显式带版本号的 ICU 规则集上,而不是发行版自带的任意 glibc;不需要自然语言排序时,builtin provider 的 C.UTF-8(PostgreSQL 17+)在 OS 升级面前完全不可变。已有的 libc 数据库可以用 CREATE COLLATION 加并发重建索引,把个别列迁到 ICU collation。
  • OS 升级与 PostgreSQL 大版本升级分开做。 pg_upgrade 只复制数据文件、不重建索引,把两类升级塞进同一个窗口等于风险加倍,出了异常也难以归因。分两个窗口做,中间穿插版本比对和 amcheck 检查;升级路径见 复制、故障切换与升级
  • 每次 glibc/ICU 升级后复查。 版本比对查询成本很低,每个 OS 补丁周期后跑一次,变了什么就重建什么。
  • 清楚查询依赖什么。 损坏通过索引扫描和有序输出暴露——索引与 EXPLAIN 讲如何看哪些执行计划依赖受影响的索引。
审计索引的排序规则风险
我的 PostgreSQL(版本)集群运行在(发行版及版本)上,计划升级 OS 到(目标版本)。数据库默认 collation 是(datcollate / datlocprovider)。
1. 给我盘点所有依赖 libc 或 default collation 的索引的 SQL,排除 C/POSIX,包含表达式索引。
2. 演示如何用 pg_collation_actual_version 比对 pg_collation.collversion 与 OS 当前版本。
3. 为受影响的索引生成 amcheck 验证脚本,以及按 pg_relation_size 排序的 REINDEX INDEX CONCURRENTLY 计划。
4. 根据下面我的索引列表,标出哪些索引没有风险并说明原因。
我的索引:(贴 \di+ 输出)

Last updated on

On this page