ARC:Adaptive Replacement Cache(论文笔记)

1 分钟阅读

发布于:

论文:ARC: A Self-Tuning, Low Overhead Replacement Cache(Megiddo, Modha,FAST 2003)

ARC 是存储系统里非常经典的缓存替换算法之一(ZFS 采用)。它的核心价值不是“命中率比 LRU 高一点”,而是提供了一种更工程化的能力:在工作负载从 recency(近期性)切换到 frequency(频率性)时,缓存策略能够自适应调整,避免 LRU 在某些扫描/混合负载下被“拖垮”。

本文按“问题 → 结构 → 调参机制 → 复杂度/实现 → 观测与调优”做笔记,图用 ASCII,避免 Mermaid。


1. 背景:LRU 为什么会在扫描场景失效

LRU 假设“最近访问的未来更可能被访问”。这在许多负载成立,但在扫描(scan)场景会出现典型问题:

  • 大范围顺序扫描会把缓存全部替换掉(cache pollution)
  • 扫描结束后,原本的热数据已被逐出,命中率暴跌

同时,纯 LFU(按频率)又容易被“历史热点”绑架,对近期热点响应慢。

ARC 的目标:同时兼顾 recency 与 frequency,并能自动在两者间调节。


2. ARC 的数据结构:两条队列 + 两条幽灵队列

ARC 维护四个列表(通常按 LRU 顺序组织):

  • T1:最近访问过一次的缓存条目(recency)
  • T2:最近访问过至少两次的缓存条目(frequency)
  • B1:T1 的幽灵条目(只保存 key,不保存数据)
  • B2:T2 的幽灵条目(只保存 key,不保存数据)

直观示意:

Cache (data):
  T1: [MRU ... LRU]   (seen once)
  T2: [MRU ... LRU]   (seen multiple times)

Ghost (keys only):
  B1: [MRU ... LRU]   (recently evicted from T1)
  B2: [MRU ... LRU]   (recently evicted from T2)

其中缓存容量为 (c),ARC 维护一个自适应参数 (p):

  • 目标:T1 的大小靠近 (p),T2 的大小靠近 (c - p)
  • (p) 会根据命中在 B1/B2 的情况动态调整

3. 命中与插入规则(高层)

3.1 命中在缓存(T1/T2)

  • 命中 T1:说明“近期性”有效,同时该条目获得第二次访问 → 移到 T2 的 MRU
  • 命中 T2:继续保持在 T2 的 MRU
hit(T1): move to T2 MRU
hit(T2): move to T2 MRU

3.2 命中幽灵(B1/B2):用于调参

命中幽灵的含义是“最近被逐出的东西又被访问了”,说明驱逐策略可能偏了:

  • 命中 B1:说明 T1(recency)不够大,应该提升 p(多给 recency 配额)
  • 命中 B2:说明 T2(frequency)不够大,应该降低 p(多给 frequency 配额)

调参直觉:

if hit(B1): p = min(c, p + delta)
if hit(B2): p = max(0, p - delta)

delta 通常与队列大小比例有关(论文中有更细的建议),关键是:调整幅度随工作负载变化自动收敛


4. Replace():统一的驱逐函数

ARC 的实现核心通常被归结为一个 replace():

  • 当需要腾出空间时,根据当前 p 与 T1/T2 的大小决定从哪边驱逐

直觉规则(简化):

if |T1| > p:
  evict from T1 LRU -> move key to B1 MRU
else:
  evict from T2 LRU -> move key to B2 MRU

含义:

  • 如果 recency(T1)超出目标 p,就从 T1 驱逐;
  • 否则从 frequency(T2)驱逐。

5. 为什么需要幽灵队列:让“失败模式”可观测、可反馈

没有幽灵队列时,很难知道“刚驱逐出去的东西是否马上又被需要”。

幽灵队列提供了一种低成本信号:

  • B1 命中:recency 被低估(需要更大的 T1)
  • B2 命中:frequency 被低估(需要更大的 T2)

这让 ARC 成为“自调参”算法,而不是固定策略。


6. 复杂度与工程实现要点

6.1 时间复杂度

ARC 的关键操作是:

  • 哈希查找 key 是否在 T1/T2/B1/B2
  • 从双向链表移动节点、从尾部驱逐

因此单次访问的摊还复杂度接近 O(1)。

6.2 空间开销

幽灵队列只存 key(或 key 的哈希/指纹),开销通常可控,但要注意:

  • key 很大时需做压缩存储
  • 幽灵队列大小一般与 cache 容量同量级(最多 2c 级别的 key 记录)

6.3 并发与锁

在存储系统(例如文件系统缓存)中,ARC 往往处于高并发路径:

  • 全局锁会成为瓶颈
  • 常见做法:分段锁、每 CPU/每 NUMA 局部队列 + 周期性平衡(实现更复杂)

7. 工作负载直觉:ARC 在哪里赢

7.1 扫描 + 热点混合

扫描会让 LRU 发生污染;ARC 会通过 B1/B2 的反馈把 p 调小(增加 T2),避免热点被挤掉。

7.2 热点迁移

热点从 A 切到 B:

  • 纯 LFU 容易被 A 的历史频率锁死
  • ARC 的 recency 侧(T1)允许新热点快速进入,并通过第二次命中晋升到 T2

8. 工程观测指标(建议)

若系统实现了 ARC(或类似机制),建议暴露以下指标用于诊断:

  • T1/T2/B1/B2 当前长度
  • 参数 p 的时间序列(收敛情况)
  • 命中率拆分:hit(T1), hit(T2), hit(B1), hit(B2)
  • replace() 驱逐来源:evict_from_T1 vs evict_from_T2
  • 负载维度:顺序读比例、随机读比例、热点 key 分布变化

通过这些指标,可以把“缓存策略是否被扫描拖垮”从现象变成可观测信号。


9. 与其他策略的关系(简表)

策略 优点 典型失败模式
LRU 简单、近期性强时效果好 扫描污染、混合负载下热数据被挤掉
LFU 频率性强时稳定 对热点迁移反应慢、历史热点绑架
2Q 通过两个队列降低扫描污染 参数依赖经验,适配性有限
ARC 自适应 recency vs frequency 实现更复杂,需要维护更多结构与指标

10. 小结

ARC 的经典性来自“可自适应”这一工程属性:用幽灵队列把策略偏差变成信号,再用参数 p 把信号反馈到驱逐决策里,从而在扫描/混合/迁移负载下比单纯 LRU 更稳。对于存储系统而言,这种“稳定性”往往比极限命中率更重要,因为它直接影响尾延迟与整体吞吐的可预测性。