目录
4.1.5 典型的Custer模式的Redis集群配置与启动方式
0.引用及学习链接
本文与【架构师】课程中的第3.4节内容重合.
微信读书《Redis 5 设计与源码分析》哨兵与集群相关内容
1.Redis集群分类
2. 主从模式
2.1 简单介绍一下主从模式
一个大哥带着一个或多个小弟,小弟提供读服务,小弟在大哥死掉之后不可以自动补缺位,在主从模式下副本是不主动的小弟.
2.2 搭建方式
这里用的6.0.9的路径演示:
路径:
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave/redis01
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave/redis02
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave/redis03
可执行文件:
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave/redis01/redis-server
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave/redis02/redis-server
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave/redis03/redis-server
配置文件:
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave/redis01/redis.conf
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave/redis02/redis.conf
/home/muten/module/redis-6.0.9/src/redis-cluster/master-slave/redis03/redis.conf
2.3 测试与观察
2.3.1 测试主从的读写情况
测试用到的命令
redis-cli -h 127.0.0.1 -p 8001
redis-cli -h 127.0.0.1 -p 8002
redis-cli -h 127.0.0.1 -p 8003
KEYS key
HKEYS key
quit
2.3.2 查看redis启动情况
netstat -naop | grep redis
2.4 对主从模式的一些说明
主从模式的作用:
(1)备份数据,这样当一个节点损坏(指不可恢复的硬件损坏)时,数据因为有备份,可以方便恢复;
(2)负载均衡,所有客户端都访问一个节点肯定会影响 Redis 工作效率,有了主从以后,查询操作就可以通过查询从节点来完成.
对主从模式必须的理解(结论已经验证过,可以自行验证):
(1)一个Master可以有多个Slaves;
(2)默认配置下,master节点可以进行读和写,slave节点只能进行读操作,写操作被禁止;
注意:不要修改配置让slave节点支持写操作,没有意义,
原因一,写入的数据不会被同步到其他节点;
原因二,当master节点修改同一条数据后,slave节点的数据会被覆盖掉slave节点挂了不影响其他slave节点的
读和master节点的读和写,重新启动后会将数据从master节点同步过来;
(3)master节点挂了以后,不影响slave节点的读,Redis将不再提供写服务,master节点启动后Redis将重新对外
提供写服务.
(4)master节点挂了以后,不会从slave节点中重新选一个master.
关于密码:
客户端访问master需要密码;
启动slave需要密码,在配置中进行配置即可;
客户端访问slave不需要密码.
2.5 主从节点的缺点
master节点挂了以后,redis就不能对外提供写服务了,因为剩下的slave不能成为master
这个缺点影响是很大的,尤其是对生产环境来说,是一刻都不能停止服务的,所以一般
的生产坏境是不会单单只有主从模式的.所以有了下面的sentinel模式.
3. 哨兵模式
3.0 阅读内容
3.1 介绍一下哨兵模式
哨兵是Redis的高可用方案,可以在Redis Master发生故障时自动选择一个RedisSlave切换为Master,继续
对外提供服务.
本小节图中有一个Redis Master,该Master下有两个Slave。3个哨兵同时与Master和Slave建立连接,并且哨
兵之间也互相建立了连接。哨兵通过与Master和Slave的通信,能够清楚每个Redis服务的健康状态。这样,当
Master发生故障时,哨兵能够知晓Master的此种情况,然后通过对Slave健康状态、优先级、同步数据状态等的
综合判断,选取其中一个Slave切换为Master,并且修改其他Slave指向新的Master地址。
思考:
通过上文的描述,似乎只需要一个哨兵即可完成该操作。为什么实际中至少会部署3个以上哨兵并且哨兵数量最好是奇数呢?
我们通过思考哨兵的作用来回答这个问题。哨兵是Redis的高可用机制,保证了Redis服务不出现单点故障。如果
哨兵只部署一个,哨兵本身就成为了一个单点。那假如部署2个哨兵呢?当Redis的Master发生故障时,如果2个
哨兵同时执行切换操作肯定不行,哨兵之间必须先约定好由谁来执行此次切换操作,此时就涉及了哨兵之间选
leader的操作。假设2个哨兵各自投自己一票,根本选举不出leader。所以哨兵个数最好是奇数。
3.2 单个哨兵
哨兵启动之后会先与配置文件中监控的Master建立两条连接,一条称为命令连接,另一条称为消息连接。哨兵就
是通过如上两条连接发现其他哨兵和Redis Slave服务器,并且与每个Redis Slave也建立同样的两条连接。
注意:
哨兵的配置文件必须具有可写权限。因为哨兵的初始配置文件如上文所述,只配置了需要监听的Redis Master和
其他一些配置参数,当哨兵发现了其他的RedisSlave服务器和监听同一个Master的其他哨兵时,会将该信息记
录到配置文件中做持久化存储。这样,当哨兵重启后,可以直接从退出状态继续执行。
3.3 多个哨兵
多个哨兵组成哨兵集群是为了保证哨兵的高可用,在配置的时候要注意都要将将每一个哨兵的配置文件都配置所监视的集群的主机的IP和Port,
哨兵们自己会服务发现 ,通过redis定时任务发现其他slave节点,并且与它们建立连接. 示意图如3.1节中所示.
3.4 一份典型的哨兵配置文件
/监控一个名称为mymaster的Redis Master服务,地址和端口号为127.0.0.1:6379, quorum为2
sentinel monitor mymaster 127.0.0.16379 2
//如果哨兵60s内未收到mymaster的有效ping回复,则认为mymaster处于dow n的状态
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000//执行切换的超时
时间为180s
//切换完成后同时向新的Redis Master发起同步数据请求的Redis Slave个 数为1,即切换完成后依次让
每个Slave去同步数据,前一个Slave同步完成后下一个Slave才发起同步 数据的请求
sentinel parallel-syncs mymaster 1
//监控一个名称为resque的Redis Master服务,地址和端口号为127.0.0.1:6380,quorum为4
sentinel monitor resque 192.168.1.36380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5
quorum在哨兵中有两层含义:
第一层含义为:如果某个哨兵认为其监听的Master处于下线的状态,这个状态在Redis中标记为S_DOWN,即
主观下线。假设quorum配置为2,则当有两个哨兵同时认为一个Master处于下线的状态时,会标记该Master
为O_DOWN,即客观下线。只有一个Master处于客观下线状态时才会开始执行切换。
第二层含义为:假设有5个哨兵,quorum配置为4。首先,判断客观下线需要4个哨兵才能认定。其次,当开始
执行切换时,会从5个哨兵中选择一个leader执行该次选举,此时一个哨兵也必须得到4票才能被选举为
leader,而不是3票(即哨兵的大多数)。
3.5 简易搭建方式(用了哨兵集群)
cd /home/muten/module/redis-6.0.9/src/redis-cluster/
cp -rp master-slave sentinel
cd /home/muten/module/redis-6.0.9/src/
cp -p redis-sentinel redis-cluster/sentinel/redis01/
cp -p redis-sentinel redis-cluster/sentinel/redis02/
cp -p redis-sentinel redis-cluster/sentinel/redis03/
采用多个哨兵的集群确保高可用:
cd /home/muten/module/redis-6.0.9/
cp -p sentinel.conf src/redis-cluster/sentinel/redis01/sentinel.conf
cp -p sentinel.conf src/redis-cluster/sentinel/redis02/sentinel.conf
cp -p sentinel.conf src/redis-cluster/sentinel/redis03/sentinel.conf
更改sentinel.conf,
(1)第一个节点的sentinel.conf,
将port改成19001(可以防止端口冲突即可)
将sentinel monitor mymaster 处的IP和端口改成要监控的那个节点的IP和端口,我的机器上是改成:
sentinel monitor mymaster 192.168.118.138 9001 2
(2)第二个节点的sentinel.conf,
将port改成19002(可以防止端口冲突即可)
将sentinel monitor mymaster 处的IP和端口改成要监控的那个节点的IP和端口,我的机器上是改成:
sentinel monitor mymaster 192.168.118.138 9001 2
(3)第三个节点的sentinel.conf,
将port改成19003(可以防止端口冲突即可)
将sentinel monitor mymaster 处的IP和端口改成要监控的那个节点的IP和端口,我的机器上是改成:
sentinel monitor mymaster 192.168.118.138 9001 2
要注意,哨兵都要监控主节点!
3.6 sentinel相关代码分析及一些逻辑说明
3.6.1 与哨兵模式有关的代码
【Redis-6.0.8】
// 主流程中对sentinel做的工作只是进行初始化
int main(int argc, char **argv) {
if (server.sentinel_mode) {
role_char = 'X'; /* Sentinel. */
}
...
// 检测是否以sentinel模式启动
server.sentinel_mode = checkForSentinelMode(argc,argv); // line 5141
....
if (server.sentinel_mode) {
initSentinelConfig();//初始化哨兵的配置,设置监听端口和保护模式
initSentinel();// 初始化哨兵,line 5160
}
....
...
// sentinelHandleConfiguration作用是解析配置文件并初始化
loadServerConfig(configfile,options);// line 5233 main->loadServerConfig->loadServerConfigFromString->sentinelHandleConfiguration
...
sentinelIsRunning();//line 5295,随机生成一个40字节的哨兵ID,打印日志
...
}
// 时间任务中的哨兵做的工作是哨兵主要的工作逻辑
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
...
/* Run the Sentinel timer if we are in sentinel mode. */
if (server.sentinel_mode) sentinelTimer();
...
}
哨兵中每次执行serverCron时,都会调用sentinelTimer()函数。该函数会建立连接,并且定时发送心跳包
并采集信息。会在3.6.2中详细解说sentinelTimer的工作.
3.6.2 sentinelTimer做了什么
void sentinelTimer(void) {
sentinelCheckTiltCondition();
sentinelHandleDictOfRedisInstances(sentinel.masters);
sentinelRunPendingScripts();
sentinelCollectTerminatedScripts();
sentinelKillTimedoutScripts();
/* We continuously change the frequency of the Redis "timer interrupt"
* in order to desynchronize every Sentinel from every other.
* This non-determinism avoids that Sentinels started at the same time
* exactly continue to stay synchronized asking to be voted at the
* same time again and again (resulting in nobody likely winning the
* election because of split brain voting). */
server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}
sentinelTimer做了什么:
(1)建立命令连接和消息连接.消息连接建立之后会订阅Redis服务的__sentinel__:hello频道;
(2)在命令连接上每10s发送info命令进行信息采集;每1s在命令连接上发送ping命令探测存活性;每2s在命令连接上发布一条信息.
(3)检测服务是否处于主观下线状态;
(4)检测服务是否处于客观下线状态并且需要进行主从切换;
做出以下说明:
a.步骤(2)中的信息格式如下:
【sentinel_ip,sentinel_port,sentinel_runid,current_epoch,master_name,master_ip,master_port,master_config_epoch】
分别表示:
sentinel_ip-哨兵的IP
sentinel_port-哨兵的端口
sentinel_runid-哨兵的ID(即上文所述40字节的随机字符串)
current_epoch-当前纪元(用于选举和主从切换)
master_name-Redis Master的名称
master_ip-RedisMaster的IP
master_port-Redis Master的端口
master_config_epoch-Redis Master的配置纪元(用于选举和主从切换);
b.哨兵启动之后通过info命令进行信息采集,据此能够知道一个Redis Master有多少Slaves,然后在下一
次执行sentinelTimer函数时会和所有的Slaves分别建立命令连接与消息连接。而通过订阅消息连接上的消
息可以知道其他的哨兵。哨兵与哨兵之间只会建立一条命令连接,每1s发送一个ping命令进行存活性探测,
每2s推送(publish)一条消息.
c.第3步中主观下线状态的探测针对所有的Master, Slave和哨兵;
d.第4步中只会对Master服务器进行客观下线的判断。通过上文我们知道,如果有大于等于quorum个哨兵同
时认为一台Master处于主观下线状态,才会将该Master标记为客观下线;
e.一个哨兵如何知道其他哨兵对一台Master服务器的判断状态呢?
Redis会向监控同一台Master的所有哨兵通过命令连接发送如下格式的命令:
SENTINEL is-master-down-by-addr master_ip master_port current_epoch sentinel_runid或者*,
其中最后一项当需要投票时发送sentinel_runid,否则发送一个*号.
据此能够知道其他哨兵对该Master服务状态的判断,如果达到要求,就标记该Master为客观下线.
如果判断一个Redis Master处于客观下线状态,这时就需要开始执行主从切换了.
主从切换我们将在3.6.3中进行详述.
3.6.3 主从切换
当Redis哨兵方案中的Master处于客观下线状态,为了保证Redis的高可用性,此时需要执行主从切换。即将其
中一个Slave提升为Master,其他Slave从该提升的Slave继续同步数据。主从切换有一个状态迁移图,其所有状
态定义如下:
/* Failover machine different states. 故障迁移的不同状态 */
#define SENTINEL_FAILOVER_STATE_NONE 0
#define SENTINEL_FAILOVER_STATE_WAIT_START 1
#define SENTINEL_FAILOVER_STATE_SELECT_SLAVE 2
#define SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE 3
#define SENTINEL_FAILOVER_STATE_WAIT_PROMOTION 4
#define SENTINEL_FAILOVER_STATE_RECONF_SLAVES 5
#define SENTINEL_FAILOVER_STATE_UPDATE_CONFIG 6
/* 0-No failover in progress.没有进行切换 */
/* 1-Wait for failover_start_time.等待开始进行切换*/
/* 2-Select slave to promote 选择一台从服务器作为新的主服务器*/
/* 3-Slave -> Master 将被选中的从服务器切换为主服务器*/
/* 4-Wait slave to change role 等待被选中的从服务器上报状态*/
/* 5-SLAVEOF newmaster 将其他slave切换为像新的服务器要求同步数据*/
/* 6-Monitor promoted slave. 重置master,将master的IP和port更新为被选中的从服务器的IP和port*/
切换流程如下图(摘自《Redis 5设计与源码分析》)
详细流程可阅读《Redis 5设计与源码分析》,此处不再详述.
3.7 常用命令
初始化哨兵时会调用initSentinel函数,该函数中会更改哨兵可执行的命令,具体如下:
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
{"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
{"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
{"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
{"role",sentinelRoleCommand,1,"ok-loading",0,NULL,0,0,0,0,0},
{"client",clientCommand,-2,"read-only no-script",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
{"auth",authCommand,2,"no-auth no-script ok-loading ok-stale fast",0,NULL,0,0,0,0,0},
{"hello",helloCommand,-2,"no-auth no-script fast",0,NULL,0,0,0,0,0}
};
1)sentinel masters:返回该哨兵监控的所有Master的相关信息。
2)SENTINEL MASTER <name>:返回指定名称Master的相关信息。
3)SENTINEL SLAVES <master-name>:返回指定名称Master的所有Slave的相关信息。
4)SENTINEL SENTINELS <master-name>:返回指定名称Master的所有哨兵的相关信息。
5)SENTINEL IS-MASTER-DOWN-BY-ADDR <ip> <port> <current-epoch><runid>:如果runid是*,返回
由IP和Port指定的Master是否处于主观下线状态。如果runid是某个哨兵的ID,则同时会要求对该runid进
行选举投票。
6)SENTINEL RESET <pattern>:重置所有该哨兵监控的匹配模式(pattern)的Masters(刷新状态,重新建立各类连接)。
7)SENTINEL GET-MASTER-ADDR-BY-NAME <master-name>:返回指定名称的Master对应的IP和Port。
8)SENTINEL FAILOVER <master-name>:对指定的Mmaster手动强制执行一次切换。
9)SENTINEL MONITOR <name> <ip> <port> <quorum>:指定该哨兵监听一个Master。
10)SENTINEL flushconfig:将配置文件刷新到磁盘。
11)SENTINEL REMOVE <name>:从监控中去除掉指定名称的Master。
12)SENTINEL CKQUORUM <name>:根据可用哨兵数量,计算哨兵可用数量是否满足配置数量(认定客观下
线的数量);是否满足切换数量(即哨兵数量的一半以上)。
13)SENTINEL SET <mastername> [<option> <value> ...]:设置指定名称的Master的各类参数(例如
超时时间等)。
14)SENTINEL SIMULATE-FAILURE <flag> <flag> ... <flag>:模拟崩溃。
flag可以为crash-after-election或者crash-after-promotion,分别代表切换时选举完成主哨兵之后崩溃以及将被选中的从服务器推举为Master之后崩溃。
3.7 测试与观察
3.7.1 主从读写权限测试
3.7.2 查看主从关系
redis-cli -h 127.0.0.1 -p 9001
info replication
redis-cli -h 127.0.0.1 -p 9002
redis-cli -h 127.0.0.1 -p 9003
redis-cli -h 127.0.0.1 -p 19001
sentinel master mymaster
redis-cli -h 127.0.0.1 -p 19002
sentinel master mymaster
redis-cli -h 127.0.0.1 -p 19003
sentinel master mymaster
3.7.3 哨兵选举新主测试
3.8 Sentinel模式下的工作机制及几个事件
本段内容引自本文链接,可搜索【Sentinel模式】查看此链接对于Sentinel模式的介绍,包括【工作机制】,【Sentinel模式下的几个事件】
3.8.1 工作机制
* 每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个PING命令;
* 如果一个实例距离最后一次有效回复 PING 命令的时间超过down-after-milliseconds选项所指定的值,
则这个实例会被sentinel标记为主观下线;
* 如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master
的确进入了主观下线状态;
* 当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下
线状态, 则master会被标记为客观下线;
* 在一般情况下, 每个sentinel会以每 10 秒一次的频率向它已知的所有master,slave发送 INFO 命令
* 当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送 INFO 命令的频率会从
10秒一次改为1秒一次;
* 若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除;
若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除.
3.8.2 Sentinel模式下的几个事件
+reset-master :主服务器已被重置。
+slave :一个新的从服务器已经被 Sentinel 识别并关联。
+failover-state-reconf-slaves :故障转移状态切换到了 reconf-slaves 状态。
+failover-detected: 另一个 Sentinel开始了一次故障转移操作或者一个从服务器转换成了主服务器。
+slave-reconf-sent: 领头(leader)的 Sentinel 向实例发送了 [SLAVEOF](/commands/slaveof.html) 命令,为实例设置新的主服务器。
+slave-reconf-inprog :实例正在将自己设置为指定主服务器的从服务器,但相应的同步过程仍未完成。
+slave-reconf-done :从服务器已经成功完成对新主服务器的同步。
-dup-sentinel :对给定主服务器进行监视的一个或多个 Sentinel 已经因为重复出现而被移除 —— 当 Sentinel 实例重启的时候,就会出现这种情况。
+sentinel :一个监视给定主服务器的新 Sentinel 已经被识别并添加。
+sdown :给定的实例现在处于主观下线状态。
-sdown :给定的实例已经不再处于主观下线状态。
+odown :给定的实例现在处于客观下线状态。
-odown :给定的实例已经不再处于客观下线状态。
+new-epoch :当前的纪元(epoch)已经被更新。
+try-failover :一个新的故障迁移操作正在执行中,等待被大多数 Sentinel 选中(waiting to be elected by the majority)。
+elected-leader :赢得指定纪元的选举,可以进行故障迁移操作了。
+failover-state-select-slave :故障转移操作现在处于 select-slave 状态 —— Sentinel 正在寻找可以升级为主服务器的从服务器。
no-good-slave :Sentinel 操作未能找到适合进行升级的从服务器。Sentinel 会在一段时间之后再次尝试寻找合适的从服务器来进行升级,又或者直接放弃执行故障转移操作。
selected-slave :Sentinel 顺利找到适合进行升级的从服务器。
failover-state-send-slaveof-noone :Sentinel 正在将指定的从服务器升级为主服务器,等待升级功能完成。
failover-end-for-timeout :故障转移因为超时而中止,不过最终所有从服务器都会开始复制新的主服务器(slaves will eventually be configured to replicate with the new master anyway)。
failover-end :故障转移操作顺利完成。所有从服务器都开始复制新的主服务器了。
+switch-master :配置变更,主服务器的 IP 和地址已经改变。 这是绝大多数外部用户都关心的信息。
+tilt: 进入 tilt 模式。
-tilt: 退出 tilt 模式。
4.Cluster
4.1 认识一下Cluster模式
4.1.1 概念与特点
集群提供数据自动分片到不同节点的功能,并且当部分节点失效后仍然可以使用。
假设有3个Redis Master,每个Redis Master挂载一个Redis Slave,共6个Redis实例。
集群用来提供横向扩展能力,即当数据量增多之后,通过增加服务节点就可以扩展服务能力.
背后理论思想是将数据通过某种算法分布到不同的服务节点,这样当节点越多,单台节点所需提供服务的数据就越少.
redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制,高可用和分片的特性.它不需要哨兵
也能完成节点移除和故障转移的功能.需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩
展,据官方文档称可以线性扩展到100O节点.redis集群的性能和高可用件均优于之前版本的哨兵模式,且集群
配置非常简单.
4.1.2 cluster关键字
(1)slot&重定向
(2)pfail&fail
Redis集群中节点的故障状态有两种。一种为pfail(Possible failure),当一个节点A未在指定时间收到另
一个节点B对ping包的响应时,A节点会将B节点标记为pfail。另一种是,当大多数Master节点确认B为pfail之
后,就会将B标记为fail。fail状态的节点才会需要执行主从切换。
(3)自动切换&手动切换
我们知道集群中节点有两种失败状态:pfail和fail。当集群中节点通过错误检测机制发现某个节点处于fail状
态时,会自动执行主从切换。Redis中还提供一种手动执行切换的方法,即通过执行cluster failover命令。
通过手动切换方式能实现Redis主节点的平滑升级,具体步骤是:先将主节点切换为一个从节点,然后进行版本
的升级,再将升级后的版本切换回主节点。
(4)副本漂移
(5)分片迁移
(6)通信数据包类型:
ping
pong
meet
fail
update
failover(授权)
mfstart
publish
4.1.3 设计集群需要考虑的问题
4.1.1的图中有3个Redis Master,每个Redis Master挂载一个Redis Slave,共6个Redis实例。集群用来提供
横向扩展能力,即当数据量增多之后,通过增加服务节点就可以扩展服务能力。背后理论思想是将数据通过某种
算法分布到不同的服务节点,这样当节点越多,单台节点所需提供服务的数据就越少。
很显然,集群首先需要解决如下问题:
(1)分槽(slot):即如何决定某条数据应该由哪个节点提供服务;
(2)端如何向集群发起请求(客户端并不知道某个数据应该由哪个节点提供服务,并且如果扩容或者节点发生故
障后,不应该影响客户端的访问)?
(3)某个节点发生故障之后,该节点服务的数据该如何处理?
(4)扩容,即向集群中添加新节点该如何操作?5)同一条命令需要处理的key分布在不同的节点中(如Redis中
集合取并集、交集的相关命令),如何操作?
4.1.4 划分slot与重定向逻辑
(1)slot划分逻辑
Redis将键空间分为了16384个slot,然后通过【HASH_SLOT = CRC16(key) mode 16384】这个算法计算出每个
key所属的slot。
(2)请求与重定向
客户端可以请求任意一个节点,每个节点中都会保存所有16384个slot对应到哪一个节点的信息.
如果一个key所属的slot正好由被请求的节点提供服务,则直接处理并返回结果,否则返回MOVED重定向信息,
如下:
GET key
-MOVED slot IP:PORT
由-MOVED开头,接着是该key计算出的slot,然后是该slot对应到的节点IP和Port。
客户端应该处理该重定向信息,并且向拥有该key的节点发起请求。
实际应用中,Redis客户端可以通过向集群请求slot和节点的映射关系并缓存,然后通过本地计算要操作的key所
属的slot,查询映射关系,直接向正确的节点发起请求,这样可以获得几乎等价于单节点部署的性能。当集群由
于节点故障或者扩容导致重新分片后,客户端先通过重定向获取到数据,每次发生重定向后,客户端可以将新的
映射关系进行缓存,下次仍然可以直接向正确的节点发起请求。
集群中的数据分片之后由不同的节点提供服务,即每个主节点的数据都不相同,此种情况下,为了确保没有单点
故障,主服务必须挂载至少一个从服务。客户端请求时可以向任意一个主节点或者从节点发起,当向从节点发起
请求时,从节点会返回MOVED信息重定向到相应的主节点。
4.1.5 典型的Custer模式的Redis集群配置与启动方式
port 7000 //监听端口
cluster-enabled yes //是否开启集群模式
cluster-config-file nodes7000.conf //集群中该节点的配置文件
cluster-node-timeout 5000 //节点超时时间,超过该时间之后会认为处于故障状态
daemonize yes
7000端口用来处理客户端请求,除了7000端口,Redis集群中每个节点会起一个新的端口(默认为监听端口加
10000,本例中为17000)用来和集群中其他节点进行通信。cluster-config-file指定的配置文件需要有可写
权限,用来持久化当前节点状态。
节点可以直接使用redis-server命令启动,如下:
redis-server /path/to/redis-cluster.conf
/home/muten/module/redis-6.0.9/src/redis-cluster/cluster/start-all.sh
4.1.6 部分关键代码逻辑
(1)初始化函数逻辑clusterInit:
main(){
...
if (server.cluster_enabled) clusterInit();
...
}
clusterInit函数会加载配置并且初始化一些状态指标,监听集群通信端口。
除此之外,该函数执行了如下一些回调函数的注册。
(1)集群通信端口建立监听后,注册回调函数clusterAcceptHandler。当节点之间建立连接时先由该函数进行处理。
(2)当节点之间建立连接后,为新建立的连接注册读事件的回调函数clusterRead-Handler。
(3)当有读事件发生时,当clusterReadHandler读取到一个完整的包体后,调用cluster-ProcessPacket解析
具体的包体。集群之间通信数据包的解析都在该函数内完成。
(2)Redis时间任务函数serverCron中会调度集群的周期性函数clusterCron:
serverCron{
...
if (server.cluster_enabled) clusterCron();
...
}
clusterCron函数执行如下操作:
(1)向其他节点发送MEET消息,将其加入集群;
(2)每1s会随机选择一个节点,发送ping消息;
(3)如果一个节点在超时时间之内仍未收到ping包的响应(cluster-node-timeout配置项指定的时间),则将其
标记为pfail;
(4)检查是否需要进行主从切换,如果需要则执行切换;
(5)检查是否需要进行副本漂移,如果需要,执行副本漂移操作.
注意:
a.对于步骤(1),当在一个集群节点A执行CLUSTER MEET ip port命令时,会将“ip:port”指定的节点B加入该集
群中,但该命令执行时只是将B的“ip:port”信息保存到A节点中,然后在clusterCron函数中为A节点“ip:port”
指定的B节点建立连接并发送MEET类型的数据包.
b.对于步骤(3),Redis集群中节点的故障状态有两种.一种为pfail(Possible failure),当一个节点A未在
指定时间收到另一个节点B对ping包的响应时,A节点会将B节点标记为pfail。另一种是,当大多数Master节点
确认B为pfail之后,就会将B标记为fail. fail状态的节点才会需要执行主从切换.
(3) Redis除了在serverCron函数中进行调度之外,在每次进入事件循环之前,会在before-Sleep函数中执行一些操作:
void beforeSleep(struct aeEventLoop *eventLoop) {
...
if (server.cluster_enabled) clusterBeforeSleep();
...
}
clusterBeforeSleep()函数会执行如下操作:
(1)检查主从切换状态,如果需要,执行主从切换相关操作;
(2)更新集群状态,通过检查是否所有slot都有相应的节点提供服务以及是否大部分主服务都是可用状态,来决
定集群处于正常状态还是失败状态;
(3)刷新集群状态到配置文件.
可以看到,clusterCron和clusterBeforeSleep函数中都会进行主从切换相关状态的判断,如果需要进行主从切换,还会进行切换相关的操作. 下一小节说说主从切换.
4.1.7 主从切换
(1)主动切换
集群中节点有两种失败状态:pfail和fail。当集群中节点通过错误检测机制发现某个节点处于fail状态时,会自动执行主从切换.
集群之间会互相发送心跳包,心跳包中会包括从发送方视角所记录的关于其他节点的状态信息.
当一个节点收到心跳包之后,如果检测到发送方(假设为A)标记某个节点(假设为B)处于pfail状态,则接收
节点(假设为C)会检测B是否已经被大多数主节点标记为pfail状态。
如果是,则C节点会向集群中所有节点发送一个fail包,通知其他节点B已经处于fail状态。当一个主节点(假设
为B)被标记为fail状态后,该主节点的所有Slave执行周期性函数clusterCron时,会从所有的Slave中选择一
个复制偏移量最大的Slave节点(即数据最新的从节点,假设为D),然后D节点首先将其当前纪(currentEpoch)
加1,然后向所有的主节点发送failover授权请求包,当获得大多数主节点的授权后,开始执行主从切换.
注意概念:
(1)currentEpoch:
集群当前纪元,类似Raft算法中的term,是一个递增的版本号.
正常状态下集群中所有节点的currentEpoch相同。每次选举时从节点首先将currentEpoch加1,然后进行选
举。投票时同一对主从的同一个currentEpoch只能投一次,防止多个Slave同时发起选举后难以获得票的大多
数。注意currentEpoch为所有Master节点中配置纪元的最大值.
(2)configEpoch:
每个主节点的配置纪元。当因为网络分区导致多个节点提供冲突的信息时,通过configEpoch能够知道哪个节点
的信息最新.
切换流程如下(假设被切换的主节点为M,执行切换的从节点为S):
(1)S先更新自己的状态,将自己声明为主节点。并且将S从M中移除;
(2)由于S需要切换为主节点,所以将S的同步数据相关信息清除(即不再从M同步数据);
(3)将M提供服务的slot都声明到S中;
(4)发送一个PONG包,通知集群中其他节点更新状态.
(2)手动切换
通过手动切换方式能实现Redis主节点的平滑升级,具体步骤是:先将主节点切换为一个从节点,然后进行版本的升级,再将升级后的版本切换回主节点 ,该过程不会有任务服务的中断.
当一个从节点接收到cluster failover命令之后,执行手动切换,流程如下:
(1)该从节点首先向对应的主节点发送一个mfstart包,通知主节点从节点要开始进行手动切换;
(2)主节点会阻塞所有客户端命令的执行,之后主节点在周期性函数clusterCron中发送ping包时会在包头部分做
特殊标记;
(3)当从节点收到主节点的ping包并且检测到特殊标记之后,会从包头中获取主节点的复制偏移量;
(4)从节点在周期性函数clusterCron中检测当前处理的偏移量与主节点复制偏移量是否相等,当相等时开始执行切换流程;
(5)切换完成后,主节点会将阻塞的所有客户端命令通过发送+MOVED指令重定向到新的主节点.
通过该过程可以看到,手动执行主从切换时不会丢失任何数据,也不会丢失任何执行命令,只在切换过程中会有暂时的停顿.
注意:
redisServer结构体中有两个字段,clients_paused和clients_pause_end_time,当需要阻塞所有客户端命
令的执行时,首先将clients_paused置为1,然后将clients_pause_end_time设置为当前时间加2倍的
CLUSTER_MF_TIMEOUT(默认值为5s),客户端发起命令请求时会调用processInputBuffer,该函数会检测当
前是否处于客户端阻塞状态,如果是,则不会继续执行命令.
具体切换流程同自动切换.
4.1.8 副本漂移
4.1.1的第一个图中集群中共有6个实例,三主三从,每对主从只能有一个处于故障状态.
假设一对主从同时发生故障,则集群中的某些slot会处于不能提供服务的状态,从而导致集群失效.
为了提高可靠性,我们可以在每个主服务下边各挂载两个从服务实例如4.1.1中的第二个图,则共需要增加3个实
例.但假设若集群中有100个主服务,为了更高的可靠性,就需要增加100个实例.
有什么方法既能提高可靠性,又可以做到不随集群规模线性增加从服务实例的数量呢?
Redis中提供了一种副本漂移的方法,如图22-5;
我们只给其中一个主C增加两个从服务, 假设主A发生故障,主A的从A1会执行切换,切换完成之后从A1变为主
A1,此时主A1会出现单点问题. 当检测到该单点问题后,集群会主动从主C的从服务中漂移一个给有单点问题的
主A1做从服务,如图22-6所示.
我们详细介绍Redis中如何实现副本漂移,在周期性调度函数clusterCron中会定期检测如下条件:
(1)是否存在单点的主节点,即主节点没有任何一台可用的从节点;
(2)是否存在有两台及以上可用从节点的主节点;
如果以上两个条件都满足,则从有最多可用从节点的主节点中选择一台从节点执行副本漂移,
选择标准为按节点名称的字母序从小到大,选择最靠前的一台从节点执行漂移.
漂移具体过程如下:
(1)从C的记录中将C1移除;
(2)将C1所记录的主节点更改为A1;
(3)在A1中添加C1为从节点;
(4)将C1的数据同步源设置为A1.
可以看到,漂移过程只是更改一些节点所记录的信息,之后会通过心跳包将该信息同步到所有的集群节点.
4.1.9 分片迁移
有很多情况下需要进行分片的迁移,例如增加一个新节点之后需要把一些分片迁移到新节点,或者当删除一个节
点之后,需要将该节点提供服务的分片迁移到其他节点,甚至有些时候需要根据负载重新配置分片的分布.
Redis集群中分片的迁移,即slot的迁移,需要将一个slot中所有的key从一个节点迁移到另一个节点.
我们通过如下一些Redis的命令看看具体实现(假设以下命令都在节点A执行).
(1)CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]:在A节点中增加指定的slot(即指定的slot由A提供服务). 注意,如果指定的slot已经有节点在提供服务,该命令会报错.
(2)CLUSTER DELSLOTS slot1 [slot2] ... [slotN]:在A节点中删除指定的slot(即指定的slot不再由A提供服务).
(3)CLUSTER SETSLOT slot NODE node:将slot指定为由node节点提供服务.
(4)CLUSTER SETSLOT slot MIGRATING node:将slot从A节点迁移到指定的节点.注意,slot必须属于A节点,否则会报错.
(5)CLUSTER SETSLOT slot IMPORTING node:将slot从指定节点迁移到A节点.
执行cluster addslots和cluster delslots之后只会修改A节点的本地视图,之后A节点会通过心跳包将配置同
步到集群中其他节点.
我们通过一个实例说明一个slot具体的迁移过程,假设“slot中10000”现在由A提供服务,需要将该slot从A迁移到B,需要执行以下命令:
CLUSTER SETSLOT slot IMPORTING A // 在B节点执行
CLUSTER SETSLOT slot MIGRATING B // 在A节点执行
当客户端请求属于“slot 10000”的key时,仍然会直接向A发送请求(或者通过其他节点通过MOVED重定向到A
节点),如果A中找到该key,则直接处理并返回结果。如果A中未找到该key,则返回如下信息:
GET key
-ASK 10000 B
客户端收到该回复后,首先需要向B发送一条asking命令,然后将要执行的命令发送给B.
生产中使用Redis提供的redis-cli命令来做分片迁移,redis-cli首先在A、B节点执行如上两条命令,然后
在A节点执行如下命令:
cluster getkeysinslot slot count
这条命令会从节点A的slot,例如10000中取出“count”个key,然后对这些key依次执行迁移命令,如下:
migrate target_ip target_port key 0 timeout
target_ip和target_port指向B节点,0为数据库ID(集群中的所有节点只能有0号数据库)。当所有key都
迁移完成后,redis-cli会向所有集群中的节点发送如下命令:
cluster setslot slot node node-id
其中slot本例中为10000, node-id为B。至此,一个slot迁移完毕。当此时再向A节点发送slot为10000的请
求时,A节点会直接返回MOVED重定向到B节点.
4.1.10 通信数据包类型
#define CLUSTERMSG_TYPE_PING 0 /* Ping */
#define CLUSTERMSG_TYPE_PONG 1 /* Pong (reply to Ping) */
#define CLUSTERMSG_TYPE_MEET 2 /* Meet "let's join" message */
#define CLUSTERMSG_TYPE_FAIL 3 /* Mark node xxx as failing */
#define CLUSTERMSG_TYPE_PUBLISH 4 /* Pub/Sub Publish propagation */
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 5 /* May I failover? */
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 6 /* Yes, you have my vote */
#define CLUSTERMSG_TYPE_UPDATE 7 /* Another node slots configuration */
#define CLUSTERMSG_TYPE_MFSTART 8 /* Pause clients for manual failover */
#define CLUSTERMSG_TYPE_MODULE 9 /* Module cluster API message. */
#define CLUSTERMSG_TYPE_COUNT 10 /* Total number of message types. */
0-ping包
1-pong包
2-meet包
3-fail包
4-public包
5-failover授权请求包
6-failover授权确认包
7-update包
8-手动failover包
9-模块相关包
10-计数边界
5、6和8三种包只有包头没有包体,剩余所有的包都由包头和包体两部分组成.
包头格式相同,包体内容根据具体的类型填充.
各种包的介绍可阅读《Redis 5设计与源码分析》或者之后写的相关文章,本文此处不再叙述了.
4.2 搭建方式
4.3 集群创建过程
[root@localhost cluster]# redis-cli --cluster create 192.168.118.138:10001 192.168.118.138:10002 192.168.118.138:10003 192.168.118.138:10004 192.168.118.138:10005 192.168.118.138:10006 --cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 192.168.118.138:10005 to 192.168.118.138:10001
Adding replica 192.168.118.138:10006 to 192.168.118.138:10002
Adding replica 192.168.118.138:10004 to 192.168.118.138:10003
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: 21ec8ebc9aeca3fb55c09a2fad897cd753bbb3b6 192.168.118.138:10001
slots:[0-5460] (5461 slots) master
M: a1f4ab3f969b737cc6424400d4b5485250505e7e 192.168.118.138:10002
slots:[5461-10922] (5462 slots) master
M: 66176dc615ee552a66ca804684dac893352f2b6e 192.168.118.138:10003
slots:[10923-16383] (5461 slots) master
S: ba122df6a0ad030cd7abd5bd6adc11e2ab1e7ce6 192.168.118.138:10004
replicates 66176dc615ee552a66ca804684dac893352f2b6e
S: 3c1d2e97f080dc87ff6e4f963192620ce96cdc0f 192.168.118.138:10005
replicates 21ec8ebc9aeca3fb55c09a2fad897cd753bbb3b6
S: e81d5325efce7d4de85df32bd7f7e6224ed26285 192.168.118.138:10006
replicates a1f4ab3f969b737cc6424400d4b5485250505e7e
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
..
>>> Performing Cluster Check (using node 192.168.118.138:10001)
M: 21ec8ebc9aeca3fb55c09a2fad897cd753bbb3b6 192.168.118.138:10001
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: ba122df6a0ad030cd7abd5bd6adc11e2ab1e7ce6 192.168.118.138:10004
slots: (0 slots) slave
replicates 66176dc615ee552a66ca804684dac893352f2b6e
S: 3c1d2e97f080dc87ff6e4f963192620ce96cdc0f 192.168.118.138:10005
slots: (0 slots) slave
replicates 21ec8ebc9aeca3fb55c09a2fad897cd753bbb3b6
M: 66176dc615ee552a66ca804684dac893352f2b6e 192.168.118.138:10003
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: e81d5325efce7d4de85df32bd7f7e6224ed26285 192.168.118.138:10006
slots: (0 slots) slave
replicates a1f4ab3f969b737cc6424400d4b5485250505e7e
M: a1f4ab3f969b737cc6424400d4b5485250505e7e 192.168.118.138:10002
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
简单方法:
cd /home/muten/module/redis-6.0.9/utils/create-cluster/
[root@localhost create-cluster]# ./create-cluster start
[root@localhost create-cluster]# ./create-cluster create
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:30005 to 127.0.0.1:30001
Adding replica 127.0.0.1:30006 to 127.0.0.1:30002
Adding replica 127.0.0.1:30004 to 127.0.0.1:30003
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: f1173d12152c3873960b604f0eb85fba3c75716c 127.0.0.1:30001
slots:[0-5460] (5461 slots) master
M: 492766e515f91371eab038dbf60b72a7cef1c2d5 127.0.0.1:30002
slots:[5461-10922] (5462 slots) master
M: 68f8e17099a733c740d1e4d978c2b11fe2d756ab 127.0.0.1:30003
slots:[10923-16383] (5461 slots) master
S: 00d968a1ad4d8230f3906febcd92caf33db6b131 127.0.0.1:30004
replicates f1173d12152c3873960b604f0eb85fba3c75716c
S: af336a47ae44f7093d90fe295879da4fdf194bc8 127.0.0.1:30005
replicates 492766e515f91371eab038dbf60b72a7cef1c2d5
S: 795a0d0853fda5a81426bfa4bea79d7b2bd3e63e 127.0.0.1:30006
replicates 68f8e17099a733c740d1e4d978c2b11fe2d756ab
Can I set the above configuration? (type 'yes' to accept): y
[root@localhost create-cluster]# ./create-cluster create
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:30005 to 127.0.0.1:30001
Adding replica 127.0.0.1:30006 to 127.0.0.1:30002
Adding replica 127.0.0.1:30004 to 127.0.0.1:30003
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: f1173d12152c3873960b604f0eb85fba3c75716c 127.0.0.1:30001
slots:[0-5460] (5461 slots) master
M: 492766e515f91371eab038dbf60b72a7cef1c2d5 127.0.0.1:30002
slots:[5461-10922] (5462 slots) master
M: 68f8e17099a733c740d1e4d978c2b11fe2d756ab 127.0.0.1:30003
slots:[10923-16383] (5461 slots) master
S: 00d968a1ad4d8230f3906febcd92caf33db6b131 127.0.0.1:30004
replicates 68f8e17099a733c740d1e4d978c2b11fe2d756ab
S: af336a47ae44f7093d90fe295879da4fdf194bc8 127.0.0.1:30005
replicates f1173d12152c3873960b604f0eb85fba3c75716c
S: 795a0d0853fda5a81426bfa4bea79d7b2bd3e63e 127.0.0.1:30006
replicates 492766e515f91371eab038dbf60b72a7cef1c2d5
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
>>> Performing Cluster Check (using node 127.0.0.1:30001)
M: f1173d12152c3873960b604f0eb85fba3c75716c 127.0.0.1:30001
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: af336a47ae44f7093d90fe295879da4fdf194bc8 127.0.0.1:30005
slots: (0 slots) slave
replicates f1173d12152c3873960b604f0eb85fba3c75716c
M: 492766e515f91371eab038dbf60b72a7cef1c2d5 127.0.0.1:30002
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
M: 68f8e17099a733c740d1e4d978c2b11fe2d756ab 127.0.0.1:30003
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: 00d968a1ad4d8230f3906febcd92caf33db6b131 127.0.0.1:30004
slots: (0 slots) slave
replicates 68f8e17099a733c740d1e4d978c2b11fe2d756ab
S: 795a0d0853fda5a81426bfa4bea79d7b2bd3e63e 127.0.0.1:30006
slots: (0 slots) slave
replicates 492766e515f91371eab038dbf60b72a7cef1c2d5
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
[root@localhost create-cluster]# redis-cli -c -p 30001
127.0.0.1:30001> ping
PONG
127.0.0.1:30001> set msg hi
-> Redirected to slot [6257] located at 127.0.0.1:30002
OK
127.0.0.1:30002>quit
./create-cluster stop
./create-cluster clean
4.4 测试
4.4.1 用客户端去访问
redis-cli -h 127.0.0.1 -p 10001 -c(创建一个新的客户端)
本次测试用的三台机器,三主三从:
slot在三台机器上的分配,共有16284(16*1024)个slot:
0-5460
5461-10922
10923-16383
CLUSTER INFO 查看集群相关信息的命令
4.4.2 一个主节点宕机了
5.Redis各个集群模式的比较
6.问题与思考
(1 )为什么cluster模型中至少有三个master节点?
回答:最好单数,便于选举,防止脑裂.大于2才叫集群啊
(2) 主从切换完成之后,客户端和其他哨兵如何知道现在提供服务的Redis Master是哪一个呢?
回答:可以通过subscribe __sentinel__:hello频道,知道当前提供服务的Master的IP和Port。
(3) 执行切换的哨兵发生了故障,切换操作是否会由其他哨兵继续完成呢?
回答:执行切换的哨兵发生故障后,剩余哨兵会重新选主,并且重新开始执行切换流程。
(4) 故障Master恢复之后,会继续作为Master提供服务还是会作为Slave提供服务?
回答:Redis中主从切换完成之后,当故障Master恢复之后,会作为新Master的一个Slave来提供服务。