前一篇文章写了下redis主从同步的server端代码,这里补一下slave端的。
简单来讲,看了master端就知道slave端的代码大概流程了:
- 中断跟本slave的下一级slave的连接,强迫其重连SYNC;
- 给master发送PING确认其状态是否OK;
- 发送SYNC要求master做RDB快照(2.8版本以上会有PSYNC的指令,也就是部分同步,下回介绍。);
- 接收RDB文件大小;
- 接收RDB文件;
- emptyDb()清空当前数据库,rdbLoad()重新加载新的RDB文件;
- 按需startAppendOnly,然后接收master过来的累积和实时更新数据;
下面分别介绍这些步骤。
零、slave初始化-启动同步流程
redis搭建slave比较简单,有2种方式,第一种是在配置文件中指定:
这样在redis启动加载配置文件后,会设置server.masterhost等信息,同时会设置server.repl_state = REDIS_REPL_CONNECT; 这样redis会在serverCrond定时任务的后面会隔一秒调用replicationCron函数,从而开始跟master的连接;
第二种方式为启动后用上面一样的指令设置master信息,这格式化会中断跟之前的master的信息,重新跟新的master建立连接,重新SYNC数据。处理函数为:slaveofCommand。
1 | void slaveofCommand(redisClient *c) { |
2 | if (!strcasecmp(c->argv[1]->ptr, "no" ) && !strcasecmp(c->argv[2]->ptr, "one" )) { |
3 | if (server.masterhost) { |
8 | if ((getLongFromObjectOrReply(c, c->argv[2], &port, NULL) != REDIS_OK)) |
11 | if (server.masterhost && !strcasecmp(server.masterhost,c->argv[1]->ptr) |
12 | && server.masterport == port) { |
13 | redisLog(REDIS_NOTICE, "SLAVE OF would result into synchronization with the master we are already connected with. No operation performed." ); |
14 | addReplySds(c,sdsnew( "+OK Already connected to specified master\r\n" )); |
19 | sdsfree(server.masterhost); |
20 | server.masterhost = sdsdup(c->argv[1]->ptr); |
21 | server.masterport = port; |
22 | if (server.master) freeClient(server.master); |
25 | cancelReplicationHandshake(); |
28 | server.repl_state = REDIS_REPL_CONNECT; |
29 | redisLog(REDIS_NOTICE, "SLAVE OF %s:%d enabled (user request)" , server.masterhost, server.masterport); |
31 | addReply(c,shared.ok); |
这里当redis收到slaveof命令后,会中断跟目前的master建立的连接,然后会调用disconnectSlaves中断我自己的下一级slave,因为redis支持树形slave机制,类似mysql。
redis主从支持树形结构,所以这里需要先断开跟本slave的slave们的连接,让他们重连.这里需要关注重连后,新的数据如何同步的问题,比如我拿到RDB文件后,我需要将其复制一份给我的从库们. 实现的方式是让从库们重新发起sync指令,当然此时估计他们sync后的数据为空。
这里有个疑问,如果是树形的架构,正在同步数据的从库连接被断开,1秒后重新尝试连接,然后重新发送PING,SYNC,同步RDB文件,又重新建立了连接,这样是不是就悲剧了 ?
- 如果我自己slave-serve-stale-data 设置为off了,那么此时断开连接的我的二级slave们给我发送PING,SYNC指令的时候,我是不会处理的,只有info,slaveof等命令会处理,这样我的slave们无法同步成功,会因为我拒绝而在syncWithMaster里面因为阻塞读取一行”+PONG\r\n”失败而失败,再次进入REDIS_REPL_CONNECT状态尝试跟我建立连接。直到我的状态切换为REDIS_REPL_CONNECTED为止 。这种情况下没啥大问题顶多slave无法服务而已。
- 如果我自己的slave-serve-stale-data设置为on了,也就是我在没有跟master同步完RDB文件的过程中,还可以接受各种命令的,这个在processCommand里面检测的。那么也就可以接受下面我断开的这些slave的重连请求,包括PING,SYNC ! 这样他们又要求我做RDB快照,而且我真的去做快照,做完还发送给他们是不是悲剧了? 问题在于我还没有跟我的master同步完RDB数据的时候,我是否应该叫我的slave们立即跟我重新同步。这种情况是不是就悲剧了.额,不对,刚才测试了一下,这种情况不会发生,因为syncCommand函数开头检查了一下我自己的状态是不是在REDIS_REPL_CONNECTED,不在的话我是不会接收SYNC命令的。所以我的slave们不能立即SYNC成功,直到我自己的同步搞定了为止。否则收到”Can’t SYNC while not connected with my master”而一直报错。
上面slaveofCommand关键的代码是这一行:server.repl_state = REDIS_REPL_CONNECT;设置这个标志后replicationCron会每秒检查server.repl_state的状态进行相应的操作。如果是REDIS_REPL_CONNECT,就会调用connectWithMaster去异步连接master.
1 | void replicationCron( void ) { |
5 | if (server.repl_state == REDIS_REPL_CONNECT) { |
6 | redisLog(REDIS_NOTICE, "Connecting to MASTER..." ); |
7 | if (connectWithMaster() == REDIS_OK) { |
8 | redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync started" ); |
来看看connectWithMaster的代码,挺简单的,就进行非阻塞的连接,设置连接的读写事件为syncWithMaster, 服务器状态server.repl_state 为 REDIS_REPL_CONNECTING。 这样如果连接成功,会调用syncWithMaster函数。
1 | int connectWithMaster( void ) { |
4 | fd = anetTcpNonBlockConnect(NULL,server.masterhost,server.masterport); |
6 | redisLog(REDIS_WARNING, "Unable to connect to MASTER: %s" , |
11 | if (aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL) == AE_ERR) { |
13 | redisLog(REDIS_WARNING, "Can't create readable event for SYNC" ); |
17 | server.repl_transfer_lastio = server.unixtime; |
18 | server.repl_transfer_s = fd; |
19 | server.repl_state = REDIS_REPL_CONNECTING; |
继续走syncWithMaster,syncWithMaster其实是个状态机,从发送PING,发送SYNC,等待结果,一个个处理。
一、发送PING消息
发送PING消息为了简单,redis是调用syncWrite同步阻塞发送的,发送完后将server.repl_state 设置为 REDIS_REPL_RECEIVE_PONG;也就是等待PING的状态。下次连接可读、写的时候会调用本函数去读取PING的结果。
1 | void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) { |
8 | if (server.repl_state == REDIS_REPL_CONNECTING) { |
9 | redisLog(REDIS_NOTICE, "Non blocking connect for SYNC fired the event." ); |
12 | aeDeleteFileEvent(server.el,fd,AE_WRITABLE); |
13 | server.repl_state = REDIS_REPL_RECEIVE_PONG; |
16 | syncWrite(fd, "PING\r\n" ,6,100); |
22 | if (server.repl_state == REDIS_REPL_RECEIVE_PONG) { |
28 | aeDeleteFileEvent(server.el,fd,AE_READABLE); |
32 | if (syncReadLine(fd,buf, sizeof (buf), server.repl_syncio_timeout*1000) == -1) { |
33 | redisLog(REDIS_WARNING, "I/O error reading PING reply from master: %s" , strerror ( errno )); |
后面如果需要进行AUTH验证,就会给服务器发送AUTH指令验证身份:sendSynchronousCommand(fd,”AUTH”,server.masterauth,NULL);
二、发送SYNC请求RDB 快照
这个通过syncWrite发送一条SYNC指令过去,然后准备一个临时文件打开接收数据,将连接的可读事件设置为readSyncBulkPayload就行了。然后守卫工作将server.repl_state 这个小状态机设置为REDIS_REPL_TRANSFER,也就是准备接收RDB文件过程中。
3 | if (syncWrite(fd, "SYNC\r\n" ,6,server.repl_syncio_timeout*1000) == -1) { |
4 | redisLog(REDIS_WARNING, "I/O error writing to MASTER: %s" , |
11 | snprintf( tmpfile ,256, "temp-%d.%ld.rdb" ,( int )server.unixtime,( long int )getpid()); |
12 | dfd = open( tmpfile ,O_CREAT|O_WRONLY|O_EXCL,0644); |
17 | redisLog(REDIS_WARNING, "Opening the temp file needed for MASTER <-> SLAVE synchronization: %s" , strerror ( errno )); |
22 | if (aeCreateFileEvent(server.el,fd, AE_READABLE,readSyncBulkPayload,NULL) |
25 | redisLog(REDIS_WARNING, |
26 | "Can't create readable event for SYNC: %s (fd=%d)" , |
31 | server.repl_state = REDIS_REPL_TRANSFER; |
三、接收RDB文件
上面看到了,发送SYNC指令后,跟master的连接的可读事件设置为readSyncBulkPayload了,函数读取master发过来的RDB大小以及文件内容保存到本地文件中,如果读取完毕,那么调用rdbLoad加载文件内容。并考虑重新启动startAppendOnly。这个读取是异步的,所以如果需要,这个过程中redis还是可以处理请求。当然slave-serve-stale-data 得设置为on才行。
先读取RDB文件总大小:
1 | void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask) { |
4 | if (server.repl_transfer_size == -1) { |
5 | if (syncReadLine(fd,buf,1024,server.repl_syncio_timeout*1000) == -1) { |
8 | server.repl_transfer_size = strtol (buf+1,NULL,10); |
然后就可以读取RDB文件内容了,其实就是一堆指令。注意redis为了避免阻塞,每次可读回调只读取16K的数据,然后写入RDB临时文件里面,写到一定大小,默认写死为REPL_MAX_WRITTEN_BEFORE_FSYNC 也就是8M,就进行一次刷磁盘的操作sync();避免到最后一次SYNC的时候直接卡死服务器。
2 | left = server.repl_transfer_size - server.repl_transfer_read; |
3 | readlen = (left < ( signed ) sizeof (buf)) ? left : ( signed ) sizeof (buf); |
4 | nread = read(fd,buf,readlen); |
6 | redisLog(REDIS_WARNING, "I/O error trying to sync with MASTER: %s" , |
7 | (nread == -1) ? strerror ( errno ) : "connection lost" ); |
8 | replicationAbortSyncTransfer(); |
11 | server.repl_transfer_lastio = server.unixtime; |
12 | if (write(server.repl_transfer_fd,buf,nread) != nread) { |
13 | redisLog(REDIS_WARNING, "Write error or short write writing to the DB dump file needed for MASTER <-> SLAVE synchronization: %s" , strerror ( errno )); |
16 | server.repl_transfer_read += nread; |
如果读取完成了,那么就可以加载数据了。
四、rdbLoad()重新加载新的RDB文件
如果文件全部接收完毕,redis会先清空所有数据emptyDb,然后用rdbLoad加载RDB文件到内存中。设置连接为CONNECTED状态
2 | if (server.repl_transfer_read == server.repl_transfer_size) { |
3 | if ( rename (server.repl_transfer_tmpfile,server.rdb_filename) == -1) { |
4 | redisLog(REDIS_WARNING, "Failed trying to rename the temp DB into dump.rdb in MASTER <-> SLAVE synchronization: %s" , strerror ( errno )); |
5 | replicationAbortSyncTransfer(); |
8 | redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Loading DB in memory" ); |
15 | aeDeleteFileEvent(server.el,server.repl_transfer_s,AE_READABLE); |
17 | if (rdbLoad(server.rdb_filename) != REDIS_OK) { |
18 | redisLog(REDIS_WARNING, "Failed trying to load the MASTER synchronization DB from disk" ); |
19 | replicationAbortSyncTransfer(); |
23 | zfree(server.repl_transfer_tmpfile); |
24 | close(server.repl_transfer_fd); |
25 | server.master = createClient(server.repl_transfer_s); |
26 | server.master->flags |= REDIS_MASTER; |
27 | server.master->authenticated = 1; |
28 | server.repl_state = REDIS_REPL_CONNECTED; |
29 | redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Finished with success" ); |
当切换server.repl_state 为 REDIS_REPL_CONNECTED的时候,新来的查询请求就能够被处理了,在processCommand里面就不会过滤非STALE请求,同时本slave也能接受下一级slave的SYNC指令了。
后面redis会附带启动AOF,如果需要的话。
五、总结
redis主从同步代码比较简练,不多,但功能该有的都有,很赞的。下面说点缺点:
- 不能支持增量同步(这个即将发布的2.8版本已经解决,采用backlog的形式);
- 如果系统很大,好几十G的RDB文件,靠一个连接发送RDB文件的话估计得把人耗死,而且更悲剧的问题是:在做RDB快照,以及发送RDB问的过程中,所有客户端的写操作都会记录在内存中,这个对本来内存要求高的redis又增加了负担;
- 另外redis的扩容是个问题,那么大的数据量,加载一次RDB文件得好几个小时,简直无法忍受。
不过关于扩容作者在其博客 里面介绍了可用的方法:presharding。不多说,绝对经典。
另外redis的集群化正在开发中,可用在这里看到redis集群化的进度和概况:Redis cluster Specification (work in progress),代码里面也有最新的相关实现了,过段时间看看。