14 | 缓存的使用姿势(二):缓存如何做到高可用?
之前了解了缓存的原理、分类以及常用缓存的使用技巧。我们开始用缓存承担大部分的读压力,从而缓解数据库的查询压力,在提升性能的同时保证系统的稳定性。这时电商系统整体架构演变成这个样子:
image.png
我们在Web层和数据层之间增加了缓存层,请求会首先查询缓存,只有当缓存中没有需要的数据时才会查询数据库。
在这里你需要关注缓存命中率这个指标。一般来说,在你的电商系统中,核心缓存的命中率需要维持在99%甚至是99.9%,哪怕下降1%,系统都会遭受毁灭性打击。
这不是危言耸听。假设系统的QPS是10000/s,每次调用会访问10次缓存或者数据库中的数据,那么当缓存命中率仅仅减少1%,数据库每秒就会增加1000次请求。而一般来说我们单个MySQL节点的读请求量峰值就在1500/s左右,增加的这1000次请求很可能会给数据库造成极大的冲击。
影响如此可怕,更不要说缓存节点故障了。而图中单点部署的缓存节点就成了缓存系统中最大的隐患,那我们如何提升缓存的可用性呢?
重点:分布式缓存的高可用方案
主要选择的方案有客户端方案、中间代理层方案和服务端方案三大类:
-
客户端方案就是在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。
-
中间代理层方案就是在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用。
-
服务端方案就是redis 2.4版本后提出的Redis Sentinel方案。
掌握这些方案可以帮助你抵御部分缓存节点故障导致的缓存命中率下降的影响
客户端方案
在客户端方案中,你需要关注缓存的写和读2个方面
- 写入数据时,需要把被写入缓存的数据分散到多个节点中,即进行数据分片;
- 读数据时,可以利用多组的缓存来做容错 ,提升缓存系统的可用性。关于读数据,这里可以时候用主从和多副本两种策略,两种策略是为了解决不同的问题而提出的。
该如何做?
1.缓存数据如何分片
单一的缓存节点受到机器内存、网卡带宽和单节点请求量的限制,不能承担比较高的并发,因此我们考虑将数据分片,依照分片算法将数据打散到多个不同的节点上,每个节点上存储部分数据。
这样在某个节点故障的情况下,其他节点也可以提供服务,保证一定的可用性。
一般来讲,分片算法常见的及时Hash分片算法和一致性Hash分片算法两种。
Has分片的算法就是对缓存的Key做哈希计算,然后对总的缓存节点个数取余。可这么理解:
比如说我们部署了三个缓存节点组成一个缓存的集群,当有新的数据要写入时,我们先对这个缓存的key做比如crc32等hash算法生成hash值,然后对hash值模3,得出的结果就是要存入缓存节点的序号。
这个算法最大的优点就是简单易理解,缺点是当增加或者减少缓存节点时,缓存总的节点个数变化造成计算出来的节点发生变化,从而造成缓存失效不可用。如果采用这种方法,最好建立在你对于这组缓存命中率下降不敏感,比如下面还有另外一层缓存来兜底的情况下。
当然了,用一致性Hash算法可以很好地解决增加和删减节点时,命中率下降的问题。在这个算法中,我们将整个hash值空间组织成一个虚拟的圆环,然后将缓存节点的IP地址或者主机名做Hash取值后,放置在圆环上。当我们需要确定某一个key需要存取到哪个节点上的时候,先对这个key做同样的hash取值,确定在环上的位置,然后按照顺时针方向在环上“行走”,遇到的第一个缓存节点就是要访问的节点。比如说下面的图,key1和key2会落入到node1中,key3,4会落到node2中,key5,node3。key6,node4。
这时如果在node1和node2之间增加一个node5,你可以看到原本命中node2的key3现在命中node5,而其他的key都没有变化;
同样的道理,如果我们把node3从集群中移除,那么只会影响到node5,所以在增加和删除节点时,只有少数的key会“漂移”到其他节点上,而大部分的key命中的节点还是会保持不变,从而可以保证命中率不会大幅下降。
image.png
不过事情总有两面性。虽然这个算法对命中率的影响比较小,但还是存在问题:
- 缓存节点在圆环上分布不平均,会造成部分缓存节点的压力较大;当某个节点故障时,这个节点所要承担的所有访问会转移到另一个节点上,会对后面的节点造成压力。
- 一致性Hash算法的脏数据问题。
极端情况下,比如一个有三个节点ABC承担整体的访问,每个节点的访问量平均,A故障后,B将承担双倍的压力,当B承担不了流量崩溃后,C也将因为要承担原先3倍的流量而崩溃,这就造成了整体缓存系统的雪崩。
很可怕但是不用担心,程序员就是要能够创造性的解决各种问题,所以你可以在一致性hash算法中引入虚拟节点的概念。
它讲一个缓存节点计算多个hash值分散到圆环的不同位置,这样既实现了数据的平均,而且当某一个节点故障或者退出的时候,它原先承担的key将以更加平均的方式分配到其他节点上,从而避免雪崩的发生。
其次,就是一致性hash算法的脏数据问题。为什么会产生脏数据?比如说,在集群中有两个节点AB,客户端初始写入一个key为k,值为3的缓存数据到A中。这时如果要更新K的值为4,但是缓存A恰好和客户端连接出现了问题,那这次写入请求会写入到B中,
接下来缓存A和客户端的连接恢复,当客户端要获取K的值时,就会获取到存再A中的脏数据3,而不是B中的4。
所以在使用一致性hash算法时一定要设置缓存的过期时间。这样当发生漂移时,之前存储的脏数据可能已经过期,就可以减少存在脏数据的几率。
很显然,数据分片最大的优势就是缓解缓存节点的存储和访问压力,但同时它也让缓存的使用更加负载,在批量获取场景下,单个节点的访问量并没有减少,同时节点数太多会造成缓存访问的SLA("服务等级协议",SLA代表了网站服务可用性)得不到很好的保证,因为根据木桶原则,SLA取决于最慢、最坏的节点情况,节点数过多也会增加出现问题的概率,因此推荐4到6个节点为佳。
2.Memcached的主从机制
redis本身支持主从的部署方式,但是Memcached并不支持,所以我们今天主要来了解一下Memcached的主从机制是如何在客户端实现的。
在之前的项目中,我就遇到了单个节点故障导致数据穿透的问题,这时我为每一组master配置一组slave,更新数据时主从同步更新,读取时优先从slave中读数据,如果读不到树就穿透到Master读取,并且将数据回种到slave中以保持slave数据的热度。
主从机制最大的优点是当木一个slave宕机时,还会有master作为兜底,不会有大量请求穿透到数据库的情况发生,提升了缓存系统的高可用性。
image.png3.多副本
其实,主从方式已经能够解决大部分场景的问题,但是对于极端流量的场景下,一组slave通常来说不能完全承担所有流量,slave网卡宽带可能会成为瓶颈。
为了解决这个问题,我们考虑在master/slave之前增加一层副本层,整体架构是这样:
image.png
在这个方案中,当客户端发起查询请求时,请求首先会从多个副本组中选取一个副本组发起查询,如果查询失败就继续查询master/salve,并且将查询的结果回种到所有副本组中,避免副本组中脏数据的存在。
基于成本考虑,每一个副本组容量比Master和slave要小,因此它只存储了更加热的数据。在这套架构中,master和slave的请求量会大大减少,为了保证他们存储数据的热度,在时间后只能怪我们会把master和slave作为一组副本组使用。
中间代理层方案
虽然客户端方案已经能解决大部分的问题,但是只能在单一语言系统之间复用。例如微博用java实现了这一套逻辑,PHP就难以复用。而中间代理层的方案就可以解决这个问题。你可以将客户端解决方案的经验移植到代理层中,通过通用的协议(如redis协议)来实现在其他语言中的复用。
如果你来自研缓存代理层,你就可以将客户端方案中的高可用逻辑封装在代理层代码里,这样用户在使用你的代理层的时候就不需要关心缓存的高可用是如何做的,只需要依赖代理层就好了。
除此以外,业界也有很多中间代理层方案,它们的原理基本上可以由一张图来概括:
image.png
从图中可以看出,所有缓存的读写请求都是经过代理层完成。代理层是无状态的,主要负责读写请求的路由功能,并且在其中内置了一些高可用的逻辑。
服务端方案
redis在2.4版本中提出了redis sentinel模式来解决主从redis部署时的高可用问题,它可以在主节点挂了以后自动将从节点提升为主节点,保证整体集群的可用性,整体架构如图:
image.png
redis sentinel也是集群部署的,这样可以避免sentinel节点挂掉后造成无法自动故障恢复的问题,每一个sentinel节点都是无状态的。在sentinel会配置master的地址,sentinel会时刻监控master的状态,当发现master在配置的时间间隔内无响应,就认为master已经挂了,sentinel会从从节点中选取一个提升为主节点,并且把所有其他的从节点作为新主的从节点。sentinel集群内部在仲裁的时候,会根据配置的值来决定当有几个sentinel节点认为主挂掉可以做主从切换的操作,也就是集群内部需要对缓存节点的状态达成一致才行。
redis sentinel不属于代理层模式,因为对于缓存的写入和读取请求不会经过sentinel节点,sentinel节点在架构上和主从是平级的,是作为管理者存在的,所以可以认为是在服务端提供的一种高可用方案。