最近项目需要,所以研究了一下Redis主从的TCP链接是如何维持的,发现这一块还没有资料说明过,所以就把打算把这个东西记录一下分享出来。
这个问题可以拆成以下几个部分来说,包括:
- Redis如何建立主从关系
- Redis如何检测断链
- Redis如何重新建立主从关系
下面一一说明。
一、Redis如何建立主从关系
redis使用slaveof ip host
指令来建立主从关系,主从关系的建立会引发持续的数据同步。值得一提的是,redis的设计认为这个关系的建立是持续性的,所以也设计了重连机制、对master的验证机制、高效的数据重新同步的方法。下面具体描述这个过程及提到的几个概念。
建立主从关系
前备条件:
实例A:127.0.0.1 6379
实例B:127.0.0.1 6380
我们希望实例A实时备份实例B,所以我们对实例A发送指令slaveof 127.0.0.1 6380
,这个逻辑如下图所示。
*图1 slaveofCommand指令发送与执行过程
- redis-cli发送指令
slaveof 127.0.0.1 6379
- redis-server_6379接收
slaveof 127.0.0.1 6379
,执行slaveofCommand
指令,将ip及port写到server结构体里,并修改状态标志位,同时回复OK - redis-server_6379执行周期函数
serverCron
,其中会执行函数replicationCron
,这里会检测状态标志位,并调用connectWithMaster
,这个函数最后会调用非阻塞的connect
,并且安装事件处理函数syncWithMaster
,当连接成功后,syncWithMaster
被调用,启动与Master的同步协议或者是连接失败处理,如果是失败,则后续会一直尝试重连,所以我们会看到主从建立连接失败的情况下syncWithMaster
每秒都会被执行一次。
二、Redis如何检测断链
断链这个问题比较复杂,需要考虑较多的情况,包括正常关闭、半打开、异常关闭,底层的网络编程一般来说都跑不开这几个话题,redis使用c语言作为开发语言,自己开发了一套IO复用机制,把这几个问题处理得很好,这里我不打算再叙述这几个问题,下面直奔主题。
1、主监测从
主从关系建立后,redis从会以1秒为周期,向redis主发送replconf ack offset
指令,用以向master表达当前复制偏移量,并且让master得以以此作为依据判断redis从是否仍然存活,即心跳机制。
2、从监测主
主从关系建立后,redis主会以repl-ping-slave-period
为周期,向redis从发送PING
指令。repl-ping-slave-period
可以在配置文件当中更改,不能小于1,因为replicationCron的周期是1,这是redis能处理的最小定时事件了(这是错误的,serverCron的周期才是redis能处理的最小定时事件)。
redis从以此为依据,建立心跳检测机制。redis从每次收到ping
指令,会更新server..master->lastinteraction
时间,并且之后每次执行replicationCron
都会检测距离上次主从交互的时间是否超过了repl-timeout
,这个也提供在配置文件当中。如果时间超出了,则redis从认为redis主已经挂掉,则开始清理这个master。
所以我们可以得出结论,redis从基于repl-ping-slave-period
及repl-timeout
这两个配置来建立对于master的检测机制,前者最小不能小于replicationCron的周期,后者最小不能小于前者。
正常关闭
即Redis主主动发出Fin包,发起TCP关闭的流程。
redis从会为redis主建立客户端,redis发出Fin
包,向redis从发送一个EOF
。当redis从收到Fin
包,产生可读事件,readQureyFromClient
被调用,最终调用read
返回0,获得EOF
,感知到对端发起的关闭流程。
半打开状态
即对端已经无法通信,但是本地无法感知到,除非主动发起通信流程。
这种情况包括:
1、网络不通
2、主Redis进程崩溃或者主机down
3、主Redis主机网线被拿掉
4、中间网络设备的网络被拿掉
这种情况Redis从需要依赖心跳机制来监测,并且主动发起TCP关闭流程,将[Redis从-->Redis主]
方向的TCP连接关闭。
根据我们所描述的从监测主的原理,这个情况都可以被发现。
三、Redis如何重新建立主从关系
Redis从向Redis主重新发起连接
Server结构体定义了masterhost及masterport两个域,用来存储master的ip地址与端口。
`struct redisServer {`
`...`
`char* server.masterhost;`
`int masterport;`
`...`
`}`
当redis接受了slaveof ip port
指令后,就将ip及port写到以上两个域当中。并且执行server.repl_state = REPL_STATE_CONNECT
。
尔后在replicationCron函数当中根据根据以上操作,执行connect操作。具体如下:
`replicationCron()`
`-->connectWithMaster()`
`-->anetTcpNonBlockBestEffortBindConnect()`
`-->aeCreateFileEvent()`
`replicationSendAck()`
replicationCron()
是Redis惟一的定时事件的处理器,每秒被触发一次。所以如果从与主断掉了,从每秒会尝试重新一次。
但是会尝试多久,这个还需要再验证。
每次执行时,会检查是否需要连接master,如果需要就会调用connectWithMaster()
。
connectWithMaster()
使用非阻塞connect()
去连接master,并且在connect()
返回后马上绑定了syncWithMaster()
。
这个地方很精妙,也很体现Redis作者功力的地方。原本我以为syncWithMaster只有在连接成功后才会被绑定到描述符上,但事实并不是这样。
connect()
因为是非阻塞式的,所以我们没有办法立即得知是否连接成功了,但是当connect()
的结果确定后,内核会反映到描述符上,如果成功则会产生可写事件,如果失败就会产生可读、可写事件。这个时候利用getsockopt()
就可以判断connect()
是否成功了。
这个地方还有一个注意事项,非阻塞式connect()
会立即返回-1,但是fd是由sock()
创建的,所以这个时候并不是说fd可以用来与远程服务器通信,但是我们的确可以用他来与内核通信,但是这个fd并不会出现在/proc文件系统里。
aeCreateFileEvent()
会将anetTcpNonBlockBestEffortBindConnect()
产生的fd及可读、可写事件与syncWithMaster()
绑定到一起,当fd产生可读可写事件时,syncWithMaster()
执行,如果连接失败,那么这个情况就会在syncWithMaster()
被处理掉。