点击下方“JavaEdge”,选择“设为星标”

第一时间关注技术干货!

免责声明~ 任何文章不要过度深思! 万事万物都经不起审视,因为世上没有同样的成长环境,也没有同样的认知水平,更「没有适用于所有人的解决方案」; 不要急着评判文章列出的观点,只需代入其中,适度审视一番自己即可,能「跳脱出来从外人的角度看看现在的自己处在什么样的阶段」才不为俗人。 怎么想、怎么做,全在乎自己「不断实践中寻找适合自己的大道」

0 导言

缓存是一种强大的技术,广泛应用于计算机系统的各个方面,从缓存等硬件到操作系统、网络浏览器,尤其是后端开发。对于 Meta 这样的公司来说,缓存是非常重要的,因为它可以帮助他们减少延迟、扩展繁重的工作负载并节省资金。由于他们的用例非常依赖缓存,这就给他们带来了另一系列问题,那就是缓存失效。

多年来,Meta 已将缓存一致性从 99.9999(6 个 9)提高到 99.99999999(10 个 9),即其缓存集群中,100 亿次缓存写入中只有不到 1 次不一致。本文讨论:

  1. 什么是缓存失效和缓存一致性

  2. Meta 为何如此重视缓存一致性,以至于连六个九都不够?

  3. Meta 的监控系统如何帮助他们改进缓存失效、缓存一致性并修复错误

1 缓存失效和缓存一致性

顾名思义,缓存并不保存数据真实来源,因此当真实来源中的数据发生变化时,应该有一个主动失效陈旧缓存条目的过程。若失效过程处理不当,会在缓存中无限期地留下与真实源中不同的不一致值。

咋才能使缓存失效呢?

可设置一个 TTL 保持缓存的新鲜度,就不会有来自其他系统的缓存失效。但本文讨论mata的缓存一致性问题,我们将假设无效操作是由缓存本身以外的其他系统执行的。

先看咋引入缓存不一致:

打开网易新闻 查看精彩图片

假设 1、2、3、4 依次递增的时间戳

  1. 缓存先尝试从数据库中填充值

  2. 但 x=42 的值到达缓存前,一些操作更新了数据库中 x=43 的值

  3. 数据库为 x=43 发送缓存失效事件,该事件在 x=42 之前到达缓存,缓存值被设置为 43

  4. 事件 x =42 现在到达缓存,缓存被设置为 42,于是出现不一致

为解决此问题,可用version字段来执行冲突解决,旧版本就不会覆盖新版本。这种解决方案适用互联网 99% 的公司,但由于其系统复杂性,即使这样的解决方案也可能无法满足 Meta 的运营规模。

2 为啥Meta如此关注缓存一致性?

  • 从Meta的角度,缓存不一致几乎与数据库数据丢失一样糟糕

  • 而从用户角度,缓存不一致可能导致非常糟糕的用户体验

当你在 Ins 上向用户发送 DM 时,在幕后会有一个用户到主存储的映射,用户的信息就存储在主存储中。 想象有三个用户。鲍勃、玛丽和爱丽丝。两个用户都向 Alice 发送一条信息。鲍勃在美国,爱丽丝在欧洲,玛丽在日本。因此,系统会查询离用户居住地最近的地区,从而将信息发送到 Alice 数据存储区。在这种情况下,当 TAO 复制品查询 BOB 和 Mary 居住的地区时,它们都有不一致的数据,因此它将消息发送到了没有 Alice 消息的地区。

打开网易新闻 查看精彩图片

上述情况下,会出现信息丢失和糟糕的用户体验,因此这是 meta 要解决的首要问题之一。

3 监测

要解决缓存失效和缓存一致性问题,先测量。如能精确测量缓存一致性,并在缓存出现不一致记录时发出警报,Meta 就能确保他们的测量结果不包含任何误报,因为值班工程师会学会忽略它,这样该指标就会失去信任,变得毫无用处。

深入探讨 Meta 解决方案前,最简单的解决方案就是记录和跟踪每次缓存状态的变化。工作负载小时,该方案可行,但 Meta 系统每天的缓存填充量超过 10 万亿次。记录和跟踪所有的缓存状态会将一个准备就绪的缓存工作负载变成一个极其繁重的工作负载,甚至都不用考虑咋调试。

4 Polaris

Polaris 在很高的层次上以客户身份与有状态服务交互,并且不假定对服务内部结构有任何了解。Polaris 工作原则:缓存最终应与数据库保持一致。Polaris 收到无效事件后会查询所有副本,以验证是否发生其他违规事件。如 Polaris 接收到 x=4 版本 4 的无效事件,它就会以客户端身份检查所有缓存副本,以验证是否有任何违反不变式的情况发生。若某副本返回 x=3 @ 版本 3,Polaris 就会将其标记为不一致,并重新获取样本,随后再针对同一目标缓存主机进行检查。Polaris 会以特定的时间尺度(如一分钟、五分钟或十分钟)报告不一致情况。

打开网易新闻 查看精彩图片

这种多时间尺度的设计不仅使 Polaris 能够在内部拥有多个队列,从而高效地实施后退和重试,而且对于防止产生误报也至关重要。

4.1 举例

假设 Polaris 接收到一个version=4 的无效信息 x = 4。但当 Polaris 检查缓存时,却找不到 x 的条目,因此应将其标记为不一致。此时,有两种可能:

  1. x 在版本 3 不可见,但版本 4 的写入是对Key的最新写入,这确实是缓存不一致

  2. 可能版本 5 的写入删除了键 x,也许 Polaris 只是看到了比失效事件中的数据更新的数据视图

哪种正确?

为验证,Polaris 需查询数据库。绕过缓存的查询可能是计算密集型的,也可能使数据库面临风险,因为保护数据库和扩展读取重型工作负载是缓存最常见的两种使用情况。因此,不能向系统发送过多查询。

Polaris 解决方案是延迟执行此类检查,并调用数据库,直到不一致样本跨过设定的阈值(如 1min或 5 min)。Polaris 产品的指标是 "在 M 分钟内九分之九的 cahce 写入是一致的"。因此,现在 Polaris 提供的指标是:在 5 分钟的时间范围内,99.99999999 次缓存是一致的。

编码示例

一个咋产生缓存不一致的编码示例,看 polaris 咋帮 Meta 解决的一个 bug。设有一高速缓存,维护着K到Meta数据的映射和K到version的映射:

打开网易新闻 查看精彩图片
cache_data = {}
cache_version = {}
meta_data_table = {"1": 42}
version_table = {"1": 4}
  1. 当读请求到来,先检查缓存值,如缓存中无该值,则从数据库返回该值:

def read_value(key):
value = read_value_from_cache(key)
if value is not None:
return value
else:
return meta_data_table[key]


def read_value_from_cache(key):
if key in cache_data:
return cache_data[key]
else:
fill_cache_thread = threading.Thread(target=fill_cache(key))
fill_cache_thread.start()
return None

2.缓存返回 None 结果,然后开始从数据库填充缓存。我在这里使用了线程来使进程异步。

def fill_cache_metadata(key):
meta_data = meta_data_table[key]
print("Filling cache meta data for", meta_data)
cache_data[key] = meta_data

def fill_cache_version(key):
time.sleep(2)
version = version_table[key]
print("Filling cache version data for", version)
cache_version[key] = version

def write_value(key, value):
version = 1
if key in version_table:
version = version_table[key]
version = version + 1

write_in_databse_transactionally(key, value, version)
time.sleep(3)
invalidate_cache(key, value, version)

def write_in_databse_transactionally(key, data, version):
meta_data_table[key] = data
version_table[key] = version

3.与此同时,当版本数据被填入缓存时,数据库会有新的写入请求来更新元数据值和版本值。此时此刻,这看起来像是一个错误,但其实不是,因为缓存失效应使缓存恢复到与数据库一致的状态(在缓存中添加了 time.sleep,并在数据库中添加了写入函数,以重现该问题)。

def invalidate_cache(key, metadata, version):
try:
cache_data = cache_data[key][value] ## To produce error
except:
drop_cache(key, version)

def drop_cache(key, version):
cache_version_value = cache_version[key]
if version > cache_version_value:
cache_data.pop(key)
cache_version.pop(key)
  1. 之后,在缓存失效过程中,由于某些原因导致失效失败,在这种情况下,异常处理程序有条件放弃缓存。

    删除缓存函数的逻辑是,如果最新值大于 cache_version_value,则删除该键,但在我们的情况下并非如此。因此,这会导致在缓存中无限期地保留陈旧的元数据

记住,这只是错误可能发生的非常简单的变体,实际的错误更加错综复杂,涉及到数据库复制和跨区域通信。只有当上述所有步骤都按此顺序发生时,才会触发错误。不一致性很少被触发。错误隐藏在交错操作和瞬时错误后面的错误处理代码中。

5 一致性跟踪

既然你已接到 Polaris 缓存不一致的呼唤,最重要的就是检查日志,看问题在哪。正如之前所讨论,记录每个缓存数据变化几乎不可能,但若只记录有可能导致变化的变化呢?

打开网易新闻 查看精彩图片

若看到上面代码,那么如果缓存没有收到失效事件或失效没有起作用,就会出现问题。从 oncall 角度,需检查:

  • 缓存服务器是否收到了无效信息?

  • 服务器是否正确处理了失效?

  • 之后该条目是否变得不一致?

Meta 构建了一个有状态跟踪库,可记录和跟踪紫色小窗口中的缓存突变,所有有趣而复杂的交互都会触发错误,导致缓存不一致。

6 结论

对任何分布式系统,可靠的监控和日志系统必不可少,才能确保我们抓住漏洞,并在抓住漏洞后迅速找到根本原因,减少问题。以 Meta 为例,Polaris 发现异常后立即发出警报。利用一致性跟踪的信息,值班工程师只用了不到 30 分钟就找到了错误所在。

参考:

  • https://engineering.fb.com/2022/06/08/core-infra/cache-made-consistent/

  • 在这里找到我创建错误的实现方法: https://github.com/Mayank-Sharma-27/meta-cache-made-consistent

  • Linkedin: https://www.linkedin.com/in/mayank-sharma-2002bb10b/

关注我,紧跟本系列专栏文章,咱们下篇再续!

★ 作者简介:魔都架构师,多家大厂后端一线研发经验,在分布式系统设计、数据平台架构和AI应用开发等领域都有丰富实践经验。 各大技术社区头部专家博主。具有丰富的引领团队经验,深厚业务架构和解决方案的积累。 负责: 中央/分销预订系统性能优化 活动&券等营销中台建设 交易平台及数据中台等架构和开发设计 车联网核心平台-物联网连接平台、大数据平台架构设计及优化 LLM Agent应用开发 区块链应用开发 大数据开发挖掘经验 推荐系统项目 目前主攻市级软件项目设计、构建服务全社会的应用系统。 ”

参考:

  • 编程严选网

编程严选网:http://www.javaedge.cn/ 专注分享软件开发全生态相关技术文章、视频教程资源、热点资讯等,全站资源免费学习,快来看看吧~ 【编程严选】星球

欢迎长按图片加好友,我会第一时间和你分享软件行业趋势面试资源学习方法等等。

添加好友备注【技术群交流】拉你进技术交流群

关注公众号后,在后台私信:

  • 更多教程资源应有尽有,欢迎关注并加技术交流群,慢慢获取

  • 为避免大量资源被收藏白嫖而浪费各自精力,以上资源领取分别需要收取1元门槛费!