极客时间已完结课程限时免费阅读

14 | 缓存的使用姿势(二):缓存如何做到高可用?

14 | 缓存的使用姿势(二):缓存如何做到高可用?-极客时间

14 | 缓存的使用姿势(二):缓存如何做到高可用?

讲述:唐扬

时长14:27大小13.23M

你好,我是唐扬。
前面几节课,我带你了解了缓存的原理、分类以及常用缓存的使用技巧。我们开始用缓存承担大部分的读压力,从而缓解数据库的查询压力,在提升性能的同时保证系统的稳定性。这时,你的电商系统整体的架构演变成下图的样子:
我们在 Web 层和数据库层之间增加了缓存层,请求会首先查询缓存,只有当缓存中没有需要的数据时才会查询数据库。
在这里,你需要关注缓存命中率这个指标(缓存命中率 = 命中缓存的请求数 / 总请求数)。一般来说,在你的电商系统中,核心缓存的命中率需要维持在 99% 甚至是 99.9%,哪怕下降 1%,系统都会遭受毁灭性的打击。
这绝不是危言耸听,我们来计算一下。假设系统的 QPS 是 10000/s,每次调用会访问 10 次缓存或者数据库中的数据,那么当缓存命中率仅仅减少 1%,数据库每秒就会增加 10000 * 10 * 1% = 1000 次请求。而一般来说我们单个 MySQL 节点的读请求量峰值就在 1500/s 左右,增加的这 1000 次请求很可能会给数据库造成极大的冲击。
命中率仅仅下降 1% 造成的影响就如此可怕,更不要说缓存节点故障了。而图中单点部署的缓存节点就成了整体系统中最大的隐患,那我们要如何来解决这个问题,提升缓存的可用性呢?
我们可以通过部署多个节点,同时设计一些方案让这些节点互为备份。这样,当某个节点故障时,它的备份节点可以顶替它继续提供服务。而这些方案就是我们本节课的重点:分布式缓存的高可用方案。
在我的项目中,我主要选择的方案有客户端方案、中间代理层方案和服务端方案三大类:
客户端方案就是在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。
中间代理层方案是在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用。
服务端方案就是 Redis 2.4 版本后提出的 Redis Sentinel 方案。
掌握这些方案可以帮助你,抵御部分缓存节点故障导致的,缓存命中率下降的影响,增强你的系统的鲁棒性。

客户端方案

在客户端方案中,你需要关注缓存的写和读两个方面:
写入数据时,需要把被写入缓存的数据分散到多个节点中,即进行数据分片;
读数据时,可以利用多组的缓存来做容错,提升缓存系统的可用性。关于读数据,这里可以使用主从和多副本两种策略,两种策略是为了解决不同的问题而提出的。
下面我就带你一起详细地看一下到底要怎么做。
1. 缓存数据如何分片
单一的缓存节点受到机器内存、网卡带宽和单节点请求量的限制,不能承担比较高的并发,因此我们考虑将数据分片,依照分片算法将数据打散到多个不同的节点上,每个节点上存储部分数据。
这样在某个节点故障的情况下,其他节点也可以提供服务,保证了一定的可用性。这就好比不要把鸡蛋放在同一个篮子里,这样一旦一个篮子掉在地上,摔碎了,别的篮子里还有没摔碎的鸡蛋,不至于一个不剩。
一般来讲,分片算法常见的就是 Hash 分片算法和一致性 Hash 分片算法两种。
Hash 分片的算法就是对缓存的 Key 做哈希计算,然后对总的缓存节点个数取余。你可以这么理解:
比如说,我们部署了三个缓存节点组成一个缓存的集群,当有新的数据要写入时,我们先对这个缓存的 Key 做比如 crc32 等 Hash 算法生成 Hash 值,然后对 Hash 值模 3,得出的结果就是要存入缓存节点的序号。
这个算法最大的优点就是简单易理解,缺点是当增加或者减少缓存节点时,缓存总的节点个数变化造成计算出来的节点发生变化,从而造成缓存失效不可用。所以我建议你,如果采用这种方法,最好建立在你对于这组缓存命中率下降不敏感,比如下面还有另外一层缓存来兜底的情况下。

当然了,用一致性 Hash 算法可以很好地解决增加和删减节点时,命中率下降的问题。在这个算法中,我们将整个 Hash 值空间组织成一个虚拟的圆环,然后将缓存节点的 IP 地址或者主机名做 Hash 取值后,放置在这个圆环上。当我们需要确定某一个 Key 需要存取到哪个节点上的时候,先对这个 Key 做同样的 Hash 取值,确定在环上的位置,然后按照顺时针方向在环上“行走”,遇到的第一个缓存节点就是要访问的节点。比方说下面这张图里面,Key 1 和 Key 2 会落入到 Node 1 中,Key 3、Key 4 会落入到 Node 2 中,Key 5 落入到 Node 3 中,Key 6 落入到 Node 4 中。
这时如果在 Node 1 和 Node 2 之间增加一个 Node 5,你可以看到原本命中 Node 2 的 Key 3 现在命中到 Node 5,而其它的 Key 都没有变化;同样的道理,如果我们把 Node 3 从集群中移除,那么只会影响到 Key 5 。所以你看,在增加和删除节点时,只有少量的 Key 会“漂移”到其它节点上,而大部分的 Key 命中的节点还是会保持不变,从而可以保证命中率不会大幅下降。
不过,事物总有两面性。虽然这个算法对命中率的影响比较小,但它还是存在问题:
缓存节点在圆环上分布不平均,会造成部分缓存节点的压力较大;当某个节点故障时,这个节点所要承担的所有访问都会被顺移到另一个节点上,会对后面这个节点造成压力。
一致性 Hash 算法的脏数据问题。
极端情况下,比如一个有三个节点 A、B、C 承担整体的访问,每个节点的访问量平均,A 故障后,B 将承担双倍的压力(A 和 B 的全部请求),当 B 承担不了流量 Crash 后,C 也将因为要承担原先三倍的流量而 Crash,这就造成了整体缓存系统的雪崩。
说到这儿,你可能觉得很可怕,但也不要太担心,我们程序员就是要能够创造性地解决各种问题,所以你可以在一致性 Hash 算法中引入虚拟节点的概念。
它将一个缓存节点计算多个 Hash 值分散到圆环的不同位置,这样既实现了数据的平均,而且当某一个节点故障或者退出的时候,它原先承担的 Key 将以更加平均的方式分配到其他节点上,从而避免雪崩的发生。
其次,就是一致性 Hash 算法的脏数据问题。为什么会产生脏数据呢?比方说,在集群中有两个节点 A 和 B,客户端初始写入一个 Key 为 k,值为 3 的缓存数据到 Cache A 中。这时如果要更新 k 的值为 4,但是缓存 A 恰好和客户端连接出现了问题,那这次写入请求会写入到 Cache B 中。接下来缓存 A 和客户端的连接恢复,当客户端要获取 k 的值时,就会获取到存在 Cache A 中的脏数据 3,而不是 Cache B 中的 4。
所以,在使用一致性 Hash 算法时一定要设置缓存的过期时间,这样当发生漂移时,之前存储的脏数据可能已经过期,就可以减少存在脏数据的几率。
很显然,数据分片最大的优势就是缓解缓存节点的存储和访问压力,但同时它也让缓存的使用更加复杂。在 MultiGet(批量获取)场景下,单个节点的访问量并没有减少,同时节点数太多会造成缓存访问的 SLA(即“服务等级协议”,SLA 代表了网站服务可用性)得不到很好的保证,因为根据木桶原则,SLA 取决于最慢、最坏的节点的情况,节点数过多也会增加出问题的概率,因此我推荐 4 到 6 个节点为佳。
2.Memcached 的主从机制
Redis 本身支持主从的部署方式,但是 Memcached 并不支持,所以我们今天主要来了解一下 Memcached 的主从机制是如何在客户端实现的。
在之前的项目中,我就遇到了单个主节点故障导致数据穿透的问题,这时我为每一组 Master 配置一组 Slave,更新数据时主从同步更新。读取时,优先从 Slave 中读数据,如果读取不到数据就穿透到 Master 读取,并且将数据回种到 Slave 中以保持 Slave 数据的热度。
主从机制最大的优点就是当某一个 Slave 宕机时,还会有 Master 作为兜底,不会有大量请求穿透到数据库的情况发生,提升了缓存系统的高可用性。
3. 多副本
其实,主从方式已经能够解决大部分场景的问题,但是对于极端流量的场景下,一组 Slave 通常来说并不能完全承担所有流量,Slave 网卡带宽可能成为瓶颈。
为了解决这个问题,我们考虑在 Master/Slave 之前增加一层副本层,整体架构是这样的:
在这个方案中,当客户端发起查询请求时,请求首先会先从多个副本组中选取一个副本组发起查询,如果查询失败,就继续查询 Master/Slave,并且将查询的结果回种到所有副本组中,避免副本组中脏数据的存在。
基于成本的考虑,每一个副本组容量比 Master 和 Slave 要小,因此它只存储了更加热的数据。在这套架构中,Master 和 Slave 的请求量会大大减少,为了保证它们存储数据的热度,在实践中我们会把 Master 和 Slave 作为一组副本组使用。

中间代理层方案

虽然客户端方案已经能解决大部分的问题,但是只能在单一语言系统之间复用。例如微博使用 Java 语言实现了这么一套逻辑,我使用 PHP 就难以复用,需要重新写一套,很麻烦。而中间代理层的方案就可以解决这个问题。你可以将客户端解决方案的经验移植到代理层中,通过通用的协议(如 Redis 协议)来实现在其他语言中的复用。
如果你来自研缓存代理层,你就可以将客户端方案中的高可用逻辑封装在代理层代码里面,这样用户在使用你的代理层的时候就不需要关心缓存的高可用是如何做的,只需要依赖你的代理层就好了。
除此以外,业界也有很多中间代理层方案,比如 Facebook 的Mcrouter,Twitter 的Twemproxy,豌豆荚的Codis。它们的原理基本上可以由一张图来概括:
看这张图你有什么发现吗? 所有缓存的读写请求都是经过代理层完成的。代理层是无状态的,主要负责读写请求的路由功能,并且在其中内置了一些高可用的逻辑,不同的开源中间代理层方案中使用的高可用策略各有不同。比如在 Twemproxy 中,Proxy 保证在某一个 Redis 节点挂掉之后会把它从集群中移除,后续的请求将由其他节点来完成;而 Codis 的实现略复杂,它提供了一个叫 Codis Ha 的工具来实现自动从节点提主节点,在 3.2 版本之后换做了 Redis Sentinel 方式,从而实现 Redis 节点的高可用。

服务端方案

Redis 在 2.4 版本中提出了 Redis Sentinel 模式来解决主从 Redis 部署时的高可用问题,它可以在主节点挂了以后自动将从节点提升为主节点,保证整体集群的可用性,整体的架构如下图所示:
Redis Sentinel 也是集群部署的,这样可以避免 Sentinel 节点挂掉造成无法自动故障恢复的问题,每一个 Sentinel 节点都是无状态的。在 Sentinel 中会配置 Master 的地址,Sentinel 会时刻监控 Master 的状态,当发现 Master 在配置的时间间隔内无响应,就认为 Master 已经挂了,Sentinel 会从从节点中选取一个提升为主节点,并且把所有其他的从节点作为新主的从节点。Sentinel 集群内部在仲裁的时候,会根据配置的值来决定当有几个 Sentinel 节点认为主挂掉可以做主从切换的操作,也就是集群内部需要对缓存节点的状态达成一致才行。
Redis Sentinel 不属于代理层模式,因为对于缓存的写入和读取请求不会经过 Sentinel 节点。Sentinel 节点在架构上和主从是平级的,是作为管理者存在的,所以可以认为是在服务端提供的一种高可用方案。

课程小结

这就是今天分享的全部内容了,我们一起来回顾一下重点:
分布式缓存的高可用方案主要有三种,首先是客户端方案,一般也称为 Smart Client。我们通过制定一些数据分片和数据读写的策略,可以实现缓存高可用。这种方案的好处是性能没有损耗,缺点是客户端逻辑复杂且在多语言环境下不能复用。
其次,中间代理方案在客户端和缓存节点之间增加了中间层,在性能上会有一些损耗,在代理层会有一些内置的高可用方案,比如 Codis 会使用 Codis Ha 或者 Sentinel。
最后,服务端方案依赖于组件的实现,Memcached 就只支持单机版没有分布式和 HA 的方案,而 Redis 在 2.4 版本提供了 Sentinel 方案可以自动进行主从切换。服务端方案会在运维上增加一些复杂度。
总体而言,分布式缓存的三种方案各有所长,有些团队可能在开发过程中已经积累了 Smart Client 上的一些经验;而有些团队在 Redis 运维上经验丰富,就可以推进 Sentinel 方案;有些团队在存储研发方面有些积累,就可以推进中间代理层方案,甚至可以自研适合自己业务场景的代理层组件,具体的选择还是要看团队的实际情况而定。

一课一思

结合你们过往的经历,我们来聊一聊缓存高可用的重要性吧,比如当缓存可用性下降会造成什么严重问题呢?你们又是如何来保证缓存的高可用的呢?欢迎在留言区与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 18

提建议

上一篇
13 | 缓存的使用姿势(一):如何选择缓存的读写策略?
下一篇
15 | 缓存的使用姿势(三):缓存穿透了怎么办?
 写留言

精选留言(55)

  • 👽
    2019-10-22
    大概总结了一下: 实现高可用的核心依旧是集群。多个缓存节点,提高容错率。 客户端实现:由客户端的策略决定如何写缓存,如何读缓存。性能高,但是逻辑复杂,无法跨平台。 中间件实现:所有客户端先访问中间件,然后中间件决定了缓存策略。因为引入了中间件,所以性能较差,但是可以跨平台,并且有能力的公司还可以自研中间件。 服务端实现:主从切换由服务端实现。最大的缺点是增加了运维成本。 不知道我的理解是否正确。
    展开

    作者回复: 是的👍

    40
  • longslee
    2019-10-18
    打卡。 老师好,提问:一致性Hash中,比如存在A(k1)、B(k2)、C(k3),3个节点,括号中为分配的Key,假如B节点剔除了,那么k2会漂移到C节点。 那么此时客户端请求get k2的时候,是被计算好从C节点获取呢,还是完全就拿不到需要去数据库查?如果是从C节点获取,那感觉命中率完全没有下降。 这一点确实没搞清楚,忘老师解惑,谢谢🙏

    作者回复: 1. 如果B节点剔除,那么K2的读写都会到C节点上 2. 命中率会下降,因为B节点没有被删除的时候,C节点上是没有K2数据的。B节点剔除后,第一次从C节点获取K2数据是要穿透的

    共 5 条评论
    15
  • 长期规划
    2019-10-24
    老师,您之前说4核8G的机器上,MySQL最高支撑QPS为1万,怎样本文开头又说MySQL读峰值才1500/s呢

    作者回复: 1万是基准测试的结果,在实际中sql更复杂,达不到这个性能

    共 3 条评论
    15
  • 2020-04-24
    高可用的设计思路没有其他的核心就是增加副本,针对数据就增加数据副本,针对服务就增加服务副本,针对机房就增加机房副本,增加副本引入的新问题是数据不一致性,下面各种算法什么的都是为了解决因增加副本而带来的数据不一致性问题或者节点挂了怎么使服务继续可用的策略。比如:数据怎么迁移?故障怎么隔离?故障节点恢复后怎么是否加入?怎么加入?最近热上了看火影,影分术就是鸣人的高可用方式,其他的高可用思路和这个如出一辙。从动漫中也可以看出,这个需要更多能量,对公司而言,需要更多机器和存储空间,技术复杂度也会增加一些,幸好有现成的组件避免人人都重复造轮子的资源浪费。
    展开

    作者回复: 👍

    共 4 条评论
    13
  • 👽
    2019-10-22
    另外就客户端和服务端的理解: 老实说一开始我也一脸懵逼。以为客户端就是用户端。 但是后来想通了。 应用服务器为用户提供数据接口,用户就是客户端,应用就是服务端。 但是缓存为应用服务器提供缓存服务,这时候对于缓存服务器来说应用服务器就是客户端,而缓存就是服务端。
    展开

    作者回复: 是的 :)

    共 4 条评论
    9
  • Hwan
    2019-11-03
    回答一下问题,如果缓存的高可用做的不够好的话,一旦线上发生故障,导致缓存直接崩了,再加上并发量比较大的话,可能会直接去访问数据库了,对数据库的压力比较大,严重的话,会导致数据库崩溃,然后就是各种丢数据,然后关于如何做的话,建议之前做好关于一些流量的评估,以Redis为例,现在有现成的哨兵和集群方案可以使用,当然也可以Codis,还有一点就是部署线上的时候可以先演练下
    6
  • 长期规划
    2019-12-18
    老师,主从会有延迟,写入主库,但延迟同步到从库,在同步完成前去从库读数据,读不到,这如何解决呢

    作者回复: 1. 写入的时候中缓存,这样从缓存里面读就实时了 2. 直接读主库

    共 9 条评论
    5
  • 海罗沃德
    2019-11-06
    分享一個踩過的坑,在AWS上,產品經理要做一個增加搜索速度的功能,要求把最近搜過的關鍵字跟結果緩存起來,於是就通過AWS elastic cache做了一盒緩存層,然後產品經理提出我們的項目是跨region訪問的,我在A region更改數據後在瞬間開另一個瀏覽器用deep link方式從B region再訪問,都要看到數據,然而AWS的緩存不支持跨region的數據拷貝,於是這個story的failure就變成我來背鍋
    共 2 条评论
    4
  • 无形
    2019-11-04
    还没看完就想说一下我之前的做法,我们有两级缓存,服务器应用程序自身有一个内存缓存,再有Redis缓存,如果内存缓存没有命中,应用程序会创建一个单机的资源锁(go语言,用map+chan实现),大量请求进来,只有第一个请求会获取锁,其他请求获取锁失败,调用wait方法,等待第一个请求获取数据,第一个请求先从Redis中获取数据写入到内存缓存,Redis没有命中再读取MySQL,写会到Redis,执行结束后会通过close chan的方式广播消息,通知其他请求拿到了数据,从内存中读取数据,再释放锁。这样就解决了缓存穿透的问题,同一时刻,不论多大的并发量,真正到存储查询数据的请求只会有一个。
    展开

    作者回复: 其实更多的会在redis和mysql之间增加并发的控制,因为redis还是可以扛很好的并发的

    共 9 条评论
    5
  • 阿卡牛
    2019-10-29
    老师,这个客户端方案中的客户端指哪里。一般我理解的客户端是浏览器或手机app...

    作者回复: 指的是你的应用服务器,是缓存的使用者

    4
  • aoe
    2020-09-28
    在分享老师文章的时候,学到了一致性哈希算法的不同实现 1. 哈希环法 2. google的jump consistent hash 3. Maglev一致性哈希法
    3
  • kamida
    2020-03-21
    分片策略是怎么配置的呢 比如说客户端或者中间层怎么知道哪个node负责consistent hash ring上的key的?有zookeeper? 还有我记得sentinel不支持分片的吧

    作者回复: 分片策略是由算法确定的

    共 3 条评论
    2
  • 长期规划
    2019-12-19
    老师,Redis Cluster中使用了hash槽,我理解跟一致性hash其实是等价的,对吧。不过,Redis在分片间还实现了在新增节点时自动迁移数据

    作者回复: 算法上不太一样哦

    共 3 条评论
    2
  • 蓝魔丶
    2019-10-25
    老师,memcached的主从机制会考虑分片实现多个主吗,还是单机全量?多副本方案中提到会选用一个主从作为一个副本组,是一个master多个slave的结构吗?但是画图没有标示这种结构

    作者回复: 1. 会考虑分片 2. 是的

    2
  • 张德
    2019-12-05
    老师 最近学习 这里还有一个问题 在Redis的sentinal方案中 读缓存是读Slave结点吗?? 感觉只有读Slave结点 才能实现更高的读请求性能 但是读Slave节点时 Master中的数据还没有同步过来 没有读到结果 或者读到旧数据脏数据的话 这个应该怎么解决 ??? 还是说 读请求也只能读Master结点

    作者回复: 这个和sentinal无关的。只要做了主从分离,读的都是slave节点。sentinal是来监控主库,在主库出现问题的时候可以帮助你做从库提主

    2
  • Fourty Seven
    2019-11-11
    它将一个缓存节点计算多个 Hash 值分散到圆环的不同位置,这样既实现了数据的平均,而且当某一个节点故障或者退出的时候,它原先承担的 Key 将以更加平均的方式分配到其他节点上,从而避免雪崩的发生。 老师,这句话啥意思了?是说把一个节点分布到多个位置?

    作者回复: 是的,因为虚拟了多个节点,这些节点会分布在圆环的多个位置

    1
  • 张德
    2019-11-07
    老师请教一下 客户端一致性哈希算法 四到六个结点 如果四个结点的话 删除一个结点 依然会有四分之一的请求无法命中缓存 感觉这个一直性哈希算法和普通的哈希算法 没有太大区别呀 老师能不能再仔细简介一下这个地方 看不出一致性哈希算法的优势

    作者回复: 在一致性hash下,如果删除一个节点,比如在一个环上有a,b,c三个节点,b故障,那么只有b的数据会迁移到c上;但是普通hash会重新计算hash值,理论上会影响所有的数据

    共 4 条评论
    1
  • 胖胖虎
    2019-10-28
    看到老师最后的总结: 总体而言,分布式缓存的三种方案各有所长,有些团队可能在开发过程中已经积累了 Smart Client 上的一些经验;而有些团队在 Redis 运维上经验丰富,就可以推进 Sentinel 方案;有些团队在存储研发方面有些积累,就可以推进中间代理层方案,甚至可以自研适合自己业务场景的代理层组件,具体的选择还是要看团队的实际情况而定。 如果产品是从0开始,各方面没有太多积累,合理的选择是什么?之前我自己选的是Redis-Sentinel,因为这是官方支持的高可用模式,但是看到老师说对运维要求比较高,想了解一下需要重点关注哪些方面。
    展开
    共 2 条评论
    1
  • 小喵喵
    2019-10-20
    老师,关于一致性hash有几点疑问,请老师解答: 1.为了防止hash环的倾斜,由实际节点虚拟出来的一部分虚拟节点是如何保证虚拟节点能均匀排列呢?还是说增加虚拟节点的算法可以自定义虚拟节点插入的位置呢? 2.假设一个真实节点宕机了,那是不是那个真实节点相对应的虚拟节点也“宕机”(不可用)了? 3.一致性hash算法在java里或者.net里面有现成的实现吗?若有是对应那个类呢?
    展开

    作者回复: 1. 从概率学上保证,如果你虚拟出来的节点足够多,你无法保证绝对的均匀 2. 是的 虚拟节点也宕机了 3. 貌似是没有的,不过在一些memcached客户端代码中都会有实现

    1
  • distdev
    2019-10-19
    另外您说redis cluster不是高可用方案 为什么?

    作者回复: sorry,我之前回复有误,在redis cluster中有高可用策略来保证的

    1