为什么需要 Cluster
当数据量过大时,redis 响应会特别慢。
主要是 Redis RDB 持久化机制导致的,Redis 会 Fork 子进程完成 RDB 持久化操作,fork 执行的耗时与 Redis 数据量成正相关(因为子进程会复制一份父进程的页表,数据量越多,页表越大)。
而 Fork 执行的时候会阻塞主线程,由于数据量过大导致阻塞主线程过长,所以出现了 Redis 响应慢的表象。
除了使用大内存主机,还可以用 Redis Cluster 集群,集群主要解决了大数据量存储导致的各种慢问题,同时也便于横向拓展。
- 垂直拓展:升级单个 Redis 的硬件配置,比如增加内存容量、磁盘容量、使用更强大的 CPU。
- 部署简单,但是当数据量大并且使用 RDB 实现持久化,会造成阻塞导致响应慢。另外受限于硬件和成本,拓展内存的成本太大,比如拓展到 1T 内存。
- 水平拓展:横向增加 Redis 实例个数,每个节点负责一部分数据。
- 便于拓展,同时不需要担心单个实例的硬件和成本的限制。但是,切片集群会涉及多个实例的分布式管理问题,需要解决如何将数据合理分布到不同实例,同时还要让客户端能正确访问到实例上的数据。
什么是 Cluster 集群
Redis 集群是一种分布式数据库方案,集群通过分片(sharding)来进行数据管理(「分治思想」的一种实践),并提供复制和故障转移功能。
将数据划分为 16384 的 slots,每个节点负责一部分槽位。槽位的信息存储于每个节点中。
它是去中心化的,如图所示,该集群有三个 Redis 节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。
三个节点相互连接组成一个对等的集群,它们之间通过 Gossip
协议相互交互集群信息,最后每个节点都保存着其他节点的 slots 分配情况。
集群搭建
关于 Redis Cluster 集群搭建详细步骤,请点击 -> 《Redis 6.X Cluster 集群搭建》。
连接各个节点的工作可以通过 CLUSTER MEET
命令完成:CLUSTER MEET <ip> <port>
。(gossip 通信协议的命令)
具体的做法是其中一个node向另外一个 node(指定 ip 和 port) 发送 CLUSTER MEET 命令,这样就可以让两个节点进行握手(handshake操作) ,握手成功之后,node 节点就会将握手另一侧的节点添加到当前节点所在的集群中。
这样一步步的将需要聚集的节点都圈入同一个集群中,如下图:
Cluster 实现原理
Redis 3.0 开始,官方提供了 Redis Cluster 方案实现了切片集群,该方案就实现了数据和实例的规则。Redis Cluster 方案采用哈希槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。
哈希槽与 Redis 实例映射
集群的整个数据库被分为 16384 个槽(slot),集群中的每个节点可以处理 0 个或最多 16384 个槽。关于为什么 Redis 集群的最大槽数见 《为什么Redis集群的最大槽数是16384个?》
通过 cluster create
创建,Redis 会自动将 16384 个 哈希槽平均分布在集群实例上,比如 N 个节点,每个节点上的哈希槽数 = 16384 / N 个。
除此之外,可以使用 cluster addslots
命令,指定每个实例上的哈希槽个数。能者多劳,加入集群中的 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
当 16384 个槽都分配完全,Redis 集群才能正常工作。
数据 与 哈希槽 的映射
Key 与哈希槽映射过程可以分为两大步骤:
-
根据键值对的 key,使用 CRC16 算法,计算出一个 16 bit 的值;
-
将 16 bit 的值对 16384 执行取模,得到 0 ~ 16383 的数表示 key 对应的哈希槽。
Cluster 还允许用户强制某个 key 挂在特定槽位上,通过在 key 字符串里面嵌入 tag 标记,这就可以强制 key 所挂在的槽位等于 tag 所在的槽位。
键值对数据、哈希槽、Redis 实例之间的映射关系如下:
Cluster 节点间的内部通信方式
有集群,集群往往需要维护一定的元数据,比如实例的ip地址,缓存分片的 slots 信息等,所以需要一套分布式机制来维护元数据的一致性。这类机制一般有两个模式:分散式和集中式:集中式、Gossip 协议。浅析redis cluster介绍与gossip协议
- 分散式机制 将元数据存储在部分或者所有节点上,不同节点之间进行不断的通信来维护元数据的变更和一致性。Redis Cluster,Consul 等都是该模式。
- 集中式机制 是将集群元数据集中存储在外部节点或者中间件上,比如 zookeeper。旧版本的 kafka 和 storm 等都是使用该模式。
模式 | 优点 | 缺点 |
---|---|---|
集中式 | 数据更新及时,时效好,元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的外部节点中,其他节点读取的时候立即就可以感知到; | 较大数据更新压力,更新压力全部集中在外部节点,作为单点影响整个系统 |
分散式 | 数据更新压力分散,元数据的更新比较分散,不是集中某一个节点,更新请求比较分散,而且有不同节点处理,有一定的延时,降低了并发压力 | 数据更新延迟,可能导致集群的感知有一定的滞后 |
分散式的元数据模式有多种可选的算法进行元数据的同步,比如说 Paxos、Raft 和 Gossip。Paxos 和 Raft 等都需要全部节点或者大多数节点(超过一半)正常运行,整个集群才能稳定运行,而 Gossip 则不需要半数以上的节点运行。
Gossip 协议,顾名思义,就像流言蜚语一样,利用一种随机、带有传染性的方式,将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。对你来说,掌握这个协议不仅能很好地理解这种最常用的,实现最终一致性的算法,也能在后续工作中得心应手地实现数据的最终一致性。
Redis Cluster 中的每个节点都维护一份自己视角下的当前整个集群的状态,主要包括:
- 当前集群状态
- 集群中各节点所负责的 slots信息,及其migrate状态
- 集群中各节点的master-slave状态
- 集群中各节点的存活状态及怀疑Fail状态
也就是说上面的信息,就是集群中Node相互八卦传播流言蜚语的内容主题,而且比较全面,既有自己的更有别人的,这么一来大家都相互传,最终信息就全面而且一致了。
Redis Cluster 的节点之间会相互发送多种消息,较为重要的如下所示:
- MEET:通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群,然后新节点就会开始与其他节点进行通信;
- PING:节点按照配置的时间间隔向集群中其他节点发送 ping 消息,消息中带有 (1)自己的状态,还有 (2)自己维护的集群元数据:slots到节点映射,和 (3)部分其他节点的元数据;
- PONG: 节点用于回应 PING 和 MEET 的消息,结构和 PING 消息类似,也包含自己的状态和其他信息,也可以用于信息广播和更新;
- FAIL: 节点 PING 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。
每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其他节点接收到ping之后返回pong。
通过上述这些消息,集群中的每一个实例都能获得其它所有实例的状态信息。这样一来,即使有新节点加入、节点故障、Slot 变更等事件发生,实例间也可以通过 PING、PONG 消息的传递,完成集群状态在每个实例上的同步。
节点间的通信频率
Redis 官方给的 Redis Cluster 的规模上限是 1000 个实例。
到底是什么限制了集群规模呢?
关键就在于实例间的通信开销
redis集群内节点,每秒都在发ping消息。规律如下
- 每秒会随机选取5个节点,找出最久没收到 ping 消息的节点发送ping消息
- 每100毫秒(1秒10次)都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster-node-timeout/2 则立刻发送ping消息
当集群规模变大,就会进一步导致实例间网络通信延迟怎加。可能会引起更多的 PING 消息频繁发送。
Cluster 复制与故障转移
Master 用于处理数据槽,Slave 节点则通过《https://www.cnblogs.com/suBlog/p/17450108.html》方式同步主节点数据。
当 Master 下线,Slave 代替主节点继续处理请求。主从节点之间并没有读写分离, Slave 只用作 Master 宕机的高可用备份。
如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。
在《Redis 高可用 —— 主从复制》中知道哨兵集群通过监控、自动切换主库、通知客户端实现故障自动切换。
而 Redis集群不需要 sentinel 哨兵也能自动完成 节点移除和故障转移的功能。
Redis Cluster 可以为每个主节点设置若干个从节点,单主节点故障时,Redis 集群(不靠哨兵)会自动将其中某个从节点提升为主节点。
故障检测
一个节点认为某个节点失联了并不代表所有的节点都认为它失联了。只有当大多数负责处理 slot 节点都认定了某个节点下线了,集群才认为该节点需要进行主从切换。
Redis 集群节点采用 Gossip
协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这点失联信息。
如果一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。
选主流程
- 集群中设立一个自增计数器,初始值为 0 ,每次执行故障转移选举,计数就会+1。
- 检测到主节点下线的从节点向集群所有master广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,所有收到消息、并具备投票权的主节点都向这个从节点投票。
- 如果收到消息、并具备投票权的主节点未投票给其他从节点(Paxos 理论,见 https://www.cnblogs.com/suBlog/p/17554677.html),则返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示支持。
- 参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,如果收集到的选票 大于等于 (n/2) + 1 支持,n代表所有具备选举权的master,那么这个从节点就被选举为新主节点。
- 如果这一轮从节点都没能争取到足够多的票数,则发起再一轮选举(自增计数器+1),直至选出新的master。
跟哨兵类似,两者都是基于 Raft 算法 来实现的,流程如图所示:
故障转移
从下线的 Master 的 Slave 节点列表选择一个节点 成为新主节点后,这个新主节点将做以下三件事情:
- 新主节点会撤销所有对已下线主节点的 slot 指派,并将这些 slots 指派给自己。
-
新的主节点向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
-
新的主节点开始接收处理槽有关的命令请求,故障转移完成。
客户端如何定位数据所在Redis实例
Redis 实例会将自己的哈希槽信息通过 Gossip 协议发送给集群中其他的实例,实现了哈希槽分配信息的扩散。这样,集群中的每个实例都有所有哈希槽与实例之间的映射关系信息。
- 当客户端连接任何一个实例,实例就将哈希槽与实例的映射关系响应给客户端,客户端就会将哈希槽与实例映射信息缓存在本地。
- 当客户端请求时,会计算出键所对应的哈希槽(将 key 通过 CRC16 计算出一个值再对 16384 取模得到哈希槽)
- 通过本地缓存的哈希槽实例映射信息定位到数据所在实例上,再将请求发送给对应的实例。
重新分配哈希槽(如扩容时)
集群中的实例通过 Gossip 协议互相传递消息获取最新的哈希槽分配信息,但是,客户端无法感知。
Redis Cluster 提供了重定向机制:客户端将请求发送到实例上,这个实例没有相应的数据,该 Redis 实例会告诉客户端将请求发送到其他的实例上。
Redis 如何告知客户端重定向访问新实例呢?分为以下两种情况:
MOVED 错误
(数据已经迁移到其他实例上)当客户端将一个键值对操作请求发送给某个实例,而这个键所在的槽并非由自己负责的时候,该实例会返回一个 MOVED 错误指引转向正在负责该槽的节点。
GET namespace:key (error) MOVED 16330 172.17.18.2:6379
该响应表示客户端请求的键值对所在的哈希槽 16330 迁移到了 172.17.18.2 这个实例上,端口是 6379。这样客户端就与 172.17.18.2:6379 建立连接,并发送 GET 请求。
同时,客户端还会更新本地缓存(以后都往新的实例上发),将该 slot 与 Redis 实例对应关系更新正确。
ASK 错误
(如果需要访问的 key 所在 Slot 正在从从 实例 1 迁移到 实例 2)
GET namespace:key (error) ASK 16330 172.17.18.2:6379
响应 ASK 错误信息,并指引客户端转向正在迁移的目标节点 172.17.18.2。
注意:ASK 错误指令并不会更新客户端缓存的哈希槽分配信息。
所以客户端再次请求 Slot 16330 的数据,还是会先给 172.17.18.1
实例发送请求,只不过节点会响应 ASK 命令让客户端给新实例发送一次请求。
参考: