当向量检索延迟 >500ms 时,如何采用 LRU 缓存优化?

解读

在国内真实业务场景里,向量检索(如 Milvus、Proxima、VikingDB 等)单次 P99 延迟一旦超过 500ms,就会直接拖垮大模型整体链路,导致用户体验“卡顿”甚至触发 SLA 违约。面试官问“LRU 缓存优化”,并不是让你背一段链表+哈希的代码,而是考察你能否在 LLMOps 视角 下,把“缓存”做成可灰度、可观测、可回滚的在线系统组件,同时兼顾数据一致性成本。因此,回答必须围绕“缓存什么、怎么缓存、缓存多大、缓存多久、缓存错了怎么办”五个维度展开,并给出可落地的中国互联网公司级别方案。

知识点

  1. 向量检索延迟根因:HNSW 图索引层数深、Segment 未加载到内存、磁盘 IOPS 被打满、网卡小包重传、K 值过大、过滤表达式复杂。
  2. LRU 本质O(1) 读写的定长双端队列+哈希表,淘汰最久未使用,命中率服从 Zipf 分布
  3. 缓存 Key 设计
    • 文本 Query → 归一化+SHA256(64 字节定长,避免空格大小写干扰)
    • 过滤条件 → JSON 排序后序列化(保证语义等价)
    • Top-K 与阈值 → 直接拼接到 Key 后缀,防止“K=10 缓存命中却返回 K=5”的错包。
  4. Value 设计
    • 只存 (id, score) 列表,不存原始向量,内存占用 < 原数据 5%
    • Score 采用 FP16 压缩,节省 50% 内存
  5. 并发模型
    • 读路径:无锁 RwLock + Arc<Pointer>Copy-on-Write 更新,保证 P99 读延迟 <1ms
    • 写路径:单线程异步队列批量合并,避免 Cache-Stampede
  6. 容量与 TTL
    • 线上 8C32G 容器,给 LRU 分配 4G,约可缓存 800 万条 Top-20 结果
    • TTL 分层:热门 Query 6h、普通 1h、长尾 10min,基于访问频次动态调整
  7. 一致性策略
    • 异步刷新:监听 MQ 的“向量库版本号”事件,版本号变化即失效对应分区缓存
    • 兜底回源:缓存未命中并发限制 10 个协程,防止雪崩
  8. 观测指标
    • 命中率延迟降低比例缓存污染率(错误命中/总命中)、内存利用率
    • 通过 Prometheus + Grafana 大盘实时告警,命中率低于 65% 即自动扩容副本

答案

线上实战我采用三级缓存架构

  1. 本地 LRU:嵌入在 Golang 服务进程内,容量 4G,Key 为“归一化 Query+过滤条件+K 值”的 SHA256,Value 为 FP16 压缩后的 (id,score) 列表;读路径无锁,写路径批量合并,P99 读延迟 0.8ms
  2. Redis 共享缓存:当本地 LRU 未命中,查询 Redis Cluster(单分片 16 线程,管道批处理),RTT 额外增加 3ms,命中率再提升 18%
  3. 向量库回源:Redis 也未命中才访问 Milvus 2.3,开启 GPU 索引并调大 search_resources 到 2 卡,把原来 600ms 的 P99 降到 120ms;同时用单飞限流(令牌桶 200 QPS)防止打爆后端。

灰度上线两周后,整体命中率 72%平均检索延迟从 520ms 降到 95msGPU 使用量减少 35%用户侧首 token 时间缩短 220ms,完全满足 SLA。若后续业务扩容,只需水平扩展无状态 Pod,本地 LRU 随容器一起扩,无需改动任何代码

拓展思考

  1. 缓存穿透攻击:国内黑产常用随机字符串刷接口,导致 LRU 失效。可引入 BloomFilter 前置拦截 + 空结果缓存 1min,并把异常 Query 写入 WAF 规则库实时封禁。
  2. 多租户隔离:ToB 场景下,A 租户缓存不能污染 B 租户。可在 Key 前加 tenant_id 盐值,并给每个租户设置独立内存配额超出后触发 LRU 强制回收,防止“大租户”挤占资源。
  3. 冷热分层存储:对于超过 7 天的长尾 Query,可把本地 LRU 淘汰数据异步转存到 SSD 盘上的 RocksDB,实现二级扩展缓存内存+磁盘混合命中率提升到 85%,成本仅增加 5%
  4. 与模型微调联动:若业务方频繁问“公司最新财报”,缓存命中率虽高,但答案可能过期。可结合日志挖掘把高频 Query 自动加入指令微调数据集让模型直接生成答案绕过向量检索,实现缓存与模型自我进化闭环