redis集群

[TOC]

Redis 主从架构

单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发

redis-master-slave

主节点和从节点数据同步,也就复制的问题

哨兵模式

消息通知:实例故障了通知管理员和客户端新的地址

故障发现

故障转移

故障发现

一、检测主观下线状态
  • 在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线
二、检测客观下线状态
  • 当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线 了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作

当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,当某个sentinel被半数以上的sentinel设置成为领头sentinel,那么这个sentinel称为领头sentinel并由领头Sentinel对下线主服务器执行故障转移操作(raft)

1
Raft算法

故障自动转移

领头的sentinel负责故障转移
从从服务器列表中选择一个节点作为新节点

1 过滤掉:下线、5s内没有回应InFO、与主服务器失联超过down-after-milliseconds的从服务器
2 选择从服务器中优先级最高的。若具有多个同样优先级的,则继续
3 选择复制偏移量最大的从节点。若具有多个复制偏移量相同的,则继续
4 选择 run id最小的从服务器

sentinel会对1中选出的从服务器发送slaveof no one使其成为主服务器
sentinel向其余从服务器发送命令,让他们成为新主服务器的从服务器,并同步数据
sentinel会关注刚刚下线的主服务器,并让它成为从服务器,当它恢复的时候就去同步数据

官方Redis Cluster 方案(服务端路由查询)

1
2
3
1.主从复制不能实现高可用
2.随着公司发展,用户数量增多,并发越来越多,业务需要更高的QPS,而主从复制中单机的QPS可能无法满足业务需求
3.数据量的考虑,现有服务器内存不能满足业务数据的需要时,单纯向服务器添加内存不能达到要求,此时需要考虑分布式需求,把数据分布到不同服务器上

redis cluster在设计的时候,就考虑到了去中心化,去中间件,也就是说,集群中的每个节点都是平等的关系,都是对等的,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。

那么redis 是如何合理分配这些节点和数据的呢?

Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做哈希槽 (hash slot)的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384

  • Redis集群采用P2P的Gossip(流言)协议, Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播,如下图所示:

img

  • 通信过程说明:
    • 1)集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000
    • 2)每个节点在固定周期内通过特定规则选择几个节点发送ping消息
    • 3)接收到ping消息的节点用pong消息作为响应
  • 集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从角色变化、槽信息 变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的

Gossip消息

  • Gossip协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip消息,了解这些消息有助于我们理解集群如何完成信息交换
  • 常用的Gossip消息可分为:
    • meet消息:用于通知新节点加入。消息发送者通知接收者加入到当前 集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的 ping、pong消息交换
    • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其 他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消 息发送封装了自身节点和部分其他节点的状态数据。
    • pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确 认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内 广播自身的pong消息来通知整个集群对自身状态进行更新
    • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。具体细节将在后面的“故障转移”文章中说明
  • 它们的通信模式如下图所示:

img

MOVED重定向

  • 概念:在集群模式下,bRedis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。如下图所示

img

ASK重定向

  • Redis集群支持在线迁移槽(slot)和数据来完成水平伸缩,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。例如当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点,如下图所示

img

  • 当出现上述情况时,客户端键命令执行流程将发生变化,如下所示:
    • 1)客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端
    • 2)如果键对象不存在,则可能存在于目标节点,这时源节点会回复ASK重定向异常。格式如下:(error) ASK {slot} {targetIP}:{targetPort}
    • 3)客户端从ASK重定向异常提取出目标节点信息,发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息
  • ASK重定向整体流程如下图所示:

img

  • ASK与MOVED虽然都是对客户端的重定向控制,但是有着本质区别:
    • ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存
    • 但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存

故障转移

  • Redis集群自身实现了高可用。高可用首先需要解决集群部分失败的场景:当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。本文介绍故障转移的细节,分析故障发现和替换故障节点的过程

故障发现

  • 当集群内某个节点出现问题时,需要通过一种健壮的方式保证\识别出节点是否发生了故障**
  • 因此故障发现也是通过消息传播机制实现的,主要环节包括:
    • 主观下线 (pfail):指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况
    • 客观下线(fail):指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移

主观下线

  • 集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会\认为接收节点存在故障**,把接收节点标记为主观下线(pfail)状态**

客观下线

  • 当某个节点判断另一个节点主观下线后,相应的\节点状态会跟随消息在集群内传播**

  • 通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程

  • 假设节点a标记节点b为主观下线,

    一段时间后节点a通过消息把节点b的状态发送到其他节点,当节点c接受到消息并解析出消息体含有节点b的pfail状态时,会触发客观下线流程

    ,如下图所示:

    • 1)当消息体内含有其他节点的pfail状态会判断发送节点的状态,如果发送节点是主节点则对报告的pfail状态处理,从节点则忽略
    • 2)找到pfail对应的节点结构,更新clusterNode内部下线报告链表
    • 3)根据更新后的下线报告链表告尝试进行客观下线

img

故障恢复

  • 故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用
  • 下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会\触发故障恢复流程,**如下图所示:

img

资格检查

  • 每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点
  • 如果从节点与主节点断线时间超过cluster-node-time*cluster-slavevalidity-factor,**则当前从节点不具备故障转移资格**。参数cluster-slavevalidity-factor用于从节点的有效因子,默认为10

准备选举时间

  • 当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程

发起选举

  • 当从节点定时任务

    检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:

    • (1)更新配置纪元:配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元标示当前主节点的版本,所有主节点的配置纪元都不相等,从节点会复制主节点的配置纪元,整个集群又维护一个全局的配置纪元,用于记录集群内所有主节点配置纪元的最大版本。从节点每次发起投票时都会自增集群的全局配置纪元
    • (2)广播选举消息:在集群内广播选举消息(FAILOVER_AUTH_REQUEST),并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举

选举投票

  • 只有持有槽的主节点才会处理故障选举消息,因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的 选举消息将忽略
  • \投票过程其实是一个领导者选举的过程\,**如集群内有N个持有槽的主节点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从节点
  • Redis集群没有直接使用从节点进行领导者选举,主要因为从节点数必须大于等于3个才能保证凑够N/2+1个节点,将导致从节点资源浪费。使用 集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完 成选举过程
  • 当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主节点操作,例如集群内有5个持有槽的主节点,主节点b故障后还有4个, 当其中一个从节点收集到3张投票时代表获得了足够的选票可以进行替换主 节点操作,如下图所示

img

替换主节点

  • 当从节点收集到足够的选票之后,触发替换主节点操作:
    • 1)当前从节点取消复制变为主节点
    • 2)执行clusterDelSlot操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己
    • 3)向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息

基于客户端分配

img

简介

Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool

优点

优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强

缺点

  • 由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。
  • 客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化

基于代理服务器分片

img

简介

客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端

特征

  • 透明接入,业务程序不用关心后端Redis实例,切换成本低
  • Proxy 的逻辑和存储的逻辑是隔离的
  • 代理层多了一次转发,性能有所损耗

业界开源方案

  • Twtter开源的Twemproxy
  • 豌豆荚开源的Codis

hash方法

哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将
该数据分配的节点序号为:hash(key)%N。
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,
将导致大量的数据迁移。

顺序分布

将数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如 User 表的 ID 范围为 1 ~ 7000,使用
顺序分布可以将其划分成多个子表,对应的主键范围为 1 ~ 1000,1001 ~ 2000,…,6001 ~ 7000。
顺序分布相比于哈希分布的主要优点如下:

  • 能保持数据原有的顺序;
  • 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。

一致性hash

将哈希空间 [0, 2n-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值
之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将它前一个节点C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。

image-20201106202450535

虚拟节点

上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。
数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。
解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节
点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。

16384

(1)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
如上所述,在消息头中,最占空间的是myslots[CLUSTER_SLOTS/8]。
当槽位为65536时,这块的大小是:
65536÷8÷1024=8kb
因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
(2)redis的集群主节点数量基本不可能超过1000个。
如上所述,集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。
那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
(3)槽位越小,节点少的情况下,压缩率高
Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。
如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。