先说结论,不是所有 RAG 都值得把检索和推理拆开。
我维护的一个知识库问答系统,初期是"一台 GPU 机器跑推理,旁边再起一台 4 核 8G 跑向量库"。架构图画得很漂亮,两个服务互相独立,将来还能各自扩缩容。上线之后 P95 延迟反而从 1.8 秒涨到 3.4 秒。
后来我把两套服务合回一台机器,延迟回到 2 秒以内,还省了一台机器。下面是我当时算的账。
拆开之后多出来的开销在哪
那 3.4 秒我一段一段量过:
| 环节 | 耗时 | 说明 |
|---|---|---|
| 推理生成 | 1.5s | 两台机器都一样 |
| 跨机传 chunk | ~40ms | 每次 5-8 个片段,几十 KB |
| 序列化与反序列化 | ~10ms | JSON |
| 推理服务排队 | 1.1s | 真正的元凶 |
排队那 1.1 秒不在向量库里,在推理服务里。原因是我把检索做成了同步阻塞调用:HTTP 请求进去,向量库算完返回,推理才开始拿下一段数据。整条链路是串行的。
这账我一开始算错了。拆机器省下的是故障隔离和独立扩容的潜力,多付的是每次请求多一次跨机往返 + 多一次串行等待。并发上不去的时候,这个交换纯亏。
反直觉的地方:GPU 机器的 CPU 大部分是闲的
我对这件事的认知偏差挺大,默认"推理机器 = GPU 机器 = CPU 跑满"。
实际跑一个 7B 模型做生成,GPU 在算的那几秒里 CPU 基本在等。真正吃 CPU 的是它前后的准备工作:
- tokenizer 把 prompt 切成 token
- detokenizer 把生成的 token 组装回文本
- 本地跑的 embedding 模型
- HNSW 的图遍历
- rerank 的交叉编码器打分
HNSW 查一百万条向量,单次查询的 CPU 时间在毫秒级,这个量级完全塞得进 GPU 推理的间隙里。
所以同机的真实收益不是"少一台机器",是把串行变成并行——GPU 在算第 N 个 token 的时候,CPU 可以同时算第 N+1 个请求的向量检索和 embedding。
我后来在同一台 16 核机器上实测了两种形态:
| 部署方式 | QPS | P50 | P95 |
|---|---|---|---|
| 同机,8+8 核划分 | 11 | 1.7s | 2.9s |
| 两台 8 核机器 | 8 | 2.2s | 4.6s |
同机部署有一个必须踩过才知道的坑
挪到同一台机器之后,推理服务的 P99 突然从 3 秒涨到 11 秒,而且不是一直这样——机器重启后那十几分钟特别慢,之后自己就好了。
我一开始完全不知道往哪查,以为是网络抖动。
翻了两天日志才发现:HNSW 的索引文件被推理服务加载模型权重时的内存读冲出了 page cache。
向量库用 mmap 加载索引,索引文件在磁盘上,靠操作系统缓存加速。加载一次模型要读几十 GB 权重,Linux 的 page cache 被冲刷,HNSW 查询就得真去读磁盘。
这个坑的特征是平时没事、加载模型那会儿出事,因为权重加载只发生在启动那一刻,之后缓存慢慢回暖就又好了。所以线上表现是"重启后一段时间特别慢然后自己好了",非常容易被当成玄学。
解决有三条,我按这个顺序用:
- 索引不大的时候,直接全量读进内存,别用 mmap
- 调大
vm.usemap_count,或者对索引文件用mlock锁住 - 调整启动顺序:先让索引进内存,再加载模型
什么时候确实该拆
不是所有场景都该同机。我用三条判断,满足两条就拆:
第一,检索侧的内存和推理侧抢同一台机器。 一百万条 1024 维 float32 向量本身就是 4GB,加上 HNSW 的图结构还要往上走一截。两块内存加模型权重挤在同一台机器上,OOM killer 就开始挑人下手,而且它挑谁不挑谁完全看当时的内存快照。
第二,检索和推理的负载曲线完全不同。 白天问答高峰、晚上批量灌库——拆开能省钱,检索那台缩到最小规格,推理那台上 GPU。
第三,故障隔离真的有价值。 向量库挂了的时候,我希望 RAG 降级成"没有检索的问答",而不是整个接口 503。
折中的做法我最后固定成:同一台物理机,两个容器,但用 cgroup 把核数划死。
services:
llm:
image: vllm/vllm-openai:latest
deploy:
resources:
limits:
cpus: "8.0"
memory: 48G
vector:
image: qdrant/qdrant:latest
deploy:
resources:
limits:
cpus: "8.0"
memory: 32G
划死核数比给一个 cpus: "1.0" 这种软限制重要得多。软限制下两个容器照样互相抢,只是抢得没那么凶;硬划之后至少能保证推理服务永远拿得到 8 个核。
我自己记下来的判断顺序
- 先在一台机器上把整条链路跑通,别一上来就拆
- 看 P95 和 P99,别只盯 P50,同机的抖动全堆在长尾上
- 长尾差先查 page cache 和 CPU 争抢,别急着加机器
- 只有"内存不够"或者"负载曲线不同"成立时,拆机才真的省钱
- 拆完记得量一遍出网流量,向量查询本身带宽不大,但日志和 trace 可能比预想的多
最后提一句,上面这些 QPS 和延迟是我自己那台机器上的量级,不是厂商的承诺值。实例规格、带宽单价这类东西变动比软件快得多,还是以各家官方页面为准。