5.4 硬件加速与缓存预热 参数的软杠杆用尽之后,剩下的提升空间在硬件与缓存。本节承接 5.3:先看指令集与加速器各自能买来什么、什么情况下不值得买,再处理两个不花钱却常被忽略的杠杆——页缓存预热与查询缓存。旧版教程的"硬件加速策略""缓存与预热机制"两节在此合并,因为它们都在回答同一个问题:数据搬运和距离计算这两笔账,怎么让硬件替你付得更便宜。 硬件账:每一层买来什么 距离计算是密集的乘加运算,硬件演进直接改写它的单价。指令集层面,现代 CPU 的向量化指令(各类 AVX 扩展)能把单次乘加的宽度翻上数倍,主流向量库的内核都做了自动向量化,你需要做的只是别用太老的编译产物与硬件——这一层几乎免费,却是所有上层优化的地基。内存带宽常比算力先见顶:3.
参数的软杠杆用尽之后,剩下的提升空间在硬件与缓存。本节承接 5.3:先看指令集与加速器各自能买来什么、什么情况下不值得买,再处理两个不花钱却常被忽略的杠杆——页缓存预热与查询缓存。旧版教程的"硬件加速策略""缓存与预热机制"两节在此合并,因为它们都在回答同一个问题:数据搬运和距离计算这两笔账,怎么让硬件替你付得更便宜。
距离计算是密集的乘加运算,硬件演进直接改写它的单价。指令集层面,现代 CPU 的向量化指令(各类 AVX 扩展)能把单次乘加的宽度翻上数倍,主流向量库的内核都做了自动向量化,你需要做的只是别用太老的编译产物与硬件——这一层几乎免费,却是所有上层优化的地基。内存带宽常比算力先见顶:3.1 算过全量扫描的搬运量,索引扫描虽然只碰部分数据,高并发下带宽依然是shared资源,加内存条换四通道乃至八通道主板,对吞吐的提升经常立竿见影。GPU 是争议最大的选项:它擅长的是批量小批量的密集计算,恰好匹配高并发短查询的画像,云上常见数百到上千的查询每秒单卡提升;但它的适用有边界——查询需要凑批才有效率(单条低频查询反而被调度开销拖累)、内存容量按显存计价(768 维亿级向量又回到量化压缩的账)、以及运维复杂度(驱动、批处理队列)。经验法则是:单机 CPU 扛不住、且查询有明显并发波峰的业务再考虑 GPU,否则把钱花在内存与网络上有更高的性价比。
| 加速层次 | 典型收益 | 适用条件 | 代价 |
|---|---|---|---|
| 向量化指令集 | 距离计算数倍加速 | 几乎无条件 | 需新版编译产物 |
| 内存带宽升级 | 高并发吞吐显著提升 | 带宽先于算力见顶时 | 硬件更换成本 |
| GPU 推理 | 高并发短查询大幅提升 | 并发波峰明显、可凑批 | 显存计价、运维复杂 |
| 更快存储介质 | 冷数据读取、恢复提速 | 磁盘索引或内存映射场景 | 容量单价较高 |
向量检索的两级缓存各自独立起作用。页缓存服务内存映射的段文件:进程重启后缓存是空的,头一批请求要承担从盘读数据的页缺失,表现为"刚重启的副本特别慢"——这就是预热要解决的问题。查询结果缓存服务重复查询:搜索入口的热词、推荐场景的热门物品向量,命中时连索引都不用碰。两者都不需要买任何硬件,需要的只是把预热纳入发布流程。
# 副本上线后的预热脚本骨架(伪代码) def warmup(node, sample_queries): # 第一层:把查询期的候选数据读进页缓存 for region in node.segment_regions(): for page in region.pages(): node.touch(page) # 触发页调入,不求结果 # 第二层:用真实查询样本把索引路径与缓存打热 for q in sample_queries: node.search(embed(q), top_k=10) # 第三层:把高频热点查询的结果直接写进查询缓存 for hot in top_hot_queries(days=7): node.search(embed(hot), top_k=10, cache=True) warmup(new_replica, sample_from_yesterday(2000)) router.add_to_pool(new_replica) # 预热完才接入流量
这段骨架的关键是顺序:预热完成之前,新副本不该接入流量池,否则头一波真实请求就成了"预热",用户体验替发布流程买了单。要量化预热的收益很简单——对比冷启动副本的首分钟延迟曲线与预热后的即可:
import time def measure(node, queries, label): t0 = time.perf_counter() for q in queries[:200]: node.search(embed(q), top_k=10) avg = (time.perf_counter() - t0) / 200 print(f"{label} 平均 {avg*1000:.1f} ms") measure(cold_replica, probes, "冷启动前两百条") # 常见是热态的数倍 measure(warmed_replica, probes, "预热后两百条") # 回到稳态水平
性能工程容易只盯着数据库侧,而生产链路的延迟构成通常是"嵌入编码加检索加重排"。嵌入服务自己的缓存(相同文本的向量缓存)、批处理窗口(把并发请求凑批推理)、与 GPU 的搭配,同样值得按本节的方法度量。一个实用的分解习惯:在网关层为每条查询打上三段耗时(编码、检索、重排),延迟毛刺出现时先看分段曲线再决定动哪一层——5.3 的误区二在此重申一次:先分解,再优化。
先分清瓶颈的形态。如果单条查询在低并发下就慢(内存映射的页缺失频繁、扫描路径过长),加内存直接有效——热数据驻留后延迟立即回落;如果单条查询不慢、并发上去才慢(带宽见顶、调度排队),加机器分摊并发更有效。判据是两条曲线:单查询延迟随并发的变化与页缺失率。页缺失率高先加内存,页缺失率低而延迟随并发线性上涨就横向扩容。两个动作都做之前,先把 5.1 的延迟分解跑一遍——如果瓶颈在嵌入服务,加多少内存都是南辕北辙。
预热要生效,得长在发布流程里而不是靠人记得。一套常见的编排:新副本启动后先过健康检查(进程活着、端口通),进入"预热中"状态——此时负载均衡不导流;预热脚本跑完(页缓存触发加样本查询加热点写入缓存),探活查询的延迟连续若干次低于阈值,副本才标记为"就绪"并进入流量池;回滚场景同理,退役副本先摘流量再停进程,避免对端触发重试风暴。把这套编排写进部署模板,预热的执行就从纪律变成了默认。验证方式也简单:每次发布后看副本接入流量后的首十分钟延迟曲线,一条平线就是预热编排在工作,一个尖峰就是编排有漏洞。
软硬杠杆都上齐了,最后一节回答那个贯穿全章的问题:这些改动到底有没有用,怎么证明。