本文将对集群的节点、槽指派、命令执行、重新分片、转向、故障转移、消息等各个方面进行深入拆解。
目的在于掌握什么是 Cluster ?Cluster 分片原理,客户端定位数据原理、故障切换,选主,什么场景使用 Cluster,如何部署集群 …...
“65 哥:码哥,自从用上了你说的哨兵集群实现故障自动转移后,我终于可以开心的跟女朋友么么哒也不怕 Redis 宕机深夜宕机了。 可是最近遇到一个糟心的问题,Redis 需要保存 800 万个键值对,占用 20 GB 的内存。 我就使用了一台 32G 的内存主机部署,但是 Redis 响应有时候非常慢,使用 INFO 命令查看 latest_fork_usec 指标(最近一次 fork 耗时),发现特别高。 ”
“65 哥:随着业务规模的拓展,数据量越来越大。主从架构升级单个实例硬件难以拓展,且保存大数据量会导致响应慢问题,有什么办法可以解决么? ”
保存大量数据,除了使用大内存主机的方式,我们还可以使用切片集群。俗话说「众人拾材火焰高」,一台机器无法保存所有数据,那就多台分担。
使用 Redis Cluster 集群,主要解决了大数据量存储导致的各种慢问题,同时也便于横向拓展。
两种方案对应着 Redis 数据增多的两种拓展方案:垂直扩展(scale up)、水平扩展(scale out)。
垂直拓展:升级单个 Redis 的硬件配置,比如增加内存容量、磁盘容量、使用更强大的 CPU。
水平拓展:横向增加 Redis 实例个数,每个节点负责一部分数据。
水平拓展与垂直拓展
“65 哥:那这两种方案都有什么优缺点呢?
”
Redis 集群是一种分布式数据库方案,集群通过分片(sharding)来进行数据管理(「分治思想」的一种实践),并提供复制和故障转移功能。
将数据划分为 16384 的 slots,每个节点负责一部分槽位。槽位的信息存储于每个节点中。
它是去中心化的,如图所示,该集群有三个 Redis 节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。
Redis 集群架构
Gossip
协议相互交互集群信息,最后每个节点都保存着其他节点的 slots 分配情况。一个 Redis 集群通常由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。
CLUSTER MEET
命令完成:CLUSTER MEET <ip> <port>
。CLUSTER MEET
命令,可以让 node 节点与 ip 和 port 所指定的节点进行握手(handshake),当握手成功时,node 节点就会将 ip 和 port 所指定的节点添加到 node 节点当前所在的集群中。CLUSTER MEET
“65 哥:数据切片后,需要将数据分布在不同实例上,数据和实例之间如何对应上呢?
”
集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0 个或最多 16384 个槽。
Key 与哈希槽映射过程可以分为两大步骤:
根据键值对的 key,使用 CRC16 算法,计算出一个 16 bit 的值;
将 16 bit 的值对 16384 执行取模,得到 0 ~ 16383 的数表示 key 对应的哈希槽。
Cluster 还允许用户强制某个 key 挂在特定槽位上,通过在 key 字符串里面嵌入 tag 标记,这就可以强制 key 所挂在的槽位等于 tag 所在的槽位。
“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 实例之间的映射关系如下:
数据、Slot与实例的映射
“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 哥:我知道哨兵通过监控、自动切换主库、通知客户端实现故障自动切换,
”Cluster
又如何实现故障自动转移呢?
Gossip
协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这点失联信息。“65 哥:新的主节点如何选举产生的?
”
集群的配置纪元 +1,是一个自曾计数器,初始值 0 ,每次执行故障转移都会 +1。
检测到主节点下线的从节点向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,表示这个主节点支持从节点成为新的主节点。
参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。
如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
集群Leader选举
“65 哥,我来考考你:“Redis Cluster 方案通过哈希槽的方式把键值对分配到不同的实例上,这个过程需要对键值对的 key 做 CRC 计算并对 哈希槽总数取模映射到实例上。如果用一个表直接把键值对和实例的对应关系记录下来(例如键值对 1 在实例 2 上,键值对 2 在实例 1 上),这样就不用计算 key 和哈希槽的对应关系了,只用查表就行了,Redis 为什么不这么做呢?”
”
“65 哥:客户端又怎么确定访问的数据到底分布在哪个实例上呢?
”
Redis 客户端定位数据所在节点
“65 哥:哈希槽与实例之间的映射关系由于新增实例或者负载均衡重新分配导致改变了咋办?
”
“65 哥:Redis 如何告知客户端重定向访问新实例呢?
”
GET 公众号:码哥字节
(error) MOVED 16330 172.17.18.2:6379
MOVED 指令
“65 哥:如果某个 slot 的数据比较多,部分迁移到新实例,还有一部分没有迁移咋办?
”
GET 公众号:码哥字节
(error) ASK 16330 172.17.18.2:6379
ASK 错误
172.17.18.1
实例发送请求,只不过节点会响应 ASK 命令让客户端给新实例发送一次请求。MOVED
指令则更新客户端本地缓存,让后续指令都发往新实例。“65 哥:有了 Redis Cluster,再也不怕大数据量了,我可以无限水平拓展么?
”
“65 哥:到底是什么限制了集群规模呢?
”
Gossip
协议传播节点的数据,Gossip
协议工作原理大概如下:PING
消息发送给挑选出来的实例,用于检测实例状态以及交换彼此的信息。PING
消息中封装了发送者自身的状态信息、部分其他实例的状态信息、Slot 与实例映射表信息。PING
消息后,响应 PONG
消息,消息包含的信息跟 PING
消息一样。Gossip
协议可以在一段时间之后每个实例都能获取其他所有实例的状态信息。PING
,PONG
的消息传播完成集群状态在每个实例的传播同步。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字节
uint32_t notused1; //4字节
} clusterMsgDataGossip;
Gossip
消息,就需要发送 104 字节。如果集群是 1000 个实例,那么每个实例发送一个 PING
消息则会占用 大约 10KB。Bitmap
。PING
消息大约 12KB。PONG
与PING
消息一样,一发一回两个消息加起来就是 24 KB。集群规模的增加,心跳消息越来越多就会占据集群的网络通信带宽,降低了集群吞吐量。“65 哥:码哥,发送 PING 消息的频率也会影响集群带宽吧?
”
Redis Cluster 的实例启动后,默认会每秒从本地的实例列表中随机选出 5 个实例,再从这 5 个实例中找出一个最久没有收到 PING 消息的实例,把 PING 消息发送给该实例。
“65 哥:随机选择 5 个,但是无法保证选中的是整个集群最久没有收到 PING 通信的实例,有的实例可能一直没有收到消息,导致他们维护的集群信息早就过期了,咋办呢?
”
PONG
消息的时间 > cluster-node-timeout / 2
。那么就立刻给这个实例发送 PING
消息,更新这个节点的集群状态信息。PING
消息,降低这个频率可能会导致集群每个实例的状态信息无法及时传播。PONG
消息接收是否超过 cluster-node-timeout / 2
,这个是 Redis 实例默认的周期性检测任务频率,我们不会轻易修改。cluster-node-timeout
的值:集群中判断实例是否故障的心跳时间,默认 15 S。cluster-node-timeout
调成 20 秒或者 30 秒,这样 PONG
消息接收超时的情况就会缓解。cluster-node-timeout
时长才能检测出这个故障,影响集群正常服务。Gossip
协议传播集群实例信息,所以通信频率是限制集群大小的主要原因,主要可以通过修改 cluster-node-timeout
调整频率。