开发知识

Meta如何将其缓存一致性提高至99.99999999

来源: 小技术君  日期:2024-04-23 18:19:52  点击:25  属于:开发知识

简介

缓存是计算机系统中的一种强大技术,从硬件缓存到操作系统、Web浏览器,尤其是后端开发中都有广泛应用。对于像Meta这样的公司,缓存非常重要,它有助于降低延迟、处理大量工作负载,并节省成本。由于Meta的应用场景非常缓存密集,这给他们带来了另一组问题,即缓存失效。

多年来,Meta已将其缓存一致性水平从99.9999(六个九)提高到99.99999999(十个九),这意味着他们的缓存集群中不到十亿次写入中只有不到1次会导致不一致。

本文将重点讨论以下几个主要部分:

  • 缓存失效和缓存一致性是什么?
  • Meta为什么如此深刻关注缓存一致性,即使六个九还不够?
  • Meta的监控系统如何帮助他们改善缓存失效和缓存一致性,并解决Bug。

缓存失效和缓存一致性

根据定义,缓存不保存数据的真实来源,因此在源数据发生更改时,应主动使过期的缓存条目失效。如果在失效过程中出现问题,会导致缓存中的值与源数据不一致。

那么我们如何使缓存失效?

我们可以使用TTL(生存时间)来保持数据的新鲜度,以确保没有其他系统引起的缓存失效。但在本文中,我们将假设失效操作是由缓存之外的某个组件执行的。

首先让我们看看如何引入缓存不一致性:

请假设1、2、3、4是递增序列中的时间戳。

  • 缓存首先尝试从数据库获取值。
  • 但在值 x=42 到达缓存之前,某个操作更新了数据库中的值为 x=43。
  • 数据库发送了 x=43 的缓存失效事件,并在 x=42 到达之前到达缓存,将缓存值设置为43。
  • 现在事件 x=42 到达缓存,将缓存设置为42,从而引入了不一致性。

为了解决这个问题,我们可以使用版本字段来执行冲突解决,使旧版本永远不会覆盖当前版本。这种解决方案对于互联网上几乎99%的公司都有效,但是Meta操作的规模可能使其不足以解决问题,因为其系统的复杂性。

为什么Meta如此关注缓存一致性?

从Meta的角度来看,缓存不一致性几乎与数据库数据丢失一样严重,而从用户的角度来看,可能会导致非常糟糕的用户体验。

当您在Instagram上向用户发送私信时,在幕后,存在着将用户映射到存储其消息的主要存储的过程。

在这里假设有三个用户:Bob、Mary和Alice。这些用户都向Alice发送消息。Bob在美国,Alice在欧洲,Mary在日本。因此,系统将在接近用户所在地区的最近区域进行查询,以将消息发送到Alice的数据存储区域。在这种情况下,当TAO副本在BOB和Mary所在的区域查询时,它们都有不一致的数据,因此它将消息发送到区域,该区域没有Alice的消息。

在上述情况下,可能会导致消息丢失和糟糕的用户体验,因此这是Meta需要解决的重要问题之一。

监控

为了解决缓存失效和缓存一致性问题,第一步是进行测量。如果我们能够准确测量缓存的一致性,并在缓存中出现不一致的条目时发出警报,Meta确保他们的测量不包含任何误报,因为值班工程师会学会忽略它,这个指标将失去信任并变得无用。

在深入探讨Meta实施的实际解决方案之前,最简单的解决方案可能是记录和跟踪每个缓存状态的变化。但是,对于大型工作负载的情况,Meta的系统每天处理超过10万亿次的缓存填充。记录和跟踪所有缓存状态将会使本来已经很重的缓存工作负载变得极其繁重,更不用说调试了。

Polaris

Polaris在非常高的层面上,作为客户端与一个有状态服务进行交互,并且假设没有对服务内部的了解。Polaris的工作原理是“缓存应该最终与数据库一致”。Polaris接收失效事件并查询所有副本,以验证是否存在任何其他违反约束的情况。例如:

如果Polaris接收到一个失效事件,表示 x=4,版本为4,它会作为客户端检查所有缓存副本,以验证是否存在任何不变量的违反情况。如果一个副本返回 x=3 @ 版本3,Polaris会将其标记为不一致,并重新排队以稍后对其进行相同目标缓存主机的检查。Polaris会在一分钟、五分钟或十分钟的时间范围内报告不一致性。

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

让我们通过一个例子来理解:

假设Polaris接收到一个失效事件,表示 x=4,版本为4。但是当Polaris检查缓存时,找不到键 x 的条目,这应该被标记为不一致。在这种情况下,有两种可能性:

  • 在版本3时 x 是不可见的,但版本4的写入是密钥的最新写入,并且确实存在缓存不一致性。
  • 可能存在版本5的写入删除了键 x,也许Polaris只是看到了比失效事件中的更近期的数据视图。

现在,我们如何确保这两种情况中的哪一种是正确的?

为了验证,在这两种情况中,Polaris需要通过查询数据库来检查。绕过缓存的查询可能需要大量计算资源,并且可能会使数据库面临风险,因为保护数据库和扩展读取重负载是缓存的两个最常见用例。因此,我们不能向系统发送太多查询。

Polaris通过延迟执行此类检查并直到不一致样本超过设置的阈值(例如1分钟或5分钟)时才对数据库进行调用来解决此问题。Polaris生成的指标是“M分钟内缓存写入的 N 个九的一致性”。因此,目前Polaris提供了一个指标,即缓存在五分钟的时间尺度上的一致性达到99.99999999。

现在让我们看看Polaris如何帮助Meta使用编码示例解决Bug。

让我们通过一个编码示例来理解流程:

假设一个缓存维护一个键到元数据映射和键到版本映射。

cache_data = {}
cache_version = {}
meta_data_table = {"1": 42}
version_table = {"1": 4}

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

def fill_cache(key):
    fill_cache_metadata(key)
    fill_cache_version(key)

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

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)

read_thread = threading.Thread(target=read_value, args=("1"))
write_thread = threading.Thread(target=write_value, args=("1",43))
print_thread = threading.Thread(target=print_values)

在缓存失效过程中,如果由于某种原因导致失效操作失败,并且异常处理程序具有在这种情况下删除缓存的条件。

请记住,这只是可能触发Bug的非常简化的示例,实际的Bug还涉及数据库复制和跨区域通信。该Bug只有在以上所有步骤按特定顺序发生时才会触发。该Bug隐藏在交错操作和瞬态错误背后的错误处理代码中。

一致性追踪

现在您是值班工程师,收到了Polaris的缓存不一致性警报,最重要的是检查日志以确定问题可能出现在哪里。正如之前讨论的,记录每个缓存数据更改几乎是不可能的,但是如果我们只记录有可能导致更改的数据呢?

  • 如果我们看一下上面实现的代码,问题可能在于如果缓存未收到失效事件或失效操作未生效。从值班工程师的角度来看,我们需要检查以下内容:
  • 缓存服务器是否接收到了失效操作?
  • 服务器是否正确处理了失效操作?
  • 项目是否在此后变

得不一致?

Meta构建了一个有状态追踪库,在这个小窗口中记录和跟踪缓存变异,所有有趣和复杂的交互触发导致缓存不一致性的Bug。

结论

对于任何分布式系统来说,可靠的监控和日志系统至关重要,以确保我们能够捕获Bug,一旦捕获到Bug,我们就能够快速找到根本原因,从而减轻问题。借鉴Meta的例子,Polaris识别出了异常并立即触发了警报。有了一致性追踪的信息,值班工程师们不到30分钟就找到了Bug的位置。

参考链接:https://engineering.fb.com/2022/06/08/core-infra/cache-made-consistent/