KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
06 · 选型与运维 — keel 龙骨
前面五章把参数和代价都量过了,这一章收口到两个决策:用什么(pgvector / Milvus / faiss),以及什么时候重建索引。选型的答案不在「谁最先进」,而在你手上有什么、能接受什么代价。重建索引是一次性成本,但时机选错会把一次性成本变成线上事故。
前面五章把参数和代价都量过了,这一章收口到两个决策:用什么(pgvector / Milvus / faiss),以及什么时候重建索引。选型的答案不在「谁最先进」,而在你手上有什么、能接受什么代价。重建索引是一次性成本,但时机选错会把一次性成本变成线上事故。
现场
团队要上一个检索服务,三个候选摆在桌上:直接用项目里已经在跑的 PostgreSQL 加 pgvector;上一套独立的 Milvus;或者自己把 faiss 嵌进服务里。三个都被叫做「向量数据库」,但它们解决的不是同一层问题。
构建代价与落盘大小(本机实测)
同一批 10 万条 128 维数据,四种索引的构建与查询成本(06-build-cost.txt):
index | train_s | add_s | disk_MB | single_ms | batch_qps
FlatL2 (每次全扫) | - | 0.007 | 51.20 | 0.3078 | 6044.8
IVF-Flat nlist=256 np=16 | 0.449 | 0.082 | 52.13 | 0.1608 | 25025.8
IVF-PQ nlist=256 m=32 | 27.976 | 0.313 | 4.26 | 0.2550 | 81292.9
HNSW M=32 efC=200 efS=100 | - | 5.862 | 78.42 | 0.5281 | 27707.3
读完这张表要记住四点:
- Flat 的「建索引」是假的。0.007s 只是拷贝,因为它没有索引结构。它的代价推迟到了每次查询(
batch_qps只有 6044,四种里最低)。 - PQ 的训练最贵,但运行最便宜。
IVF-PQ m=32要训练 27.976s(对比 IVF-Flat 的 0.449s,差 60 倍),因为要迭代求解m个子空间的码本。换来的是落盘 4.26MB(IVF-Flat 的 1/12)和最高的批量吞吐 81292 QPS——扫压缩编码比扫原始向量便宜。 - HNSW 建得慢、占得多、单条查询还不算快。建图 5.862s,落盘 78.42MB(比数据本身 51.20MB 还大),单条查询 0.5281ms 比 IVF-Flat 的 0.1608ms 慢。它的优势要在高召回区间才显现(第 03 章:
M=32, efSearch=400召回 0.9364,IVF 同 QPS 下只有 0.6324)。 - 单条延迟和批量吞吐不是一回事。
single_ms是单核路径,batch_qps是跨查询并行的墙钟吞吐。两者受线程调度影响都有波动,别拿一个去推另一个。构建耗时和落盘大小是确定性的,可信度最高。
容量与成本估算
把第 02 章的「每向量字节数」实测值拿出来,直接乘规模。N=1000 万、d=128 时:
| 索引 | 每向量字节(实测) | 1000 万条估算 | 相对 Flat |
|---|---|---|---|
| Flat | 512.0 | 4.77 GiB | 1.00x |
| IVF-Flat(nlist=256) | 521.3 | 4.85 GiB | 1.02x |
| HNSW M=16 | 656.3 | 6.11 GiB | 1.28x |
| HNSW M=32 | 784.2 | 7.30 GiB | 1.53x |
| IVF-PQ m=16 | 26.6 | 254 MiB | 0.05x |
| IVF-PQ m=32 | 42.6 | 406 MiB | 0.08x |
估算方法就一句话:先用 N × d × 4 算出原始向量内存,再按索引家族的倍率加上去。IVF 系几乎不额外占内存(它把向量原样存进倒排列表),HNSW 要额外付 30%~50% 的图开销,PQ 是数量级地砍内存但换召回。
两个容易算错的点:第一,这只是向量本身,标量字段、id、副本数、以及查询时的临时内存都不含在内,实际部署要再留余量。第二,副本数是乘法——3 副本意味着上面每个数字再乘以 3。
选型:三个候选各解决哪一层
| 维度 | pgvector(PostgreSQL 扩展) | Milvus(分布式服务) | faiss(库) |
|---|---|---|---|
| 形态 | 库内扩展,跟着 PG 走 | 独立集群,四层架构 + etcd/MinIO/WAL | 进程内库,无服务 |
| 事务与一致性 | 单机事务,写后立即可见 | 分布式,一致性级别可选(默认 bounded staleness) | 无,自己保证 |
| 过滤 | WHERE 下推,走标准 planner |
partition / partition key 物理隔离 | IDSelector 手动下推 |
| 规模上限 | 单机内存/磁盘,绑定 PG 实例 | 分片可水平扩 | 单机内存为主,DiskANN 可放盘 |
| 运维面 | 无新增组件,复用 PG 运维 | 需要维护 etcd、对象存储、WAL | 无服务,但持久化/重试/监控全自己写 |
| 典型选择 | 已有 PG、数据量中小、要 SQL 和事务 | 亿级、多租户、要副本与滚动升级 | 离线批处理、嵌入式、自建服务的最底层 |
选型的判断顺序:
- 如果已有 PostgreSQL 且数据量在单机能扛的范围,先算 pgvector 的容量(上一个表 × 副本)。能装下、过滤走
WHERE、能接受单机,很多场景到这里就结束了,不需要新组件。本机 10 万条建 HNSW 索引 8.25s、索引 79MB,量级可以参考。 - 如果需要水平扩、多副本、跨租户物理隔离,或者数据量已经超过单机内存,上 Milvus。代价是要接受它带来的整条依赖链(第 04 章)和最终一致性。
- 如果是离线任务、或你要自己建一个检索服务,用 faiss 做内核,自己在上面加持久化、并发、重试。这条路最灵活也最费工。
一个常见的误判:把 faiss 和 Milvus 当成「同一层的两个选项」。faiss 是库,Milvus 是服务,Milvus 内部就用 faiss 系的索引。你要选的不是「用 faiss 还是 Milvus」,而是「要不要一个服务」。
重建索引的时机
索引是数据的派生物,数据变了索引就会失真。触发重建的情况有三类:
- 换 embedding 模型(或换模型版本):向量空间整个变了,旧索引全部作废,必须重建。第 07 章(记忆系统)的铁律是「建立索引和执行查询使用同一个模型与版本」,这条在索引层同样成立。
- 数据分布漂移:IVF 的质心是在老数据上训练的,新数据分布变了以后簇划分失效,召回会悄悄下降。这类问题监控不出来召回,只能靠定期重训。
- 首次上线/参数变更:
nlist、M、m这类构建期参数改了必须重建;nprobe、efSearch是查询期参数,可以热调不用重建。第 02 章区分过这两类,运维上也要分开。
重建流程里最需要设计的是重建期间的服务连续性:
flowchart TD
T["触发: 换模型 / 分布漂移 / 改构建期参数"] --> B["旁路构建新索引<br/>pgvector: CREATE INDEX CONCURRENTLY"]
B --> V{"构建成功?"}
V -->|"失败"| RB["放弃新索引<br/>保留旧索引继续服务"]
V -->|"成功"| Q["同一查询集验收<br/>recall / p99 / QPS"]
Q --> P{"达标?"}
P -->|"否"| RB
P -->|"是"| SW["原子切换: 新向量 + 新索引<br/>回滚方案同时准备"]
SW --> M["监控线上 recall 漂移<br/>与索引大小"]
RB -.->|"服务不中断"| SERV["旧索引仍在服务"]
SW -.->|"切换出错"| RB
style T fill:#e3f2fd,color:#0d3b66
style B fill:#f1f8e9,color:#33691e
style V fill:#fff3e0,color:#8a4b00
style RB fill:#ffebee,color:#b71c1c
style Q fill:#e8f5e9,color:#1b5e20
style P fill:#fff3e0,color:#8a4b00
style SW fill:#e8f5e9,color:#1b5e20
style M fill:#e8f5e9,color:#1b5e20
style SERV fill:#e8f5e9,color:#1b5e20
两个细节别省:验收必须用同一套查询集(否则前后不可比),回滚方案要和新索引同时准备好(切换出错能立刻退回旧索引)。重建是一次性成本,但你要在低峰做——本机 10 万条建 HNSW 要 5.8s,PQ 训练要 28s,按这个比例外推到千万级,就是几十分钟到小时级。
副本与故障域
选型表里没写的一件事是副本。副本决定了两个东西:读吞吐的横向扩展能力,和单机故障时的可用性。
向量检索的副本和普通无状态服务不一样。无状态服务加副本只是多开几个进程;向量库加副本要把索引也复制一份——上面算的容量是单副本的,三副本就是三倍内存。所以「加副本提升 QPS」在向量检索里是一笔昂贵的买卖,先确认你的瓶颈是 QPS 还是延迟:如果单机 QPS 已经够、只是想要故障容错,加副本是对的;如果是想靠副本摊 QPS,先算清楚内存翻几倍值不值。
故障域也要分层看。第 04 章说过,Milvus 的可用性上限等于 etcd、对象存储、WAL 里最弱的那个。副本能扛住 Query Node 挂掉,但扛不住 etcd 抖动或对象存储不可达——那些是共享依赖,不是副本能覆盖的。做容量和可用性规划时,把「索引节点」和「共享依赖」分开列风险清单。
对 pgvector 这条路,副本就是 PostgreSQL 的流复制(见 PostgreSQL 工程课 06 章)。备库能扛读,但要注意备库的召回和主库一致的前提是索引也同步过去了——逻辑复制在向量类型上的行为和物理复制不同,上线前得验证一次。
监控
向量检索的监控和普通服务不一样,因为很多故障是静默的:
recall 抽检:抽查一批已知答案的查询,算召回。这是唯一能发现「索引失真」的指标,延迟和错误率都发现不了。返回条数分布:第 05 章那个「LIMIT 10 返回 0~4 条」的问题,只能靠这个发现。p50 与 p99 延迟:缓存悬崖(第 00 章)会让两者差一个数量级,只看 p50 会漏。索引内存 / 索引大小:HNSW 索引可能比数据还大(本章实测 78.42MB vs 51.20MB),容量规划要按索引算,不是按数据算。重建耗时与成功率:重建窗口是风险窗口。
主动破坏
三件事都可以在本机复现,任选其一:
- 把
nprobe或efSearch在线上配置里顺手调大(比如从 16 调到 256),观察批量 QPS 掉多少(第 03 章:nlist=256从nprobe=16到 64,QPS 从 29974 掉到 7497)。这类「安全起见调大」的改动通常没人 review。 - 把
06_build_cost.py里的HNSW M=32改成M=64,重跑,看落盘大小从 78.42MB 涨到多少。用它反推你 1000 万条数据的容量会不会超预算。 - 换索引家族但不换验收标准:把 IVF-Flat 换成 IVF-PQ(
m=32),跑同一套查询集,看召回从 0.2821 掉到 0.2434(第 02 章)。如果验收只测了延迟,这次变更会「通过」上线,然后召回悄悄下降。
生产边界
- 教学替身:单机 faiss,写盘即持久化。生产里的索引要么在 PG 里(走 WAL),要么在 Milvus 的对象存储 + WAL 里,要么你自己实现。
- 上线要盯的指标:上面「监控」一节那五条。
- 失败策略:把「重建索引」做成一等流程(旁路构建、同集验收、原子切换、可回滚),而不是运维临时执行的一条命令。容量按「索引大小 × 副本数」算,不按原始数据算。
动手
- 跑
06_build_cost.py,把 HNSW 的M改成 64,记录落盘大小和批量 QPS 的变化,算出「每多 10% 内存换来多少 QPS」。 - 用本章的「每向量字节 × N」公式,给你手上真实项目算一次容量(含副本数),和你的机器内存对一下,判断该选 Flat/IVF/HNSW/PQ 里的哪一档。
- 给一个假想的重建流程写检查清单:触发条件、旁路构建方式、验收查询集、切换与回滚步骤,各写一句可执行的话。
自测
- 为什么 PQ 的训练时间(27.976s)比 IVF-Flat(0.449s)贵两个数量级,但运行时的批量吞吐反而更高(81292 vs 25026 QPS)?
- HNSW 落盘 78.42MB 比原始数据 51.20MB 还大。如果按「数据大小」做容量规划,会在哪一步出问题?
nprobe可以热调、nlist必须重建。为什么这两个参数在运维上要分开管理?- 一个团队已经在用 PostgreSQL,数据量 200 万条。你会直接上 Milvus,还是先用 pgvector?给出你的判断依据和验证步骤。
- 「重建索引是一次性成本」这句话在什么情况下是错的?(提示:想想数据持续写入和分布漂移)
本课到此结束。回头看一遍:第 00 章你会算精确检索的账,第 01 章你会选度量,第 02 章你认识四个索引家族,第 03 章你会画召回-延迟曲线并定验收线,第 04 章你理解分布式向量库多出的层与一致性代价,第 05 章你处理过滤与混合检索,第 06 章你会选型、估容量、设计重建流程。这些判断就是这门课想给你的东西——下次 AI 给你生成一段向量检索代码时,你能看出它在哪一步偷了懒。