redis高可用(cluster集群分片篇)
我们知道主从复制和哨兵机制保障了高可用,就读写分离而言虽然slave节点扩展了主从的读并发能力,但是写能力和存储能力是无法进行扩展,就只能是master节点能够承载的上限。如果面对海量数据,那么必然需要构建master(主节点分片)之间的集群,同时必然需要吸收高可用(主从复制和哨兵机制)能力,即每个master分片节点还需要有slave节点,这是分布式系统中典型的纵向扩展(集群的分片技术)的体现,
一、什么是分片技术(Cluster)?
我们知道主从复制和哨兵机制保障了高可用,就读写分离而言虽然slave节点扩展了主从的读并发能力,但是写能力和存储能力是无法进行扩展,就只能是master节点能够承载的上限。如果面对海量数据,那么必然需要构建master(主节点分片)之间的集群,同时必然需要吸收高可用(主从复制和哨兵机制)能力,即每个master分片节点还需要有slave节点,这是分布式系统中典型的横向扩展(集群的分片技术)的体现,所以在Redis 3.0版本中对应的设计就是Redis Cluster。
其实出了构建master集群,我们也可以升级单个 Redis 的硬件配置,比如增加内存容量、磁盘容量,这属于纵向扩展。纵向扩展受限于硬件和成本,而横向扩展需要解决分布式管理的问题。
二、节点(clusterNode)
一个Redis集群通常由多个节点(node)组成,在刚开始的时候, 每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。
一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器 在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。
clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点当前的配置纪元、节点的IP地址和端口号等等。 每个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的 clusterNode结构,以此来记录其他节点的状态。
struct clusterNode {
//创建节点的时间
mstime_t ctime;
//节点的名字,由40个十六进制字符组成
例如68eef66df23420a5862208ef5b1a7005b806f2ff
char name[REDIS_CLUSTER_NAMELEN];
//节点标识
//使用各种不同的标识值记录节点的角色(比如主节点或者从节点)
//以及节点目前所处的状态(比如在线或者下线)。
int flags;
//节点当前的配置纪元,用于实现故障转移
uint64_t configEpoch;
//节点的IP地址
char ip[REDIS_IP_STR_LEN];
//节点的端口号
int port;
//保存连接节点所需的有关信息
clusterLink *link;
...
};
每个节点都保存着一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是在线还是下线, 集群包含多少个节点等等。
typedef struct clusterState {
//指向当前节点的指针
clusterNode *myself;
//集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
//集群当前的状态:是在线还是下线
int state;
//集群中至少处理着一个槽的节点的数量
int size;
//集群节点名单(包括myself节点)
//字典的键为节点的名字,字典的值为节点对应的clusterNode结构
dict *nodes;
// ...
} clusterState;
三、哈希槽指派
Redis-cluster没有使用一致性hash,而是引入了哈希槽的概念,每个key通过CRC16(key)&16383
来决定放置哪个槽。Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384(2的14次方)个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点负责一部分槽位,可以处理0个或最多16384个槽。
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群 处于下线状态(fail)。
eg.执行以下命令可以将槽0至槽5000指派给端口为7000的节点负责
clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽:
struct clusterNode {
// ...
unsigned char slots[16384/8];
int numslots;
// ...
};
slots属性是一个二进制位数组(bit array),这个数组的长度为 16384/8=2048个字节,共包含16384个二进制位。 Redis以0为起始索引,16383为终止索引,对slots数组中的16384个 二进制位进行编号,并根据索引i上的二进制位的值来判断节点是否负责处理槽i:
- 如果slots数组在索引i上的二进制位的值为1,那么表示节点负责处理槽i。
- 如果slots数组在索引i上的二进制位的值为0,那么表示节点不负责处理槽i。
一个节点会将自己的slots数组通过消息发送给集群中的其他节点,以此告知其他节点自己目前负责处理哪些槽。
clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息:
typedef struct clusterState {
// ...
clusterNode *slots[16384];
// ...
} clusterState;
slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针:
- 如果slots[i]指针指向一个clusterNode结构,那么表示槽i已经指派 给了clusterNode结构所代表的节点。
简单说一下为什么采用16384个槽?
CRC16 算法,产生的hash值有 16 bit 位,可以产生 65536(2^16)个值 ,也就是说值分布在 0 ~ 65535 之间,但为什么槽数确实16384呢?
正常的心跳数据包携带节点的完整配置,它能以幂等方式来更新配置。如果采用 16384 个插槽,占空间 2KB (16384/8);如果采用 65536 个插槽,占空间 8KB (65536/8)。Redis Cluster 不太可能扩展到超过 1000 个主节点,太多可能导致网络拥堵。16384 个插槽范围比较合适,当集群扩展到1000个节点时,也能确保每个master节点有足够的插槽,8KB 的心跳包看似不大,但是这个是心跳包每秒都要将本节点的信息同步给集群其他节点。比起 16384 个插槽,头大小增加了4倍,ping消息的消息头太大了,浪费带宽。
四、Cluster总线
每个Redis Cluster节点有一个额外的TCP端口用来接受其他节点的连接。这个端口与用来接收client命令的普通TCP端口有一个固定的offset。该端口等于普通命令端口加上10000.例如,一个Redis服务器在端口6379接受客户端连接,那么它的集群总线端口16379也会被打开。
五、请求重定向
Redis cluster采用去中心化的架构,集群的主节点各自负责一部分槽,客户端如何确定key到底会映射到哪个节点上呢?这就要用到请求重定向。
在cluster模式下,节点对请求的处理过程如下:
1.检查当前key是否存在当前NODE?
- 通过crc16(key)&16384计算出slot
- 查询负责该slot负责的节点,得到节点指针
- 该指针与自身节点比较
2.若slot不是由自身负责,则返回MOVED重定向;
3.若slot由自身负责,且key在slot中,则返回该key对应结果;
4.若key不存在此slot中,检查该slot是否正在迁出(MIGRATING)?
5.若key正在迁出,返回ASK错误重定向客户端到迁移的目的服务器上;
6.若Slot未迁出,检查Slot是否导入中?
7.若Slot导入中且有ASKING标记,则直接操作;
否则返回MOVED重定向。
解释一下什么是MOVED重定向和ASK重定向:
redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),所以 相关槽所属的键值对也会从源节点被动迁移到目标节点。
Moved 重定向
- 槽命中:直接返回结果
- 槽不命中:即当前键命令所请求的键不在当前请求的节点中,则当前节点会向客户端发送一个Moved 重定向,客户端根据Moved 重定向所包含的内容找到目标节点,再一次发送命令。
ASK重定向
Ask重定向发生于集群伸缩时,集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用Ask重定向来解决此种情况。
smart客户端
上述两种重定向的机制使得客户端的实现更加复杂,提供了smart客户端(JedisCluster)来减低复杂性,追求更好的性能。客户端内部负责计算/维护键-> 槽 -> 节点映射,用于快速定位目标节点。
实现原理:
- 从集群中选取一个可运行节点,使用 cluster slots得到槽和节点的映射关系
- 将上述映射关系存到本地,通过映射关系就可以直接对目标节点进行操作(CRC16(key) -> slot -> node),很好地避免了Moved重定向,并为每个节点创建JedisPool。
六、状态维护
1.Gossip协议
Redis 集群是去中心化的,彼此之间状态同步靠 gossip 协议通信,集群的消息有以下几种类型:
Meet
通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。Ping
节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。Pong
节点收到 ping 消息后会回复 pong 消息,消息中同样带有自己已知的两个节点信息。Fail
节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。
2.心跳的实现和维护
什么时候发送心跳?
Redis节点会记录其向每一个节点上一次发出ping和收到pong的时间,心跳发送时机与这两个值有关。通过下面的方式既能保证及时更新集群状态,又不至于使心跳数过多:
-
每次Cron向所有未建立链接的节点发送ping或meet
-
每1秒从所有已知节点中随机选取5个,向其中上次收到pong最久远的一个发送ping
-
每次Cron向收到pong超过timeout/2的节点发送ping
-
收到ping或meet,立即回复pong
发送哪些心跳数据?
-
Header,发送者自己的信息
(1)所负责slots的信息
(2)主从信息
(3)ip port信息
(4)状态信息
-
Gossip,发送者所了解的部分其他节点的信息
(1)ping_sent, pong_received
(2)ip,port信息
(3)状态信息,比如发送者认为该节点已经不可达,会在状态信息中标记其为PFAIL或FAIL
Gossip的存在使得集群状态的改变可以更快的达到整个集群。每个心跳包中会包含多个Gossip包,那么多少个才是合适的呢,redis的选择是N/10,其中N是节点数,这样可以保证在PFAIL投票的过期时间内,节点可以收到80%机器关于失败节点的gossip,从而使其顺利进入FAIL状态。
七、扩容和缩容
扩容
1.首先将新节点加入到集群中,可以通过在集群中任何一个客户端执行cluster meet 新节点ip:端口,或者通过redis-trib add node添加,新添加的节点默认在集群中都是主节点。
2.迁移数据 迁移数据的大致流程是,首先需要确定哪些槽需要被迁移到目标节点,然后获取槽中key,将槽中的key全部迁移到目标节点,然后向集群所有主节点广播槽(数据)全部迁移到了目标节点。
缩容
缩容的大致过程与扩容一致,需要判断下线的节点是否是主节点,以及主节点上是否有槽,若主节点上有槽,需要将槽迁移到集群中其他主节点,槽迁移完成之后,需要向其他节点广播该节点准备下线。最后需要将该下线主节点的从节点指向其他主节点,当然最好是先将从节点下线。
更多推荐
所有评论(0)