相关文章
Redis源码剖析——主从复制(1)
Redis源码剖析——主从复制(2)
Redis源码剖析——主从复制(3)
Redis源码剖析——主从复制(5)—共享复制缓冲区的方案
目录
1:masterTryPartialResynchronization函数
2:addReplyReplicationBacklog函数
前言
本文主要讲解主节点部分重同步的实现,以及主从复制中的其他功能。
主节点在收到从节点发来的PSYNC命令之前,主节点的部分重同步流程,与完全重同步流程是一样的。在收到PSYNC命令后,主节点调用masterTryPartialResynchronization函数,尝试进行部分重同步。
首先看一下部分重同步的实现原理,然后在看具体的实现。
一、部分重同步原理
Redis实现的部分重同步功能,依赖于以下三个属性:
a:主节点的复制偏移量和从节点的复制偏移量;
b:主节点的复制积压队列( replication backlog );
c:Redis实例的运行ID;
主节点维持一个积压队列。当它收到客户端发来的命令请求时,除了将该命令请求缓存到从节点的输出缓存,还会将命令追加到积压队列中。
积压队列中的每个字节,都有一个全局性的偏移量。主节点维持一个计数器作为复制偏移量,当主节点回复从节点”+FULLRESYNC <runid> <offset>”信息时,其中的offset就是当前主节点的复制偏移量的值。当从节点收到该消息后,保存<runid>,取出<offset>作为自己的复制偏移量的初始值。
当主节点收到客户端发来的,长度为len的命令请求之后,就会将len增加到复制偏移量上。然后将该命令请求追加到积压队列中,并且发给每个从节点。从节点收到主节点发来的命令之后,同样会将命令长度len增加到自己的复制偏移量上,这就保证了主从节点上复制偏移量的一致性,也就是数据库状态的一致性。
积压队列是一个空间有限的循环队列,随着命令的追加,不断覆盖之前的命令,积压队列中累积的命令偏移量范围也在不断发生变化。
当从节点断链一段时间,然后重连主节点时,向主节点发来”PSYNC <runid> <offset>”命令。其中的<runid>就是断链前保存的主节点运行ID,<offset>就是自己的复制偏移量加1,表示需要接收的下一条命令首字节的偏移量。
主节点收到该”PSYNC”消息后,首先判断<runid>是否与自己的运行ID匹配,如果不匹配,则不能执行部分重同步;然后判断偏移量<offset>是否还在积压队列中累积的命令范围内,如果在,则说明可以进行部分重同步。
要理解部分重同步,必须理解积压队列的实现。
二、积压队列的实现
Redis中的积压队列server.repl_backlog,是一个固定大小的循环队列。所谓循环队列,举个简单的例子,假设server.repl_backlog的大小为10个字节,则向其中插入数据”abcdefg”之后,该积压队列的内容如下:
现在插入数据”hijklmn”,则积压队列的内容如下:
也就是说,插入数据时,一旦到达了积压队列的尾部,则重新从头部开始插入,覆盖最早插入的内容。
要理解积压队列,关键在于理解下面的,有关积压队列的属性:
server.master_repl_offset:一个全局性的计数器。该属性只有存在积压队列的情况下才会增加计数。当存在积压队列时,每次收到客户端发来的,长度为len的请求命令时,就会将server.master_repl_offset增加len。
该属性也就是所谓的主节点上的复制偏移量。当从节点发来PSYNC命令后,主节点回复从节点"+FULLRESYNC <runid> <offset>"消息时,其中的offset就是取的主节点当时的server.master_repl_offset的值。这样当从节点收到该消息后,将该值保存在复制偏移量server.master->reploff中。
进入命令传播阶段后,每当主节点收到客户端的命令请求,则将命令的长度增加到server.master_repl_offset上,然后将命令传播给从节点,从节点收到后,也会将命令长度加到server.master->reploff上,从而保证了主节点上的复制偏移量server.master_repl_offset和从节点上的复制偏移量server.master->reploff的一致性。
需要注意的,server.master_repl_offset的值并不是严格的从0开始增加的。它只是一个计数器,只要能保证主从节点上的复制偏移量一致即可。比如如果它的初始值为10,发送给从节点后,从节点保存的复制偏移量初始值也为10,当新的命令来临时,主从节点上的复制偏移量都会相应增加该命令的长度,因此这并不影响主从节点上偏移量的一致性。
server.repl_backlog_size:积压队列server.repl_backlog的总容量。
server.repl_backlog_idx:在积压队列server.repl_backlog中,每次写入新数据时的起始索引,是一个相对于server.repl_backlog的索引。当server.repl_backlog_idx 等于server.repl_backlog的长度server.repl_backlog_size时,置其值为0,表示从头开始。
以上面那个积压队列为例,server.repl_backlog_idx的初始值为0,插入”abcdefg”之后,该值变为7;插入”hijklmn”之后,该值变为4。
server.repl_backlog_histlen:积压队列server.repl_backlog中,当前累积的数据量的大小。该值不会超过积压队列的总容量server.repl_backlog_size。
server.repl_backlog_off:在积压队列中,最早保存的命令的首字节,在全局范围内(而非积压队列内)的偏移量。在累积命令流时,下列等式恒成立:
server.master_repl_offset - server.repl_backlog_off + 1 = server.repl_backlog_histlen。
还是以上面那个积压队列为例:如果在插入”abcdefg”之前,server.master_repl_offset的初始值为2,则插入”abcdefg”之后,积压队列中当前的数据量,也就是属性server.repl_backlog_histlen的值为7。属性server.master_repl_offset的值变为9,此时命令的首字节为”a”,它在全局的偏移量就是3。满足上面的等式。
在插入”hijklmn”之后,积压队列中当前的数据量,也就是属性server.repl_backlog_histlen的值为10。属性server.master_repl_offset的值变为16。此时最早保存的命令首字节为”e”,它在全局的偏移量是7,满足上面的等式。
根据上面的等式,主节点的积压队列中累积的命令流,首字节和尾字节在全局范围内的偏移量分别是server.repl_backlog_off和server.master_repl_offset。
当从节点断链重连后,向主节点发送”PSYNC <runid> <offset>”消息,其中的<offset>表示需要接收的下一条命令首字节的偏移量。也就是server.master->reploff + 1。
主节点判断<offset>的值,如果该值在下面的范围内,就表示可以进行部分重同步:
[server.repl_backlog_off, server.repl_backlog_off + server.repl_backlog_histlen]。如果<offset>的值为server.repl_backlog_off+ server.repl_backlog_histlen,也就是server.master_repl_offset + 1,说明从节点断链期间,主节点没有收到过新的命令请求。
三、部分重同步
1:masterTryPartialResynchronization函数
主节点收到从节点的”PSYNC <runid> <offset>”消息后,调用函数masterTryPartialResynchronization尝试进行部分重同步。该函数返回REDIS_ERR表示不能进行部分重同步;返回REDIS_OK表示可以进行部分重同步。该函数的代码如下:
int masterTryPartialResynchronization(redisClient *c) {
long long psync_offset, psync_len;
//获取从服务器欲要复制的主服务器运行id
char *master_runid = c->argv[1]->ptr;
char buf[128];
int buflen;
// 检查从服务器欲要复制的主服务器运行id是否和当前主服务器运行id一致,只有一致的情况下才有部分重同步(PSYNC)的可能
if (strcasecmp(master_runid, server.runid)) {
// 从服务器欲要复制的主服务器运行id是否和当前主服务器运行id不一致,且<runid>参数不为'?',即不是强制执行完整同步的命令
if (master_runid[0] != '?') {
//redisLog(REDIS_NOTICE,"Partial resynchronization not accepted: " "Runid mismatch (Client asked for runid '%s', my runid is '%s')", master_runid, server.runid);
// 从服务器提供的<runid>为 '?' ,表示强制完整重同步(FULL RESYNC)
} else {
//redisLog(REDIS_NOTICE,"Full resync requested by slave.");
}
//直接跳转到完整重同步
goto need_full_resync;
}
// 取出命令中从服务器请求同步起始偏移量位置,即<offset>对象中取出psync_offset参数
if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) !=
REDIS_OK) goto need_full_resync;
// 如果服务器复制挤压缓冲区中无内容、或想要恢复的那部分数据已经被覆盖、或起始复制偏移量不在复制挤压缓冲区偏移量范围内
// 直接跳转到完整重同步
if (!server.repl_backlog ||
psync_offset < server.repl_backlog_off ||
psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen))
{
// 执行 FULL RESYNC
//redisLog(REDIS_NOTICE, "Unable to partial resync with the slave for lack of backlog (Slave request was: %lld).", psync_offset);
//如果从服务器欲要复制的起始偏移量大于全局偏移量,发出警告
if (psync_offset > server.master_repl_offset) {
//redisLog(REDIS_WARNING, "Warning: slave tried to PSYNC with an offset that is greater than the master replication offset.");
}
goto need_full_resync;
}
/*
* 程序运行到这里,说明可以执行局部复制 partial resync
* 1) 将客户端状态设为 salve
* 2) 向 slave 发送 +CONTINUE ,表示 partial resync 的请求被接受
* 3) 发送 backlog 中,客户端所需要的数据
*/
if (!(c->flags & REDIS_SLAVE)){
//将REDIS_SLAVE标记记录到从节点客户端的标志位中,以标识该客户端为从节点客户端
c->flags |= REDIS_SLAVE;
// 将从节点客户端添加到 slave 列表中
listAddNodeTail(server.slaves,c);
}
//从服务器的复制状态设置”REDIS_REPL_ONLINE“
c->replstate = REDIS_REPL_ONLINE;
//更新主服务器与从服务器最近一次交互的时间
c->repl_ack_time = server.unixtime;
// 向从服务器发送一个同步 +CONTINUE ,表示 PSYNC 局部复制可以执行
buflen = snprintf(buf,sizeof(buf),"+CONTINUE\r\n");
if (write(c->fd,buf,buflen) != buflen) {
//发送同步回复信号失败,异步关闭该从服务器
freeClientAsync(c);
return REDIS_OK;
}
// 发送 backlog 中的内容(也即是从服务器缺失的那些内容)到从服务器
psync_len = addReplyReplicationBacklog(c,psync_offset);
//redisLog(REDIS_NOTICE, "Partial resynchronization request accepted. Sending %lld bytes of backlog starting from offset %lld.", psync_len, psync_offset);
// 更新状态良好的从服务器数量
refreshGoodSlavesCount();
return REDIS_OK;
//完整重同步预处理操作
need_full_resync:
// 刷新 psync_offset,使其变成全局偏移量
psync_offset = server.master_repl_offset;
// 刷新 psync_offset
if (server.repl_backlog == NULL) psync_offset++;
// 向从服务器发送 +FULLRESYNC ,表示需要完整重同步
buflen = snprintf(buf,sizeof(buf),"+FULLRESYNC %s %lld\r\n",
server.runid,psync_offset);
if (write(c->fd,buf,buflen) != buflen) {
freeClientAsync(c);
return REDIS_OK;
}
return REDIS_ERR;
}
首先比对"PSYNC"命令参数中的运行ID和本身的ID号是否匹配,如果不匹配,则需要进行完全重同步,因此直接跳到完整同步的处理流程即可;
然后取出"PSYNC"命令参数中的从节点复制偏移到psync_offset中,该值表示从节点需要接收的下一条命令首字节的偏移量。接下来根据积压队列的状态判断是否可以进行部分重同步,判断的条件上一节中已经讲过了,不再赘述。
经过上面的检查后,说明可以进行部分重同步了。因此:首先将REDIS_SLAVE标记增加到客户端标志位中;然后将从节点客户端的复制状态置为REDIS_REPL_ONLINE,因为只有当c->replstate为REDIS_REPL_ONLINE时,在函数prepareClientToWrite中,才为socket描述符注册可写事件,这样才能将输出缓存中的内容发送给从节点客户端;
更新主服务器与从服务器最近一次交互的时间,根据需要将REDIS_SLAVE标记记录到从节点客户端的标志位中,以标识该客户端为从节点客户端,并将从节点客户端添加到 slave 列表中;
接下来,直接向客户端的socket描述符上输出"+CONTINUE\r\n"命令,这里不能用输出缓存,因为输出缓存只能用于累积命令流。之前主节点向从节点发送的信息很少,因此内核的输出缓存中应该会有空间,因此这里直接的write操作一般不会出错;
接下来,调用addReplyReplicationBacklog,将积压队列中psync_offset之后的数据复制到客户端输出缓存中,注意这里不需要设置server.slaveseldb为-1,因为从节点是接着上次连接进行的;
最后,调用refreshGoodSlavesCount,更新当前状态正常的从节点数量;
2:addReplyReplicationBacklog函数
主节点确认可以为从节点进行部分重同步时,首先就是调用addReplyReplicationBacklog函数,将积压队列中,全局偏移量为offset的字节,到尾字节之间的所有内容,追加到从节点客户端的输出缓存中。该函数的代码如下:
long long addReplyReplicationBacklog(redisClient *c, long long offset) {
long long j, skip, len;
//打印服务器需要局部复制的起始偏移量
//redisLog(REDIS_DEBUG, "[PSYNC] Slave request offset: %lld", offset);
//判断复制积压缓冲区内无内容则直接返回
if (server.repl_backlog_histlen == 0) {
//redisLog(REDIS_DEBUG, "[PSYNC] Backlog history len is zero");
return 0;
}
//打印复制积压缓冲区的大小
//redisLog(REDIS_DEBUG, "[PSYNC] Backlog size: %lld", server.repl_backlog_size);
//打印复制积压缓冲区的首地址(可以被还原的第一个字节)的偏移量
//redisLog(REDIS_DEBUG, "[PSYNC] First byte: %lld", server.repl_backlog_off);
//打印复制积压缓冲区中数据的长度
//redisLog(REDIS_DEBUG, "[PSYNC] History len: %lld", server.repl_backlog_histlen);
//打印复制积压缓冲区当前最新偏移量
//redisLog(REDIS_DEBUG, "[PSYNC] Current index: %lld", server.repl_backlog_idx);
//从offset开始复制,跳过前面无用的内容
skip = offset - server.repl_backlog_off;
//redisLog(REDIS_DEBUG, "[PSYNC] Skipping: %lld", skip);
//计算获得当前最新偏移量在backlog中的索引
j = (server.repl_backlog_idx +
(server.repl_backlog_size-server.repl_backlog_histlen)) %
server.repl_backlog_size;
//redisLog(REDIS_DEBUG, "[PSYNC] Index of first byte: %lld", j);
//计算从服务器局部复制的起始偏移量在backlog中的索引
j = (j + skip) % server.repl_backlog_size;
//计算本次局部复制的内容长度
len = server.repl_backlog_histlen - skip;
//redisLog(REDIS_DEBUG, "[PSYNC] Reply total length: %lld", len);
while(len) {
//判断backlog中的内容是否已经成环
//如果成环,则需要分两次取出内容,如果不成环,可以一次直接取出内容
long long thislen =
((server.repl_backlog_size - j) < len) ?
(server.repl_backlog_size - j) : len;
//打印出局部复制内容的长度
//redisLog(REDIS_DEBUG, "[PSYNC] addReply() length: %lld", thislen);
//将局部复制的内容以字符串对象的形式写入客户端c的回复缓冲区中
addReplySds(c,sdsnewlen(server.repl_backlog + j, thislen));
len -= thislen;
j = 0;
}
//返回局部复制的内容长度
return server.repl_backlog_histlen - skip;
}
在该函数中,首先计算需要在积压队列中跳过的字节数skip,offset为从节点所需数据的首字节的全局偏移量,server.repl_backlog_off表示积压队列中最早累积的命令首字节的全局偏移量,因此skip等于offset - server.repl_backlog_off;
在该函数中,首先计算需要在积压队列中跳过的字节数skip,offset为从节点所需数据的首字节的全局偏移量,server.repl_backlog_off表示积压队列中最早累积的命令首字节的全局偏移量,因此skip等于offset - server.repl_backlog_off;
接下来,计算获得当前最新偏移量在backlog中的索引j,server.repl_backlog_idx-1表示积压队列中,命令尾字节在积压队列中的索引,server.repl_backlog_size表示积压队列的总容量, server.repl_backlog_histlen表示积压队列中累积的命令的大小,因此得到j的值为:(server.repl_backlog_idx+(server.repl_backlog_size-server.repl_backlog_histlen))%server.repl_backlog_size;
接下来,将j置为需要数据首字节相对于积压队列中的索引;然后计算总共需要复制的字节数len;然后就是将数据循环追加到从节点客户端的输出缓存中(追加之前,已经在函数syncCommand保证该输出缓存为空);
3:feedReplicationBacklog函数
主节点收到客户端发来的命令请求后,除了需要将命令累积到从节点的输出缓存中,还需要将该命令追加到积压队列中。feedReplicationBacklog函数就是用于实现将命令追加到积压队列中的函数。
void feedReplicationBacklog(void *ptr, size_t len) {
unsigned char *p = ptr;
// 将长度累加到主服务器的全局 offset 中
server.master_repl_offset += len;
// 环形 buffer ,每次写尽可能多的数据,并在到达尾部时将 idx 重置到头部
while(len) {
// 计算复制积压缓冲区中剩余可用空间
size_t thislen = server.repl_backlog_size - server.repl_backlog_idx;
// 如果剩余空间足以容纳要写入的内容,那么直接将写入数据长度设为 len
if (thislen > len) thislen = len;
// 将 p 中的 thislen 字节内容复制到 backlog
memcpy(server.repl_backlog+server.repl_backlog_idx,p,thislen);
// 更新 idx ,指向新写入的数据之后
server.repl_backlog_idx += thislen;
// 如果写入达到尾部,那么将索引重置到头部
if (server.repl_backlog_idx == server.repl_backlog_size)
server.repl_backlog_idx = 0;
// 减去已写入的字节数
len -= thislen;
// 将指针移动到已被写入数据的后面,指向未被复制数据的开头
p += thislen;
// 增加复制积压缓冲区中数据实际长度
server.repl_backlog_histlen += thislen;
}
// histlen 的最大值只能等于 backlog_size
// 另外,当 histlen 大于 repl_backlog_size 时,
// 表示写入数据的前头有一部分数据被自己的尾部覆盖了
// 举个例子,例如 abcde 要写入到一个只有三个字节的环形数组中
// 且假设索引为 0
// 那么 abc 首先被写入,数组为 [a, b, c]
// 然后 de 被写入,数组为 [d, e, c]
if (server.repl_backlog_histlen > server.repl_backlog_size)
server.repl_backlog_histlen = server.repl_backlog_size;
// 记录程序可以依靠 backlog 来还原的数据的第一个字节的偏移量
// 比如 master_repl_offset = 10086
// repl_backlog_histlen = 30
// 那么 backlog 所保存的数据的第一个字节的偏移量为
// 10086 - 30 + 1 = 10056 + 1 = 10057
// 这说明如果从服务器如果从 10057 至 10086 之间的任何时间断线
// 那么从服务器都可以使用 PSYNC
server.repl_backlog_off = server.master_repl_offset -
server.repl_backlog_histlen + 1;
}
函数中,首先将len增加到主节点复制偏移量server.master_repl_offset中;
然后进入循环,将ptr追加到积压队列中,在循环中:
首先计算本次追加的数据量thislen。server.repl_backlog_size表示积压队列的总容量,server.repl_backlog_idx-1表示积压队列中,累积的命令尾字节在积压队列中的索引,因此thislen等于server.repl_backlog_size-server.repl_backlog_idx,表示在积压队列的尾部之前,还可以追加多少字节。如果thislen大于len,则调整其值;
然后将p中的thislen个字节,复制到首地址为server.repl_backlog+server.repl_backlog_idx的内存中;
接下来更新server.repl_backlog_idx的值,如果其值等于积压队列的总容量,表示已经到达积压队列的尾部,因此下一次添加数据时,需要重新从头部开始,因此置server.repl_backlog_idx为0;
然后更新len和p;
最后更新server.repl_backlog_histlen的值;该值表示积压队列中累积的命令总量;
server.repl_backlog_histlen的值最大不能超过积压队列的总容量,因此将所有数据追加到积压队列后,如果其值已经大于总容量server.repl_backlog_size,则重新置其值为server.repl_backlog_size;
最后,更新server.repl_backlog_off的值,使其满足等式:
server.repl_backlog_histlen=server.master_repl_offset-server.repl_backlog_off+1;
四、定时监测函数replicationCron
主从节点为了探测网络是连通的,每隔一段时间,都会向对方发送一定的心跳信息。
从节点在接受完RDB数据之后,清空本身数据库时,以及加载RDB数据时,都会时不时的向主节点发送一个换行符”\n”(通过回调函数replicationSendNewlineToMaster实现);而且,当从节点本身的复制状态变为REDIS_REPL_CONNECTED之后,每隔1秒钟就会向主节点发送一个 "REPLCONF ACK <offset>"命令。以上的”\n”和"REPLCONF”命令都是从节点向主节点发送的心跳消息。
主节点每隔一段时间,也会向从节点发送”PING”命令,以及换行符”\n”。这是主节点向从节点发送的心跳消息。
主从节点收到对方发来的消息后,都会更新一个时间戳。双方都会定时检查各自时间戳的最后更新时间。这样,当主从节点间长时间没有交互时,说明网络出现了问题,主从双方都可以探测到该问题,从而断开连接;
以上这些探测功能就是在定时执行的函数replicationCron中实现的,该函数每隔1秒钟调用一次。该函数的代码如下:
void replicationCron(void) {
// 尝试连接到主服务器
// 当从节点处于握手期间状态,
// 距离最近一次更新server.repl_transfer_lastio的时间已经超过了最大超时时间
if (server.masterhost &&
(server.repl_state == REDIS_REPL_CONNECTING ||
server.repl_state == REDIS_REPL_SEND_AUTH ||
server.repl_state == REDIS_REPL_RECEIVE_PONG ||
server.repl_state == REDIS_REPL_SEND_PORT ||
server.repl_state == REDIS_REPL_SEND_PSYNC) &&
(time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
{
//redisLog(REDIS_WARNING,"Timeout connecting to the MASTER...");
// 调用函数undoConnectWithMaster,断开与主节点间的连接
undoConnectWithMaster();
}
// 当从节点处于REDIS_REPL_TRANSFER状态(接收RDB数据)
// 距离最近一次更新server.repl_transfer_lastio的时间已经超过了最大超时时间
if (server.masterhost && server.repl_state == REDIS_REPL_TRANSFER &&
(time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
{
//redisLog(REDIS_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in redis.conf to a larger value.");
// 调用函数replicationAbortSyncTransfer,终止本次复制过程
//停止下载 RDB 文件,关闭与主服务器的连接,将从服务器状态调整至REDIS_REPL_CONNECT
replicationAbortSyncTransfer();
}
// 从服务器曾经连接上主服务器,但现在超时
if (server.masterhost && server.repl_state == REDIS_REPL_CONNECTED &&
(time(NULL)-server.master->lastinteraction) > server.repl_timeout)
{
//redisLog(REDIS_WARNING,"MASTER timeout: no data nor PING received...");
// 释放主服务器
freeClient(server.master);
}
// 尝试连接主服务器
if (server.repl_state == REDIS_REPL_CONNECT) {
//redisLog(REDIS_NOTICE,"Connecting to MASTER %s:%d", server.masterhost, server.masterport);
if (connectWithMaster() == REDIS_OK) {
//redisLog(REDIS_NOTICE,"MASTER <-> SLAVE sync started");
}
}
// 定期向主服务器发送 ACK 命令
// 不过如果主服务器带有 REDIS_PRE_PSYNC 的话就不发送
// 因为带有该标识的版本为 < 2.8 的版本,这些版本不支持 ACK 命令
if (server.masterhost && server.master &&
!(server.master->flags & REDIS_PRE_PSYNC))
replicationSendAck();
/* 如果服务器有从服务器,定时向它们发送 PING 。
*
* 这样从服务器就可以实现显式的 master 超时判断机制,
* 即使 TCP 连接未断开也是如此。
*/
//每隔server.repl_ping_slave_period秒
if (!(server.cronloops % (server.repl_ping_slave_period * server.hz))) {
listIter li;
listNode *ln;
robj *ping_argv[1];
/* 首先,向所有已连接 slave (状态为 ONLINE)发送 PING */
ping_argv[0] = createStringObject("PING",4);
replicationFeedSlaves(server.slaves, server.slaveseldb, ping_argv, 1);
decrRefCount(ping_argv[0]);
/* 其次,向那些正在等待 RDB 文件的从服务器(状态为 BGSAVE_START 或 BGSAVE_END)发送 "\n"
* 这个 "\n" 会被从服务器忽略,它的作用就是用来防止主服务器因为长期不发送信息而被从服务器误判为超时 */
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
redisClient *slave = ln->value;
if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START ||
slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) {
if (write(slave->fd, "\n", 1) == -1) {
}
}
}
}
/* Disconnect timedout slaves. */
// 断开超时从服务器
if (listLength(server.slaves)) {
listIter li;
listNode *ln;
// 遍历所有从服务器
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
redisClient *slave = ln->value;
// 略过未 ONLINE 的从服务器
if (slave->replstate != REDIS_REPL_ONLINE) continue;
// 不检查旧版的从服务器
if (slave->flags & REDIS_PRE_PSYNC) continue;
// 释放超时从服务器
if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout)
{
//redisLog(REDIS_WARNING, "Disconnecting timedout slave: %s:%d", slave->ip, slave->port);
// 释放
freeClient(slave);
}
}
}
// 在没有任何从服务器的 N 秒之后,释放 backlog
if (listLength(server.slaves) == 0 && server.repl_backlog_time_limit &&
server.repl_backlog)
{
time_t idle = server.unixtime - server.repl_no_slaves_since;
if (idle > server.repl_backlog_time_limit) {
// 释放
freeReplicationBacklog();
//redisLog(REDIS_NOTICE, "Replication backlog freed after %d seconds " "without connected slaves.", (int) server.repl_backlog_time_limit);
}
}
// // 在没有任何从服务器,AOF 关闭的情况下,清空 script 缓存
// // 因为已经没有传播 EVALSHA 的必要了
// if (listLength(server.slaves) == 0 &&
// server.aof_state == REDIS_AOF_OFF &&
// listLength(server.repl_scriptcache_fifo) != 0)
// {
// replicationScriptCacheFlush();
// }
// 更新符合给定延迟值的从服务器的数量
refreshGoodSlavesCount();
}
server.repl_timeout属性是用户在配置文件中配置的"repl-timeout"选项的值,表示主从复制期间最大的超时时间,默认为60秒;
在读取客户端发来的消息的函数recvData中,每次从socket描述符上读取到数据后,就会更新客户端结构中的lastinteraction属性。
从从节点向主节点建链开始,到读取完主节点发来的RDB数据为止,也就是复制状态从REDIS_REPL_CONNECTING到REDIS_REPL_TRANSFER期间,每当从节点读取到主节点发来的信息后,都会更新server.repl_transfer_lastio属性为当时的Unix时间戳;
当从节点处于REDIS_REPL_CONNECTING状态或者握手状态时,并且最后一次更新server.repl_transfer_lastio的时间已经超过了最大超时时间,则调用函数undoConnectWithMaster,断开与主节点间的连接;
当从节点处于REDIS_REPL_TRANSFER状态(接收RDB数据),并且最后一次更新server.repl_transfer_lastio的时间已经超过了最大超时时间,则调用函数replicationAbortSyncTransfer,终止本次复制过程。停止下载 RDB 文件,关闭与主服务器的连接,将从服务器状态调整至REDIS_REPL_CONNECT;
因此,当从节点处于REDIS_REPL_CONNECTED状态时(命令传播阶段),如果最后一次更新server.master->lastinteractio的时间已经超过了最大超时时间,则调用函数freeClient,断开与主节点间的连接;
以上就是从节点探测网络是否连通的方法;
如果当前从节点的复制状态为REDIS_REPL_CONNECT,则调用connectWithMaster开始向主节点发起建链请求。从节点收到客户端发来的”SLAVEOF”命令,或从节点实例启动,从配置文件中读取到了"slaveof"选项后,就将复制状态置为REDIS_REPL_CONNECT,而在此处开始向主节点发起TCP建链;
如果当前从节点的server.master属性已配置好,说明该从节点已处于REDIS_REPL_CONNECTED状态,并且主节点支持PSYNC命令的情况下,调用函数replicationSendAck向主节点发送"REPLCONF ACK <offset>"消息,这就是从节点向主节点发送心跳消息;
主节点每隔一定时间也会向从节点发送心跳消息,以使从节点可以更新属性server.repl_transfer_lastio的值。
首先是每隔server.repl_ping_slave_period秒,向从节点输出缓存以及积压队列中追加"PING"命令;
然后就是轮训列表server.slaves,对于处于REDIS_REPL_WAIT_BGSAVE_START状态的从节点,或者处于REDIS_REPL_WAIT_BGSAVE_END状态的从节点,且当目前是无硬盘复制的RDB转储时,直接调用write向从节点发送一个换行符;
当主节点将从节点的复制状态置为REDIS_REPL_ONLINE后,每当收到从节点发来的换行符"\n"(从节点加载RDB数据时发送)或者"REPLCONF ACK <offset>"信息时,就会更新该从节点客户端的repl_ack_time属性。
因此,主节点轮训server.slaves列表,如果其中的某个从节点的repl_ack_time属性的最近一次的更新时间,已经超过了最大超时时间,则调用函数freeClient,断开与从节点间的连接;
以上就是主节点探测网络是否连通的方法;
在freeClient函数中,每当释放了一个从节点客户端后,都会判断列表server.slaves当前长度,如果其长度为0,说明该主节点已经没有连接的从节点了,因此就会设置属性server.repl_no_slaves_since为当时的时间戳;
server.repl_backlog_time_limit属性值表示当主节点没有从节点连接时,积压队列最长的存活时间,该值默认为1个小时。
因此,如果主节点当前已没有从节点连接,并且配置了server.repl_backlog_time_limit属性值,并且积压队列还存在的情况下,则判断属性server.repl_no_slaves_since最近一次更新时间是否已经超过配置的server.repl_backlog_time_limit属性值,若已超过,则调用freeReplicationBacklog释放积压队列;
最后,调用refreshGoodSlavesCount,更新当前状态正常的从节点数量。
五、min-slaves选项
Redis主节点可以配置"min-slaves-to-write"和"min-slaves-max-lag"两个选项用于防止主节点在不安全的情况下执行写命令。
这两个选项的意义在于:如果从节点与主节点的最后交互时间,距离当前时间小于"min-slaves-max-lag"的值,则认为该从节点状态是连接的。主节点定时计算当前状态为连接的从节点数目,如果该数目小于"min-slaves-to-write"的值,则主节点拒绝执行写数据库的命令。
计算当前状态为连接的从节点数目,是通过函数refreshGoodSlavesCount实现的。该函数会在定时函数replicationCron中调用,也就是每隔1秒就会调用一次。该函数的代码如下:
void refreshGoodSlavesCount(void) {
listIter li;
listNode *ln;
int good = 0;
//判断服务器是否开启了min-slaves-max-lag选项 或 server.repl_min_slaves_max_lag选项
if (!server.repl_min_slaves_to_write ||
!server.repl_min_slaves_max_lag) return;
//遍历从服务器列表
listRewind(server.slaves,&li);
while((ln = listNext(&li))) {
redisClient *slave = ln->value;
// 计算每个从服务器的延迟值
time_t lag = server.unixtime - slave->repl_ack_time;
// 计入将每个延迟值小于设定值的从服务器进行 GOOD 累计
if (slave->replstate == REDIS_REPL_ONLINE &&
lag <= server.repl_min_slaves_max_lag) good++;
}
// 更新状态良好的从服务器数量
server.repl_good_slaves_count = good;
}
从节点的复制状态为REDIS_REPL_ONLINE之后,主节点收到从节点发来的”REPLCONF ACK <offset>”命令时,就会更新该从节点客户端repl_ack_time属性,以此属性判断从节点与主节点的最后交互时间。
该函数中,如果没有配置server.repl_min_slaves_to_write或者server.repl_min_slaves_max_lag,则直接返回;
然后轮训列表server.slaves,针对其中的每个从节点客户端,得到其slave->repl_ack_time属性与当前时间的差值,如果该差值小于等于server.repl_min_slaves_max_lag的值,则说明该从节点状态良好,计数器加1。
最后将状态良好的从节点数目更新到server.repl_good_slaves_count中。
在处理客户端命令的函数processCommand中,有下面的代码:
// 如果服务器没有足够多的状态良好服务器
// 并且 min-slaves-to-write 选项已打开
// 只要当前要执行的是写数据库命令,则会回复客户端错误信息,并直接返回而不再处理。(此处条件中没有标出写命令,后续再补)
if (server.repl_min_slaves_to_write &&
server.repl_min_slaves_max_lag &&
server.repl_good_slaves_count < server.repl_min_slaves_to_write)
{
flagTransaction(c);
addReply(c, shared.noreplicaserr);
return REDIS_OK;
}
因此,只要当前要执行的是写数据库命令,而且server.repl_good_slaves_count的值小于server.repl_min_slaves_to_write的值,则会回复客户端错误信息,并直接返回而不再处理。