前言
redis高可用有3种方式:主从,哨兵,集群
集群模式通过分片来解决写热点和数据容量问题,同时支持主从复制功能,解决读热点问题,并提供故障转移功能,实现高可用
本文将介绍集群中槽位的表示,在集群中执行命令的流程,重新分配槽位,以及复制与故障转移的流程
槽位
集群的数据被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽
每个节点会为集群中所有节点维护clusterNode 结构,用户记录其分配的槽位
struct clusterNode {
// ...
unsigned char slots[16384/8];
int numslots;
// ...
};
clusterNode.slots能表示16384个比特位,如果数组中某下标对应的值为1,,表示该下标对应的槽位属于该节点
同时也会记录集群中,每个槽位被分配给了哪个节点:
typedef struct clusterState {
// ...
clusterNode *slots[16384];
// ...
} clusterState;
clusterState.slots数组的每个元素代表一个节点,表示每个下标对应的槽位,被分配给了哪个节点
这里用两个结构保存的原因是:
-
想知道槽i是否已经被指派,或者槽i被指派给了哪个节点,查clusterState.slots
- 若通过clusterNode查找,需要遍历所有的节点信息
-
每次要将节点A的槽指派信息传播给其他节点时,访问clusterNode结构即可
- 若通过clusterState查找,需要遍历16384个槽位
也就是说,当一种结构无法满足多种业务场景时,redis通过冗余数据的方式,为每种业务场景都维护高效的数据结构
在集群中执行命令
当集群中全部16384个槽位都被指定后,集群就会进入上线状态
某节点收到客户端的命令后,会检查要处理的键属于哪个槽:
- 如果槽就在当前节点,直接执行命令
-
如果槽没有指派给当前节点,那么会向客户端返回一个
MOVED错误,包含槽位和目标地址,指引客户端转向至正确的节点,并再次发送之前想要执行的命令
怎么计算某个键属于哪个槽位?
-
如果客户端没有指定。内部用如下公式确定:
CRC16(key) & 16383。先计算出key的CRC-16校验和,再&16383,计算出0到16383中间的一个数,就是槽位
重新分配槽位
当集群中有节点下线,或进行扩缩容时,就需要重新分配槽位
由Redis的集群管理软件redis-trib执行:
假设要迁移的槽位是slot
- 对目标节点发送命令,让目标节点准备好从源节点导入属于槽slot的键值对
- 对源节点发送命令,让源节点准备好将属于槽slot的键值对迁移至目标节点
- 向源节点发送命令,获得最多count个属于槽slot的键值对的键名
- 对于步骤3获得的每个键名,都向源节点发送命令,将被选中的键原子地从源节点迁移至目标节点
- 重复执行步骤3,4,直到属于槽slot的元素都迁移完为止
-
想集群中广播槽位指派信息
在进行重新分片期间,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里
面,而另一部分键值对则保存在目标节点里面
当源节点收到客户端的命令,且要处理的恰好就在被迁移的槽位里:
- 源节点现在本地查找,如果能找到,就执行该命令
-
如果没找到,则该键可能本来就没有,也可能已经迁移到目标节点了,因此向客户端返回一个
ASK错误,指引客户端转向正在导入槽的目标节点,再次发送之前想要执行的命令
接到ASK错误的客户端会根据错误提供的IP地址和端口号,转向至正在导入槽的目标节点,然后首先向目标节点发送一个ASKING命令,之后再重新发送原本想要执行的命令
ASKING命令唯一要做的就是打开发送该命令的客户端的REDIS_ASKING标识
为啥需要ASKING命令?
- 因为此时该槽位还没正式分配给目标节点,如果直接访问,该节点将向客户端返回一个MOVED错误
-
如果目标节点发现该槽位正在迁入,且该客户端有REDIS_ASKING标识,就破例执行一次命令
MOVED和ASK的区别
ASK和MOVED错误都会导致客户端转向,它们的区别在于:
- MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽
i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点
-
ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,不会对客户端今后发送关于槽
i的命令请求产生任何影响
复制与故障转移
Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求
故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线,并将该信息发给集群中其他节点
如果半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线,将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,这样所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线
故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态时,将开始对下线主节点进行故障转移:
- 从节点会向集群广播一条
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票
- 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条
CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点
- 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并统计票数,如果大于半数,这个从节点就会当选为新的主节点
- 在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于半数张支持票的从节点只会有一个
-
如果没有一个从节点收到足够多的票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止
注意从节点并不是一发现主节点进入已下线状态就进行选举,而是会延迟一定时间
为啥要延迟?
-
确保主节点已下线状态有时间在集群中传播,因为如果立刻发起选举,可能其他主节点还没收到该节点已下线信息,进而拒绝投票
延迟时间的计算公式为:500ms + random(0 ~ 500)ms + SLAVE_RANK * 1000ms
其中SLAVE_RANK代表从节点已经从主节点复制数据的总量,RANK越小,代表数据越新,这样理论上有最新数据的从节点会先发起选举,符合一般业务要求