# 扩展部署与运行代价

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

---

到目前为止，我们只证明了检索行为。生产采用还要回答：

```text
每个节点有没有同一扩展构建？
数据库对象是谁创建和升级的？
索引建造、更新、WAL、备库与恢复要付什么代价？
文本怎样变成向量，谁有权把它发给谁？
```

本节把 PostgreSQL 机制映射到 Pigsty 4.5，但仍以 live 节点、系统目录和实际
证据为准。

## 15.6.1 安装检索扩展并核对版本 {#item-15-6-1}

### 复用第 14 章的三层状态

检索扩展仍然有三层：

```text
package/support files on every host
  -> backend can load compatible library
  -> pg_extension object exists in this database
```

`pg_trgm` 随 PostgreSQL contrib 交付；`vector` 的项目/包常叫 pgvector，而
SQL 扩展名是 `vector`。不要混写：

```text
package alias: pgvector
SQL:           CREATE EXTENSION vector
catalog:       pg_extension.extname = 'vector'
```

本章不会安装或升级它们，而是要求第 14 章最终状态：

```text
pg_trgm 1.6
vector  0.8.4
schema  shop_ch14
exact extension markers
```

如果版本、owner、schema 或 marker 不符，`context.sql` 拒绝。下游实验不能
悄悄接管上游扩展。

### Pigsty 中分开“装包”和“启用”

Pigsty 4.5 当前文档指出：

- `pgvector` 随 `pgsql-main` 默认安装；
- `pg_trgm` 位于默认启用扩展列表；
- 可通过 `pg_packages`/`pg_extensions` 影响包；
- 可通过 `pg_default_extensions` 影响数据库默认启用对象。

参见
[Pigsty Default Extensions](https://pigsty.io/docs/pgsql/ext/extension/)。

本章给出的
[`pigsty-declaration.example.yml`](/labs/ch15/pigsty-declaration.example.yml)
只是合并片段：

```yaml
all:
  vars:
    pg_version: 18
    pg_packages:
      - pgsql-main
      - pgvector
    pg_default_extensions:
      - { name: pg_trgm, schema: public }
    pg_databases:
      - name: pg36_shop
        owner: pg36_owner
        extensions:
          - { name: vector, schema: public }
```

实际版本中 `pgvector` 可能已被 `pgsql-main` 包别名覆盖，重复声明是否允许、
包名如何展开、目标 OS/PG major 是否有构建，都要用目标 inventory 与
Pigsty 文档确认。

教学夹具把扩展放在 `shop_ch14`，是为了凸显 namespace/owner。生产片段使用
`public` 只是示例，不是推荐所有扩展都堆进 `public`。schema 选择必须同时
满足：

- extension 是否 relocatable/是否强制 schema；
- 应用是否用全限定名称；
- `search_path` 安全；
- dump/restore；
- 运维与升级脚本；
- 权限最小化。

### L1 每个节点都要核对

物理复制会把数据库对象复制到备库，但不会把 OS 软件包和动态库通过 WAL
复制过去。主库 `CREATE EXTENSION vector` 成功，某备库仍可能因缺
`vector.so` 在查询、恢复或升主后失败。

对每个 L1 host 收集：

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

test -r "$(pg_config --sharedir)/extension/vector.control"
test -r "$(pg_config --sharedir)/extension/pg_trgm.control"
```

再核对目标 major 的 package manager 版本与文件 hash。不能从当前 shell
PATH 里的 `pg_config` 猜正在运行的 server；先从实例确认 server major，
再找对应安装树。

数据库内：

```sql
SELECT
  e.extname,
  e.extversion,
  n.nspname AS schema_name,
  pg_get_userbyid(e.extowner) AS owner,
  e.extrelocatable
FROM pg_extension AS e
JOIN pg_namespace AS n
  ON n.oid = e.extnamespace
WHERE e.extname IN ('pg_trgm', 'vector')
ORDER BY e.extname;
```

控制文件可见性：

```sql
SELECT
  name,
  version,
  installed,
  superuser,
  trusted,
  relocatable
FROM pg_available_extension_versions
WHERE name IN ('pg_trgm', 'vector')
ORDER BY name, version;
```

两份目录回答不同问题：

- `pg_available_extension_versions`：server 支持文件允许哪些版本；
- `pg_extension`：当前 database 创建了哪个对象版本。

### 安装权限不要交给应用

本章角色边界：

```text
platform/admin:
  package / untrusted extension / lifecycle

pg36_owner (NOLOGIN or controlled SET ROLE):
  schema, tables, reviewed migrations

pg36_app:
  SELECT search interface
  no table DML
  no extension lifecycle
```

`pg36_app` 对 `product_search` UPDATE 必须返回：

```text
SQLSTATE 42501
permission denied for table product_search
```

真实商品服务当然需要写入，但写角色不等于在线读角色必须拥有表级任意 UPDATE。
可使用：

- 受控迁移角色；
- 列级权限；
- 参数化函数；
- 独立异步 embedding worker；
- 队列/作业表状态机。

尤其不要让业务请求角色创建、升级或删除扩展。

### 证据边界

本章正式运行：

```text
Homebrew PostgreSQL 18.6
direct PostgreSQL service
Pigsty 4.5 docs reviewed
Pigsty L1 execution not run
```

所以 manifest 明确：

```text
validation_path=direct-postgresql
pigsty_l1=not-run
```

移植到 Pigsty 后要补：

```text
inventory commit
repository/package resolution
every-node file/version/hash
database extension catalog
primary/replica query
failover/failback
backup clean restore
```

## 15.6.2 观察索引体积、构建、查询和维护 {#item-15-6-2}

### 先列出四类物理对象

本章商品表有：

```text
heap + toast (if needed)
FTS GIN
title trigram GIN
embedding HNSW
active/category partial B-tree
```

基础盘点：

```sql
SELECT
  pg_relation_size('shop_ch15.product_search') AS heap_bytes,
  pg_total_relation_size('shop_ch15.product_search') AS total_bytes,
  pg_relation_size('shop_ch15.product_search_fts_idx') AS fts_bytes,
  pg_relation_size('shop_ch15.product_search_title_trgm_idx') AS trgm_bytes,
  pg_relation_size('shop_ch15.product_search_embedding_hnsw_idx') AS hnsw_bytes;
```

17 行夹具看到的 8/16/32 KiB 级数字主要由最小页分配决定，不能计算
“每百万商品多少 GiB”。`size-catalog.csv` 只是证明对象存在且非零。

生产 size baseline 应记录：

```text
row count and active count
average text/vector width
model dimension/type
index definition/options
data and query distribution
build/update age
server/filesystem/compression
```

否则两个 size 数字不可比较。

### 构建要观察过程与阻塞

初始离线载入通常：

```text
load data
-> analyze
-> create indexes
```

比逐行维护索引高效。在线已有表则通常考虑：

```sql
CREATE INDEX CONCURRENTLY ...
```

它减少对写入的阻塞，却有更长构建时间、额外扫描、资源占用和失败状态。运行时
观察：

```sql
SELECT
  pid,
  datname,
  relid::regclass,
  index_relid::regclass,
  command,
  phase,
  lockers_total,
  lockers_done,
  blocks_total,
  blocks_done,
  tuples_total,
  tuples_done
FROM pg_stat_progress_create_index;
```

并结合：

```sql
SELECT *
FROM pg_locks
WHERE pid = :builder_pid;
```

发布后必须确认 `pg_index.indisvalid/indisready/indislive`，不能只看 DDL
client exit。

### 查询观测分机械与用户两层

机械层：

```sql
EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS)
SELECT ...;
```

看：

- index/seq/bitmap path；
- actual rows 与估算；
- filter removed rows；
- buffers 与 temp I/O；
- planning/execution time；
- 生效的非默认 settings。

用户层：

```text
query segment
candidate/result count
quality metric
P50/P95/P99 latency
timeout/error/no-result
model/index/release version
```

执行快但 recall 差，不是成功；quality 高但 P99 超时，也不是成功。

Pigsty 默认启用/预加载的 `pg_stat_statements` 可帮助聚合 SQL 调用与耗时，
但要为检索 query 保持可归一的 SQL 形状，并将业务 query id/segment 放在
受控日志或 trace，而不是拼进 SQL 注释造成 statement 指纹爆炸。

### 写入成本要单独压测

文本变化会更新两个 GIN 和 generated `tsvector`；向量变化会更新 HNSW。
测：

```text
rows/s
WAL bytes/row
CPU
index growth
autovacuum cadence
dead tuples
replica replay lag
checkpoint/write latency
```

可以在受控事务前后比较：

```sql
SELECT pg_current_wal_lsn();
-- representative batch
SELECT pg_current_wal_lsn();
```

再用 `pg_wal_lsn_diff` 估算该受控批次产生的 WAL。并发生产环境还有其他
事务，不能把全局 LSN 差未经隔离就归给一个作业。

### HNSW 维护不是普通 B-tree 的复制粘贴

pgvector 官方说明 HNSW vacuum 可能耗时，并给出先 `REINDEX INDEX
CONCURRENTLY` 再 `VACUUM` 的一种加速建议。这是需要谨慎评估的维护动作，
不是每晚固定模板：

- reindex 需要额外磁盘与构建资源；
- concurrently 有更长窗口和失败恢复；
- vacuum 仍要处理 heap；
- 副本会重放相关 WAL；
- 索引重建期间 recall/latency 要观测；
- 新 index 的参数与 opclass 必须一致。

先在代表性副本/演练环境测周期，再决定维护阈值。

### 监控矩阵

| 维度 | PostgreSQL 证据 | Pigsty/平台视图 |
|---|---|---|
| SQL 调用/耗时 | `pg_stat_statements`、logs | dashboard/alerts |
| 执行计划 | `EXPLAIN`、auto_explain | 集中日志 |
| index 使用 | `pg_stat_user_indexes` | 表/索引面板 |
| 表生命周期 | `pg_stat_user_tables` | vacuum/bloat 面板 |
| 构建进度 | `pg_stat_progress_create_index` | 变更任务证据 |
| WAL/副本 | LSN、replication views | HA/replication 面板 |
| 主机资源 | PostgreSQL/OS stats | node/PG exporter |
| 质量/ANN recall | 自建 eval job | release/SLO dashboard |

最后一行不能由通用数据库 exporter 自动推导。检索质量是业务测量，必须把
evaluation job 当成一等生产组件。

## 15.6.3 外部嵌入生成的权限、费用与数据边界 {#item-15-6-3}

### 不要从数据库触发器同步调用外部 API

一个危险设计：

```text
UPDATE product title
  -> trigger
  -> HTTP embedding API
  -> wait inside transaction
```

它把：

- 外部网络延迟；
- rate limit；
- provider outage；
- 费用；
- 密钥；
- 不确定重试；

放进数据库锁与事务寿命。远端已收费成功、本地事务却回滚时，还会出现不可
原子化的副作用。

更稳健的异步结构：

```text
transaction:
  update source text
  record source_text_hash / desired_model / pending job
  commit

worker:
  claim job with bounded lease
  build exact versioned input
  call model service
  validate dimension/norm
  write vector + model_id + source_hash
  mark success or retry state
```

可用 outbox、作业表或消息系统实现；第 13 章已经讨论过触发器与异步边界。

### 幂等身份

一次 embedding 任务的自然 key 可以是：

```text
(document_id, source_text_hash, model_id, template_version)
```

写回时做 compare-and-set：

```sql
UPDATE product_embedding
SET embedding = :vector,
    status = 'ready',
    embedded_at = clock_timestamp()
WHERE product_id = :id
  AND source_text_hash = :hash
  AND model_id = :model
  AND status = 'processing';
```

如果源文本在推理期间变化，旧结果不能覆盖新版本。重试同一 key 应复用已完成
结果或安全 upsert，避免重复收费。

### worker 最小权限

embedding worker 通常需要：

- 读取允许外发的字段；
- 读取/更新自己的 job；
- 写特定 vector/model/status 列；
- 不需要 DDL、扩展 owner、超级用户；
- 不需要任意读其他敏感 schema。

API secret 放在 secret manager/受控运行环境，不存进 SQL、YAML 仓库、表
comment 或 evidence manifest。Pigsty 负责 PostgreSQL 平台，不意味着模型
密钥应注入数据库 server 进程。

若文本不能离开边界，可以：

- 在受控网络自托管模型；
- 只对批准字段生成；
- 做脱敏/分区处理；
- 或拒绝向量方案。

“先接 API，之后再补合规”不是试点策略。

### 费用模型

至少估算：

\[
Cost =
InitialBackfillTokens \times Price
+ DailyChangedTokens \times Price
+ QueryTokens \times Price
+ RetryWaste
+ Storage/Index/Compute
\]

还包括：

- backfill 期间 API 并发与限流；
- 模型升级全量重算；
- 双版本存储与索引；
- 失败重试和重复调用；
- query embedding cache；
- 数据出口与网络；
- 本地推理 GPU/CPU 和运维。

费用控制要有：

```text
per-job token/byte limit
daily/project budget
rate/concurrency limit
retry ceiling and dead-letter state
backfill pause/resume
cost attribution by model/version
```

缓存 query vector 时，key 必须包含 exact normalized text、model、template 与
版本；只按原始字符串缓存会在模型切换时串用旧空间。

### 数据生命周期要双向传播

删除或更正 source document 时，检查：

```text
primary row
vector row/index
job queue and retry payload
query/result caches
logs/traces
offline evaluation exports
backups and retention
provider-side retained data
```

物理删除在 PostgreSQL 中还受 MVCC、vacuum、WAL 与备份保留影响。法律上的
删除承诺必须和备份/恢复政策一致，不能只执行一条 `DELETE`。

### 故障与降级

模型服务不可用时，搜索不一定要整体不可用：

```text
vector generation outage:
  keep last valid vector with staleness marker
  queue updates

query embedding outage:
  fall back to FTS + trigram
  expose degraded-mode metric

ANN/index incident:
  exact path for small filtered sets
  or lexical-only bounded fallback
```

每种 fallback 都要在离线标注集上测质量、在线压测容量，且不能放宽权限过滤。

### 上线前问题

- 哪些字段能外发，依据是什么？
- provider 是否保留输入/输出、是否用于训练？
- 模型版本是否可固定，变更如何通知？
- 单次、每日、回填、升级的费用上限是什么？
- 谁能读原文、调用服务、写 vector？
- 如何证明 vector 对应当前 source hash？
- 失败如何重试，何时进入 dead letter？
- 模型切换如何双写、评估、回退？
- 删除、备份与 provider 侧数据如何协调？
- 无模型服务时，业务还能提供什么质量的结果？

回答不了这些问题时，向量 PoC 可以继续离线，不能进入生产写链路。

---

[上一节：混合检索与排序验证](../05/) · [返回本章目录](../) · [下一节：实战：`pg36_shop` 商品混合检索 PoC](../07/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
