跳至内容
15.6 扩展部署与运行代价

15.6 扩展部署与运行代价

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

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

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

15.6.1 安装检索扩展并核对版本

复用第 14 章的三层状态

检索扩展仍然有三层:

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。不要混写:

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

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

pg_trgm 1.6
vector  0.8.4
schema  shop_ch14
exact extension markers

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

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

Pigsty 4.4 当前文档指出:

  • pgvectorpgsql-main 默认安装;
  • pg_trgm 位于默认启用扩展列表;
  • 可通过 pg_packages/pg_extensions 影响包;
  • 可通过 pg_default_extensions 影响数据库默认启用对象。

参见 Pigsty Default Extensions

本章给出的 pigsty-declaration.example.yml 只是合并片段:

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 收集:

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, 再找对应安装树。

数据库内:

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;

控制文件可见性:

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 创建了哪个对象版本。

安装权限不要交给应用

本章角色边界:

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_appproduct_search UPDATE 必须返回:

SQLSTATE 42501
permission denied for table product_search

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

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

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

证据边界

本章正式运行:

Homebrew PostgreSQL 18.4
direct PostgreSQL service
Pigsty 4.4 docs reviewed
Pigsty L1 execution not run

所以 manifest 明确:

validation_path=direct-postgresql
pigsty_l1=not-run

移植到 Pigsty 后要补:

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

15.6.2 观察索引体积、构建、查询和维护

先列出四类物理对象

本章商品表有:

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

基础盘点:

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 应记录:

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 数字不可比较。

构建要观察过程与阻塞

初始离线载入通常:

load data
-> analyze
-> create indexes

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

CREATE INDEX CONCURRENTLY ...

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

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;

并结合:

SELECT *
FROM pg_locks
WHERE pid = :builder_pid;

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

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

机械层:

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

看:

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

用户层:

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。 测:

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

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

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 CONCURRENTLYVACUUM 的一种加速建议。这是需要谨慎评估的维护动作, 不是每晚固定模板:

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

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

监控矩阵

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

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

15.6.3 外部嵌入生成的权限、费用与数据边界

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

一个危险设计:

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

它把:

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

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

更稳健的异步结构:

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 可以是:

(document_id, source_text_hash, model_id, template_version)

写回时做 compare-and-set:

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 和运维。

费用控制要有:

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 时,检查:

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

故障与降级

模型服务不可用时,搜索不一定要整体不可用:

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 可以继续离线,不能进入生产写链路。


上一节:混合检索与排序验证 · 返回本章目录 · 下一节:实战:pg36_shop 商品混合检索 PoC · 查看全书目录 · 查看索引中心

最后更新于