Redis主从复制之repl_backlog

1. 前言

  1. 注意,repl_backlog只针对部分复制(Partial Replication),而非全量复制。
  2. 注意REdis的主节点把所有从节点也当作一个Client看待,正常的数据同步并不涉及repl_backlog。当从节点断开重连,这个时候repl_backlog的作用就体现出来了。截至到5.0.5版本,从节点重启用不上repl_backlog,原因是从节点没有保存repl_backlog的信息,无法实现部分同步,但可少量改动REdis源代码,实现从节点重启后的部分复制。
  3. 正常情况下,主节点会往从节点连接缓冲区写一份数据,同时往repl_backlog也写一份数据,所有从节点共享同一份repl_backlog,因此可以考虑repl_backlog配置大一点,以容忍从节点更长时间失联。
  4. 从节点向主节点发送命令PSYNC,触发部分复制。

2.repl_backlog原理

master怎么知道slave与自己的数据差异在哪里呢?

这就要说到全量同步时的repl_baklog文件了。

这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。

repl_baklog中会记录Redis处理过的命令日志及offset,包括master当前的offset,和slave已经拷贝到的offset:

image-20210725153359022

slave与master的offset之间的差异,就是salve需要增量拷贝的数据了。

随着不断有数据写入,master的offset逐渐变大,slave也不断的拷贝,追赶master的offset:

image-20210725153524190

直到数组被填满:

image-20210725153715910

此时,如果有新的数据写入,就会覆盖数组中的旧数据。不过,旧的数据只要是绿色的,说明是已经被同步到slave的数据,即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。

但是,如果slave出现网络阻塞,导致master的offset远远超过了slave的offset:

image-20210725153937031

如果master继续写入新数据,其offset就会覆盖旧的数据,直到将slave现在的offset也覆盖:

image-20210725154155984

棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。此时如果slave恢复,需要同步,却发现自己的offset都没有了,无法完成增量同步了。只能做全量同步。

image-20210725154216392

3. 配置项

  1. REdis的复制分全量复制和部分复制,全量复制是个很重的过程,而部分复制则是轻量的,部分复制实际是一个增量复制。
  2. REdis的主节点创建和维护一个环形缓冲复制队列(即repl_backlog),从节点部分复制(增量复制)的数据均来自于repl_backlog。
  3. 主节点只有一个repl_backlog,所有从节点共享,直接相关的配置项有两个:
配置项名 配置项说明
repl-backlog-size 环形缓冲复制队列大小,可不带单位,但同时支持单位:b、k、kb、m、mb、g、gb,单位不区分大小写,其中k、m、g间的计算倍数是1000,而kb、mb和gb的计算倍数是1024。
repl-backlog-ttl 环形缓冲复制队列存活时长(所有slaves不可用时,保留repl_backlog多长时间,单位:秒)

4. redisServer

结构体redisServer是REdis的第一核心结构,repl_backlog是它的组成成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct redisServer {
/* My current replication offset */
long long master_repl_offset;
/* Accept offsets up to this for replid2. */
long long second_replid_offset;
/* Replication backlog for partial syncs */
char *repl_backlog; // 环形缓冲复制队列
/* Backlog circular buffer size */
long long repl_backlog_size; // 环形缓冲复制队列容量
/* Backlog actual data length */
long long repl_backlog_histlen; // 环形缓冲复制队列已用大小(影响是否能部分复制)
/* Backlog circular buffer current offset,
that is the next byte will'll write to.*/
// 实际上谈不上空闲,因为总是环绕覆盖写,
// 理解为最新数据的截止位置更为合适,更新的数据总是从这里开始写入到repl_backlog中。
long long repl_backlog_idx; // 环形缓冲复制队列空闲起始位置(写从这里开始)
/* Replication "master offset" of first
byte in the replication backlog buffer.*/
long long repl_backlog_off; // 数据在环形缓冲复制队列的起始位置(读从这里开始)
/* Time without slaves after the backlog
gets released. */
time_t repl_backlog_time_limit; // 环形缓冲复制队列生存时长
/* We have no slaves since that time.
Only valid if server.slaves len is 0. */
time_t repl_no_slaves_since; // 无可用从节点的发生时间
/* Min number of slaves to write. */
int repl_min_slaves_to_write; // 最小需写的从节点数
/* Max lag of slaves to write. */
int repl_min_slaves_max_lag;
/* Number of slaves with lag <= max_lag. */
int repl_good_slaves_count;
/* Send RDB to slaves sockets directly. */
int repl_diskless_sync; // 不落磁盘(无盘)往从节点发送RDB(全量复制)
/* Delay to start a diskless repl BGSAVE. */
// 无盘复制时,延迟指定的时长,以等待更多的从节点
int repl_diskless_sync_delay;
};

5. feedReplicationBacklog-写repl_backlog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* Add data to the replication backlog.
* This function also increments the global replication offset stored at
* server.master_repl_offset, because there is no case where we want to feed
* the backlog without incrementing the offset. */
// 主要被replicationFeedSlaves调用
// 写len长的数据ptr到repl_backlog中
// repl_backlog是一个环形buffer,不存在溢出的问题,策略是最新数据覆盖最老数据。
// 如果参数len大于repl_backlog_size,
// 则repl_backlog没有实际意义,
// 因为无法存储一份完整数据。
void feedReplicationBacklog(void *ptr, size_t len) {
unsigned char *p = ptr;
server.master_repl_offset += len;
/* This is a circular buffer, so write as much data we can at every
* iteration and rewind the "idx" index if we reach the limit. */
// 因为repl_backlog是环形buffer,
// 剩余的空间可能容纳不了len长的数据,
// 当不够时,就需要环绕从头开始写,
// 因此这里需while循环。
while(len) {
// repl_backlog_size为repl_backlog的容量大小,
// 由配置项决定repl_backlog_size值决定,
// repl_backlog_idx是repl_backlog空闲区域的起始位置,
// 这两个值相减得到repl_backlog可用大小。
size_t thislen = server.repl_backlog_size - server.repl_backlog_idx;
// 如果thislen大于len,则表示足够容纳
if (thislen > len) thislen = len;
memcpy(server.repl_backlog+server.repl_backlog_idx,p,thislen);
// 空闲位置往后挪动
server.repl_backlog_idx += thislen;
// 如果空闲位置达到容量大小,则环绕回去从0开始
if (server.repl_backlog_idx == server.repl_backlog_size)
server.repl_backlog_idx = 0;
len -= thislen;
p += thislen;
// repl_backlog_histlen记录了repl_backlog中的数据大小
server.repl_backlog_histlen += thislen;
}
// 修正存储在repl_backlog中的数据大小,
// 它不可能超过repl_backlog_size值。
if (server.repl_backlog_histlen > server.repl_backlog_size)
server.repl_backlog_histlen = server.repl_backlog_size;
/* Set the offset of the first byte we have in the backlog. */
server.repl_backlog_off = server.master_repl_offset -
server.repl_backlog_histlen + 1;
}

6. addReplyReplicationBacklog-读repl_backlog

当主节点判断可部分复制时,会记录如下日志:

1
Partial resynchronization request from %s accepted. Sending %lld bytes of backlog starting from offset %lld.

给从节点的响应头为“+CONTINUE replid\r\n”或“+CONTINUE\r\n”。函数addReplyReplicationBacklog的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* Feed the slave 'c' with the replication backlog starting from the
* specified 'offset' up to the end of the backlog. */
// 被masterTryPartialResynchronization调用
// 而masterTryPartialResynchronization被syncCommand调用(对应命令PSYNC)。
// 从repl_backlog取数据发给slave,
// 数据的开始位置由offset指定。
long long addReplyReplicationBacklog(client *c, long long offset) {
long long j, skip, len;
serverLog(LL_DEBUG, "[PSYNC] Replica request offset: %lld", offset);
// repl_backlog_histlen为0,
// 表示repl_backlog中无数据。
if (server.repl_backlog_histlen == 0) {
serverLog(LL_DEBUG, "[PSYNC] Backlog history len is zero");
return 0;
}
serverLog(LL_DEBUG, "[PSYNC] Backlog size: %lld",
server.repl_backlog_size);
serverLog(LL_DEBUG, "[PSYNC] First byte: %lld",
server.repl_backlog_off);
serverLog(LL_DEBUG, "[PSYNC] History len: %lld",
server.repl_backlog_histlen);
serverLog(LL_DEBUG, "[PSYNC] Current index: %lld",
server.repl_backlog_idx);
/* Compute the amount of bytes we need to discard. */
skip = offset - server.repl_backlog_off;
serverLog(LL_DEBUG, "[PSYNC] Skipping: %lld", skip);
/* Point j to the oldest byte, that is actually our
* server.repl_backlog_off byte. */
j = (server.repl_backlog_idx +
(server.repl_backlog_size-server.repl_backlog_histlen)) %
server.repl_backlog_size;
serverLog(LL_DEBUG, "[PSYNC] Index of first byte: %lld", j);
/* Discard the amount of data to seek to the specified 'offset'. */
j = (j + skip) % server.repl_backlog_size;
/* Feed slave with data. Since it is a circular buffer we have to
* split the reply in two parts if we are cross-boundary. */
len = server.repl_backlog_histlen - skip;
serverLog(LL_DEBUG, "[PSYNC] Reply total length: %lld", len);
while(len) {
long long thislen =
((server.repl_backlog_size - j) < len) ?
(server.repl_backlog_size - j) : len;
serverLog(LL_DEBUG, "[PSYNC] addReply() length: %lld", thislen);
addReplySds(c,sdsnewlen(server.repl_backlog + j, thislen));
len -= thislen;
j = 0;
}
return server.repl_backlog_histlen - skip;
}