哨兵是Redis的高可用方案,可以在Redis Master发生故障时自动选择一个Redis Slave切换为Master,继续对外提供服务。集群提供数据自动分片到不同节点的功能,并且当部分节点失效后仍然可以使用。
22.1 哨兵
哨兵通过与Master和Slave的通信,能够清楚每个Redis服务的健康状态。
当Master发生故障时,哨兵能够知晓,然后通过对Slave健康状态、优先级、同步数据状态等的综合判断,选取其中一个Slave切换为Master,并且修改其他Slave指向新的Master地址。
为什么实际中至少会部署3个以上哨兵并且数量最好是奇数呢?
那假如部署2个哨兵呢?当Redis的Master发生故障时,选leader,假设2个哨兵各自投自己一票,根本选举不出leader。所以哨兵个数最好是奇数。
图22-1 哨兵部署方案
我们思考如下问题。
1)切换完成之后,客户端和其他哨兵如何知道现在提供服务的Redis Master是哪一个呢?
2)假设执行切换的哨兵发生了故障,切换操作是否会由其他哨兵继续完成呢?
3)当故障Master恢复之后,会继续作为Master提供服务还是会作为一个Slave提供服务?
22.1.1 哨兵简介
典型的哨兵配置文件
//监控一个名称为mymaster的Redis Master服务,地址和端口号为127.0.0.1:6379,quorum为2
sentinel monitor mymaster 127.0.0.1 6379 2
//如果哨兵60s内未收到mymaster的有效ping回复,则认为mymaster处于down的状态
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.3 6380 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处于客观下线状态时才会开始执行切换。 (认为客观下线的哨兵num)
第二层含义为:假设有5个哨兵,quorum配置为4。首先,判断客观下线需要4个哨兵才能认定。其次,当开始执行切换时,会从5个哨兵中选择一个leader执行该次选举,此时一个哨兵也必须得到4票才能被选举为leader,而不是3票(即哨兵的大多数)。 (选举leader的哨兵num)
配置文件中首先配置了需要监控的Redis Master服务器,然后设置了一些服务相关的参数,并没有Redis Slave和其他哨兵的配置。而通过图22-1,我们看到每个哨兵都必须与所有监控的Redis Master下的Slave服务器以及其他监控该Master的哨兵建立连接。显然,哨兵只通过配置文件是不能知道这些信息的。进一步,如果在配置文件中硬编码写出从服务器和其他哨兵的信息,会丧失灵活性。
22.1.2 代码流程
图22-2 单个哨兵连接示意图
哨兵启动之后会先与配置文件中监控的Master建立两条连接,一条称为命令连接,另一条称为消息连接。哨兵就是通过如上两条连接发现其他哨兵和Redis Slave服务器,并且与每个Redis Slave也建立同样的两条连接。
哨兵启动
哨兵可以直接使用redis-server命令启动,如下:
redis-server /path/to/sentinel.conf --sentinel
redis-server中具体的代码流程如下:
int main(int argc, char **argv) {
...
//检测是否以sentinel模式启动
server.sentinel_mode = checkForSentinelMode(argc,argv);
...
if (server.sentinel_mode) {
initSentinelConfig(); // 将监听端口置为26379
initSentinel(); // 更改哨兵可执行命令。哨兵中只能执行有限的几种服务端命令,如ping,sentinel,subscribe,publish,info等等。该函数还会对哨兵进行一些初始化
}
...
sentinelIsRunning(); //随机生成一个40字节的哨兵ID,打印启动日志
}
哨兵的配置文件必须具有可写权限。哨兵的初始配置文件只配置了需要监听的Redis Master和其他一些配置参数,当哨兵发现了其他的Redis Slave服务器和监听同一个Master的其他哨兵时,会将该信息记录到配置文件中做持久化存储。这样,当哨兵重启后,可以直接从退出状态继续执行。
建立命令连接和消息连接
主流程只是进行了一些初始化, Redis的时间任务serverCron里面建立命令连接和消息连接。
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();
...
}
void sentinelTimer(void) {
sentinelCheckTiltCondition();
sentinelHandleDictOfRedisInstances(sentinel.masters);
sentinelRunPendingScripts();
sentinelCollectTerminatedScripts();
sentinelKillTimedoutScripts();
/* 不断更改Redis"计时器中断" 的频率,以使每个Sentinel彼此不同步。 这种不确定性避免了哨兵在同一时间开始*继续保持同步,要求一次又一次地在上进行投票(由于没有人投票,导致没有人可能赢得*选举)*/
server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}
serverCron 子函数-> sentinelTimer 函数:
哨兵中每次执行serverCron时,都会调用sentinelTimer()函数。该函数会建立连接,并且定时发送心跳包并采集信息。
主要功能如下。
1)建立命令连接和消息连接。消息连接建立之后会订阅Redis服务的__sentinel__:hello频道。
2)在命令连接上每10s发送info命令进行信息采集;每1s在命令连接上发送ping命令探测存活性;每2s在命令连接上发布一条信息,信息格式如下。
sentinel_ip,sentinel_port,sentinel_runid,current_epoch,master_name,master_ip,master_port,master_config_epoch
上述参数分别代表哨兵的IP、哨兵的端口、哨兵的ID(即上文所述40字节的随机字符串)、当前纪元(用于选举和主从切换)、Redis Master的名称、Redis Master的IP、Redis Master的端口、RedisMaster的配置纪元(用于选举和主从切换)。
3)检测服务是否处于主观下线状态。
4)检测服务是否处于客观下线状态并且需要进行主从切换。
哨兵启动之后通过info命令进行信息采集,能够知道一个Redis Master有多少Slaves,
然后在下一次执行sentinelTimer函数时会和所有的Slaves分别建立命令连接与消息连接。
而通过订阅消息连接上的消息可以知道其他的哨兵。
哨兵与哨兵之间只会建立一条命令连接,每1s发送一个ping命令进行存活性探测,每2s推送(publish)一条消息。
第3步中主观下线状态的探测针对所有的Master,Slave和哨兵。
第4步中只会对Master服务器进行客观下线的判断。如果有大于等于quorum个哨兵同时认为一台Master处于主观下线状态,才会将该Master标记为客观下线。
一个哨兵如何知道其他哨兵对一台Master服务器的判断状态呢?
Redis会向监控同一台Master的所有哨兵通过命令连接发送如下格式的命令:
SENTINEL is-master-down-by-addr master_ip master_port current_epoch sentinel_runid或者*
其中最后一项当需要投票时发送sentinel_runid,否则发送一个*号。据此能够知道其他哨兵对该Master服务状态的判断,如果达到要求,就标记该Master为客观下线。如果判断一个Redis Master处于客观下线状态,这时就需要开始执行主从切换了。
22.1.3 主从切换
当Master处于客观下线状态,此时需要执行主从切换。即将其中一个Slave提升为Master,其他Slave从该提升的Slave继续同步数据。主从切换有一个状态迁移图,其所有状态定义如下:
#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 //等待被选中的从服务器上报状态
//将其他Slave切换为向新的主服务器要求同步数据
#define SENTINEL_FAILOVER_STATE_RECONF_SLAVES 5
//重置Master,将Master的IP:PORT设置为被选中从服务器的IP:PORT
#define SENTINEL_FAILOVER_STATE_UPDATE_CONFIG 6
图22-3 主从切换状态转换图
当一个哨兵发现一台Master处于主观下线状态时,会首先将切换状态更新为SEN-TINEL_FAILOVER_STATE_WAIT_START,并且将当前纪元加1。然后发送如下命令要求其他哨兵给自己投票:
SENTINEL is-master-down-by-addr master_ip master_port current_epoch sentinel_runid或者*
此时最后一项参数为sentinel_runid,即该哨兵的ID,第5项current_epoch在开始执行切换后会加1。当从哨兵中选出一个主哨兵之后,接下来的切换都由该主哨兵执行。
主哨兵首先会将当前切换状态更改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE,即开始选择一台
从服务器作为新的主服务器。那么,假设有多台从服务器,该选择哪台呢?
Redis中选择规则
1)如果该Slave处于主观下线状态,则不能被选中。
2)如果该Slave 5s之内没有有效回复ping命令或者与主服务器断开时间过长,不能被选中。
3)如果slave-priority为0,则不能被选中(slave-priority可以在配置文件中指定。正整数,值越小优先级越高,当指定为0时,不能被选为主服务器)。
4)在剩余Slave中比较优先级,优先级高的被选中;如果优先级相同,则有较大复制偏移量的被选中;否则按字母序选择排名靠前的Slave。
选举的slave切换为Master
当选中从服务器之后,将当前切换状态更改为SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE,并且在下一次时间任务调度时执行该步骤。该状态需要把选择的Redis Slave切换为Redis Master,即哨兵向该Slave发送如下命令:
MULTI //开启一个事务
SLAVEOF NO ONE //关闭该从服务器的复制功能,将其转换为一个主服务器
CONFIG REWRITE //将redis.conf文件重写(会根据当前运行中的配置重写原来的配置)
//关闭连接到该服务的客户端(关闭之后客户端会重连,重连时会重新获取Redis Master的地址)
CLIENT KILL TYPE normal
EXEC //执行事务
执行完该步骤之后会将切换状态更新为SENTINEL_FAILOVER_STATE_WAIT_PROMOTION。上一步我们向被选中的从服务器发送了slaveof no one命令,执行完之后Redis中并没有处理返回值,而是在下一次info命令的返回中检查该从服务器的role字段,如果返回role:master,说明该从服务器已变更自己的角色为主服务器。于是切换状态变更为SENTINEL_FAILOVER_STATE_RECONF_SLAVES。
注意 在该步骤变更状态为SENTINEL_FAILOVER_STATE_RECONF_SLAVES之前,如果切换超时,哨兵可以放弃本次切换,放弃之后会从第一步开始重新执行切换。但是如果进行到该步骤,则只能继续执行,不会检测超时。
在该步骤设置SENTINEL_FAILOVER_STATE_RECONF_SLAVES后,哨兵会依次向其他从服务器发送切换主服务器的命令,如下:
MULTI //开启一个事务
SLAVEOF IP PORT //将该服务器设置为向新的主服务器请求数据
CONFIG REWRITE //将redis.conf文件重写(会根据当前运行中的配置重写原来的配置)
//关闭连接到该服务的客户端(关闭之后客户端会重连,重连时会重新获取Redis Master的地址)
CLIENT KILL TYPE normal
EXEC //执行事务
如果所有的从服务器都已更新完毕,则切换状态更新为SENTINEL_FAILOVER_STATE_UPDATE_CONFIG。该步骤会将哨兵中监听的Master(旧Master)重置为被选中的从服务器(新Master),并且将旧Master也配置为新Master的从服务器。然后将切换状态更新为SENTINEL_FAILOVER_STATE_NONE。
至此,主从切换已完成。
22.1.4 常用命令
在22.1.2节,初始化哨兵时会调用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}
};
哨兵中只可以执行有限的几种命令。本节主要介绍哨兵中独有的命令:sentinel。类似其他命令的执行流程,该命令会调用sentinelCommand函数。该命令的几种重点形式。
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之后崩溃。
小结
本节通过一个常见的哨兵部署方案介绍哨兵的主要功能,
然后通过哨兵启动过程的追踪和主从切换的过程介绍了哨兵在Redis中具体的实现逻辑。
最后介绍了哨兵中sentinel相关的常用命令。
通过本节介绍,我们回答一下之前提出的3个问题。
1)主从切换完成之后,客户端和其他哨兵如何知道现在提供服务的RedisMaster是哪一个呢?
回答 :可以通过subscribe__sentinel__:hello频道,知道当前提供服务的Master的IP和Port。
2)执行切换的哨兵发生了故障,切换操作是否会由其他哨兵继续完成呢?
回答 :执行切换的哨兵发生故障后,剩余哨兵会重新选主,并且重新开始执行切换流程。
3)故障Master恢复之后,会继续作为Master提供服务还是会作为Slave提供服务?
回答 :Redis中主从切换完成之后,当故障Master恢复之后,会作为新Master的一个Slave来提供服务。
22.2 集群
图22-4 集群部署方式
图中有3个Redis Master,每个Redis Master挂载一个Redis Slave,共6个Redis实例。
集群用来提供横向扩展能力,即当数据量增多之后,通过增加服务节点就可以扩展服务能力。背后理论思想是将数据通过某种算法分布到不同的服务节点,这样当节点越多,单台节点所需提供服务的数据就越少。
集群首先需要解决如下问题。
1)分槽(slot):即如何决定某条数据应该由哪个节点提供服务;
2)端如何向集群发起请求(客户端并不知道某个数据应该由哪个节点提供服务,并且如果扩容或者节点发生故障后,不应该影响客户端的访问)?
3)某个节点发生故障之后,该节点服务的数据该如何处理?
4)扩容,即向集群中添加新节点该如何操作?
5)同一条命令需要处理的key分布在不同的节点中(如Redis中集合取并集、交集的相关命令),如何操作?
22.2.1 集群简介
Redis将键空间分为了16384个slot,然后通过如下算法:
HASH_SLOT = CRC16(key) mod 16384
计算出每个key所属的slot。客户端可以请求任意一个节点,每个节点中都会保存所有16384个slot对应到哪一个节点的信息。如果一个key所属的slot正好由被请求的节点提供服务,则直接处理并返回结果,否则返回MOVED重定向信息,如下:
GET key
-MOVED slot IP:PORT
由-MOVED开头,接着是该key计算出的slot,然后是该slot对应到的节点IP和Port。客户端应该处理该重定向信息,并且向拥有该key的节点发起请求。
实际应用中,Redis客户端可以通过向集群请求slot和节点的映射关系并缓存,然后通过本地计算要操作的key所属的slot,查询映射关系,直接向正确的节点发起请求,这样可以获得几乎等价于单节点部署的性能。
当集群由于节点故障或者扩容导致重新分片后,客户端先通过重定向获取到数据,每次发生重定向后,客户端可以将新的映射关系进行缓存,下次仍然可以直接向正确的节点发起请求。
接着考虑图22-4,集群中的数据分片之后由不同的节点提供服务,即每个主节点的数据都不相同,此种情况下,为了确保没有单点故障,主服务必须挂载至少一个从服务。客户端请求时可以向任意一个主节点或者从节点发起,当向从节点发起请求时,从节点会返回MOVED信息重定向到相应的主节点。
注意 Redis集群中,客户端只能在主节点执行读写操作。
如果需要在从节点中进行读操作,需要满足如下条件:
①首先在客户端中执行readonly命令;
②如果一个key所属的slot由主节点A提供服务,则请求该key时可以向A所属的从节点发起读请求。该请求不会被重定向。当一个主节点发生故障后,其挂载的从节点会切换为主节点继续提供服务。
最后,当一条命令需要操作的key分属于不同的节点时,Redis会报错。Redis提供了一种称为hash tags的机制,由业务方保证当需要进行多个key的处理时,将所有key分布到同一个节点,
该机制实现原理如下:
如果一个key包括{substring}这种模式,则计算slot时只计算"{"和"}"之间的子字符串。即keys{sub}1、keys{sub}2、keys{sub}3计算slot时都会按照sub串进行。这样保证这3个字符串会分布到同一个节点。
22.2.2 代码流程
首先看一份典型的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
redis-server启动
执行该条命令后,redis-server中具体的代码流程如下:
main(){
...
if (server.cluster_enabled) clusterInit();
...
}
clusterInit集群初始化
clusterInit函数会加载配置并且初始化一些状态指标,监听集群通信端口。除此之外,该函数执行一些回调函数的注册。
void clusterInit(void) {
int saveconf = 0;
server.cluster = zmalloc(sizeof(clusterState));
server.cluster->myself = NULL;
server.cluster->currentEpoch = 0;
server.cluster->state = CLUSTER_FAIL;
server.cluster->size = 1;
server.cluster->todo_before_sleep = 0;
server.cluster->nodes = dictCreate(&clusterNodesDictType,NULL);
server.cluster->nodes_black_list =
dictCreate(&clusterNodesBlackListDictType,NULL);
server.cluster->failover_auth_time = 0;
server.cluster->failover_auth_count = 0;
server.cluster->failover_auth_rank = 0;
server.cluster->failover_auth_epoch = 0;
server.cluster->cant_failover_reason = CLUSTER_CANT_FAILOVER_NONE;
server.cluster->lastVoteEpoch = 0;
for (int i = 0; i < CLUSTERMSG_TYPE_COUNT; i++) {
server.cluster->stats_bus_messages_sent[i] = 0;
server.cluster->stats_bus_messages_received[i] = 0;
}
server.cluster->stats_pfail_nodes = 0;
memset(server.cluster->slots,0, sizeof(server.cluster->slots));
clusterCloseAllSlots();
/* 锁定集群配置文件,以确保每个节点都使用*自己的nodes.conf. */
if (clusterLockConfig(server.cluster_configfile) == C_ERR)
exit(1);
/* 加载或创建新的节点配置. */
if (clusterLoadConfig(server.cluster_configfile) == C_ERR) {
myself = server.cluster->myself =
createClusterNode(NULL,CLUSTER_NODE_MYSELF|CLUSTER_NODE_MASTER);
serverLog(LL_NOTICE,"No cluster configuration found, I'm %.40s",
myself->name);
clusterAddNode(myself);
saveconf = 1;
}
if (saveconf) clusterSaveConfigOrDie(1);
/* We need a listening TCP port for our cluster messaging needs. */
server.cfd_count = 0;
/*端口号大小限制 */
int port = server.tls_cluster ? server.tls_port : server.port;
if (port > (65535-CLUSTER_PORT_INCR)) {
exit(1);
}
if (listenToPort(port+CLUSTER_PORT_INCR,
server.cfd,&server.cfd_count) == C_ERR)
{
exit(1);
} else {
int j;
// 1)集群通信端口建立监听后,注册回调函数clusterAcceptHandler。当节点之间建立连接时先由该函数进行处理。
2)当节点之间建立连接后,为新建立的连接注册读事件的回调函数clusterReadHandler。
3)当有读事件发生时,当clusterReadHandler读取到一个完整的包体后,调用clusterProcessPacket解析具体的包体。22.2.6节介绍的集群之间通信数据包的解析都在该函数内完成。
for (j = 0; j < server.cfd_count; j++) {
if (aeCreateFileEvent(server.el, server.cfd[j], AE_READABLE,
clusterAcceptHandler, NULL) == AE_ERR)
serverPanic("Unrecoverable error creating Redis Cluster " "file event.");
}
}
/* The slots -> keys map is a radix tree. Initialize it here. */
server.cluster->slots_to_keys = raxNew();
memset(server.cluster->slots_keys_count,0,
sizeof(server.cluster->slots_keys_count));
/* Set myself->port / cport to my listening ports, we'll just need to
* discover the IP address via MEET messages. */
myself->port = port;
myself->cport = port+CLUSTER_PORT_INCR;
if (server.cluster_announce_port)
myself->port = server.cluster_announce_port;
if (server.cluster_announce_bus_port)
myself->cport = server.cluster_announce_bus_port;
server.cluster->mf_end = 0;
resetManualFailover();
clusterUpdateMyselfFlags();
}
时间任务函数serverCron
类似哨兵,Redis时间任务函数serverCron中会调度集群的周期性函数,如下:
serverCron(){
/* Run the Redis Cluster cron. */
run_with_period(100) {
if (server.cluster_enabled) clusterCron();
}
}
clusterCron函数
执行操作:
1)向其他节点发送MEET消息,将其加入集群。
注意 当在一个集群节点A执行CLUSTER MEET ip port命令时,会将"ip:port"指定的节点B加入该集群中。但该命令执行时只是将B的"ip:port"信息保存到A节点中,然后在clusterCron函数中为A节点和"ip:port"指定的B节点建立连接并发送MEET类型的数据包。MEET数据包格式见22.2.6下的第3节。
2)每1s会随机选择一个节点,发送ping消息(消息内容详情见22.2.6节下的第1节关于ping包的介绍)。
3)如果一个节点在超时时间之内仍未收到ping包的响应(cluster-node-timeout配置项指定的时间),则将其标记为pfail。
注意 Redis集群中节点的故障状态有两种。一种为pfail(Possible failure),当一个节点A未在指定时间收到另一个节点B对ping包的响应时,A节点会将B节点标记为pfail。另一种是,当大多数Master节点确认B为pfail之后,就会将B标记为fail。fail状态的节点才会需要执行主从切换。
4)检查是否需要进行主从切换,如果需要则执行切换(见22.2.3节)。
5)检查是否需要进行副本漂移,如果需要,执行副本漂移操作(见22.2.4节)。
/* This is executed 10 times every second */
void clusterCron(void) {
dictIterator *di;
dictEntry *de;
int update_state = 0;
int orphaned_masters; /* How many masters there are without ok slaves. */
int max_slaves; /* Max number of ok slaves for a single master. */
int this_slaves; /* Number of ok slaves for our master (if we are slave). */
mstime_t min_pong = 0, now = mstime();
clusterNode *min_pong_node = NULL;
static unsigned long long iteration = 0;
mstime_t handshake_timeout;
iteration++; /* Number of times this function was called so far. */
{
static char *prev_ip = NULL;
char *curr_ip = server.cluster_announce_ip;
int changed = 0;
if (prev_ip == NULL && curr_ip != NULL) changed = 1;
else if (prev_ip != NULL && curr_ip == NULL) changed = 1;
else if (prev_ip && curr_ip && strcmp(prev_ip,curr_ip)) changed = 1;
if (changed) {
if (prev_ip) zfree(prev_ip);
prev_ip = curr_ip;
if (curr_ip) {
prev_ip = zstrdup(prev_ip);
strncpy(myself->ip,server.cluster_announce_ip,NET_IP_STR_LEN);
myself->ip[NET_IP_STR_LEN-1] = '