Redis Cluster介绍
redis cluster是Redis的分布式解决方案,在3.0版本推出后有效地解决了redis分布式方面的需求,在3.0之前为了解决容量高可用用方面的需求基本上只能通过客户端分片+redis sentinel或者代理(twemproxy、codis)方案解决、redis cluster非常优雅地解决了redis集群方面的问题。
Redis Cluster目标
- 高性能
- 线性扩容
- 高可用
Redis Cluster安装
Redis Cluster提供的功能
- 数据自动分片
集群中每个节点都会负责一定数量的slot,每个key会映射到一个具体的slot,通过这种方式就可能找到key具体保存在哪个节点上了。 - 提供hash tags功能
通过hash tag功能可以将多个不同key映射到同一个slot上,这样就能够提供multi-key操作,hash tag的使用的方式是在key中包含“{}”,这样只有在“{...}”中字串被用于hash计算。 - 自动失效转移和手动失效转移
- 减少硬件成本和运维成本。
Redis Cluster原理
数据分布
Redis采用区中心的设计方案,通过虚拟16384个槽,将每个key映射到每一个具体的槽上,而每个redis节点可以负责管理一定数量的槽,假设有三个redis-cluster中有三个主节点,其槽可能分布如下图:
节点通信
- Gossip协议
- meet消息
用于通知新节点加入,消息发送这通知消息接收者加入集群,消息接收者回复pong消息,当新节点加入到集群后各节点就通过ping、pong消息进行信息交换。 - ping
集群中每个节点每秒向集群中多个节点发送ping消息,用于节点活性检测和状态交换。 - pong
pong作为meet消息和ping消息的响应消息,响应自身状态。也可以通过pong消息向集群中其他阶段广播自身的状态。 - fail
fail消息用于向集群中其他阶段广播某个节点已经下线。 - 消息格式
这些ping、pong等消息具体包括哪些信息呢?在cluster.h
头文件中可以看到消息主要是由消息头和消息体组成:
/* 消息*/
typedef struct {
char sig[4]; /* Siganture "RCmb" (Redis Cluster message bus). */
uint32_t totlen; /* Total length of this message */
uint16_t ver; /* Protocol version, currently set to 1. */
uint16_t port; /* TCP base port number. */
uint16_t type; /* Message type */
uint16_t count; /* Only used for some kind of messages. */
uint64_t currentEpoch; /* The epoch accordingly to the sending node. */
uint64_t configEpoch; /* The config epoch if it's a master, or the last
epoch advertised by its master if it is a
slave. */
uint64_t offset; /* Master replication offset if node is a master or
processed replication offset if node is a slave. */
char sender[CLUSTER_NAMELEN]; /* Name of the sender node */
unsigned char myslots[CLUSTER_SLOTS/8];
char slaveof[CLUSTER_NAMELEN];
char myip[NET_IP_STR_LEN]; /* Sender IP, if not all zeroed. */
char notused1[34]; /* 34 bytes reserved for future usage. */
uint16_t cport; /* Sender TCP cluster bus port */
uint16_t flags; /* Sender node flags */
unsigned char state; /* Cluster state from the POV of the sender */
unsigned char mflags[3]; /* Message flags: CLUSTERMSG_FLAG[012]_... */
union clusterMsgData data; /* message body*/
} clusterMsg;
/* message body difinition
*/
union clusterMsgData {
/* PING, MEET and PONG */
struct {
/* Array of N clusterMsgDataGossip structures */
clusterMsgDataGossip gossip[1];
} ping;
/* FAIL */
struct {
clusterMsgDataFail about;
} fail;
/* PUBLISH */
struct {
clusterMsgDataPublish msg;
} publish;
/* UPDATE */
struct {
clusterMsgDataUpdate nodecfg;
} update;
};
/* MEET, PING and PONG*/
typedef struct {
char nodename[CLUSTER_NAMELEN];
uint32_t ping_sent;
uint32_t pong_received;
char ip[NET_IP_STR_LEN]; /* IP address last time it was seen */
uint16_t port; /* base port last time it was seen */
uint16_t cport; /* cluster port last time it was seen */
uint16_t flags; /* node->flags copy */
uint32_t notused1;
} clusterMsgDataGossip;
/* fail message */
typedef struct {
char nodename[CLUSTER_NAMELEN];
} clusterMsgDataFail;
扩容&缩容
扩容
当集群出现容量限制或者其他一些原因需要扩容时,redis cluster提供了比较优雅的集群扩容方案。
- 首先将新节点加入到集群中,可以通过在集群中任何一个客户端执行cluster meet 新节点ip:端口,或者通过redis-trib add node添加,新添加的节点默认在集群中都是主节点。
- 迁移数据
迁移数据的大致流程是,首先需要确定哪些槽需要被迁移到目标节点,然后获取槽中key,将槽中的key全部迁移到目标节点,然后向集群所有主节点广播槽(数据)全部迁移到了目标节点。直接通过redis-trib工具做数据迁移很方便。 现在假设将节点A的槽10迁移到B节点,过程如下:
B:cluster setslot 10 importing A.nodeId
A:cluster setslot 10 migrating B.nodeId
循环获取槽中key,将key迁移到B节点
A:cluster getkeysinslot 10 100
A:migrate B.ip B.port "" 0 5000 keys key1[ key2....]
向集群广播槽已经迁移到B节点
cluster setslot 10 node B.nodeId
缩容
缩容的大致过程与扩容一致,需要判断下线的节点是否是主节点,以及主节点上是否有槽,若主节点上有槽,需要将槽迁移到集群中其他主节点,槽迁移完成之后,需要向其他节点广播该节点准备下线(cluster forget nodeId)。最后需要将该下线主节点的从节点指向其他主节点,当然最好是先将从节点下线。
故障转移
故障发现
节点之间通过gossip消息进行通信,当A节点发送PING消息给B节点,若没有收到B节点回复的PONG消息,持续cluster_node_timeout时长,则A节点会判定B节点已经下线了。待A判定B节点下线后,就会向集群广播B节点下线的消息。若集群中大部分节点都认为B节点下线后就会真正地下线B节点。
当某个节点判断另外一个节点下线后,相应的节点状态会跟随消息在集群内传播,当集群中半数以上的主节点都标记该节点下线时,就会出发下线B节点的操作。集群中每个节点在收到其他节点发送的pfail状态时,都会尝试触发下线的操作,只要当前节点是主节点且半数以上主节点判定某节点下线就会向集群中广播fail消息,立即下线问题节点,从而触发从节点的故障转移流程。
故障恢复
当问题节点下线后,如果该下线节点是带有槽的主节点,则需要从它的从节点选出一个替换它,当问题节点的从节点发现其主节点下线时,将会出发故障恢复流程。
确定参与选主的节点
并不是所有的从节点都能参与到故障恢复的流程中,若从节点与问题主节点的断线时间超过cluster_node_timeout * cluster-slave-validity-factor时,该从节点不能参与到后续恢复流程。
选主
- 准备选主
cluster内部通过一个延迟出发的机制,从节点中具有更大复制偏移量的从节点具有优先发起选主的权利。从节点维护着一个执行故障选主的时间,并且有定时任务检测选主时间,若达到故障选主时间后,则发起选主。 - 发起选主
1) 更新配置纪元(epoch)
配置纪元是一个只增不减的整数,每个主节点自身都维护一个配置纪元,所有主节点的配置纪元都不想等,从节点会复制主节点的配置纪元。
2)广播选主消息
在集群内广播选主消息(FAIL_AUTH_REQUEST),并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。 - 选主投票
只有持有槽的主节点才会处理故障选主消息,因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内的其他从节点的选主消息将忽略。
投票过程其实是一个领导者选举的过程,如集群内有N个持有槽的主节点代表有N张票,只要有N/2 + 1张票投给同一个从节点,则该从节点就将晋升为master。每个配置纪元代表了一次选主的周期,在开始投票后的cluster-node-timeout * 2的时间内没有获得足够数量的投票,则本次选举作废,需要发起下一次选主。 - 替换主节点
当从节点收到足够选票后,触发替换直接点的操作:
1)当前从节点取消复制变为主节点。
2)执行cluster delslot葱啊做撤销故障主节点负责的槽,并执行cluster addslot把这些槽委派给自己,
3)向集群广播PONG消息,通知集群内所有节点当前从节点变为主节点并接管故障主节点的槽信息。
请求重定向
由于redis-cluster采用去中心化的架构,集群的主节点各自负责一部分槽,客户端不确定key到底会映射到哪个节点上。redis-cluste通过重定向解决这个问题。在没有使用cluster模式时,redis对请求的处理很简单,若key存在于自身节点,则直接返回结果,若key不存在则告诉客户端key不存在,在使用cluster模式时,对请求的处理就变得复杂起来。在cluster模式下,节点对请求的处理过程如下:
- 检查当前key是否存在当前NODE?
1)通过crc16(key)/16384计算出slot
2)查询负责该slot负责的节点,得到节点指针
3)该指针与自身节点比较 - 若slot不是由自身负责,则返回MOVED重定向
- 若slot由自身负责,且key在slot中,则返回该key对应结果
- 若key不存在此slot中,检查该slot是否正在迁出(MIGRATING)?
- 若key正在迁出,返回ASK错误重定向客户端到迁移的目的服务器上
- 若Slot未迁出,检查Slot是否导入中?
- 若Slot导入中且有ASKING标记,则直接操作
- 否则返回MOVED重定向
MOVED重定向
每个客户端可以随便发起请求到集群中的任意一个节点,包括从节点。节点将会解析请求并计算该key对应slot是否由该节点负责,若不是由该节点负责的槽则返回一个MOVED错误。如下:
127.0.0.1:6379> get key1
(error) MOVED 9189 127.0.0.1:6382
其中MOVED错误信息会包含该key对应的slot以及slot所在节点的ip和端口。
客户端并不要求保存slot与节点的对应关系,但是为了高性能,客户端应该保存一下slot与节点的对应关系。发送请求时只需要先计算key对应的slot然后通过slot获取对应的节点,待客户端收到MOVED错误时更新一下slot与节点的对应关系。
ASK重定向
ASK重定向主要是解决slot迁移时,同一个槽信息存在两个节点上,但是槽中的key还没有全部迁移完成,避免客户端重新获取slot重新获取slot与节点的对应关系。
127.0.0.1:6379> get key1
(error) ASK 9189 127.0.0.1:6382
其格式与MOVED错误差不多,包括该key对应的slot,以及迁出目节点的ip和端口。
PUB&SUB
在集群模式下,所有的publish命令都会向所有节点(包括从节点)进行广播,造成每条publish数据都会在集群内所有节点传播一次,加重了带宽负担,对于在有大量节点的集群中频繁使用pub,会严重消耗带宽,不建议使用。
cluster的限制
- 对multi-key操作支持不够,虽然支持hash tag,但在迁移槽的过程中也会出现不可用。
- 只支持单层复制;
- 不支持节点自动发现,必须手动广播meet消息。
读写分离
- redis-cluster默认并不支持读写分离,默认所有从节点上的读写,都会重定向到key对接槽的主节点上;
- 可以通过readonly设置当前连接可读,但只对连接有效;
- 主从节点依然存在数据不一致的问题;
- 通过readwrite取消当前连接的可读状态;