赞
踩
分布式缓存
2.1 分布式?
考虑之前在缓存引入小节中所描述的,我们在原有的单层db结构中引入了缓存memcached:
在这种单实例缓存架构下,随着业务规模的不断增长,我们发现存在如下几个问题:
1、容量问题
单一服务节点无法突破单机内存上限。目前微博数据已经超过千亿数据,虽然无需所有数据都放入缓存当中,但在保证一定的命中率的情况下(注:微博核心缓存命中率需要达到99%)缓存部分数据,也需要以百GB甚至是TB为单位的内存容量。而单台服务器的内存总有上限(目前微博使用的常用服务器内存上限128G),也即单实例缓存无法满足缓存所有数据的需要
2、服务高可用(HA)
在单实例场景下,缓存如果因为网络波动、机器故障等原因不可用,原来由缓存承担的请求会全部落到DB上,最终系统会如同雪崩般,全部crash。
3、扩展问题
单一服务节点无法突破单实例请求峰值上限。微博业务是存在热点突发事件的,也即随时会有可能出现数倍于日常请求峰值的情况。虽然我们在做资源的评估的时候,会确保资源的冗余度足够满足请求翻倍(一般为2~4倍)的情况。但我们也需要后端资源是可以线性扩展的
下面我们也会针对上面的几个问题,一步一步说明相应的解决方案。
2.2 分布式缓存实现
微博线上服务都是由多个服务端节点存储数据。由于memcached本身不支持分布式,所以需要客户端或者中间层代理实现分布式节点获取。在今天的描述中,为了简化问题,会主要以客户端实现分布式为主,即应用服务器(客户端)根据内部的算法,选择特定的缓存节点,存取数据。
2.2.1 数据分片(Sharding)
前面提到单实例memcached主要会有以下几个问题:
1、请求量请求量超过单端口可承受极限
2、数据容量超过单实例内存容量。
为了解决以上两个问题,我们的做法是从单端口实例扩展为一组实例。一组memcached由多个memcached实例组成,每个实例上只缓存数据总量的一部分数据,客户端在请求的时候,需要决定从哪个memcached中获取数据(hash)。
常见的hash算法可以使用:
1、取模hash
如图10所示,客户端根据请求的key,做取模,最终根据结果选择对应的memcached节点。这种方式实现简单。易于理解,缺点在于加减节点的时候,会造成较大的震荡:每加、减一个节点,hash方式全部改变,整体命中率会下降的非常快。
2、一致性hash
一致性哈希的算法描述可以参考https://zh.wikipedia.org/wiki/%E4%B8%80%E8%87%B4%E5%93%88%E5%B8%8C
一致性哈希算法的主要优点在于加减节点,对于服务整体的震荡较小。并且在某个节点crash后,可以将后续请求,转移至另一个节点中,具备一定的防单点特性。
2.2.2 主从双层结构
从单台实例增加到一组缓存后,我们可以解决单端口容量、访问量不足的问题,但是如果出现某一台缓存挂了的情况。请求依然会落到后端的DB上。
上面也提到了一致性hash的算法,可以通过一致性hash的方式,来减少损失。
但基于一致性哈希策略的分布式实现在微博业务场景下也存在一些问题:
1、微博线上业务对缓存命中率要求高。某台缓存挂了,会导致缓存整体命中率下降(即使一致性hash会在一定时间后将数据重新种到另一个节点上),对于命中率要求在99%以上的Feed流核心业务场景来说,命中率的下降是难以接受的
2、一致性hash存在请求漂移的情况,假设某一段时间服务因网络因素访问某个服务节点失败,则在这时候,会将数据的更新、获取都迁移到下一个节点上。后续网络恢复后,应用服务探测到服务节点可用,则继续从原服务节点中获取数据,这就导致了在故障期间所做的更新操作,对于原服务节点不可见了
目前我们对于这种单点问题主要是通过引入主从缓存结构来解决的。主从结构示意图如下图11所示:
服务端在上行逻辑中,进行双写操作——由应用服务负责更新master、slave数据。
下行获取数据,先获取master数据,当master返回空,或者无法取到数据的时候,访问slave。
在这种模式下,为了避免两份数据带来的不一致问题,需要以master数据为准。即如果有更新数据操作,需要从master中获取数据,再对master进行cas更新。更新成功后,才更新slave。如果cas多次后都失败,则对master、slave进行delete操作,后续让请求穿透回种即可。
2.2.3 横向线性扩展
在双层结构下,我们可以很好的解决单点问题,即某一个节点如果crash了,请求可以被slave承接住,请求不会直接落在DB上。
但这种架构仍然存在一些问题:
1、带宽问题。由于存在热点访问的情况,线上经常出现单个服务节点的带宽跑满的情况。
2、请求量问题。随着业务的不断发展,并发请求数超过了单个节点的机器上限。数据分片、双层结构都不能解决这种问题。
上面的两个问题,其实总结起来是如何快速横向扩展系统的支撑能力。对于这个问题,我们的解决思路为增加数据的副本数。即让数据副本存在于多个节点中,从而平摊原本落在一个节点的请求。
从我们经验来看,对于线性扩展,可以在原来的master上引入一层L1层缓存。整体示意图如12所示:
上行操作需要对L1进行多写。写缓存的顺序为master-slave-L1(所有),写失败则进行delete操作,后续由穿透请求进行回种。
L1可以由多组缓存组成,每组缓存相互独立。应用服务在获取数据的时候,先从L1中选取一组资源,然后再进行hash选取特定节点。对于multiget的场景也是先选取一组缓存,然后才对这组缓存进行multiget操作。如果L1获取不到数据,再依次获取master、slave数据。获取成功,则回种到L1中。
在采用L1的模式中,数据也是以master中数据为主的。即如果有更新数据的需要,需要从master中获取数据原本,再进行cas更新。如果cas更新成功,才同时更新slave、L1资源。如果对master的操作失败,则进行delete all操作,让后续请求穿透回种。
当线上流量、请求量达到一个水位的时候,我们会进行L1的扩容——增加一组、或几组L1缓存,从而提升系统整体的承载能力。此时系统的整体响应请求量是可以做到线性扩展的。
可以看到,双层结构下,slave作为主的备份存在。假设线上master缓存命中率为99%,则落在slave上的请求只有1%,并且这1%的请求都是很偏、很少人访问到的。可以想象,在这种情况下,如果master真的出现问题,请求全部落在slave上,slave也是没有任何数据可供访问的。Slave作为防单点措施是失败的。
引入L1后,slave过冷并没有被解决,同时,由于master被放置到L1之下,也遇到了slave的问题,master的数据也存在过冷的风险。为了解决上面的问题,我们在线上配置的时候,会将整组slave做为L1的一组资源进行配置,让slave以L1的身份承担部分的热请求。同时为了解决master过冷的问题,我们也会让应用服务在选择L1的时候有一定的概率落空,从而让master作为L1逻辑分组,去承担部分热请求。整体结构图如图13所示:
结尾
本文主要结合目前微博平台的线上业务场景,根据个人对memcached的使用的经验,以及分布式架构情况做了介绍。行文仓促,肯定有很多细节需要继续完善,如果有问题或者建议,可以微博私信 @微博平台架构或讲师 @LierD 一起继续探讨。
随着微博业务的蓬勃发展,相信还会有更多的技术挑战在等着我们去解决,去征服。
作者
赖佳俊(微博昵称:@LierD )
2013年7月毕业于哈尔滨工业大学后,加入新浪微博工作至今,担任系统研发工程师。曾先后参与微博Feed流优化、后端服务稳定性建设等项目。关注jvm调优,微服务,分布式存储系统。 业余爱好三国杀,dota,喜欢自己做饭下厨。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。