Meta如何将缓存一致性提高到99.99999999%( 二 )


Meta如何将缓存一致性提高到99.99999999%

文章插图
这种多时间尺度的设计不仅允许Polaris内部拥有多个队列,以有效实现回退和重试,对于防止误报也至关重要 。
让我们通过另一个例子来理解这一点:
假设Polaris接收到一个版本为4的失效事件x=4 。但当Polaris检查缓存时,却找不到x的条目 , 它应该将此标记为不一致 。在这种情况下,有两种可能性 。
  • 在版本3时,x是不可见的 , 但版本4的写入是该键上的最新写入 , 这确实是一个缓存不一致 。
  • 可能是版本5的写入删除了键x,也许Polaris只是看到了比失效事件中更新的数据 。
现在 , 我们如何确定这两种情况中哪一种是正确的?
为了验证这两种情况Polaris需要通过查询数据库进行检查 。绕过缓存的查询可能是计算密集型的,并且也可能使数据库面临风险,因为保护数据库和扩展读取密集型工作负载是缓存的两个最常见的用例 。因此,我们不能向系统发送太多查询 。
Polaris的解决方案是,延迟执行此类检查并调用数据库 , 直到不一致的样本超过设定的阈值(例如1分钟或5分钟) , 从而解决了这个问题 。Polaris的产品指标表述为“在M分钟内,N个九的缓存写入是一致的 。”因此,目前Polaris提供了一个指标:表示在五分钟的时间尺度内,99.99999999%的缓存是一致的 。
现在,让我们通过一个编码示例,了解Polaris如何帮助Meta解决由缓存不一致性引起的bug:
假设有一个缓存,它维护着密钥到元数据的映射和密钥到版本的映射 。
Meta如何将缓存一致性提高到99.99999999%

文章插图
cache_data = https://www.isolves.com/it/cxkf/bk/2024-04-15/{}
【Meta如何将缓存一致性提高到99.99999999%】cache_version = {}
meta_data_table = {"1": 42}
version_table = {"1": 4}
1.当读取请求到来时,首先在缓存中检查该值 。如果缓存中不存在该值,则从数据库中返回该值 。
def read_value(key):
value = https://www.isolves.com/it/cxkf/bk/2024-04-15/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(key):
fill_cache_metadata(key)
fill_cache_version(key)
def fill_cache_metadata(key):
meta_data = https://www.isolves.com/it/cxkf/bk/2024-04-15/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.同时,当版本数据填入缓存时,数据库可能会有新的写入请求,更新元数据值和版本值 。此时这看似是一个bug , 但实际不是,因为缓存失效应使缓存恢复到与数据库一致的状态(注意,我在缓存中添加了 time.sleep,并在数据库中添加了写入函数 , 以复现该问题) 。
def invalidate_cache(key, metadata, version):
try:
cache_data = https://www.isolves.com/it/cxkf/bk/2024-04-15/cache_data[key][value] ## To produce error
except:
drop_cache(key, version)
def drop_cache(key, version):
cache_version_value = https://www.isolves.com/it/cxkf/bk/2024-04-15/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)
4.后来,在缓存失效过程中,由于某些原因导致失效失败,在这种情况下 , 异常处理程序有条件放弃缓存 。
丢弃缓存函数的逻辑是,如果最新值大于 cache_version_value , 那么就删除该键,但在我们实际情况中并非如此 。因此,这将导致在缓存中无限期地保留陈旧的元数据 。


推荐阅读