技术不是万能的,程序员也不是最厉害的,一定要搞清楚,不要觉得「老子天下第一」。一旦有了这个意识,可能会耽误我们的成长。
技术是为了解决问题的,如果说一个技术不能解决问题,那这个技术就一文不值。
不要去炫技,没有意义。
集群安装
====
点击 -> 《Redis 6.X Cluster 集群搭建》查看
一个 Redis 集群通常由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。
连接各个节点的工作可以通过 CLUSTER MEET
命令完成:CLUSTER MEET
。
向一个节点 node 发送 CLUSTER MEET
命令,可以让 node 节点与 ip 和 port 所指定的节点进行握手(handshake),当握手成功时,node 节点就会将 ip 和 port 所指定的节点添加到 node 节点当前所在的集群中。
就好像 node 节点说:“喂,ip = xx,port = xx 的老哥,要不要加入「码哥字节」技术群,加入集群就找到了一条大神成长之路,关注「码哥字节」公众号回复「加群」,是兄弟就跟我一起来!”
关于 Redis Cluster 集群搭建详细步骤,请点击文末左下角「阅读原文」或者点击 -> 《Redis 6.X Cluster 集群搭建》查看,官方关于 Redis Cluster 的详情请看:https://redis.io/topics/cluster-tutorial。
Cluster 实现原理
============
65 哥:数据切片后,需要将数据分布在不同实例上,数据和实例之间如何对应上呢?
Redis 3.0 开始,官方提供了 Redis Cluster 方案实现了切片集群,该方案就实现了数据和实例的规则。Redis Cluster 方案采用哈希槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。
跟着「码哥字节」一起进入 Cluster 实现原理探索之旅……
将数据分成多份存在不同实例上
集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0 个或最多 16384 个槽。
Key 与哈希槽映射过程可以分为两大步骤:
根据键值对的 key,使用 CRC16 算法,计算出一个 16 bit 的值;
将 16 bit 的值对 16384 执行取模,得到 0 ~ 16383 的数表示 key 对应的哈希槽。
Cluster 还允许用户强制某个 key 挂在特定槽位上,通过在 key 字符串里面嵌入 tag 标记,这就可以强制 key 所挂在的槽位等于 tag 所在的槽位。
哈希槽与 Redis 实例映射
65 哥:哈希槽又是如何映射到 Redis 实例上呢?
在 部署集群的样例中通过 cluster create
创建,Redis 会自动将 16384 个 哈希槽平均分布在集群实例上,比如 N 个节点,每个节点上的哈希槽数 = 16384 / N 个。
除此之外,可以通过 CLUSTER MEET
命令将 7000、7001、7002 三个节点连在一个集群,但是集群目前依然处于下线状态,因为三个实例都没有处理任何哈希槽。
可以使用 cluster addslots
命令,指定每个实例上的哈希槽个数。
65 哥:为啥要手动制定呢?
能者多劳嘛,加入集群中的 Redis 实例配置不一样,如果承担一样的压力,对于垃圾机器来说就太难了,让牛逼的机器多支持一点。
三个实例的集群,通过下面的指令为每个实例分配哈希槽:实例 1
负责 0 ~ 5460 哈希槽,实例 2
负责 5461~10922 哈希槽,实例 3
负责 10923 ~ 16383 哈希槽。
redis-cli -h 172.16.19.1 –p 6379 cluster addslots 0,5460
redis-cli -h 172.16.19.2 –p 6379 cluster addslots 5461,10922
redis-cli -h 172.16.19.3 –p 6379 cluster addslots 10923,16383
键值对数据、哈希槽、Redis 实例之间的映射关系如下:
Redis 键值对的 key 「码哥字节」「牛逼」经过 CRC16 计算后再对哈希槽总个数 16394 取模,模数结果分别映射到实例 1 与实例 2 上。
切记,当 16384 个槽都分配完全,Redis 集群才能正常工作。
复制与故障转移
65 哥:Redis 集群如何实现高可用呢?Master 与 Slave 还是读写分离么?
Master 用于处理槽,Slave 节点则通过《Redis 主从架构数据同步》方式同步主节点数据。
当 Master 下线,Slave 代替主节点继续处理请求。主从节点之间并没有读写分离, Slave 只用作 Master 宕机的高可用备份。
Redis Cluster 可以为每个主节点设置若干个从节点,单主节点故障时,集群会自动将其中某个从节点提升为主节点。
如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。
不过 Redis 也提供了一个参数cluster-require-full-coverage
可以允许部分节点故障,其它节点还可以继续提供对外访问。
比如 7000 主节点宕机,作为 slave 的 7003 成为 Master 节点继续提供服务。当下线的节点 7000 重新上线,它将成为当前 70003 的从节点。
65 哥:在《Redis 高可用篇:Sentinel 哨兵集群原理》我知道哨兵通过监控、自动切换主库、通知客户端实现故障自动切换,
Cluster
又如何实现故障自动转移呢?
一个节点认为某个节点失联了并不代表所有的节点都认为它失联了。只有当大多数负责处理 slot 节点都认定了某个节点下线了,集群才认为该节点需要进行主从切换。
Redis 集群节点采用 Gossip
协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这点失联信息。
关于 Gossip
协议可阅读悟空哥的一篇文章:《病毒入侵,全靠分布式》
如果一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。
当一个 Slave 发现自己的主节点进入已下线状态后,从节点将开始对下线的主节点进行故障转移。
从下线的 Master 及节点的 Slave 节点列表选择一个节点成为新主节点。
新主节点会撤销所有对已下线主节点的 slot 指派,并将这些 slots 指派给自己。
新的主节点向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
新的主节点开始接收处理槽有关的命令请求,故障转移完成。
65 哥:新的主节点如何选举产生的?
集群的配置纪元 +1,是一个自曾计数器,初始值 0 ,每次执行故障转移都会 +1。
检测到主节点下线的从节点向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,表示这个主节点支持从节点成为新的主节点。
参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。
如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
跟哨兵类似,两者都是基于 Raft 算法来实现的,流程如图所示:
用表保存键值对和实例的关联关系可行么
65 哥,我来考考你:“Redis Cluster 方案通过哈希槽的方式把键值对分配到不同的实例上,这个过程需要对键值对的 key 做 CRC 计算并对 哈希槽总数取模映射到实例上。如果用一个表直接把键值对和实例的对应关系记录下来(例如键值对 1 在实例 2 上,键值对 2 在实例 1 上),这样就不用计算 key 和哈希槽的对应关系了,只用查表就行了,Redis 为什么不这么做呢?”
使用一个全局表记录的话,假如键值对和实例之间的关系改变(重新分片、实例增减),需要修改表。如果是单线程操作,所有操作都要串行,性能太慢。
多线程的话,就涉及到加锁,另外,如果键值对数据量非常大,保存键值对与实例关系的表数据所需要的存储空间也会很大。
而哈希槽计算,虽然也要记录哈希槽与实例时间的关系,但是哈希槽的数量少得多,只有 16384 个,开销很小。
客户端如何定位数据所在实
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
例
=============
65 哥:客户端又怎么确定访问的数据到底分布在哪个实例上呢?
Redis 实例会将自己的哈希槽信息通过 Gossip 协议发送给集群中其他的实例,实现了哈希槽分配信息的扩散。
这样,集群中的每个实例都有所有哈希槽与实例之间的映射关系信息。
在切片数据的时候是将 key 通过 CRC16 计算出一个值再对 16384 取模得到对应的 Slot,这个计算任务可以在客户端上执行发送请求的时候执行。
但是,定位到槽以后还需要进一步定位到该 Slot 所在 Redis 实例。
当客户端连接任何一个实例,实例就将哈希槽与实例的映射关系响应给客户端,客户端就会将哈希槽与实例映射信息缓存在本地。
当客户端请求时,会计算出键所对应的哈希槽,在通过本地缓存的哈希槽实例映射信息定位到数据所在实例上,再将请求发送给对应的实例。
重新分配哈希槽
65 哥:哈希槽与实例之间的映射关系由于新增实例或者负载均衡重新分配导致改变了咋办?
集群中的实例通过 Gossip 协议互相传递消息获取最新的哈希槽分配信息,但是,客户端无法感知。
Redis Cluster 提供了重定向机制:客户端将请求发送到实例上,这个实例没有相应的数据,该 Redis 实例会告诉客户端将请求发送到其他的实例上。
65 哥:Redis 如何告知客户端重定向访问新实例呢?
分为两种情况:MOVED 错误、ASK 错误。
MOVED 错误(负载均衡,数据已经迁移到其他实例上):当客户端将一个键值对操作请求发送给某个实例,而这个键所在的槽并非由自己负责的时候,该实例会返回一个 MOVED 错误指引转向正在负责该槽的节点。
GET 公众号:码哥字节
(error) MOVED 16330 172.17.18.2:6379
该响应表示客户端请求的键值对所在的哈希槽 16330 迁移到了 172.17.18.2 这个实例上,端口是 6379。这样客户端就与 172.17.18.2:6379 建立连接,并发送 GET 请求。
同时,客户端还会更新本地缓存,将该 slot 与 Redis 实例对应关系更新正确。
65 哥:如果某个 slot 的数据比较多,部分迁移到新实例,还有一部分没有迁移咋办?
如果请求的 key 在当前节点找到就直接执行命令,否则时候就需要 ASK 错误响应了,槽部分迁移未完成的情况下,如果需要访问的 key 所在 Slot 正在从从 实例 1 迁移到 实例 2,实例 1 会返回客户端一条 ASK 报错信息:客户端请求的 key 所在的哈希槽正在迁移到实例 2 上,你先给实例 2 发送一个 ASKING 命令,接着发发送操作命令。
GET 公众号:码哥字节
(error) ASK 16330 172.17.18.2:6379
比如客户端请求定位到 key = 「公众号:码哥字节」的槽 16330 在实例 172.17.18.1 上,节点 1 如果找得到就直接执行命令,否则响应 ASK 错误信息,并指引客户端转向正在迁移的目标节点 172.17.18.2。
注意:ASK 错误指令并不会更新客户端缓存的哈希槽分配信息。
所以客户端再次请求 Slot 16330 的数据,还是会先给 172.17.18.1
实例发送请求,只不过节点会响应 ASK 命令让客户端给新实例发送一次请求。
MOVED
指令则更新客户端本地缓存,让后续指令都发往新实例。
集群可以设置多大?
=========
65 哥:有了 Redis Cluster,再也不怕大数据量了,我可以无限水平拓展么?
答案是否定的,Redis 官方给的 Redis Cluster 的规模上线是 1000 个实例。
65 哥:到底是什么限制了集群规模呢?
关键在于实例间的通信开销,Cluster 集群中的每个实例都保存所有哈希槽与实例对应关系信息(Slot 映射到节点的表),以及自身的状态信息。
在集群之间每个实例通过 Gossip
协议传播节点的数据,Gossip
协议工作原理大概如下:
从集群中随机选择一些实例按照一定的频率发送 PING
消息发送给挑选出来的实例,用于检测实例状态以及交换彼此的信息。 PING
消息中封装了发送者自身的状态信息、部分其他实例的状态信息、Slot 与实例映射表信息。
实例接收到 PING
消息后,响应 PONG
消息,消息包含的信息跟 PING
消息一样。
集群之间通过 Gossip
协议可以在一段时间之后每个实例都能获取其他所有实例的状态信息。
所以在有新节点加入,节点故障,Slot 映射变更都可以通过 PING
,PONG
的消息传播完成集群状态在每个实例的传播同步。
Gossip 消息
发送的消息结构是 clusterMsgDataGossip
结构体组成:
typedef struct {
char nodename[CLUSTER_NAMELEN]; //40字节
uint32_t ping_sent; //4字节
uint32_t pong_received; //4字节
char ip[NET_IP_STR_LEN]; //46字节
uint16_t port; //2字节
uint16_t cport; //2字节
uint16_t flags; //2字节