主从同步

主从

知识准备

IO复用:

https://zhuanlan.zhihu.com/p/367591714

两种触发

水平触发:这次不处理,下次还是会触发

边缘触发:只触发一次

两种事件

可读事件:内核缓冲区有数据可读

可写事件:内核缓冲区还有空间即 对方tcp窗口还没满。在IO多路复用情况下,两个Socket啥事没干,可写事件是会一直触发的

Redis:

  1. 一般地客户端的可读事件会一直注册在那个epoll wait里的,没处理事件会持续触发,避免数据丢失
  2. 而写事件,只有在输出缓冲区有数据时注册,完成后立马注销

主从复制

命令执行以及架构

1
2
3
4
5
6
7
8

​主节点设requirepass, 从节点设masterauth password

slaveof host port #当前节点成为某个节点的从节点

slaveof no one #解除复制

REPLICAOF #5.0以上对slaveof的替代

主从复制详细过程

场景说明

  1. 初次建立主从关系,必定是全量同步。(从节点会把本身旧的数据给删了)
  2. 全量同步完成后,主从节点进入命令传播阶段,此时主节点会将每个写命令同时发送给所有从节点
  3. 如果此时网络闪断导致主从连接断开,之后重新连接时,从节点会发送PSYNC命令携带之前保存的主节点runID 和 复制偏移量 (很像断线重连哦
  4. 主节点检查runID 是否匹配,且复制偏移量是否还在复制积压缓冲区范围内
  5. 如果条件满足,主节点只需要发送缺失的那部分命令即可,这就是部分同步

初次主从同步

(从)执行slaveof ip host

1
2
3
4
5
6
slaveofCommand()   ->  replicationSetMaster()

replicationSetMaster(){
//改个状态然后啥都不做
server.repl_state = REDIS_REPL_CONNECT;
}

(从)下一次事件循环 【建立tcp连接】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
serverCron()  -> replicationCron()  ->  connectWithMaster()

replicationCron(){
if (server.repl_state == REDIS_REPL_CONNECT) {
connectWithMaster()
}
}

connectWithMaster(){
anetTcpNonBlockConnect()
aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL)
server.repl_transfer_lastio = server.unixtime;
server.repl_transfer_s = fd;

// 将状态改为已连接
server.repl_state = REDIS_REPL_CONNECTING;
}


(从)再下一次事件循环 【发送ping】

1
2
3
4
5
6
7
8
9
10
11
syncWithMaster(){
if (server.repl_state == REDIS_REPL_CONNECTING) {
// 手动发送同步 PING ,暂时取消监听写事件
aeDeleteFileEvent(server.el,fd,AE_WRITABLE);
// 更新状态
server.repl_state = REDIS_REPL_RECEIVE_PONG;
// 同步发送 PING
syncWrite(fd,"PING\r\n",6,100);
return;
}
}

(主)两个事件,回pong,同步从节点信息给客户端

1
2
readQueryFromClient()  ->    pingCommand()   ->  addReply()
sendReplyToClient()

5,(从)主服务器写数据 触发可读事件 ( 触发注册了的syncWithMaster)

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
50
51
52
53
//2.8之前,Redis从节点断线重连 智能全量重同步

syncWithMaster(){
// 接收 PONG 命令
if (server.repl_state == REDIS_REPL_RECEIVE_PONG){
// 手动同步接收 PONG ,暂时取消监听读事件
aeDeleteFileEvent(server.el,fd,AE_READABLE);
syncReadLine()//读取pong
}
//auth 认证
sendSynchronousCommand(fd,"AUTH",server.masterauth,NULL);
//REPLCONF命令,获取master监听的端口
sendSynchronousCommand(fd,"REPLCONF","listening-port",port,NULL);

// 根据返回的结果决定是执行部分 resync ,还是 full-resync
psync_result = slaveTryPartialResynchronization(fd);

// 可以执行部分 resync
if (psync_result == PSYNC_CONTINUE) {
redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Master accepted a Partial Resynchronization.");
// 返回
return;
}

// 主服务器不支持 PSYNC ,发送 SYNC
if (psync_result == PSYNC_NOT_SUPPORTED) {
// 向主服务器发送 SYNC 命令
syncWrite(fd,"SYNC\r\n",6,server.repl_syncio_timeout*1000)
}
//到这里 psync_result == PSYNC_FULLRESYNC 或 PSYNC_NOT_SUPPORTED
// 新建临时文件, 用来接收传输过来的rdb文件
snprintf(tmpfile,256,"temp-%d.%ld.rdb",(int)server.unixtime,(long int)getpid());
//安装读事件处理器(readQueryFromClient变为 readSyncBulkPayload)指针函数的改变
aeCreateFileEvent(server.el,fd, AE_READABLE,readSyncBulkPayload,NULL)
// 设置状态
server.repl_state = REDIS_REPL_TRANSFER;
}

slaveTryPartialResynchronization(){
//组装参数
//PSYNC ? -1 完整重同步
//PSYNC runid offset 部分重同步
// 向主服务器发送 PSYNC 命令 (同步请求并等待)
reply = sendSynchronousCommand(fd,"PSYNC",psync_runid,psync_offset,NULL);
if(reply == "+FULLRESYNC"){
return PSYNC_FULLRESYNC;
}
if(reply == "+CONTINUE"){
// 安装 readQueryFromClient 或者 sendReplyToClient
return PSYNC_CONTINUE;
}
return PSYNC_NOT_SUPPORTED;
}

6,(主)收到 sync命令,进行准备

两种:全同步装备rdb文件,部分重同步 同步命令

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
"sync"/"psync" readQueryFromClient()  -> syncCommand()

void syncCommand(redisClient *c){
// 如果这是一个从服务器,但与主服务器的连接仍未就绪,那么拒绝 SYNC
if (server.masterhost && server.repl_state != REDIS_REPL_CONNECTED) {
addReplyError(c,"Can't SYNC while not connected with my master");
return;
}
if("psync"){
if(masterTryPartialResynchronization()){
return;
}
}

if (server.rdb_child_pid != -1) {
//正好有可用的rdb文件
}
else{
//fork子进程进行rdb
rdbSaveBackground();
}
//设置状态
c->replstate = REDIS_REPL_WAIT_BGSAVE_END;
// 添加到 slave 列表中
listAddNodeTail(server.slaves,c);
}

rdb执行完后,为从客户端安装写事件

1
2
3
4
5
6
serverCron()   ->   backgroundSaveDoneHandler()  -> updateSlavesWaitingBgsave()

updateSlavesWaitingBgsave(){
//遍历所有slave,如果slave状态为 slave->replstate == REDIS_REPL_WAIT_BGSAVE_END
aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendBulkToSlave, slave)
}

发送rdb 文件

1
2
sendBulkToSlave()  //主
readSyncBulkPayload() //从

超时和典型问题

默认60s从节点不回复,视为超时

REPLCONF ACK:从节点每秒上报偏移量,主节点检测延迟

典型为题:

数据不一致场景:

网络延迟,解决:监控master和slave的offset

从节点执行key * 阻塞,解决:避免在从节点执行慢查询

全量复制风暴:

导致主节点压力大,解决:错峰同步,级联更新