缓存有助于减少延迟,提高重读工作负载的可扩展性,并且节省成本。实际上缓存是无处不在的,它也在你的手机和你的浏览器中运行。例如,CDN和DNS本质上是地理复制的缓存。正是由于许多缓存在幕后工作,你现在才能阅读这篇文章。

Phil Karlton有句名言:"计算机科学中只有两个难题:缓存失效和命名"。如果你曾经处理过的无效缓存,那么你很有可能遇到过缓存不一致这个恼人的问题。

在Meta,我们运营着世界上最大的高速缓存,包括TAO和Memcache。多年来,我们将TAO的缓存一致性提高了一个档次,从99.9999%(六个九)提高到99.99999999%(十个九)。

当涉及到缓存无效时,我们相信我们现在有一个有效的解决方案来弥补理论和实践之间的差距。这篇博文中的原则和方法广泛适用于大多数(如果不是所有)的缓存服务。无论你是在Redis中缓存Postgres数据,还是将分散数据具像化,都是如此。

我们希望能帮助减少工程师必须处理的缓存失效问题,并帮助增强缓存的一致性。

定义缓存失效和缓存一致性

根据定义,缓存并不是你数据的真实来源(例如,数据库)。缓存失效描述的是当真实源中的数据发生变化时,主动将陈旧的缓存条目失效的过程。如果缓存失效处理不当,就会在缓存中无限期地保留一个不一致的值。

缓存失效涉及到一个必须由缓存自身以外的程序来执行的动作。一些程序(例如,客户端或公共/子系统)需要告诉缓存其中数据发生了变化。仅仅依靠TTL来保持有效性的缓存,不在本文讨论范围之内。在这篇文章的其余部分,我们将假设存在缓存失效操作。

为什么这个看似简单的过程在计算机科学中被认为是个困难的问题?下面是个简单的例子,说明如何引入缓存不一致的问题。

缓存首先尝试从数据库中填充x。但是在 "x=42 "到达缓存主机之前,有人将x设置为43。缓存失效事件 "x=43 "首先到达缓存主机,将x设置为43。"x=42 "到达了缓存,将x设置为42。现在数据库中"x=43 ",而缓存中 "x=42 "。

有很多方法来解决这个问题,其中之一就是维护版本字段。这样我们就可解决冲突,因为旧的数据不应该覆盖新的数据。但是,如果缓存条目 "x=43 @version=2 "在 "x=42 "到达之前就失效了呢?在这种情况下,缓存数据依然是错误的。

缓存失效的挑战不仅来自于失效协议的复杂性,还来自于监控缓存一致性和如何确定缓存不一致的原因。设计一个一致的缓存与操作一个一致的缓存有很大不同,就像设计Paxos协议与构建在生产中实际运行的Paxos一样,都有很大区别。

我们为什么要关心缓存的一致性

我们必须解决复杂的缓存失效问题吗?在某些情况下,缓存的不一致性几乎和数据库数据丢失一样严重。从用户的角度来看,它甚至和数据丢失没有区别。

让我们来看看另一个关于缓存不一致如何导致脑裂的例子。Meta公司使用消息将其从用户在主存储数据的映射到TAO中。它经常进行移动,以保证用户可以就近访问。每次你向某人发送消息时,系统都会查询TAO,以找到消息的存储位置。许多年前,当TAO的一致性较差时,一些TAO副本在重新移动后会出现不一致的数据,如下例所示。

想象一下,在将Alice的主消息存储从区域2切换到区域1后,Bob和Mary,都向Alice发送了消息。当Bob向Alice发送消息时,系统查询了靠近Bob居住地的区域的TAO副本,并将消息发送到区域1。当Mary向Alice发送消息时,系统查询了靠近Mary居住地的地区的TAO副本,命中了不一致的TAO副本,并将消息发送到了地区2。Bob和Mary将他们的消息发送到不同的区域,而两个区域都没有爱丽丝消息的完整副本。

缓存失效模型

了解缓存失效的困难之处尤其具有挑战性。让我们从一个简单的模型开始。缓存的核心是一个有状态的服务,它将数据存储在一个可寻址的存储介质中。分布式系统本质上是一种状态机。如果每个状态转换都能正确执行,我们就会有一个按预期工作的分布式系统。否则,系统就会问题。所以,关键的问题是:对于有状态的服务,什么在改数据?

静态缓存有一个非常简单的缓存模型(例如,简化的CDN接近这个模型)。数据是不可改变的。没有缓存主动失效。对于数据库来说,数据只有在写入(或复制)时才会发生变化。我们通常对数据库的每一个状态变化都有日志。每当发生异常时,日志可以帮助我们了解发生了什么,缩小问题的范围,并找出问题所在。构建容错的分布式数据库(这已经很困难了),有其独特的挑战。这些只是简化的模型。

对于像TAO和Memcache这样的动态缓存,数据在读取(缓存填充)和写入(缓存失效)的路径上都会发生变化。这种组合使得多竞态条件成为可能,而缓存失效则是一个困难的问题。缓存中的数据是不持久的,这意味着有时候对解决冲突很重要的版本信息会被清除出去。结合所有这些特点,动态缓存产生的竞态条件超出了我们的想象。

而且,记录和跟踪每一个缓存状态的变化几乎是不现实的。缓存经常被引入来扩展重读的工作负载。这意味着大部分的缓存状态变化都来自缓存填充路径。以TAO为例。它每天提供超过四亿次的查询。即使缓存命中率达到99%,我们每天也要进行超过10万亿次的缓存填充。记录和追踪所有的缓存状态变化会使一个重读的缓存工作负载变成一个极重写的日志系统工作负载。调试一个分布式系统已经带来了巨大的挑战。调试一个没有缓存状态变化的日志或追踪的分布式系统,基本是不可能的。

尽管有这些挑战,我们还是提高了TAO的缓存一致性,这些年来从99.9999提高到99.99999999。在文章的其余部分,我们将解释我们是如何做到的,并强调一些未来的工作。

针对一致性的可观察性

为了解决缓存失效和缓存一致性问题,第一步涉及测量。我们要测量高速缓存的一致性,并在高速缓存中出现不一致的条目时发出警报。测量不能包含任何假阳性。人类的大脑可以很容易地调出噪音。如果存在任何误报,人们很快就会学会忽略它,而这个测量也变得毫无用处。我们还需要测量是精确的,因为我们谈论的是测量超过10个九的一致性。如果一个修正已经落地,我们要保证我们可以定量地测量它带来的改进。

为了解决测量问题,我们建立了一个名为Polaris的服务。对于一个有状态的服务中的任何异常,只有当客户能够以这种或那种方式观察到它,它才是一个异常。否则,它就根本不重要。基于这一原则,Polaris 专注于测量违反客户可观察不变量的情况。

在高层次上,Polaris作为客户端与有状态的服务进行交互,并且不假设了解服务内部。这使得它是通用的。Meta有几十个服务使用Polaris。"缓存最终应该与数据库一致 "是Polaris监控的一个典型的客户端可观察到的不变因素,特别是在异步缓存失效的情况下。在这种情况下,Polaris假装是一个缓存服务器并接收缓存失效事件。例如,如果Polaris收到一个无效事件,说 "x=4 @version 4",它就会作为客户查询所有的缓存副本,以验证是否有任何违反该不变性的情况发生。如果一个缓存副本返回 "x=3 @version 3",Polaris将其标记为不一致,并重新等待样本,以便以后针对同一目标缓存主机进行检查。Polaris在某些时间尺度上报告不一致,例如一分钟、五分钟或十分钟。如果这个样本在一分钟后仍然显示为不一致,Polaris就将其报告为相应时间尺度的不一致。

这种多时间尺度的设计不仅允许Polaris在内部存在多个队列,以有效地实现回退和重试,而且对于防止产生误报也是至关重要的。

我们来看看一个更有趣的例子。假设Polaris收到一个 "x=4 @version 4 "的无效信息。但是当它查询一个缓存副本时,得到的答复是x不存在。目前还不清楚Polaris是否应该将此作为一个不一致的标记。有可能x在版本3的时候是不存在的,版本4的写入是对key的最新写入,而这种情况确实是缓存不一致。也有可能是第5个版本的操作删除了x,也许Polaris只是看到了失效事件中的数据更新的视图。

为了区分这两种情况,我们需要绕过缓存,检查数据库中的内容。绕过缓存的查询是非常密集的运算。它们也会使数据库面临风险,因为保护数据库和扩展重读工作负载是缓存最常见的用例之一。因此,我们不能绕过缓存发送太多的查询。Polaris通过延迟执行计算密集型操作来解决这个问题,直到不一致的样本跨越报告时间尺度(如一分钟或五分钟)。真正的缓存不一致和对同一key的竞争写操作是很少的。因此,在它跨越下一个时间尺度边界之前才进行一致性检查有助于消除执行大部分数据库查询。

我们还在Polaris发给缓存服务器的查询中加入了一个特殊的标志。因此,Polaris会知道目标缓存服务器是否已经看到并处理了缓存失效事件。这一点信息使Polaris能够区分瞬时的缓存不一致(通常由复制/验证滞后引起)和 "永久 "的缓存不一致(旧版本还无限期地存在于缓存中)。

Polaris也提供观测指标,如"N个9的缓存写入在M分钟内是一致的"。在文章的开头,我们提到,通过一项改进,我们将TAO的缓存一致性从99.9999%提高到99.99999999%。Polaris提供了5分钟时间尺度的指标。换句话说,99.99999999%的缓存写入在5分钟内是一致的。在TAO中5分钟内,100亿次缓存写入中不到1次会出现不一致。

我们将Polaris部署为一个单独的服务,这样它就可以独立于生产服务及其工作负载进行扩展。如果我们想测量到更多的数据,我们可以只增加Polaris的吞吐量或在更长的时间窗口上执行聚合。

一致性追踪

在大多数图中,我们用一个简单的盒子来表示缓存。在现实中,省略了许多依赖关系和数据流之后,看起来可能像这样。

缓存可以在不同的时间点从不同的上游填充,这些上游可以是在同一region内或跨region。升级、分片移动、故障恢复、网络分区和硬件故障都有可能触发导致缓存不一致的问题。

然而,正如前面提到的,记录和追踪每一个缓存数据的变化是不切实际的。但是,如果我们只在缓存不一致的地方和时候(或者缓存失效可能被错误地处理)记录和跟踪缓存的突变,会怎么样呢?在这个庞大而复杂的分布式系统中,任何组件的缺陷都可能导致缓存不一致,是否有可能找到一个引入大部分(如果不是全部)缓存不一致的地方?

我们的任务变成了寻找一个简单的解决方案来帮助我们管理这种复杂性。我们想从单个缓存服务器的角度来评估整个缓存一致性问题。最后,不一致的问题必须在一个缓存服务器上出现。从它的角度来看,它只关心几个方面。

  • 它是否收到了失效信息?

  • 它是否正确地处理了这个失效信息?

  • 之后缓存是否变得不一致了?

这就是我们在文章开头解释的那个例子,现在用一个时空图来说明。如果我们把注意力集中在底部的缓存时间轴上,我们可以看到在客户端写完之后,有一个窗口,在这个窗口中,失效和缓存填充都在竞争更新缓存。一段时间后,缓存将处于静止状态。在这种状态下,缓存的填充仍然会大量发生,但从一致性的角度来看,由于没有写入,它已经沦为一个静态的缓存,所以它的意义不大。

我们建立了一个有状态的库,记录和跟踪这个小的紫色窗口中的缓存突变,在这个窗口中,所有相关的复杂交互都会引发导致缓存不一致的问题。它涵盖了缓存的过期,甚至没有日志也能告诉我们是否无效事件从未到达。它被嵌入到几个主要的缓存服务中,并贯穿于整个失效管道。它缓冲了最近修改的数据索引,用于确定后续的缓存状态变化是否应该被记录下来。它还支持代码追踪,所以我们会知道每个被追踪查询的确切代码路径。

这种方法帮助我们发现并修复了许多缺陷。它为诊断缓存的不一致提供了一个系统性的、更可扩展的方法。事实证明,它非常有效。

我们今年发现并修复的一个线上错误

在一个系统中,我们对每条数据进行了版本排序和冲突解决。在这种情况下,我们在缓存中观察到 "metadata=0 @version4",而数据库中包含 "metadata=1 @version4"。缓存无限期地保持不一致。这种状态应该是不可能的。你会如何处理这个问题?如果我们能得到导致最终不一致状态的每一个步骤的完整时间线,那该有多好?

一致性追踪正好提供了我们需要的时间线。

在系统中,一个非常罕见的操作以事务方式更新了底层数据库的两个表—元数据表和版本表。

根据一致性追踪,我们知道发生了以下情况。

  1. 缓存试图添加版本数据和元数据。

  2. 在第一轮中,缓存首先填充了旧的元数据。

  3. 接下来,一个写事务以原子方式更新了元数据表和版本表。

  4. 在第二轮中,缓存写入了新的版本数据。这里,缓存填充操作与数据库事务交错进行。因为竞态窗口很小,所以这种情况很少发生。你可能会想,"这就是bug。"。但是实际上到目前为止,一切都按预期进行,因为缓存失效应该可以把缓存恢复一致。

  5. 稍后,在尝试将缓存项更新为新元数据和新版本时,出现了缓存无效。这几乎总是有效的,但这次没有。

  6. 缓存失效在缓存主机上遇到了一个罕见的瞬时错误,这触发了错误处理代码。

  7. 错误处理程序将该条目删除。伪代码看起来是这样的。

drop_cache(key, version);

如果条目的版本低于指定的版本,则将其放入缓存。但是,不一致的缓存项包含最新版本。所以这段代码什么也没做,将过时的元数据无限期地留在缓存中。这就是bug。我们在这里把这个例子简化了很多。实际的bug甚至更加复杂,涉及到数据库复制和跨区域通信。只有当以上所有的步骤都发生,并且以这个顺序具体发生时,这个bug才会被触发。不一致的情况很少出现。该错误隐藏在交互操作和瞬时错误背后的错误处理代码中。

许多年前,如果有人对代码和服务了如指掌并且他们足够幸运的话,要花几周时间才能找到这种错误的根本原因。在这种情况下,Polaris发现了异常情况,并立即发出警报。通过一致性追踪的信息,值班工程师花了不到30分钟就可以找到这个错误。

未来的缓存一致性工作

我们已经分享了我们如何用一种通用的、系统的、可扩展的方法来增强我们的缓存一致性。展望未来,我们想让我们所有缓存的一致性在物理上尽可能地接近100%。分散的二级指数的一致性带来了一个有趣的挑战。我们也在测量并有目的地改善读取时的缓存一致性。最后,我们正在为分布式系统建立高水平的一致性API,想想针对分布式系统的C++的std::memory_order。

原文链接:

https://engineering.fb.com/2022/06/08/core-data/cache-invalidation/?continueFlag=5d7598b8068e4850d16d3bc686805488

参考阅读:

  • Junit执行器Runner探索之旅

  • Web3在遥远的未来?不,它已经来了!

  • 百度工程师教你玩转设计模式(单例模式)

  • Kafka 负载均衡在 vivo 的落地实践

  • 记一次 JMeter 压测 HTTPS 性能问题

本文由高可用架构翻译。技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

活动预告

↓↓↓

GIAC 全球互联网架构大会 2022 将于 7 月 22 - 23 日在深圳举行,本届 GIAC 议题共设置有 24 个专题,覆盖各类架构热点领域,每个主题由业内知名架构师、技术负责人等专家担任出品人,负责议题选取和质量把控。本次大会包括基础架构(由bilibili技术委员会主席毛剑担任出品人)和高并发&高可用(由菜鸟架构委员会主席钟勇担任出品人)等专题,将有更多本文相关内容演讲,点击阅读原文查看 GIAC 详细日程。

点击【阅读原文】,了解更多活动信息。

Facebook 是怎么保证缓存一致性的相关推荐

  1. 10 张图打开 CPU 缓存一致性的大门

    前言 直接上,不多 BB 了. 正文 CPU Cache 的数据写入 随着时间的推移,CPU 和内存的访问性能相差越来越大,于是就在 CPU 内部嵌入了 CPU Cache(高速缓存),CPU Cac ...

  2. 内存模型是怎么解决缓存一致性的

    转载自  内存模型是怎么解决缓存一致性的 在再有人问你Java内存模型是什么,就把这篇文章发给他这篇文章中,我们介绍过关于Java内存模型的来龙去脉. 我们在文章中提到过,由于CPU和主存的处理速度上 ...

  3. 面试官:缓存一致性问题怎么解决?

    关于Redis的其他的一些面试问题已经写过了,比如常见的缓存穿透.雪崩.击穿.热点的问题,但是还有一个比较麻烦的问题就是如何保证缓存一致性. 对于缓存和数据库的操作,主要有以下两种方式. 先删缓存,再 ...

  4. 【并发编程】CPU多级缓存与缓存一致性

    CPU多级缓存与缓存一致性 定义 cpu缓存是位于CPU与内存之间的临时存储器,它的容量比内存小的多,但是交换速度却比内存要快得多 为什么需要CPU cache? cpu的频率太快了,快到主存跟不上, ...

  5. 缓存一致性协议和CPU缓存架构(MESI协议)、伪共享

    目录 简介 CPU高速缓存 为什么要有CPU高速缓存 局部性原理 缓存一致性 缓存一致性的要求 总线窥探 工作原理 窥探协议 一致性协议 MESI协议 总线事务 总线仲裁 总线锁定 缓存锁定 伪共享问 ...

  6. Java并发编程(六):从CPU缓存一致性协议到JMM(Java内存模型)

    注:本系列主要注重并发编程这块儿,JVM内容很多,会另外开专栏总结,此系列可能只是会稍微提及 一.跨平台和JVM 经过前面几篇博文的介绍,我们知道,任何编程语言编写的程序要想被计算机执行,都必须被翻译 ...

  7. 图解MESI(缓存一致性协议)

    文章目录 Java内存模型 CPU缓存一致性 CPU Cache和内存数据不一致 写直达 写回 多个CPU核心的数据不一致 总线嗅探 MESI(缓存一致性协议) 总结 Java内存模型 做Java开发 ...

  8. 代码评审-如何保证缓存与数据库的读写一致性?

    我们从近期代码评审过程中的一段代码,开始探讨缓存和数据库的一致性问题. 探讨前置 一般来说,使用缓存主要为了提升应用性能和降低DB的直接负载,从场景上来说可以接受最终一致性方案, 如果业务场景要求 & ...

  9. 怎么保证缓存和数据库一致性

    背景 缓存是软件开发中一个非常有用的概念,数据库缓存更是在项目中必然会遇到的场景.而缓存一致性的保证,更是在面试中被反复问到,这里进行一下总结,针对不同的要求,选择恰到好处的一致性方案. 缓存是什么 ...

最新文章

  1. python和c哪个好学-c语言和python哪个容易
  2. Redhat安装gtk2.0和pkg-config
  3. 深有体会的积极人生态度
  4. 小tip: base64:URL背景图片与web页面性能优化(转载)
  5. 49-Python 安装pythoncom库和pyHook
  6. [BZOJ2733] [HNOI2012] 永无乡 (splay启发式合并)
  7. 使用Kmeans聚类分析对复杂的数据进行分类
  8. 带你快速入门计算机网络模型与5G协议!!
  9. windows类书的学习心得
  10. 详解DAO类(数据库操作对象)
  11. CSS 实现面包屑导航
  12. 【NLP】句法分析一
  13. namedtuple 具名元组详解
  14. Memcached的LRU算法
  15. python seo编程_gogo闯SEO快排教程及应用编程(同步官方)
  16. !HDU 1493 QQpet exploratory park-dp
  17. Eclipse 报错The method xxx of type must override a superclass method、Description Resource Path Locati
  18. Nyist 915 +-字符串
  19. JNI 使用,原来我一直错
  20. 转自看雪——Hackshield内幕(thisIs)

热门文章

  1. 网络基础之网络协议篇(转)
  2. 功率电感的痛点:两个额定电流Isat , Irms 如何理解?
  3. 二进制、八进制、十进制、十六进制间相互转换
  4. 现代电机设计可改进便携式真空吸尘器
  5. 2020年中国水产饲料行业发展现状分析,水产需求增加带动行业发展「图」
  6. BearyChat 消息推送机器人 For PHP Laravel
  7. linux怎么写用拼音写中文为什么出错,linux上的搜狗拼音用不了啦?快来看怎么解决...
  8. 发个丰田生产模式培训资料
  9. WEBRTC 录音与会议录音
  10. 拓嘉辰丰:拼多多网店设置满返有哪些好处?