Uber是如何花费巨大精力实现缓存精确失效?


这篇文章介绍了Uber内部分布式数据库Docstore的架构、挑战以及他们构建的集成缓存解决方案CacheFront。文章详细介绍了CacheFront的设计、特性和实现,以及对最终结果的评估。通过CacheFront,Uber成功解决了在Docstore上扩展读取工作负载的挑战,并取得了显著的性能提升和资源节约。

要点:
1、Uber 的 Docstore 是一个内部分布式数据库,存储数十 PB 的数据并处理数千万个请求,是 Uber 最大的数据库引擎之一。

2、为了应对大规模低延迟读取需求,Uber 开发了集成的缓存解决方案 CacheFront for Docstore,以降低数据库引擎层的资源分配,并改善 P50 和 P99 延迟。

3、CacheFront 通过缓存预留策略、利用变更数据捕获和流服务 Flux、比较缓存等功能,提供了强一致性的缓存解决方案,并成功降低了数据库引擎的负载,实现了低延迟读取请求的需求。

缓存读取
CacheFront 使用缓存预留策略来实现缓存读取:

  1. 查询引擎层收到多一行的读取请求
  2. 如果启用了缓存,请尝试从 Redis 获取行;将响应流式传输给用户
  3. 从存储引擎检索剩余行(如果有)
  4. 使用剩余行异步填充 Redis
  5. 将剩余行流式传输给用户

缓存失效
“计算机科学中只有两件难事:缓存失效和命名。” 
——菲尔·卡尔顿

如果没有任何显式缓存失效,缓存条目将在配置的 TTL(默认情况下为 5 分钟)内过期。虽然这在某些情况下可能没问题,但大多数用户希望更改的反映速度比 TTL 更快。默认的 TTL 可以降低,但这会降低我们的缓存命中率,而不会显着提高一致性保证。

1、有条件更新
Docstore 支持条件更新,可以根据过滤条件更新一行或多行。
例如,更新指定地区所有连锁餐厅的假期安排。由于给定过滤器的结果可能会更改,因此我们的缓存层无法确定哪些行将受到条件更新的影响,直到数据库引擎中更新了实际行。

2、利用变更数据捕获CDC 来使缓存失效 
为了解决这个问题,我们利用了 Docstore 的变更数据捕获和流服务 Flux。Flux 跟踪存储引擎层中每个集群的MySQL binlog事件,并将事件发布到消费者列表。Flux 为 Docstore CDC(更改数据捕获)、复制、物化视图、数据湖摄取以及验证集群中节点之间的数据一致性提供支持。 

3、在查询引擎和 Flux 之间删除重复的缓存写入
上述缓存失效策略有一个缺陷。由于写入在读取路径和写入路径之间同时发生在缓存中,因此我们可能会无意中将过时的行写入缓存,从而覆盖从数据库检索到的最新值。

为了解决这个问题,我们根据 MySQL 中行集的时间戳来删除重复写入,这实际上作为其版本。时间戳是从 Redis 中的编码行值中解析出来的。

4、更强的点写入一致性保证
虽然 Flux 允许我们比仅仅依赖 Redis TTL 来使缓存条目失效更快,但它仍然为我们提供了最终一致性语义。然而,某些用例需要更强的一致性,例如读自己写,因此对于这些场景,我们向查询引擎添加了专用 API,允许用户在相应的写入完成后显式使缓存的行无效。

更多细节点击标题