前面的2篇文章分别介绍了Redis主从同步源码浅析-Master端 以及 Redis主从同步源码浅析-Slave端 相关的代码实现,从中我们可以看出redis主从同步的一个最大的缺点,也是阻碍大数据应用的地方便是其每次连接端开都需要重连master进行全量数据的重新同步,这个代价是可想而知的。
长连接断开在线上环境中出现得很频繁,如果需要重新同步所有RDB文件,几十G的文件,从建立RDB快照,发送文件内容到slave,然后slave执行命令一一加载进内存中,这个时间开销估计也得好几个小时,更别说树形结构的master->slave->slave, 对网卡的压力,对服务器的压力都是很恐怖的。从这方面来说,动辄几个小时甚至一天的修复时间,没人敢用Redis主从同步在生产环境中使用。
但是福音来了:即将(2013年第三季度)发布的2.8版本会解决这个问题,通过:Replication partial resynchronization 的方式,也就是部分重新同步,这里就说部分同步吧,注意不是常规情况下的新写入指令同步。
具体的增量同步功能请看作者在刚开始的想法(Designing Redis replication partial resync) 和 中间的(Partial resyncs and synchronous replication.) 以及最后的想法(PSYNC),从这里可以知道redis的部分同步功能很详细的解说。所以就不多说了,只是下面简单总结一下方便后面分析代码。
注意本文列出的代码是目前的最新代码,不是2.8版本的代码·https://github.com/antirez/redis
零、Partial Resynchronization介绍
为了避免每次重连都需要重新全量同步RDB文件,redis采用类似mysql的backlog的方式,允许slave在一定的时间内进行部分同步,只同步自己需要的部分回去,已经有的不需要同步了。注意如果重启了,那还是得重新同步,这个其实也有点悲剧,不知道后续会不会加入这个功能,实现也不太难的。
简单来讲,用口语就是:
- 对于slave :master兄,我刚刚走神了断了连接,得重新找你同步一下。如果你还是昔日的那个replrunid, 我刚才同步到的位置是这里reploff,如果还有机会请把我落下的数据马上发给我一下;否则请给我全部RDB文件;
- 对于master: slave们,如果你断了连接,请最好给我你刚才记着的runid和你算的同步到的位置发送给我,我看看是不是可以只让你同步你落下的部分;否则你得全量同步RDB文件。
根据这个设计,可想而知,master必须记住一定数目的backlog,也就是记住一段时间内的发送给slave们的命令列表,以及其起始,结束为止。slave必须在连接端开的时候记着自己同步到了什么位置,重连的时候用这位置去问master,自己是否还有机会赶上来。
一、SLAVE发起部分同步请求
大部分跟之前的2.6版本同步差不多:
- 标记server.repl_state为 REDIS_REPL_CONNECT状态;
- replicationCron定时任务检测到调用connectWithMaster函数连接master;
- slave连接成功调用syncWithMaster,发送PING指令;
- slave发送SYNC指令通知master做RDB快照;
- 接收master的RDB快照文件;
- 加载新数据;
在2.8版本部分同步的时候,将上面的第4步修改了,加入了发送PSYNC指令尝试部分同步的功能。调用slaveTryPartialResynchronization函数尝试部分同步,如果发现master不认识这个指令,那就没办法了,再次发送SYNC进行全量同步。
1 |
void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) { |
8 |
psync_result = slaveTryPartialResynchronization(fd); |
9 |
if (psync_result == PSYNC_CONTINUE) { |
10 |
redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Master accepted a Partial Resynchronization." ); |
17 |
if (psync_result == PSYNC_NOT_SUPPORTED) { |
18 |
redisLog(REDIS_NOTICE, "Retrying with SYNC..." ); |
19 |
if (syncWrite(fd, "SYNC\r\n" ,6,server.repl_syncio_timeout*1000) == -1) { |
20 |
redisLog(REDIS_WARNING, "I/O error writing to MASTER: %s" , |
slaveTryPartialResynchronization是像master发送PSYNC指令的地方。PSYNC指令的语法为:PSYNC runid psync_offset 。下面解释一下2个参数的含义。
runid就是master告诉slave的一串字符串,用来记录master的实例,避免master重启后,同步错误的情况,这个值是master在slave第一次同步的时候告诉他的,且一直不变直到master重启;
psync_offset这个参数就是slave当前同步到的数据位置,实际上是同步了多少数据,以字节为单位。master根据这个来决定是否可以增量同步以及发送哪些数据给slave。第一次同步的时候master会告诉他的。以后slave每次收到从master过来的连接后,都会增加读取的数据长度 到这个值,保存在c->reploff上面。
下面是发送PSYNC指令的代码。
1 |
int slaveTryPartialResynchronization( int fd) { |
6 |
if (server.cached_master) { |
7 |
psync_runid = server.cached_master->replrunid; |
8 |
snprintf(psync_offset, sizeof (psync_offset), "%lld" , server.cached_master->reploff+1); |
9 |
redisLog(REDIS_NOTICE, "Trying a partial resynchronization (request %s:%s)." , psync_runid, psync_offset); |
11 |
redisLog(REDIS_NOTICE, "Partial resynchronization not possible (no cached master)" ); |
13 |
memcpy (psync_offset, "-1" ,3); |
17 |
reply = sendSynchronousCommand(fd, "PSYNC" ,psync_runid,psync_offset,NULL); |
收到PSYNC指令后,master如果觉得可以进行增量同步,则会返回”+CONTINUE”, 如果必须进行全量同步,会返回”+FULLRESYNC”, 否则ERROR,这里具体待会介绍master的时候介绍。
1.只能进行全量同步
来看看如果必须进行全量同步的情况,这种情况下master会返回”+FULLRESYNC runid offset” 给slave。虽然得全量,但是还会告诉slave runid是多少,以及当前master的backlog offset位置,这样让slave下回来同步的时候能够进行部分同步。也算是互相沟通一下状态。
slave收到”+FULLRESYNC”结果后,会将runid保存到server.repl_master_runid上面,backlog offset位置放在server.repl_master_initial_offset里面。以便后面使用部分同步功能。读取完RDB文件后会设置到server.master->reploff上的。
注意PSYNC如果只能进行全量同步,master自己会做RDB快照的,不需要再次发送SYNC。看下面的代码:
1 |
int slaveTryPartialResynchronization( int fd) { |
3 |
if (! strncmp (reply, "+FULLRESYNC" ,11)) { |
4 |
char *runid = NULL, *offset = NULL; |
8 |
runid = strchr (reply, ' ' ); |
11 |
offset = strchr (runid, ' ' ); |
14 |
if (!runid || !offset || (offset-runid-1) != REDIS_RUN_ID_SIZE) { |
15 |
redisLog(REDIS_WARNING, |
16 |
"Master replied with wrong +FULLRESYNC syntax." ); |
21 |
memset (server.repl_master_runid,0,REDIS_RUN_ID_SIZE+1); |
23 |
memcpy (server.repl_master_runid, runid, offset-runid-1); |
24 |
server.repl_master_runid[REDIS_RUN_ID_SIZE] = '\0' ; |
25 |
server.repl_master_initial_offset = strtoll(offset,NULL,10); |
26 |
redisLog(REDIS_NOTICE, "Full resync from master: %s:%lld" , |
27 |
server.repl_master_runid, |
28 |
server.repl_master_initial_offset); |
31 |
replicationDiscardCachedMaster(); |
33 |
return PSYNC_FULLRESYNC; |
38 |
void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask) { |
40 |
server.master->reploff = server.repl_master_initial_offset; |
41 |
memcpy (server.master->replrunid, server.repl_master_runid, |
42 |
sizeof (server.repl_master_runid)); |
2.可以进行部分同步
如果master返回”+CONTINUE”,那就可以进行部分同步。这个比较简单,继续接收后面的数据 就行了。
3.发生错误
这个时候可能是master是老版本,不认识PSYNC,或者发生其他错误了,那就重新发送SYNC指令进行全量同步就行。
到这里还剩下几个问题,第一个是如果连接断开了,slave怎么记住master的runid和reploff位置的呢?
这个可以参考replicationCacheMaster,freeClient在断开一个连接的时候,会判断这个是不是master的连接,如果是,会调用replicationCacheMaster,将当前的状态cache住,并且断开跟本slave的下一级slave的连接。
1 |
void replicationCacheMaster(redisClient *c) { |
5 |
server.cached_master = server.master; |
7 |
replicationHandleMasterDisconnection(); |
下一个问题是slave的c->reploff如何保持跟master同步,因为他们必须绝对一致才行。
这个是通过在2端完成,双方只要是发送给对方的指令,都会讲指令的总长度加在offset上面,slave在readQueryFromClient读取连接数据的时候增加这个值。master在replicationFeedSlaves函数里面会调用feedReplicationBacklogWithObject,后者最终调用feedReplicationBacklog,进而调整offset和backlog,这个待会介绍。
1 |
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) { |
4 |
sdsIncrLen(c->querybuf,nread); |
5 |
c->lastinteraction = server.unixtime; |
6 |
if (c->flags & REDIS_MASTER) c->reploff += nread; |
到这里slave部分介绍完毕。下一部分master端。
二、MASTER接收处理PSYNC指令
在master端,SYNC和PSYNC的处理函数都是syncCommand。只是增量了一段代码检测PSYNC指令,如果是,就会调用masterTryPartialResynchronization尝试部分同步,如果不能进行部分同步,那就按照SYNC的方式处理,也就是进行全量同步,这个请参考“Redis主从同步源码浅析-Slave端”。
1 |
void syncCommand(redisClient *c) { |
12 |
if (!strcasecmp(c->argv[0]->ptr, "psync" )) { |
13 |
if (masterTryPartialResynchronization(c) == REDIS_OK) { |
14 |
server.stat_sync_partial_ok++; |
17 |
char *master_runid = c->argv[1]->ptr; |
23 |
if (master_runid[0] != '?' ) server.stat_sync_partial_err++; |
29 |
c->flags |= REDIS_PRE_PSYNC_SLAVE; |
masterTryPartialResynchronization函数处理部分同步的检查。
首先检查runid是否匹配,如果不匹配那说明master重启过了,必须全量,调转到goto need_full_resync;
如果psync_offset 介于server.repl_backlog_off 和server.repl_backlog_off + server.repl_backlog_size 之间的话,那说明slave已经同步到的位置正好在我么的backlog之间,那说明他落下的东西master是记录在backlog里面的!good,可以进行增量同步。
1 |
int masterTryPartialResynchronization(redisClient *c) { |
2 |
long long psync_offset, psync_len; |
3 |
char *master_runid = c->argv[1]->ptr; |
10 |
if (strcasecmp(master_runid, server.runid)) { |
12 |
if (master_runid[0] != '?' ) { |
13 |
redisLog(REDIS_NOTICE, "Partial resynchronization not accepted: " |
14 |
"Runid mismatch (Client asked for '%s', I'm '%s')" , |
15 |
master_runid, server.runid); |
17 |
redisLog(REDIS_NOTICE, "Full resync requested by slave." ); |
19 |
goto need_full_resync; |
23 |
if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) != |
24 |
REDIS_OK) goto need_full_resync; |
25 |
if (!server.repl_backlog || |
26 |
psync_offset < server.repl_backlog_off || |
27 |
psync_offset >= (server.repl_backlog_off + server.repl_backlog_size)) |
30 |
redisLog(REDIS_NOTICE, |
31 |
"Unable to partial resync with the slave for lack of backlog (Slave request was: %lld)." , psync_offset); |
32 |
goto need_full_resync; |
下面进行增量同步的工作包括:将这个连接加到server.slaves里面,然后给slave发送”+CONTINUE\r\n”告诉他“没事,你还可以赶得上”,然后使用addReplyReplicationBacklog把他落下的部分数据放到他的发送缓冲区中。
5 |
c->flags |= REDIS_SLAVE; |
6 |
c->replstate = REDIS_REPL_ONLINE; |
7 |
c->repl_ack_time = server.unixtime; |
8 |
listAddNodeTail(server.slaves,c); |