本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
引言
按理说最近时间这么紧张是没有时间读paper的,但是机缘巧合之下又看到了这篇文章,简单扫了一下文章的内容以后决定精读一下。开始读这篇文章是因为看题目以为会与一致性哈希沾点关系,因为前两天基于看到Dynamo为了提高反熵的效率对一致性哈希做了一些优化,如果此时能对比几种方案那当然是极好的,但是看到内容以后发现其实没有多大联系,文章描述的是facebook对于memcache的使用,虽然与原始目的相悖,但是其内容确实是很有意思,又因为这篇文章一直处在我的timeline中,所以就下定决心读了这篇文章。
问题描述
以下是一个博主在读完这篇paper以后画的一个总结图,我觉得很有代表性,基本阐述清楚了这篇文章的脉络,遂把它放在这里。
本篇文章中会讨论如下问题:
- 设计系统的前置条件以及需求
- 减少缓存延迟与应用层的简洁,引入mcrouter
- incast问题
- 减少存储服务器负载(leases,memcache池)
- 故障处理
- region内key失效与region池
- 冷集群热身
- 跨地区复制的一致性
其实以上的问题也可以看做一个简化版的paper目录,因为这篇文章更像是facebook的经验之谈,所以所有的问题都是很有意思的,实在是没办法抛弃任何一个,来吧!我们一个一个聊!
最后想在正文前说最后一个问题,为什么paper的题目要叫做Scaling Memcache at Facebook
,原因在文章的第一节和第二节都有提及,不过比较隐晦而已,就是在facebook使用时,开源的memcache
实际还是单机的,所以为什么叫做Scaling
,原因这篇文章描述的就是如何把开源的单机memcache
扩展成分布式的。这是很多人在第一次读文章没有注意到的问题。
设计系统的前置条件以及需求
首先让我们来看看这个技术诞生的背景,在第二节中描述的很清楚,在facebook的设计中,用户的读操作是远大于写操作的,这种工作负载意味着读操作在利用缓存的时候可以获得最大的受益,其次这种缓存需要同步多个不同系统上的数据,因为在facebook需要的是一个灵活的缓存策略,这意味着缓存可以从不同的源中获取数据并同步。
此时显然可以有两种功能,一种是被当做查询缓存,首先通过一个字符串的键在memcache中请求,如果没有找到,它会从数据库或者从后台服务中检索,再使用该键把结果存回memcache中。对于写的请求,Web服务器发送SQL语句到数据库,接着发送删除请求到memcache,使旧的缓存数据失效。
当然也可以把memcache当做一个更为通用的无差别缓存,因为可以把多台机器上的memcache看做一个整体,所以我们只需要做很少的事情就可以维护多个缓存之间的一致性,比如说A服务器产生了一个很庞大的数据,这是一些计算程序需要的中间数据,显然此时它们可以很方便的从memcache中获取它们需要的数据。
但是这里我们要清楚一个问题,虽然其可作为一个通用缓存,但是在论文中描述的memcache版本本身就是一个单机内存哈希表,其不具备分布式存储的能力,所以作为通用缓存实际需要一个功能强大的客户端(扩展起来也简单),这个客户端一定需要支持数据路由。当然客户端存储集群成员关系也是分布式存储中事件分发的一种方法,后面可能会写一篇文章专门聊聊这个问题。
减少获取缓存的负载
通常一个大型的页面需要获取大量的资源,而这些资源也基本不会分布在同一个数据库或者缓存中,那么如果快速的获取全部的资源就成了一个巨大的问题。
据不可靠调查,貌似有一种优化是前端先加载已经准备好的资源,比如说文字,图片,像视频这样的资源可以先用图片填充,等到数据到达一部分的时候再展示(貌似听谁说过),这样可以大幅度提升客户请求的响应速度,把响应速度从最长的请求时延降成了最短的请求时延。
n资源分布在不同的数据库中,为了减小数据库的请求压力,部分资源又被缓存在多台缓存数据库中,一个页面需要的资源经hash后存于不同的memcache服务器中。因此,web服务器必须请求多台memcache服务器,才能满足用户的请求,此时facebook团队做出了三点优化措施,分别为:
- 最大化并行请求
- 使用UDP减小时延
- 处理incast问题
其中前两点让我写我也可以想到,但是第三点没有大量的量化数据分析是很难考虑到的,这也是阅读这种工业论文的一大特色所在。
最大化并行请求其实就是解决以下一个问题,如何减小一个页面所有资源的往返请求时延呢?这个问题不仅仅取决于某一个请求的时延,更取决页面上资源之间的依赖关系,后者我们可以构建一个页面所有资源的有向无环图(DAG),那么显然此时以任意结点为根,此树的深度就是RTT的最小数量。
使用UDP减小请求时延其实也很好理解,不过我们先说说这一节提到的一个问题,就是我们的web应用并不直接和memcache
服务器通信,它们之间的通过客户端通信,这个客户端是无状态的,它封装了系统的复杂性,其提供串行化(建立DAG)、压缩、请求路由、错误处理(请求数据库)以及请求批处理,把简洁的接口留给web应用。当然这里客户端的逻辑有两种实现方法,可以是嵌入应用的一个库,或者做为一个名为mcrouter
的独立的代理程序。
我们知道TCP是一个面向连接的传输层协议,意味着两个结点通信是不仅需要三个包来建立连接(defer_accept可以去掉第三次握手),而且在数据的传输中需要心跳包来维护连接信息,并且优雅的断开连接还需要(三到四个包),这里的开销是很大的,当然在长连接的场景下可以稍微好一点,因为以上这些包的开销被均到了多个操作上。但是短连接上维护连接的代价会很大,因为数据包可能仅仅只有一个。
使用UDP代替TCP优化读操作可以很好的降低操作延迟,而且据facebook统计只有百分之零点二五的包被丢弃,其中大约百分之八十是由于延迟或丢失包,其余的是由于失序的交付,当然我们可以选择在用户态维护协议栈来避免这些问题,但是没有必要,因为我们想要提升效率,后者带来可靠性的同时必然要损耗效率,而且失效带来的开销我们可以接收,无非是查一次数据库而已,读操作使用UDP相比于TCP带来了百分之二十的时延降低:
incast问题的描述如下[6]:
大量的请求同时到达前端服务器,或是数据中心服务器同时向外发送大量的数据,造成交换机或路由器缓冲区占满,从而引发大量丢包。TCP重传机制又需要等待超时重传,增加了整个系统的时延,同时大大削减了吞吐量。
facebook团队巧妙的使用滑动窗口机制解决了这个问题,这个窗口并不是像TCP一样基于某一条连接创建,而是应用于所有的memcache请求,并不关心目的地址,以下是窗口大小与请求时延的关系:
当窗口比较小的时候,应用将不得不串行地分发更多组memcache请求,这将会增加web请求的持续时间。当窗口过大的时候,同时处理的memcache请求的数量将会引发incast拥塞。结果将会是memcache错误,应用退化到从持久化存储中取数据,这样将会导致对web请求的处理更缓慢。显然在此负载小其中300是一个很不错的窗口大小选择。
减少存储服务器负载
这里的问题其实等价于如何减少访问数据库,或者增大缓存的命中率,paper中提供了三种方式优化此问题:
- leases机制
- memcache池
- replication机制
其中租约机制主要解决了过时设置(stale sets)[9]和惊群(thundering herds,这里提高了效率可能是因为合并了打向数据库的请求,实际上paper描述的并不清楚)。
memcache池解决了memcache作为通用负载,顶层具有不同的接入模式、内存占用和服务质量要求,这可能导致:那些仍然有价值的低抖动主键在那些不再被存取的高抖动主键被踢出之前被踢出(毕竟是基于内存的数据库,需要内存淘汰),将这些不同抖动特征的数据项放在不同的池中可以提升缓存命中率。
而replication
则可以在请求量超过单台机器负载的时候提供优秀的性能,当然这里有两种方法,一个是基于主键的划分,一个是全量数据复制,[7]的3.2.3描述了大多数情况选择复制的理由。
paper中描述的浅显易懂,这里不再详细说明。
故障处理
facebook团队对于故障处理的处理显得脑洞非常之清奇。一般来讲在分布式存储这个领域,对于故障的处理一般分为以下几点:
- 故障发现(分区,结点宕机或者网络波动,一般行为为心跳包恢复超时)
- 故障恢复(这里方法很多,主要是看数据冗余怎么做,包括但不仅限于:从节点上位,重新指定主节点(leases))
- 数据同步(不细谈)
在facebook团队眼里此系统中故障分为两类:
- 由于网络或服务器故障,少量的主机无法接入
- 影响到集群内相当大比例服务器的广泛停机事件。
后者可以认定集群下线,我们转移用户的web请求到别的集群。后者可以认为是小问题,此时会有部分冗余的机器,称为Gutter
顶替失败节点的功能,在一个集群中,Gutter的数量大约为memcached服务器的1%。
当memcached客户端对它的get请求收不到回应的时候,这个客户端就假设服务器已经发生故障了,然后向特定的Gutter池再次发送请求。如果第二个请求没有命中,那么客户端将会在查询数据库之后将适当的键-值对插入Gutter机器。
这样部分memcache缓存结点宕机的时候可以大大减小数据库的负载,因为Gutter
此时会有大量的缓存命中。对于由于故障或者小范围网络事故造成的一些memcached服务器不可达的情况,Gutter将会保护后端存储免于流量激增。
region内key失效与region池
这里首先要概述了在此系统中各个名词之间的关系,如下是架构图:
显然WebServer
和Memcache
构成了Front-end Clusters
,虽然图上没有,但是我认为Mcrouter
属于memcache
的一部分,而Front-end Clusters
与Storage Cluster
构成了region
。
显然在如上一个集群中Storage Cluster
存储着权威数据,且一次写操作会使得全部的memcache
中的数据失效,此时如何更新多个缓存中的数据呢,首先是一致性的选择,显然强一致性会极大的降低吞吐量,所以更新的过程一定是异步的,那么到底是使用WebServer
广播呢,还是另寻它法呢?
facebook团队选择引入mcsqueal
:
mcsqueal
本质上是一个守护进程,每个守护进程检查数据库提交的SQL语句,提取任意的删除命令,此时可以把少量包发送给每个前段集群运行着mcrouter的指定服务器。然后mcrouter就从每个批量包中分解出单独的删除操作,将失效命令路由到所在前端集群正确的memcached服务器。这样可以显著减小运行着数据库进程机器的负载。
不通过WebServer
来执行key失效最大的原因是因为假设出现由于配置错误造成的删除命令错误路由,web server
对于这个操作的数据无法落地(为什么不落地呢?疑惑),此时这个命令就丢失了,而日志是可靠的,可以不停的重试。当然在多集群之间也有点问题,我们后面说。
多个Front-end Clusters
共享memcache
服务器集合被称为region池
。
因为memcache
的数据基于web请求而修改,如果用户请求被随机的路由到所有可获得的前端集群,这些集群中数据大概率差不多,对于很少修改,且流量较小的数据集可以使得多个Front-end Clusters
共享memcache
,这样在某些情况下可以显著减少机器数量。
冷集群热身
在新加入一个集群的时候,此时缓存命中率会很低,这样会削弱隔离后端服务的能力。所以我们可以让这个新集群的客户端从已经运行了很长时间的集群客户端中检索数据,这样这个冷集群上升到满负载的时间将会大幅度缩短。当然这可能造成一致性的问题,当然可以这是不满足读写一致性[8]的,因为完全有可能写入后还没有更新缓存,又读到旧的数据,不过这在facebook这个应用中问题不大,具体查看[7]4.3。
跨地区复制的一致性
首先点出跨地区数据中心的优势:
- 将web服务器靠近终端用户可以极大地较少延迟。
- 地理位置多元化可以缓解自然灾害和大规模电力故障的影响。
- 新的位置可以提供更便宜的电力和其它经济上的诱因。
在3-2-1原则
中也阐明了备份最好有一份异地备份。
这里的一致性主要缓存一致性,即在主备集群间的一致性问题。
从主region写可以会导致一些更新的问题,但是这些问题被mcsqueal
预防了,当然这也是不使用WebServer
进行更新缓存失效的原因。因为对主数据库的改动可能还没有传播到副本数据库,缓存失效就已经到了。接下来对副本region的数据查询将会使得memcache
中出现脏数据。使用mcsqueal
就避免了这个问题。
这一节中显示出region其实是一个无主结点架构,因为可以从no-master
写入,但是描述的非常简略,基本没有什么参考价值。比较有意思的是remote marker
机制来减小读取到旧数据的概率。其实就是用缓存不命中时附加的延迟(region间通信)来换取读取过时数据概率的下降。具体可参考[7]5。
总结
这篇paper中很多问题让人印象深刻,incast
问题,缓存一致性问题,故障处理
问题等等。而且其对于缓存的应用确实是一个对我们来说很好的指导,很多地方的问题不采坑是很难发现的。多读书,多敲代码,就这样吧。
参考: