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 无关。
哪些索引有风险
text、varchar、char及其 domain 上的 B-tree 索引,当列 collation 由 libc 提供且不是C/POSIX时——包括数据库本身使用 libc provider 时、使用数据库默认 collation 的索引。- 这些列上的唯一约束和主键,因为底层就是这样的索引。
- ICU provider 的 collation 受 ICU 库版本变化影响,与 glibc 无关。
不受影响的:C 和 POSIX collation(按字节序比较,到处稳定)、builtin provider 的 locale 如 C.UTF-8(设计上不可变,PostgreSQL 17+)、hash 索引(无顺序概念),以及 integer、bigint、uuid、timestamptz 等不可排序类型上的索引。
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;如果 datlocprovider 是 c 且 datcollate 不是 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