方案有客户端方案、中间代理层方案和服务端方案三大类。
客户端方案就是在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布 式,从而提高缓存的可用性。 中间代理层方案是在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请 求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用。 服务端方案就是 Redis 2.4 版本后提出的 Redis Sentinel 方案。
一.客户端方案
写入数据时,需要把被写入缓存的数据分散到多个节点中,即进行数据分片; 读数据时,可以利用多组的缓存来做容错,提升缓存系统的可用性。关于读数据,这里可 以使用主从和多副本两种策略,两种策略是为了解决不同的问题而提出的。
1. 缓存数据如何分片
单一的缓存节点受到机器内存、网卡带宽和单节点请求量的限制,不能承担比较高的并发, 因此我们考虑将数据分片,依照分片算法将数据打散到多个不同的节点上,每个节点上存储 部分数据。一般来讲,分片算法常见的就是 Hash 分片算法和一致性 Hash 分片算法两种。 Hash 分片的算法就是对缓存的 Key 做哈希计算,然后对总的缓存节点个数取余。比如说,我们部署了三个缓存节点组成一个缓存的集群,当有新的数据要写入时,我们先对 这个缓存的 Key 做比如 crc32 等 Hash 算法生成 Hash 值,然后对 Hash 值模 3,得出的 结果就是要存入缓存节点的序号。这个算法最大的优点就是简单易理解,缺点是当增加或者减少缓存节点时,缓存总的节点 个数变化造成计算出来的节点发生变化,从而造成缓存失效不可用。用一致性 Hash 算法可以很好地解决增加和删减节点时,命中率下降的问题。在 这个算法中,我们将整个 Hash 值空间组织成一个虚拟的圆环,然后将缓存节点的 IP 地址 或者主机名做 Hash 取值后,放置在这个圆环上。当我们需要确定某一个 Key 需要存取到 哪个节点上的时候,先对这个 Key 做同样的 Hash 取值,确定在环上的位置,然后按照顺 时针方向在环上“行走”,遇到的第一个缓存节点就是要访问的节点。在增加和删除节点时,只有少量的 Key 会“漂移”到其它节点上,而大部分的 Key 命中的节点还是会保持不变,从而可以保 证命中率不会大幅下降。
一致性哈希也有缺点,缓存节点在圆环上分布不平均,会造成部分缓存节点的压力较大;当某个节点故障时,这 个节点所要承担的所有访问都会被顺移到另一个节点上,会对后面这个节点造成压力。 一致性 Hash 算法的脏数据问题。可以在一致性 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 取决于最慢、最坏的节点的情况,节点数过 多也会增加出问题的概率。
2.Memcached 的主从方案(客户端方案)
Redis 本身支持主从的部署方式,但是 Memcached 并不支持。主从机制最大的优点就是当某一个 Slave 宕机时,还会有 Master 作为兜底,不会有大量请 求穿透到数据库的情况发生,提升了缓存系统的高可用性。
3.多副本
虽然主从方式已经能够解决大部分场景的问题,但是对于极端流量的场景下,一组 Slave 通常来说并不能完全承担所有流量,Slave 网卡带宽可能成为瓶颈。
当客户端发起查询请求时,请求首先会先从多个副本组中选取一个副本组发 起查询,如果查询失败,就继续查询 Master/Slave,并且将查询的结果回种到所有副本组 中,避免副本组中脏数据的存在。 基于成本的考虑,每一个副本组容量比 Master 和 Slave 要小,因此它只存储了更加热的数 据。在这套架构中,Master 和 Slave 的请求量会大大减少,为了保证它们存储数据的热 度,在实践中我们会把 Master 和 Slave 作为一组副本组使用。
二.中间代理层方案
虽然客户端方案已经能解决大部分的问题,但是只能在单一语言系统之间复用。例如微博使 用 Java 语言实现了这么一套逻辑,我使用 PHP 就难以复用,需要重新写一套,很麻烦。 而中间代理层的方案就可以解决这个问题。你可以将客户端解决方案的经验移植到代理层 中,通过通用的协议(如 Redis 协议)来实现在其他语言中的复用。
业界也有很多中间代理层方案,比如 Facebook 的Mcrouter,Twitter 的 Twemproxy,豌豆荚的Codis。所有缓存的读写请求都是经过代理层完成的。代理层是无状态 的,主要负责读写请求的路由功能,并且在其中内置了一些高可用的逻辑,不同的开源中间 代理层方案中使用的高可用策略各有不同。比如在 Twemproxy 中,Proxy 保证在某一个 Redis 节点挂掉之后会把它从集群中移除,后续的请求将由其他节点来完成;
三.服务端方案
Redis 在 2.4 版本中提出了 Redis Sentinel 模式来解决主从 Redis 部署时的高可用问题, 它可以在主节点挂了以后自动将从节点提升为主节点,保证整体集群的可用性。Redis Sentinel 也是集群部署的,这样可以避免 Sentinel 节点挂掉造成无法自动故障恢复 的问题,每一个 Sentinel 节点都是无状态的。在 Sentinel 中会配置 Master 的地址, Sentinel 会时刻监控 Master 的状态,当发现 Master 在配置的时间间隔内无响应,就认为 Master 已经挂了,Sentinel 会从从节点中选取一个提升为主节点,并且把所有其他的从节 点作为新主的从节点。Sentinel 集群内部在仲裁的时候,会根据配置的值来决定当有几个 Sentinel 节点认为主挂掉可以做主从切换的操作,也就是集群内部需要对缓存节点的状态 达成一致才行。
Redis Sentinel 不属于代理层模式,因为对于缓存的写入和读取请求不会经过 Sentinel 节 点。Sentinel 节点在架构上和主从是平级的,是作为管理者存在的,所以可以认为是在服 务端提供的一种高可用方案。