Scaling Memcache at Facebook论文理解

原文地址:http://nil.csail.mit.edu/6.824/2020/papers/memcache-fb.pdf

体会

这篇论文读起来很有意思,设计体现了各方面的权衡,里面不仅考虑了分布式的CAP问题,也考虑到了计算机网络发包的机制和其他方面的内容,值得仔细品味。

读这篇文章的过程中也发现自己对于分布式体系概览不是很熟悉,RAFT和Memcache的应用范围划分不是很熟悉,另外写着写着发现自己对于K8S的应用范畴也不是很了解,没有梳理成体系,需要不断加深理解。

之后有机会得多看些企业内部各种技术框架,了解各个部分的意义。

本文有意思的点

lease、UTP/TCP传输机制、分布式锁机制,整个架构设计也很精巧

论文思路梳理

单集群(memcache原语、降低负载和延迟)->业务增长多集群(如何保证集群快速启动、保持集群信息一致性)

Introduction

Memcached是一个简单的内存cache组件,支持低延迟低成本的内存访问。这篇文章描述了Facebook如何基于memcache构造一个可以响应大量请求(98%读请求,2%写请求)的分布式key-value数据库(key-value数据库可以储存各种数据,视频二进制文件、图片、关注列表等)。

其中值得注意的是,集群进行了地理划分,相距很远。

OverView

没有十全十美,任何场景都能有突出表现的分布式系统。驱动这个设计方案的因素有如下两点:

1、人们更多消费内容而非创造内容(即读请求远多于写请求)。 

2、读请求获取到资源来自不同的底层设计,例如mysql、HDFS等。因此需要能够储存来自不同源点的cache plan。

Memcached很突出的一个特点是,他提供了非常简单的操作源语(set、get、delete)。简洁明了,这个特点也使其大受欢迎。(简洁性有时候对于一个算法,一个系统来说也很重要,比如Raft,比如Zookeeper原语)

query cache的方式

读请求:首先从memcache查询数据。如果cache里没有数据,就从后台的database获取数据,并把key-value pair更新到cache中。

写请求:首先对database进行修改,然后向cache发送请求,invalidate之前的旧数据。

这里值得注意的是,之所以选择删除cache而不是更新cache的key-value键值对是因为delete具有幂等性(多次执行对结果无影响),容错性更强。

另外工程师们还对开源的memcache进行了很多修改,例如:

1、令其能储存更多数据

2、在memcache基础上设计了一套封装配置、routing service、管理功能的系统。

FaceBook与这套系统共同成长,这个过程中也在不同阶段有不同的侧重点:

1、只有一个集群的时候,主要关注read-heavy workload和wide fan-out

2、在请求过多之后,修建更多FE clusters,开始关注data replication的问题(每个集群都得有对应的mecache)。

3、当在全球修建集群之后需要关注更多问题。

最后的architecture如下图所示:

最终设计将co-located的clusters构造成一个region,然后任命一个master region用来支持写请求并把数据更新给其他storage cluster(只有主集群支持写请求,其他的被动响应)。 

在设计上工程师们也对CAP进行了权衡,重点保障AP。即通过允许用户看到旧数据来避免后台压力过大(memcache主要限制的是stale 数据的时间不能太长,而非完全不能看到旧数据)。

In a Cluster:Latency and Load

首先考虑数千个server构成一个集群面临的挑战。

Reducing Latency

用户的一个请求通常需要memcache提取数百个item。

因此在一个集群里提供了数百个memcached servers来降低负载。Item通过hash算法分布在server中,因此web servers需要与很多memcached servers进行通信。这种all-to-all通信模式可能导致incast congestion或者使得一个single server性能落后成为每个web server的瓶颈。

通常情况可以通过data replication的方式来降低负载,但是这也会导致内存利用率的降低。

这里Facebook团队主要通过改进memcache client(run on web server),使用mcrouter对序列化、压缩、request路由、错误处理、批量处理等方式进行优化提升速率。Client也会记录所有可用的server。

具体实现方式包含如下几点

Parrel requests and batching:web server对所需数据的依赖关系构造DAG图,尽可能并发地发送数据请求来获取数据。

Client-server communication:memcache server之间彼此隔离,不会互相通信。Clients会根据不同请求使用不同的通信协议进行处理。

对于get请求,Facebook认为无需过多考虑,请求失败直接默认获取错误显示异常即可,无需特别恢复,因此通过UDP协议实现。UDP无需连接,节省CPU资源。UDP比起TCP可以减少20%综合成本。对于类似set和delete的写请求,通过mcrouter构造TCP来实现。TCP降低了重试机制的设计难度。

TCP需要建立稳定连接占用大量内存资源,这使得保存每个web thread和memcache server之间的TCP连接成本很高不现实。因此使用mcrouter对TCP连接情况进行优化,降低成本。

Incast Congestion:Memcache clients实现了流量控制机制来降低incast congestion的情况。

 

为了避免整个系统内进行的并发请求数过高,以及单个的实例可能成为热点,facebook引入了滑动窗口。每次只发送窗口内的请求,并逐步滑动窗口。窗口的大小和总体延迟也有重要的关系。如果窗口过大,并发请求过多会导致问题。窗口过小,那么需要分发更多组memcache请求,增加整个请求的时间。

Reducing Load

为了降低负载,Facebook提供了下述三种方法。

Leases

leases(租约机制)主要用于处理两个问题:stale sets问题和thundering herds(惊群)问题。

stale sets问题:在Query cache和Update cache并发执行的时候,可能会导致的Query cache在read miss的情况下,往cache中写入老数据。

thundering herds问题:同时并发来很多读请求,都miss了,缓存击穿全部下降到数据库层面,打穿数据库。

lease是64-bit的taken,与客户端请求的key绑定,对于第一个问题,在写入时验证lease,可以解决这个问题;对于第二个问题,每个key 10s分配一次,当client在没有获取到lease时,可以稍微等一下再访问cache,这时往往cache中已有数据。

lease效果很好,将高峰时间段17K/s的数据库访问降低到了1.3K/s。

另外stale data本身不会直接删除,有时会将其保存下来。可以接受读取stale data的application可以读取旧数据。

解决stale-data示例:

Q: What is the "stale set" problem in 3.2.1, and how do leases solve it?

A: Here's an example of the "stale set" problem:

1. Client C1 asks memcache for k; memcache says k doesn't exist.
2. C1 asks MySQL for k, MySQL replies with value 1.
   C1 is slow at this point for some reason...
3. Someone updates k's value in MySQL to 2.
4. MySQL/mcsqueal/mcrouter send an invalidate for k to memcache,
   though memcache is not caching k, so there's nothing to invalidate.
5. C2 asks memcache for k; memcache says k doesn't exist.
6. C2 asks MySQL for k, mySQL replies with value 2.
7. C2 installs k=2 in memcache.
8. C1 installs k=1 in memcache.

Now memcache has a stale version of k, and it may never be updated.

The paper's leases would fix the example in the following way:

1. Client C1 asks memcache for k; memcache says k doesn't exist,
   and returns lease L1 to C1.
2. C1 asks MySQL for k, MySQL replies with value 1.
   C1 is slow at this point for some reason...
3. Someone updates k's value in MySQL to 2.
4. MySQL/mcsqueal/mcrouter send an invalidate for k to memcache,
   though memcache is not caching k, so there's nothing to invalidate.
   But memcache does invalidate C1's lease L1 (deletes L1 from its set
   of valid leases).
5. C2 asks memcache for k; memcache says k doesn't exist,
   and returns lease L2 to C2 (since there was no current lease for k).
6. C2 asks MySQL for k, mySQL replies with value 2.
7. C2 installs k=2 in memcache, supplying valid lease L2.
8. C1 installs k=1 in memcache, supplying invalid lease L1,
   so memcache ignores C1.

Now memcache is left caching the correct k=2.

解决thundering herds示例:

Q: What is the "thundering herd" problem in 3.2.1, and how do leases
solve it?

A: The thundering herd problem:

* key k is very popular -- lots of clients read it.
* ordinarily clients read k from memcache, which is fast.
* but suppose someone writes k, causing it to be invalidated in memcache.
* for a while, every client that tries to read k will miss in memcache.
* they will all ask MySQL for k.
* MySQL may be overloaded with too many simultaneous requests.

The paper's leases solve this problem by allowing only the first
client that misses to ask MySQL for the latest data.

Memcache Pools

使用memcache作为一个通用的caching layer需要针对不同的应用模式进行调整。

针对不同的key类型设计不同的Memcache缓存池,在Facebook中,可能有些key更新频繁,但出现read miss时对数据库的负载又比较小,而有些key更新不怎么频繁,但出现miss时对数据库的负载比较大,如果放在一起,由于Memcache是根据LRU算法来替换缓存中的数据的,那么那些更新不频繁的key可能就会被替换掉,从而在read miss时对数据库造成的负载就比较大,于是我们可以针对这些key的特点,把不同的key分发到不同大小的缓存池,符合第一种情况的key,可以缓存到小一点的pool中(反正更新也很频繁),符合第二种情况的key就放到大一点缓存池,这样做在一点程度上也能减小后端数据库的负载。


对于memcache而言,一次处理100 keys/request与1 key/request成本差不多,因此把100个items分到两个server并无太大帮助。必要的时候为了降低负载还是得进行data replication。

Handling Failures

出现错误有两种可能性:

1、一小部分的server因为网络问题崩掉了。

2、因为外部原因相当多的server崩掉了。这种情况直接将web请求导向其他集群即可。

主要考虑第一种情况,在这种情况我们依赖于一种自动补偿系统来恢复,这通常恢复需要几分钟时间,但几分钟就有可能将 DB 和后台服务击垮。为此, FB 团队专门用少量的机器配置一个小的 memcache 集群,称为 Gutter。当集群内部少量的 server 发生故障时,memcached client 会将请求先转发到 Gutter 中。可以理解为 Gutter 是备胎,平时不工作。

Gutter 与普通的 rehash 不同,后者将失联机器的负载转嫁到了剩余的 server 上,可能造成雪崩效应/链式反应。

In a Region: Replication

随着用户的访问量继续增大,你可能会想要购买更多的机器来部署 web server 和 memcached server,实现横向扩容。然而简单地横向扩容不能解决所有问题。越来越多的用户会将原本不严重的问题暴露出来:

  1. 用户增多会导致热点数量增多、单个热点热度增大
  2. 由于 memcached client 需要与所有 memcached server 通信,incast congestion 问题会更严重

因此有必要将 memcached servers 分成多个集群,将热点问题和网络问题分而治之。多个集群将继续共享同一个 DB 集群:



Regional Invalidations

部署多个 memcached server 集群,同一条数据的不同版本可能会出现在不同集群上。一种简单的解决方案是让 web server 每次发生 cache miss 时,将所有集群中的对应数据删除。显然这会造成大量的跨集群通信,又重新引发了网络问题。

既然数据在 DB 中只有一份,何不利用 DB 数据的更新日志来保证数据在不同集群间的最终一致性?

FB 在持久化层中使用 MySQL 集群,于是它们顺着思路开发了 mcsqueal 中间件,并将其部署到每个 MySQL 集群上。mcsqueal 负责读取 MySQL 的 commit log,解析其中的 SQL 语句,捕获数据更新信息,并将其广播给所有 memcached 集群。

从架构图中,不难看出 fanout 问题再次出现,大量的跨集群通信数据同样可能将网络打垮。解决方案也不难想到,即分而治之

一个区域内部部署多个 memcache 集群能够给我们带来诸多好处,除了缓解热点问题、网络拥堵问题,还能让运维人员方便地下线单个节点、集群,而不至于使得 cash miss rate 忽然增大。



Regional Pools

是否所有数据都需要在一个区域中储存多份?如果一些数据访问频率很低,存一份就足够了。基于该思路,FB 会在单个区域内单独划分一个 pool 用来存储一些访问率低的数据。



Cold Cluster Warmup

上线新的 memcache 集群时,如果不预热可能会出现大量 cache miss。因此 FB 团队构建了一个 Cold Cluster Warmup 系统,可以让新的集群在发生 cache miss 时先从已经加载好数据的集群中获取数据,而不是从持久化存储中,如此一来,集群上线就能够变得更加平滑。



Across Regions: Consistency

随着 FB 的服务推广到世界各地,将 web servers 推进到离用户最近的地方能够给用户带来更好的体验;将 FB 的数据中心同步到不同区域 (region),也能帮助提高 FB 服务的容灾能力;在新的区域可能在各方面产生规模经济效应。因此 memcache 服务也需要能够被部署到多个区域。

利用 MySQL 的复制机制,FB 将一个区域设置为 master 区域,而其它区域为只读区域,负责从 master 中同步数据。web servers 处理读请求时只需要访问本地的 DB 或缓存服务即可:

但这里将产生一个新的问题:只读区域的数据库有同步延迟,可能导致竞争条件出现。想象以下这个场景:

  1. 复制集群中的 web server A 写入数据到 master DB
  2. A 将本地 memcache 中的数据删除
  3. 复制集群中的 web server B 从 memcache 中读取数据发生 cache miss,从本地 DB 中获取数据
  4. A 写入的数据从 master DB 中同步到 replica DB,并通过 mcsqueal 将本地 memcache 中的数据删除
  5. web server B 将其读到的数据写入 memcache 中

此时,DB 与 memcache 中的数据将再次出现不一致,且必须等待数据过期之后才能恢复。如何解决这个问题?FB 在 memcache 上引入 remote marker 机制:

当 replica 区域的 web server 需要写入某数据 d 时:

  1. 在本地 memcache 上打上 remote marker,标记为 r d r_{d}rd​
  2. 将 d 写入到 master DB 中
  3. 将 d 从 memcache 中删除 (r d r_{d}rd​ 不删除)
  4. 等待 master DB 将数据同步到本地 replica DB 中,并且在 SQL 语句中埋入 r d r_{d}rd​ 的信息
  5. 本地 replica DB 通过 mcsqueal 解析 SQL 语句中,删除 remote marker r d r_{d}rd​

当 replica 区域的 web server 想要读取数据 d 发生 cache miss 时:

  • 如果 memcache 中数据 d 带了 r d r_{d}rd​,则从 master DB 中读取数据
  • 如果 memcache 中数据 d 没有 r d r_{d}rd​,则直接从本地的 replica DB 中读取数据

remote marker 机制实际上就是标记了 数据写入 master DB 但尚未同步到 replica DB 的中间状态。

参考链接:

Scaling Memcache at Facebook - 简书

阅读笔记:Scaling Memcache at Facebook - 知乎

Scaling Memcache at Facebook论文阅读笔记_几百个教授的专栏-CSDN博客

论文笔记:Scaling Memcache at Facebook | LongYuBo's Blog

伴鱼技术团队这篇写的最好

Facebook缓存技术演进:从单集群到多区域

Paper阅读:Scaling Memcache at Facebook | 张俊佳的博客

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值