关于Redis的主从同步的基本介绍这里有:Replication, 不多介绍了。本文只涉及到主库的代码,从库的相关代码改天补上。
这里主要介绍redis 2.6.13版本代码,目前2.8新增了一些功能,比如增量同步功能等,不过到目前2013-10-05还没有正式上线。总结一下几点跟下面相关的:
- 同步采用类似mysql的操作日志重放方式,将写操作分发到从库重放。
- 每次从库启动必须从主库重新同步一份全量RDB数据文件,因此不能随便停止从库;
- 数据同步采用异步将写操作指令发送给从库的方式进行。
总体来说,redis的同步原理是:slave启动时下载所有数据快照,下载快照过程中产生的新写操作日志会不断累积记录起来,发送完快照后就发送这部分增量日志,日志在slave端进行重放。下面分步讲。
零、从库发送同步指令
redis slave启动需要再从库运行”sync”指令,告诉master需要其进行后台进程保存数据快照,也就是RDB文件以及在这个过程中保存的增量日志发送到slave,SYNC指令的处理函数为syncCommand, 函数首先判断一些基本的条件比如是否允许从库,是否当前还有数据没有发送给slave。
redis为了尽量减少增加slave的时候需要做RDB文件保存的操作,会在SYNC指令处理的时候,判断一下当前是否已经有从库刚刚发送了SYNC指令而启动RDB BGSAVE,是否已经启动了BGSAVE,分如下几种情况:
- 如果有RDB进程在做快照,并且有slave在等待快照完成,那么这个新的slave不需要重新进行做快照,只需要复制前述slave就行;
- 如果有RDB进程在做快照,但没有slave在等待快照完成,也就是所有的slave都已经完成RDB文件保存操作,则需要等待当前这次RDB快照完成后,自己重新启动一次RDB快照,因为这次没来得及保存增量写操作日志。
- 如果没有RDB进程在做快照,那么当前是可以安全启动RDB快照的,那么就调用rdbSaveBackground启动快照。同时会自动记录后续写操作日志。
相关代码比较简单:
1 | void syncCommand(redisClient *c) { |
6 | if (server.rdb_child_pid != -1) { |
14 | listRewind(server.slaves,&li); |
18 | while ((ln = listNext(&li))) { |
20 | if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) break ; |
27 | copyClientOutputBuffer(c,slave); |
28 | c->replstate = REDIS_REPL_WAIT_BGSAVE_END; |
29 | redisLog(REDIS_NOTICE, "Waiting for end of BGSAVE for SYNC" ); |
34 | c->replstate = REDIS_REPL_WAIT_BGSAVE_START; |
35 | redisLog(REDIS_NOTICE, "Waiting for next BGSAVE for SYNC" ); |
39 | redisLog(REDIS_NOTICE, "Starting BGSAVE for SYNC" ); |
40 | if (rdbSaveBackground(server.rdb_filename) != REDIS_OK) { |
41 | redisLog(REDIS_NOTICE, "Replication failed, can't BGSAVE" ); |
42 | addReplyError(c, "Unable to perform background save" ); |
45 | c->replstate = REDIS_REPL_WAIT_BGSAVE_END; |
48 | if (server.repl_disable_tcp_nodelay) |
49 | anetDisableTcpNoDelay(NULL, c->fd); |
51 | c->flags |= REDIS_SLAVE; |
53 | listAddNodeTail(server.slaves,c); |
这就是SYNC指令的代码了,挺简单的,rdbSaveBackground 用fork()的方式启动数据库RDB文件快照保存进程,不是这里的重点,来看看后面的工作。
一、master分发RDB文件给slave
当RDB快照生成完成后,就会由serverCron函数定期轮询检测到,进而调用backgroundSaveDoneHandler函数,后者主要工作就是调用updateSlavesWaitingBgsave去处理SYNC指令发送后,快照生成完成后的事情:分发RDB文件给slave。
updateSlavesWaitingBgsave 函数主要有2个工作:
- 启动RDB文件发送任务;
- 启动之前由于在RDB过程中欠下的SYNC指令,为新的slave再次生成RDB快照。
第一个启动RDB文件发送任务是通过用aeCreateFileEvent注册slave连接的可写事件为sendBulkToSlave函数达到的。如下代码:
1 | void updateSlavesWaitingBgsave( int bgsaveerr) { |
7 | listRewind(server.slaves,&li); |
8 | while ((ln = listNext(&li))) { |
9 | redisClient *slave = ln->value; |
11 | if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) { |
13 | slave->replstate = REDIS_REPL_WAIT_BGSAVE_END; |
14 | } else if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) { |
16 | struct redis_stat buf; |
18 | if (bgsaveerr != REDIS_OK) { |
20 | redisLog(REDIS_WARNING, "SYNC failed. BGSAVE child returned an error" ); |
24 | if ((slave->repldbfd = open(server.rdb_filename,O_RDONLY)) == -1 || |
25 | redis_fstat(slave->repldbfd,&buf) == -1) { |
27 | redisLog(REDIS_WARNING, "SYNC failed. Can't open/stat DB after BGSAVE: %s" , strerror ( errno )); |
31 | slave->repldbsize = buf.st_size; |
33 | slave->replstate = REDIS_REPL_SEND_BULK; |
34 | aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE); |
35 | if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendBulkToSlave, slave) == AE_ERR) { |
剩下的工作就是上面第一部分提到的,如果SYNC命令的时候已经有RDB后台快照进程在工作了,而且没有slave在等待这个快照,那么只能老老实实等待这个RDB保存完毕,之后再启动一个RDB任务。所以在上面一部分轮训所有的slave的时候,会检测到是否有slave在REDIS_REPL_WAIT_BGSAVE_START状态,如果有,会设置startbgsave标记,从而用rdbSaveBackground进行RDB后台快照。
不过目前我拿到的代码中有个bug,就是如下的“if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START)”这一行永远也没法成立,因为在上面一部分已经将slave->replstate = REDIS_REPL_WAIT_BGSAVE_END;了,这应该是错误处理没有测试到的原因,毕竟该代码的条件为:SYNC指令正好碰到已有RDB快照,并且没有其他slave等待, 并且rdbSaveBackground返回ERROR。而后者返回ERROR的情形基本只有fork()失败返回-1的情况。太少见了。
对于这个bug在github已经提了个Issue #1308, https://github.com/antirez/redis/issues/1308, 希望作者看到修复吧。ps: 感谢 huangz1990帮忙确认这个bug。
2 | if (rdbSaveBackground(server.rdb_filename) != REDIS_OK) { |
5 | listRewind(server.slaves,&li); |
6 | redisLog(REDIS_WARNING, "SYNC failed. BGSAVE failed" ); |
7 | while ((ln = listNext(&li))) { |
8 | redisClient *slave = ln->value; |
10 | if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) |
关于slave连接的可写事件处理句柄sendBulkToSlave就不啰嗦了,主要就是读取RDB文件,然后发送内容给slave的工作。先发送一行总RDB文件大小给slave。
然后就是文件内容,发送文件内容是一次16K的方式发送,也就是每个可写事件一次只发送16K数据,避免太多阻塞其他连接。发送完毕后将可写事件处理句柄设置为sendReplyToClient,从而切换到发送这个过程中的累积的日志以及常规的同步。
1 | lseek(slave->repldbfd,slave->repldboff,SEEK_SET); |
2 | buflen = read(slave->repldbfd,buf,REDIS_IOBUF_LEN); |
4 | redisLog(REDIS_WARNING, "Read error sending DB to slave: %s" , |
5 | (buflen == 0) ? "premature EOF" : strerror ( errno )); |
9 | if ((nwritten = write(fd,buf,buflen)) == -1) { |
10 | redisLog(REDIS_VERBOSE, "Write error sending DB to slave: %s" , strerror ( errno )); |
14 | slave->repldboff += nwritten; |
15 | if (slave->repldboff == slave->repldbsize) { |
16 | close(slave->repldbfd); |
19 | aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE); |
20 | slave->replstate = REDIS_REPL_ONLINE; |
21 | if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendReplyToClient, slave) == AE_ERR) { |
25 | redisLog(REDIS_NOTICE, "Synchronization with slave succeeded" ); |
二、增量数据同步给slave
slave初始化时,RDB快照文件发送给slave后,slave就可以处理query请求了,但是后续的主库写操作如何同步给slave呢?答案是类似AOF的方式:增量写操作分发给slave重放。
这里有2类增量写操作:
RDB快照过程中的写操作, RDB文件发送过程中的写操作 和 常规的分发后的写操作。
其实这三类处理方式都一样,都是通过在master上及时的将期间的写入操作指令保存起来,到时候发送给slave重新运行一下。
其实现方式是在redis的call()函数每处理完一条指令,如果是写入指令,就会调用propagate()函数,从其名称即可知道其功能是“分发”的意思。propagate有2个工作,一个是保存AOF的增量日志,另外一个就是尽心slave的增量日志保存。
1 | void propagate( struct redisCommand *cmd, int dbid, robj **argv, int argc, |
5 | if (server.aof_state != REDIS_AOF_OFF && flags & REDIS_PROPAGATE_AOF) |
6 | feedAppendOnlyFile(cmd,dbid,argv,argc); |
9 | if (flags & REDIS_PROPAGATE_REPL && listLength(server.slaves)) |
10 | replicationFeedSlaves(server.slaves,dbid,argv,argc); |
replicationFeedSlaves函数进行实际上的slave增量日志暂存操作,暂存的命令放在c->buf或者c->reply列表上,暂时不会发送给客户端,因为可写事件句柄不会发送这上面的代码的,知道RDB文件发送完毕。其内容比较简单,就是讲客户端发送的指令重新拼接成字符串命令,然后追加到slave连接的暂存缓冲区中,待机发送。
1 | void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) { |
8 | listRewind(slaves,&li); |
9 | while ((ln = listNext(&li))) { |
10 | redisClient *slave = ln->value; |
14 | if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) continue ; |
19 | if (slave->slaveseldb != dictid) { |
22 | if (dictid >= 0 && dictid < REDIS_SHARED_SELECT_CMDS) { |
23 | selectcmd = shared.select[dictid]; |
24 | incrRefCount(selectcmd); |
26 | selectcmd = createObject(REDIS_STRING, |
27 | sdscatprintf(sdsempty(), "select %d\r\n" ,dictid)); |
29 | addReply(slave,selectcmd); |
30 | decrRefCount(selectcmd); |
31 | slave->slaveseldb = dictid; |
33 | addReplyMultiBulkLen(slave,argc); |
34 | for (j = 0; j < argc; j++) |
35 | addReplyBulk(slave,argv[j]); |
另外replicationFeedSlaves还可能在键过期的时候调用,这是因为redis为了保持一致性,所有键的过期都是通过master进行的。键过期的时候,master会组成一调DEL 指令,调用replicationFeedSlaves发送给所有slave删除键。
暂时写到这里,后面再写一个2.8版本的增量同步代码,然后写一个redis 集群化相关的代码学习吧。