The CacheLib Caching Engine: Design and Experiences at Scale
- 作者: Benjamin Berg, Daniel S. Berger, Sara McAllister, Isaac Grosof, Sathya Gunasekar, Jimmy Lu, Michael Uhlar, Jim Carrig, Nathan Beckmann, Mor Harchol-Balter, Gregory R. Ganger(CMU / Facebook / Microsoft Research)
- 会议: OSDI 2020
- 原文链接: usenix.org
- PDF: osdi20-berg.pdf
摘要
Web 服务几乎在系统架构的每一层都依赖缓存。通常每个缓存都由不同的团队独立实现和 维护,并针对自身功能高度特化——比如应用数据缓存与 CDN 缓存彼此独立。但这种做法 忽视了不同缓存系统所共有的那些困难挑战,大大增加了部署、维护和扩展每个缓存的 总体成本。
本文提出一种不同的缓存开发思路(已在 Facebook 成功落地):从原本互不相干的缓存 系统中抽取出一组共同的核心需求与功能。CacheLib 是一个通用缓存引擎,基于 Facebook 内部一系列缓存用例的经验设计,让缓存的开发与维护变得容易。CacheLib 于 2017 年首次在 Facebook 部署,如今支撑着 70 多个服务,涵盖 CDN、存储和应用数据 缓存。
本文描述了从独立、特化的缓存迁移到广泛采用 CacheLib 这一过程中的经验:生产环境 负载与用例的特征如何驱动了关键设计决策,Facebook 的缓存如何随时间演进(包括 部署 CacheLib 带来的显著收益),以及这些经验对未来缓存设计与研究的启示。
关键结果
- 在 Facebook,CDN 缓存承载了 70% 的 web 请求,把延迟降低了一个数量级。
- 单台缓存服务器可以替代数十台后端数据库服务器:吞吐提升 20×,命中率超过 80%。
- CacheLib 2017 年首次部署,现已支撑 70+ 个服务(CDN、存储、应用数据缓存等)。
- 缓存工作集大到需要同时使用 DRAM 和 flash;系统还必须容忍因应用更新导致的 频繁重启。
笔记
API:
allocate(PoolId, Key, size, ttlSecs) -> ItemHandle
insertOrReplace(ItemHandle) -> bool
find(Key) -> ItemHandle
Item::getMemory() -> void*
Item::markNvmUnclean()
remove(Key) -> bool
- 内存管理由 CacheLib 掌控,零拷贝
- 原子更新的范式是「allocate 新 handle → getMemory 改 → markNvmUnclean 标记脏 → insertOrReplace 使其可见」
- find 是异步的,内存里没有数据会异步从 flash 里拿
- 没有标记脏的数据不会下沉到 nvm
DRAM Cache:
实现常规套路(slab class,配置容量,不同汰换策略),不讨论。
negative caching: SocialGraph 55.6% 的请求查询的是不存在的 key, 剩下 44.4% 查有效对象,这部分的命中率是 86.5%,用得上的地方收益极大,用不上的地方完全不需要。
不做 negative caching:整体命中率 = 44.4% × 86.5% ≈ 38.4%。
做了 negative caching:整体命中率 ≈ 55.6% × 86.5% + 38.4% ≈ 86.5% (假设空命中率和有效命中率相当)。
缺少 negative caching 条目一致性相关的讨论。
串型点优化:
- T: 刚被 reset 到 most-recently-used(MRU) 位的 Item,在 T 时间内不再 reset,可以有效缓解热点问题。
- Flat combining: 类似 rocksdb write group 选 leader 攒批。
- From: Danny Hendler, Itai Incze, Nir Shavit, and Moran Tzafrir. Flat combining and the synchronization-parallelism tradeoff. In Proceedings of the twenty-second annual ACM symposium on Parallelism in algorithms and architectures, pages 355–364, 2010.
Flash Cache:LOC/SOC:
| LOC(≥2KB) | SOC(<2KB) | |
|---|---|---|
| 索引 | 精确:DRAM 里的 segmented B+ 树 | 近似:key 哈希进 set,每 set 一个 8B Bloom filter |
| 索引规模跟随 | 对象数(百万级) | flash 页数 |
| 定位粒度 | 4B、4KB 对齐地址 → 可索引 16TB | 一个 set = 一个 4KB flash page,内含多个对象 |
| 每页对象数 | 最多 1 个(>4KB 的跨页) | 多个 |
| 驱逐粒度 | 整个 region(如 16MB) | set 内单个对象 |
| 驱逐策略 | 默认 FIFO(可选 region 粒度 pseudo-LRU) | 只能 FIFO |
横向对比:
| 对比 | 场景 / 配置 | 结果 | 归因 |
|---|---|---|---|
| vs Memcached v1.6.6 | 内存 look-aside;均用 LRU、32 线程、8–144GB、1 亿对象 | 命中率相近(小缓存 Memcached 略高,大缓存略低);吞吐最多高 60% | flat combining + T=60s;T=10s 则命中率反超但吞吐降 |
| vs NGINX / ATS | HTTP flash CDN;各用默认配置、512GB flash;固定对象大小逐档扫描 | 小对象显著领先,对象变大优势收窄,最终三者都 network-bound | SOC 对小对象的专门处理;8KB 处 NGINX 略胜,原因未查明 |
| vs RocksDB | LSM 存储;生产环境缓存 SocialGraph 数据 | 命中率 53% vs 76%;达同等吞吐需多 50% CPU | 无法定向驱逐(tombstone+compaction);FIFO compaction 下“最久未更新“≠“最久未使用” |
生产验证:
- DRAM 开销:LOC <0.1%、SOC <0.2%、DRAM cache <7%,主要靠手动调 slab class,实际情况不调碎片会翻倍
- flash 层尽量使用 FIFO 驱逐策略,FTL 几乎不用搬运任何有效数据,device-level 写放大 1.05×,代价是 FIFO 不看冷热,热对象被 FIFO 驱逐了,会稍微抬高 application-level 写入放大
- 小对象的准入策略比驱逐策略重要得多,application-level 为了对齐 4KB 会有较大的写放大,不做准入控制,设备寿命直接不达标
- 准入策略:
- 固定 p 概率写入:默认使用,和配置 iops/throughput 上限类似
- reject-n:一个对象前 n 次驱逐写入都拒绝,超过 n 次才写入
- Flashield:观察数据在 DRAM 里的访问频次决定写不写 flash;问题:如果 DRAM 里存在大量0访问数据(典型扫描/中长周期负载),能准入写入的只有很少
- 高级策略:直接采样原始负载,训练模型来做准入策略,比较麻烦,但是收益足以覆盖成本
- flash 容量会超配 50%:多出来的空间留给 FTL 做腾挪
- 预热很重要:关掉重启命中率需要一段时间才能回升(数小时~数天),系统可能会突然过载;工程团队既要快速发版、也要快速回滚,系统的 uptime 不足以完全缓存
- 真实负载没有那么 Zipf,学术界和 benchmark 工具普遍假设 Zipf α ≈ 0.9
| 负载 | α |
|---|---|
| Lookaside | ≈ 1 |
| CDN | 0.7 |
| SocialGraph | 0.55 |
| Storage | 头部明显更平,只有尾部符合 Zipf |
- 真实负载有大量 churn(热点更替):某一小时的热门对象,一小时后超过 2/3 跌出前 10%;10 分钟后就有 50% 不再热门。
- 命中率和延迟同等重要:网络拥塞、后端排队,一次 miss 会带来不可预测的延迟,一次 flash 命中和一次 DRAM 命中一样有价值,尽管 flash 慢好几个数量级
通用 vs 特化
- 特性会被意外地广泛复用:发现一个特殊场景优化很好 → 被大家知道了,都在使用 → 变成了通用优化(不同系统的问题很多都是同构的)
- 事实上 FB CacheLib 前十大用户占了全部 DRAM 缓存用量的 89%,但没有任何单个服务占主导
- 稳定性:把原本各自为政的实现收敛到一个成熟、久经测试的平台
未来
- ZNS SSD