这篇paper主要价值就是设计一个无敌的分布式kv
本文目录层级如下:
- 背景:读多写少的系统
- 基本的读写操作
- write-invalidate和幂等支持
- 整体架构
- 数据类型
- 优化目标
- cluster内的优化
- 延迟优化
- 整体低延迟方案
- 请求优化:并行与合并
- 连接优化:不可靠UDP的读操作、TCP复用链接
- 突发网络拥塞方案
- 应用层滑动窗口
- 整体低延迟方案
- 负载优化
- 目标:减少单次load负载和load频率
- 整体负载减压方案
- lease机制避免过期写入和阻挡瞬时高压
- pool机制针对cache miss成本优化空间利用率
- 故障处理方案
- gutter机制:用一致性换取可用性
- 对比rehash
- 延迟优化
- region内的优化(WIP)
注:对你没看错,是memcache而不是memcached,memcache指facebook设计的缓存层服务,而它只是底层选用了memcached实现
注2:虽然facebook已经换了名字,但是文章仍然采用facebook的称谓
注3:paper全称是Scaling Memcache at Facebook,文章最下方有链接
读多写少的系统
通常互联网服务大多为读多写少的场合,facebook作为当年全球最大的社交网络也不例外
facebook给出的数据是,读请求相比写高出2个数量级
具体地,每秒读请求为billions($10^{10}$)级别,系统存储量级为$10^{12}$个条目
facebook在这里的意思是:memcache足以承担以上量级的服务
读写操作
读操作:
- 查cache
- 如未命中,查DB
- 如走了第二步,接着set kv
写操作:
- 更新DB
- 删除对应cache
写操作使用删除而非更新cache来满足幂等性
写操作之幂等支持
什么场合下需要幂等性?
可能是在有任何异常情况下
- 返回一个含糊不清的响应状态
- 很长一段时间没有响应
则允许直接重试,不必担心副作用
这个和超时机制搭配起来很不错
非幂等应该也是可以的,但需要应用层进一步做校验,token之类的,可能不好处理
整体架构
如图所示,一个region对应一个数据中心
单个region内有多个cluster的replica
在部署规模上,单cluster是对应了$10^3$量级的memcached server
数据使用一致性哈希分布于各个memcached中
一个用户与单个web server通信请求服务,单请求需要引起多个memcached通信
见后面“数据类型”和“延迟优化”章节
数据类型
这种应用场合下的数据类型是宽fan-out的
既放射状的读模式
用户表面上读一条数据(item),其实过程中涉及到读上百个数据
优化目标
优化需要是能影响到用户的,而不考虑过于局部的优化
允许读稍微陈旧(不一致)的数据,只要能提高系统整体负载能力
把读到陈旧数据的概率作为一个调参参考(?)
cluster内的优化
cluster内的优化目标为:
- 减少cache hit时的延迟
- 减少cache miss时增加的系统负载
延迟优化
前面的“整体架构”中提到web server与memcache服务的通信方式
很显然会造成all-to-all communications
而多对多会造成两个问题:
- incast congestion
- 这个有点含糊,大概意思应该是数据短时间突发会造成网络拥塞
- 我觉得跟拥塞窗口的保守状态有关
- 单server的热点瓶颈问题
通过实现replica可以解决单点问题,还顺便做到容错
不过这需要花费内存利用率上的成本
facebook的做法主要通过客户端的手段来减少延迟
(顺便一提)这个客户端的功能包括:
- 序列化
- 压缩
- 请求路由
- 错误处理
- 请求合并
客户端的优化手段包括:
- 请求优化
- 通信连接优化
请求本身的优化主要是parallel并行化以及batching合并
简而言之就是要减少网络上的round trips次数
要实现这一目的可以把数据间的依赖调整为一个DAG图,由于并不存在环,因此可以并行发出请求
同时在必要时合并多个请求,经验值给出可以合并24个key
通信连接优化分为两个层面:
- client-server通信
- 传输层连接
在通信层面上
memcached server间互不通信
facebook希望把所有复杂实现下放到无状态的客户端
client通过一个mcrouter的代理与server通信
既web server连接client(proxy)即可,proxy接口仍然保持和memcached server一致
这样避免了单个web server直连多个memcached server
在传输层上
连接尝试尽可能通过使用UDP来减少overhead,比如读请求
UDP虽然不可靠,但是实验给出的数据是:即使处于服务高峰,也仅有0.25%的读请求被抛弃
注:这里并不是基于UDP重造可靠传输轮子,论文中最多只提到UDP上会用序列号标记顺序
如果不可靠请求被抛弃或者存在乱序行为,则是作为client端的错误,这样可以简化处理
比方说,get失败了,则认为是应该走cache miss流程(但是事实上并不引起插入行为,只是复用error handle流程简化代码)
出于可靠性的考虑,set和delete操作仍使用TCP,因为TCP机制上保证了失败重试的机制,而不用基于UDP手写错误处理
在数据上,UDP的引入使得请求整体延迟降低20%
不过,即使使用TCP仍有优化的必要,因为TCP连接本身需要占用更多的内存资源(比如缓冲区),这里略过了
上述是一些整体上的延迟降低方案
对于突发的网络拥塞问题,memcache实现应用层面上的流量控制
实现上就是滑动窗口那一套,只是相比TCP不同在于来自同一个web server的请求会放入到同一个窗口(而不是维护单连接的窗口):当超出窗口范围时,主动拒绝响应,避免级联压垮server
不过具体的实现算法并没有给出,毕竟这是玄学(
负载优化
我们不仅需要依靠replica和sharding来分担负载成本
还需要减少load次数从根本上解决负载问题
除了次数以外,即使数量很少的cache miss也可能引起高负载
lease
memcache引入lease来解决两个问题:
- 过期写入(stale sets)
- 瞬时高压(thundering herds)
过期写入涉及到正确性,因为分布式下的请求是异步乱序的,可能一个key被更新后还会收到一个更加旧的写入请求
瞬时写入是指特定的一个key突然有大量的写和读请求,写请求会使得cache失效,每下一次写(使得上一次的写等同于失效)后的读都可能打穿DB,均摊下来每次读请求都非常重量级
lease的具体数据结构为64bit的token,每个lease绑定某一个key
lease的生成时机为发生cache miss时,由memcached server发出
lease可以用于判断当前的写请求(请求时携带lease)对于memcached是否已经过期
如果已经过期,则直接拒绝已减少请求次数,并且保证了正确性
对于瞬时高压问题,memcached server通过控制lease的发放频率来缓解
原理是只有拥有lease的请求才有资格访问DB并写回到cache
facebook给出的经验值是每10秒发放一次lease
在这种情况下,10s内的下一次请求会陷入等待状态
注:也可以选择重试,因为当前处理等待是因为此前含有lease的请求尚未写回到cache中,短期少量的重试可以很快得到已写回的cache中的数据
facebook实验表明这种策略使得DB请求从17K/s降低到1.3K/s
补充关于请求等待的进一步优化
前面说到的等待10秒,是在不期望读到陈旧数据的前提下
只要你接受轻微不一致数据,你可以选择直接返回(旧数据)而不必等待(最新的数据),因此并不会有性能问题
这里隐含的意思是key-value数据结构并不是一个pair of key and value,而是一个大小至少为2的按时间顺序排序的list,删除操作只不过是把数据标记为过期,添加操作是往头部插入数据
因此,对于一个请求失败的错误处理策略可以选择若干组合:
- 等待
- 重试
- 直接拿取旧值
pool
facebook注意到不同的系统有不同的workload,并且memcache是作为通用cache层,全部对接同一套memcache可能不合适
注:这种不同workload来自不同的数据访问形式、内存占用、QoS要求
为了满足不同个性的web server需求,memcache中把一个cluster的memecached server划分为不同的pool
这一部分懒得翻了,并不大感兴趣,往局部性原理靠就好了
简单地说就是把一下cache miss成本比较小的放到small pool中,而成本高的则把相应的pool搞大一点
这些pool因为共享同一块内存(起码单机内是这样),因地制宜的pool大小可以相比通用memcache更好地控制workload
另外还提到pool内进一步replica,暂时没悟出大师怎么把1张100块变成4张50RMB的道理
failures
影响负载还有一个因素是故障处理
如果memcache挂了,那就得提放访问DB引起的高峰workload(当然你也可以选择不可用而不是高可用,直接拒绝服务),还有进一步可能引发级联故障
存在2种不同类型的故障:
- 小范围内的主机无法访问,可能是个别的网络/服务器问题
- 大范围的服务挂了
如果整个cluster都大范围下线,需要必须立刻把web请求转发到其它replica cluster,以最快速度移除当前cluster内对memcache的所有的load
对于cluster内小范围的不可用,使用gutter机制来处理
gutter指代的是cluster内预留的小部分平常并不主动使用的主机
小范围不可用需要自动修复,而修复是需要一段时间的
在这一段时间内的高可用(原来应该访问到不可用服务器的请求服务)由gutter服务器(gutter pool)承担
如果gutter pool也无法提供服务才进一步直接访问DB
facebook给出的经验值为gutter占比cluster的1%
比较特殊的是gutter中的key过期速度会相比普通memcached更快,并且限制上层的load速率,尽可能使用相对过期的数据,以避免进一步加剧故障,把可用服务限制到一定水平
更新:key过期应该换一种说法,是处于故障状态时key容易快速过期,gutter的权衡做法是写入操作并不会invalidate cache,因此数据层面的表现为更倾向于使用过期数据(进一步放宽一致性以提高可用性)
facebook顺便在这里对比了用剩余机器直接rehash的做法,分析认为小范围不可用有可能是部分hot key或者访问不均衡(non-uniform key access frequency)造成的,rehash对这种做法并无帮助(hot的还是hot,non-uniform的还是non-uniform),而是倾向于用gutter削掉这部分异常流量,并且不把问题扩散到cluster内外的其它服务器,同时gutter是转移了请求直接冲入DB的风险
在facebook的实践中,这种做法消灭了高达99%的可见故障
可以说是用非常小数目的机器做了非常出色的故障处理策略
region内的优化
前面我们(不是我,是facebook)用了非常多的手段来优化了各个cluster内部的性能
但这还没完,现在聊更加庞大的region
region是多个cluster的replica集合(见“整体架构”章节的图)
——有空再写吧,写一天了!