0%

Redis08_集群

Redis集群

概述

Redis Cluster是Redis的分布式解决方案,在3.0版本后加入。在3.0之前为了解决容量高可用用方面的需求基本上只能通过 客户端分片+redis sentinel或者代理(twemproxy、codis)方案解决。

数据分布理论

分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题。
常见的分区规则有哈希分区顺序分区两种。

常见的哈希分区规则有几种:

  1. 节点取余分区:hash(key)%N计算出哈希值,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移
  2. 一致性哈希分区: 一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节 点分配一个token,范围一般在0~$2^{32}$ ,这些token构成一个哈希环。数据读写 执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于 等于该哈希值的token节点。
  3. 虚拟槽分区 :使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围 一般远远大于节点数,比如Redis Cluster槽范围是0~16383($2^{13}$)。槽是集群内数据管理和迁移的基本单位。

一致性性哈希算法

一致性哈希算法是对$2^{32}$进行取模,将整个哈希值空间组织成一个虚拟的圆环。

  1. 首先对节点进行取模,算出节点在圆环上的位置。
  2. 对每个key进行取模,算出key在圆环上的位置
  3. 顺时针离key位置最近的节点保存key对应的数据。

加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响,具有较好的容错性和可扩展性

存在的一些问题:

  1. 加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景。
  2. 普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
  3. 当使用少量节点时,节点变化将大范围影响哈希环中数据映射。

总的来说,当节点比较少的时候,容易出现数据倾斜

Hash环的数据倾斜问题

为了解决数据倾斜问题,一般不会直接对节点进行取模,而是将节点映射成多个虚拟节点(在实际应用中,通常将虚拟节点数设置为32甚至更大),然后对虚拟节点进行取模,因此即使很少的服务节点也能做到相对均匀的数据分布。

一致性哈希算法参考

Redis Cluster

Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整 数槽内,计算公式:slot=CRC16(key)&16383

集群功能限制

  1. key批量操作支持有限。(对于映射为不同slot值的key由于执行mget、mget等操作可 能存在于多个节点上因此不被支持。)
  2. key事务操作支持有限。
  3. key作为数据分区的最小粒度,因此不能将一个大的键值对象如 hash、list等映射到不同的节点。
  4. 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模 式下只能使用一个数据库空间,即db0。
  5. 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复 制结构。

集群搭建

启动节点

每个节点需要开启配置cluster-enabled yes

节点握手

节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信,达到感知对方的过程。节点握手是集群彼此通信的第一步由客户端发起命令:cluster meet {ip} {port}

cluster meet命令是一个异步命令,执行之后立刻返回。内部发起与目标节点进行握手通信。

  1. 节点6379本地创建6380节点信息对象,并发送meet消息。
  2. 节点6380接受到meet消息后,保存6379节点信息并回复pong消息。
  3. 之后节点6379和6380彼此定期通过ping/pong消息进行正常的节点通信。

两台机器握手以后,如何让其他节点获取到新加入节点的信息呢?
集群内任意节点上执行cluster meet命令加入新节点,握手状态会通过消息在集群内传播,这样其他节点会自动发现新节点并发起握手流程。

握手以后,原来集群中的节点(6379)会通过gossip协议,将新节点(6380)的信息传播给集群中其他节点。

槽指派

Redis集群把所有的数据映射到16384个槽中。每个key会映射为一个固 定的槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。 通过 cluster addslots命令为节点分配槽。

当数据库中的16384个槽都有节点在处理时,集群处于上线状态;如果任何一个槽没有得到处理,集群处于下线状态

集群模式下,Reids节点角色分为主节点和从节点。首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。使用cluster replicate {nodeId}命令让一个节点成为从节点。 其中命令执行必须在对应的从节点上执行,nodeId是要复制主节点的节点ID

节点通信

在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式P2P方式

Redis集群采用P2P的Gossip协议, Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息。

gossip协议
常用的Gossip消息可分为:

  1. ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其 他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消 息发送封装了自身节点和部分其他节点的状态数据。
  2. pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确 认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内 广播自身的pong消息来通知整个集群对自身状态进行更新。
  3. meet消息:用于通知新节点加入。
  4. fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。

数据交换流程

  1. 集群内每个节点维护定时任务默认每秒执行10次,每秒会随机选取5个节点找出最久没有通信的节点发送ping消息,用于保证Gossip信息交换的随机性。
  2. 每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信 息太长时间未更新。

根据以上规则得出每个节点每秒需要发送ping消息的数量=1+10*num(node.pong_received>cluster_nod\e_timeout/2),因此 cluster_node_timeout参数对消息发送的节点数量影响非常大

随着集群规模的增大,消耗在ping上的带宽也会随之增长,的集群每次消息通信的成本也就更高,带宽资源紧张的时候可以适当调大cluster_node_timeout参数

集群扩容/缩容

Redis集群扩容操作可分为如下步骤

  1. 准备新节点。
  2. 加入集群。
  3. 迁移槽和数据

迁移数据流程如下

  1. 对目标节点发送cluster setslot{slot}importing{sourceNodeId}命令,让目标节点准备导入槽的数据。
  2. 对源节点发送cluster setslot{slot}migrating{targetNodeId}命令,让源节点准备迁出槽的数据。
  3. 源节点循环执行cluster getkeysinslot{slot}{count}命令,获取count个属于槽{slot}的键。
  4. 在源节点上执行migrate {targetIp} {targetPort} “”0{timeout}keys{keys…}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量 迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能 单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO次数。
  5. 重复执行步骤3和步骤4直到槽下所有的键值数据迁移到目标节点。
  6. 向集群内所有主节点发送cluster setslot{slot}node{targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。

请求重定向

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

根据MOVED重定向机制,客户端可 以随机连接集群内任一Redis获取键所在节点,这种客户端又叫Dummy(傀 儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据 重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都 要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销,这不是Redis集群高效的使用方式。正因为如此通常集群客户端都采用另一种实现:Smart(智能)客户端。

Smart客户端

常见redis smart客户端:Jedis,lettuce,Redisson 这三个都是官方推荐的,要看具体使用场景,lettuce、Redisson功能更高级一点,比如支持读写分离等。

Smart客户端通过在内部维护slot→node的映射关系,本地就可实现键到 节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客 户端更新slot→node映射。

ASK重定向

当进行槽迁移时,客户端请求进来会进行ASK重定向。

ASK重定向对单键命令支持得很完善,但是,在开发中我 们经常使用批量操作,如mget或pipeline。当槽处于迁移状态时,批量操作会受到影响。

故障转移

故障发现

主观下线

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

客观下线

当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态时,会在本地找到故障节点的ClusterNode结构,保存到下线报告链表中。

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

为什么必须是负责槽的主节点参与故障发现决策?
因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主节点数据和状态信息的复制

为什么半数以上处理槽的主节点?
必须半数以上是为了应对网络分区等原因造成的集群分割情况,被分割的小集群因为无法完成从主观下线到客观下线这一关键过程,从而防止小集群完成故障转移之后继续对外提供服务。

维护下线报告链表

每个节点ClusterNode结构中都会存在一个下线链表结构,保存了其他主节点针对当前节点的下线报告。
下线报告中保存了报告故障的节点结构和最近收到下线报告的时间,当接收到fail状态时,会维护对应节点的下线上报链表。
每个下线报告都存在有效期,每次在尝试触发客观下线时,都会检测下线报告是否过期,对于过期的下线报告将被删除。如果在cluster-node-time*2 时间内该下线报告没有得到更新则过期并删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct clusterNodeFailReport {
struct clusterNode *node; /* 报告该节点为主观下线的节点 */
mstime_t time; /* 最近收到下线报告的时间 */
} clusterNodeFailReport;

//维护逻辑伪代码

def clusterNodeAddFailureReport(clusterNode failNode, clusterNode senderNode) :
// 获取故障节点的下线报告链表
list report_list = failNode.fail_reports;
// 查找发送节点的下线报告是否存在
for(clusterNodeFailReport report : report_list):
// 存在发送节点的下线报告上报
if(senderNode == report.node):
// 更新下线报告时间
report.time = now();
return 0;
// 如果下线报告不存在,插入新的下线报告
report_list.add(new clusterNodeFailReport(senderNode,now()));
return 1;

尝试客观下线

集群中的节点每次接收到其他节点的pfail状态,都会尝试触发客观下线

广播客观下线状态

向集群广播一条fail消息,通知所有的节点将故障节点标记为客观下线,fail消息的消息体只包含故障节点的ID。

  • 通知集群内所有的节点标记故障节点为客观下线状态并立刻生效。
  • 通知故障节点的从节点触发故障转移流程。

故障恢复

资格检查

每个从节点都要检查最后与主节点断线时间,如果从节点与主节点断线时间超过cluster-node-time*cluster-slave-validity-factor,不具备故障转移资格。

准备选举时间

当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么 它应该具有更高的优先级来替换故障主节点。

主节点b进入客观下线后,它的三个从节点根据自身复制偏移量设置延 迟选举时间,如复制偏移量最大的节点slave b-1延迟1秒执行,保证复制延 迟低的从节点优先发起选举。

ps : 集群中进行主从替换的时候,和哨兵机制不同,哨兵机制是选举领头Sentinel,由领头Sentinel对从服务器进行升级。Redis Cluster则是由从节点自己去做升级,这就需要从节点自己去解决优先级的问题,所以采用延迟选举的方式。

从节点发起选举

  1. 更新配置纪元(当前配置版本号)
  2. 广播选举消息
    在集群内广播选举消息(FAILOVER_AUTH_REQUEST),并记录已发 送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。

选举投票

只有持有槽的主节点才会处理故障选举消息(FAILOVER_AUTH_REQUEST),因为每个持有槽的节点在一个配置纪元 内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复 FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略。

ps Redis集群没有直接使用(故障节点的)从节点进行领导者选举,主要因为从节点数必须大于等于3个才能保证凑够N/2+1个节点,将导致从节点资源浪费。
使用集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完 成选举过程。

故障主节点也算在投票数内,集群时所有主节点最少需要部署在3台物理机上才能避免单点问题。

替换主节点

当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主节点操作。

  1. 当前从节点取消复制变为主节点。
  2. 执行clusterDelSlot操作撤销故障主节点负责的槽,并执行 clusterAddSlot把这些槽委派给自己。
  3. 向集群广播自己的pong消息,通知集群内所有的节点当前从节点变 为主节点并接管了故障主节点的槽信息。

故障恢复时间

  1. 主观下线(pfail)识别时间=cluster-node-timeout
  2. 主观下线状态消息传播时间<=cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。
  3. 从节点转移时间<=1000毫秒。由于存在延迟发起选举机制,偏移量 最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。

集群运维

集群完整性

为了保证集群完整性,默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。执行任何键命令返回(error)CLUSTERDOWN Hash slot not served错误。

当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态,对于大多数业务无法容忍这种情况,因此建议将参数cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行,不会影响其他主节点的可用性

带宽消耗

集群带宽消耗主要分为:读写命令消耗+Gossip消息消耗。

  1. 在满足业务需要的情况下尽量避免大集群。
  2. 适度提高cluster-node-timeout降低消息发送频率
  3. 如果条件允许集群尽量均匀部署在更多机器上

Pub/Sub广播问题

在集群模式下内部实现对所有的publish命令都会向 所有的节点进行广播,造成每条publish数据都会在集群内所有节点传播一次,加重带宽负担。

数据倾斜

  1. 节点和槽分配严重不均。
  2. 不同槽对应键数量差异过大(哈希算法不均匀)
  3. 集合对象包含大量元素(不建议使用大集合,可以根据业务场景对大集合进行拆分)
  4. 内存相关配置不一致。内存相关配置指hash-max-ziplist-value、setmax-intset-entries等压缩数据结构配置。(当集群大量使用hash、set等数据结构 时,如果内存压缩数据结构配置不一致,极端情况下会相差数倍的内存,从 而造成节点内存量倾斜。)

请求倾斜(热点问题)

集群内特定节点请求量/流量过大将导致节点之间负载不均,影响集群均衡和运维成本。常出现在热点键场景,当键命令消耗较低时如小对象的 get、set、incr等,即使请求量差异较大一般也不会产生负载严重不均。

当热点键对应高算法复杂度的命令或者是大对象操作如hgetall、smembers等,会导致对应节点负载过高的情况。避免方式如下:

  1. 合理设计键,热点大集合对象做拆分或使用hmget替代hgetall避免整体读取。
  2. 不要使用热键作为hash_tag,避免映射到同一槽。
  3. 对于一致性要求不高的场景,客户端可使用本地缓存减少热键调用。

集群读写分离

从节点开启只读连接

集群模式下从节点不接受任何读写请求,发送过来的键命令会重定向到 负责槽的主节点上(其中包括它的主节点)。当需要使用从节点分担主节点读压力时,可以使用readonly命令打开客户端连接只读状态。之前的复制配置slave-read-only在集群模式下无效。当开启只读状态时,从节点接收读命 令处理流程变为:如果对应的槽属于自己正在复制的主节点则直接执行读命 令,否则返回重定向信息。

客户端支持读写分离
需要客户端维护从节点的链接。

当使用从节点用于读写分离时会存在数据延迟、过期数据、从节点可用性等问题,需要根据自身业务提前作出规避。

客户端分片

Redis Sharding可以说是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。这样,客户端就知道该向哪个Redis节点操作数据。

jedis实现

  1. 采用一致性哈希算法
  2. 虚节点

代理方案

Twemproxy

codis

Redis集群管理工具CacheCloud