# 生命周期与升级耦合

LLMS 索引： [llms.txt](/llms.txt)

---

扩展有两条相互耦合、却不会自动同步的生命周期：

```text
node lifecycle:
  repository -> package -> control/SQL/library -> preload/restart

database lifecycle:
  CREATE EXTENSION -> member objects -> ALTER UPDATE -> DROP
```

运维事故往往发生在两条线暂时分离时：包已升级而对象未升级、对象已写入
catalog 而新备库缺库、备份完整却恢复环境没有旧脚本。

## 14.4.1 安装版本不等于数据库对象版本 {#item-14-4-1}

### 三个“版本”不要合成一个字段

至少记录：

| 名称 | 来源 | 示例 |
|---|---|---|
| 项目/release 版本 | upstream release/package metadata | pgvector 0.8.4 |
| 节点软件包 build | rpm/deb/image | vendor release + PG18 + OS |
| 数据库对象版本 | `pg_extension.extversion` | vector 0.8.4 |

有些包一次提供多个 SQL 对象版本和更新脚本。于是：

```text
package release = newest support files
extversion      = current objects in one database
```

两者不同并不必然错误，但必须是被管理的过渡状态。

### 先装包，再逐库迁移

一个安全的普通更新顺序：

```text
1. freeze exact package build
2. read control/release/update scripts
3. install package on standbys and primary nodes
4. verify files and loadability on every node
5. rehearse on restored/cloned database
6. inventory every database extversion/owner/dependency
7. establish query + plan + correctness baseline
8. ALTER EXTENSION ... UPDATE in controlled window
9. verify catalog/member/behavior/log/replication
10. observe before removing old support files
```

为什么先把文件铺到所有节点？因为 failover 不应把一个刚完成对象更新的
数据库交给缺动态库的备库。物理复制会复制数据目录变化，不会替你把
`$libdir` 文件复制到另一台主机。

为什么逐库？扩展是 database-scoped。一个 cluster 中：

```sql
\l
```

列出的每个数据库都有自己的 `pg_extension`；`postgres` 已经 1.6，不代表
`app`、`analytics` 或 `template` 也已经 1.6。

### 建立跨节点、跨数据库矩阵

节点侧：

```bash
pg_config --version
pg_config --sharedir
pg_config --pkglibdir

# 由包管理器/CMDB采集 exact build；不要解析一个浮动别名当版本
```

数据库侧：

```sql
SELECT
    current_database() AS database_name,
    e.extname,
    e.extversion,
    pg_get_userbyid(e.extowner) AS owner,
    n.nspname AS nominal_schema
FROM pg_extension AS e
JOIN pg_namespace AS n
  ON n.oid = e.extnamespace
ORDER BY e.extname;
```

聚合后应能回答：

```text
node A/B/C package build
  × database D1/D2/D3 extversion
  × primary/standby role
```

只保存 `\dx` 截图无法发现某台备库缺包，也无法显示其他数据库。

### control 默认版本不会升级已有对象

升级包后：

```sql
SELECT
    name,
    default_version,
    installed_version
FROM pg_available_extensions
WHERE name = 'pg_trgm';
```

可能显示：

```text
default_version=1.6
installed_version=1.3
```

含义是：

- 新执行 `CREATE EXTENSION pg_trgm` 默认创建 1.6；
- 当前数据库对象仍是 1.3；
- 只有显式 `ALTER EXTENSION ... UPDATE` 才迁移它。

不要靠重跑：

```sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;
```

期待升级。`IF NOT EXISTS` 只会 notice 并保留现有对象，也不保证同名现有
对象就是期望内容。

### 更新前读脚本，不只读 release notes

查看路径：

```sql
SELECT *
FROM pg_extension_update_paths('pg_trgm')
WHERE source = '1.3'
  AND target = '1.6';
```

再读取实际 package 中：

```text
pg_trgm--1.3--1.4.sql
pg_trgm--1.4--1.5.sql
pg_trgm--1.5--1.6.sql
```

评审：

- 是否改类型或存储格式；
- 是否重建/重写索引；
- 是否触碰扩展配置表；
- 是否删除/重命名函数与操作符；
- 是否会扫描业务数据；
- 需要何种锁；
- 失败是否能事务回滚；
- 更新后的旧应用是否仍兼容。

本章对这些文件和动态库做 SHA-256，保证复验时读的是同一构建；哈希不替代
代码评审。

### package rollback 不等于 object rollback

假设已经把对象从 1.3 更新到 1.6，再把 OS 包降回只支持 1.3：

```text
database extversion=1.6
filesystem supports only 1.3
```

这是更危险的不一致。反向对象迁移只有在扩展明确提供 downgrade path 且
数据格式兼容时才可能。更常见的回退是：

- 在更新前保留可恢复备份/快照；
- 在 clone 上验证；
- 更新后向前修复；
- 若必须回退，恢复到更新前一致时间点并协调业务数据。

因此扩展更新的“可回滚”不能只写 `apt downgrade`。

## 14.4.2 大版本升级、备份恢复与逻辑复制兼容 {#item-14-4-2}

### `pg_upgrade` 不会替你验证外部模块

PostgreSQL 官方 [`pg_upgrade`](https://www.postgresql.org/docs/18/pgupgrade.html)
文档明确提醒：所有外部模块必须与新 server 二进制兼容；`pg_upgrade`
无法检查这一点。新集群主库与备库都要安装匹配 shared libraries。

大版本升级前，对每个扩展冻结：

```text
old server major/build
old extension object version
old package build

new server major/build
new-compatible package build
target extension object version
supported transition order
```

可能的顺序取决于扩展：

```text
old PG: update extension to prerequisite version
  -> install new-PG-compatible files
  -> pg_upgrade / logical migration
  -> new PG: ALTER EXTENSION to target version
```

也可能要求另一个顺序。以扩展的目标版本升级文档为准。

`pg_upgrade --check` 通过不等于扩展可用。clone rehearsal 至少要：

- 启动新集群；
- 查询每个扩展类型/函数；
- 检查 expression index/opclass；
- 重建或验证要求重建的索引；
- 跑应用回归；
- 启动新备库；
- 验证 dump/restore 和监控。

### 物理备份包含数据，不包含 OS 供应链

物理备份复制数据库文件和 WAL。它不会自动保存：

- PostgreSQL server binary；
- control 与版本 SQL；
- 动态库；
- preload 配置的外部部署来源；
- OS package repository。

恢复手册必须能重建：

```text
compatible server binary
  + exact/compatible extension packages
  + configuration/preload
  + data directory and WAL
```

只保留最新仓库，未必能恢复三年前依赖旧扩展对象版本的备份。长期保留策略要
考虑 package snapshot、image digest 或可复现构建。

### 逻辑 dump 用声明恢复扩展

PostgreSQL 把扩展视为整体。全库 schema-only dump 中，本章看到：

```sql
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA shop_ch14;
COMMENT ON EXTENSION pg_trgm IS '...';

CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA shop_ch14;
COMMENT ON EXTENSION vector IS '...';
```

却没有：

```text
CREATE TYPE shop_ch14.vector ...
CREATE FUNCTION shop_ch14.similarity ...
```

这正是扩展机制的 dump 合同。恢复顺序隐含要求：

```text
target support files available
  -> schema/extension creation
  -> dependent application tables/indexes
  -> data
```

本章还证明一个容易忽略的选择性 dump 语义：

```bash
pg_dump --schema-only --schema=shop_ch14 ...
```

输出包含：

```text
candidate_doc table
vector column
GIN/HNSW indexes
```

但不包含 `CREATE EXTENSION`。PostgreSQL `pg_dump` 文档对 `--schema`
选择明确警告：它不保证自动带上所选对象依赖的所有对象。这个 artifact
不能单独在洁净环境恢复，必须由恢复清单显式先创建扩展。

参见 [pg_dump](https://www.postgresql.org/docs/18/app-pgdump.html)。

### clean restore 是唯一有力的恢复证据

不要在原集群上执行 dump 后立刻宣称可恢复。洁净环境要求：

- 没有预装数据库扩展对象；
- 使用冻结 server/package build；
- 从空 database 开始；
- 按 runbook 恢复；
- 验证 extension owner/schema/version/member；
- 验证业务行数/checksum；
- 验证查询、计划与权限；
- 记录时间、日志和失败。

若恢复必须“手工试几个版本直到成功”，供应合同尚未完成。

### 逻辑复制不复制 schema 与扩展生命周期

内置逻辑复制主要复制表数据变更，不替你复制 DDL、extension control files
或 `CREATE EXTENSION`。发布端列使用自定义类型时，订阅端必须预先拥有可
接受该列值、函数和索引的兼容 schema。

评审：

- publisher/subscriber 类型名与语义；
- text/binary 传输与转换能力；
- extension object version；
- DDL 发布顺序；
- replica identity；
- extension-owned 配置/metadata 是否作为普通表复制；
- 订阅端触发器/默认值/约束的执行差异；
- major/architecture 组合。

“两端都显示 extension installed”仍不足以证明版本/数据语义兼容。

如果逻辑复制被用作迁移出口，先把自定义数据转换为内置交换类型通常更容易
控制。例如本章把 `vector(3)` 显式转为 `text`，而不是要求目标立即加载
同一 extension。

参见 [Logical Replication Restrictions](https://www.postgresql.org/docs/18/logical-replication-restrictions.html)。

### 备库与 failover

物理 standby 会重放创建表、类型依赖和 extension catalog 变化，但不会
运行节点包管理器。变更前：

```text
all standbys have compatible support files
  -> preload/config staged
  -> restart completed if required
  -> primary database DDL/update
  -> replication caught up
  -> controlled switchover/failover probe
```

检查不能只 SSH 到主库。新加入节点、灾备节点、延迟副本和备份 restore
worker 都属于供应范围。

## 14.4.3 依赖扩展不可用时的降级策略 {#item-14-4-3}

### 先区分必需能力与增强能力

扩展依赖可分：

| 类型 | 例子 | 不可用时 |
|---|---|---|
| 数据可读必需 | 业务列是自定义类型 | 数据库/查询可能无法正常使用 |
| 写入必需 | trigger/function 是写入合同 | 应停止写而不是绕过不变量 |
| 查询增强 | 可重建索引/opclass | 可回退较慢原生查询 |
| 观测增强 | 统计/采样扩展 | 核心业务可运行，诊断能力下降 |
| 维护增强 | repack/调度工具 | 延后维护并告警 |

只有后两三类适合真正“降级”。把自定义列类型说成可选能力是自欺。

### 设计能力探测，但不要每次请求查 catalog

发布/启动时探测：

```sql
SELECT
    e.extname,
    e.extversion
FROM pg_extension AS e
WHERE e.extname IN ('pg_trgm', 'vector');
```

再验证所需签名与索引：

```sql
SELECT to_regprocedure('shop_ch14.similarity(text,text)');
SELECT to_regclass('shop_ch14.candidate_doc_title_trgm_idx');
```

结果进入部署 gate 或低基数健康状态，而不是每个请求动态猜。应用 feature
flag 必须与数据库迁移阶段同步：

```text
extension absent:
  extension-dependent feature disabled

extension installed and validated:
  canary reads

index built and valid:
  limited traffic

observation passed:
  normal traffic
```

### 可重建索引的降级

`pg_trgm` 例子：

```text
normal:
  title % $query
  GIN gin_trgm_ops

degraded:
  exact normalized equality
  or prefix lookup
  or PostgreSQL FTS
```

降级查询语义不同，API 要明确：

- 是否返回较少结果；
- 是否暂停 fuzzy mode；
- 延迟是否提高；
- 哪些 SLO 暂时失效。

不要悄悄返回不同业务含义。

### 自定义类型的退场顺序

以 `vector` 为例：

```text
1. freeze model/dimension and export format
2. add destination native/external representation
3. backfill with row/checksum verification
4. deploy dual-read or switched-read application
5. stop new extension-type writes
6. verify no view/function/index/table depends on type
7. drop extension-dependent indexes/columns
8. DROP EXTENSION without CASCADE
9. remove preload if any and then node package
```

依赖检查：

```sql
-- 先看 pg_depend 和业务对象定义
\d+ shop_ch14.candidate_doc
\dx+ vector

-- 最后的 DROP 必须用 RESTRICT 语义暴露遗漏
DROP EXTENSION vector;
```

绝不以：

```sql
DROP EXTENSION vector CASCADE;
```

作为“清理方便”的生产脚本。`CASCADE` 会把尚未迁走的业务对象一起删除。

### 当动态库临时缺失

若 catalog 已有扩展而节点缺 library：

- 不要继续 failover 到该节点；
- 阻断相关流量或节点晋升；
- 从受控仓库恢复匹配包；
- 验证哈希、loadability 与查询；
- 检查所有其他节点是否同样漂移；
- 解释配置管理为何未发现；
- 完成恢复/备库回归。

删除 catalog 中 extension 不是修动态库缺失的第一反应，尤其当业务类型依赖
它时。

### 降级 SLO

ADR 预先定义：

```yaml
feature: fuzzy-title-search
dependency: pg_trgm
failure_detection: deployment probe + query error alert
fallback: normalized-prefix-search
semantic_change: typo tolerance disabled
latency_budget: 200ms
maximum_duration: 2h
owner: search-team
restore_action: package parity + catalog/query verification
```

没有时间、语义和 owner 的 fallback 只是愿望。

### 本节结论

扩展生命周期的真正完成条件：

```text
can install
  + can update
  + can fail over
  + can back up and clean-restore
  + can cross major
  + can degrade or stop safely
  + can exit without CASCADE
```

`CREATE EXTENSION` 只完成第一项的一部分。

---

[上一节：扩展选型的六个问题](../03/) · [返回本章目录](../) · [下一节：用 Pigsty 管理扩展可用性](../05/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
