Redis从库备份和同步时小概率存在较严重数据错乱的Bug
先说一下影响:在主从模式下的从redis如果开启了定期BGSAVE,并且在做SYNC的时候,可能存在数据错乱的问题,目前2.8.8最新稳定版也存在这个bug。
redis的BGSAVE和slaveof触发的同步操作是互不相关的(对于从库),所以就完全有可能同时在进行备份和同步。看一下下面的代码:
/* Check if the transfer is now complete */ if (server.repl_transfer_read == server.repl_transfer_size) {//从master读取了所有的对方的RDB文件,下面可以准备加载2了 if (rename(server.repl_transfer_tmpfile,server.rdb_filename) == -1) {//这个rename操作会触发刷磁盘,改名字,将临时的文件名字改为正常的备份rdb文件,默认为dump.rdb.问题就出现在这里 redisLog(REDIS_WARNING,"Failed trying to rename the temp DB into dump.rdb in MASTER <-> SLAVE synchronization: %s", strerror(errno)); replicationAbortSyncTransfer(); return; } redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Flushing old data"); signalFlushedDb(-1); emptyDb(replicationEmptyDbCallback);//清空整个数据库 /* Before loading the DB into memory we need to delete the readable * handler, otherwise it will get called recursively since * rdbLoad() will call the event loop to process events from time to * time for non blocking loading. */ aeDeleteFileEvent(server.el,server.repl_transfer_s,AE_READABLE); redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Loading DB in memory"); if (rdbLoad(server.rdb_filename) != REDIS_OK) {//从备份文件中加载数据库。关键是:可能这个文件现在已经不是昔日从master读取回来的文件了,因为此时可能正有BGSAVE进程在那无意的给你rename成了它的过期rdb文件! redisLog(REDIS_WARNING,"Failed trying to load the MASTER synchronization DB from disk"); replicationAbortSyncTransfer(); return; } //···· }
上述代码看起来没问题,但就如上面注释所说,slave辛辛苦苦从master读取回来最新的RDB文件后,准备加载数据库的步骤为:
- 将读取回来的临时文件rename放到server.rdb_filename文件名里面;
- 然后再清空整个数据库;
- 然后调用rdbLoad(server.rdb_filename)将server.rdb_filename 文件加载到内存;
- 开始接收master的最新数据;
悲剧的可能就是在上述第一步到跟第三步里面的server.rdb_filename文件可能今非昔比了,因为此时如果有后台的BGSAVE进程由于定期事件触发启动备份后(正好大部分主从都是在从库做备份的),正好此备份程序在1和三之间完成(这中间需要清空所有数据,时间较长),于是BGSAVE进程会覆盖掉server.rdb_filename文件内容·····。
然后再第3步还是继续去加载server.rdb_filename文件到内存,实际上这个文件完全不是刚刚同步回来的文件了。这样数据库的数据就会出现错乱。
复现方式
1. 首先跑一个master进程,用8379端口, 然后启动一个slave进程,用8380端口,如下:
cd /home/wuhaiwen/redis && ./bin/redis-server conf/redis.conf cd /home/wuhaiwen/slave_redis && ./bin/redis-server-2.8 conf/redis.conf
2.分别给主从库刷3G左右数据,这样待会GDB的时候动作来得及。
3. gdb 挂住未来的从库,在备份和同步的地方打1个断点 readSyncBulkPayload:
wuhaiwen@linode:~/slave_redis$ gdb -p 23152 (gdb) break readSyncBulkPayload Breakpoint 1 at 0x42e220: file replication.c, line 734. (gdb) c Continuing.
4.然后另外开一个窗口给从库发送BGSAVE命令,触发他的备份,此时需要立即找到fork出来的新镜像进程,然后gdb挂起它!并在rdbSave 函数的rename调用之前打一个断点,称为断点A:
/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */ int rdbSave(char *filename) { char tmpfile[256]; snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); //扫描每一个数据库,写如临时文件中 /* Make sure data will not remain on the OS's output buffers */ if (fflush(fp) == EOF) goto werr; if (fsync(fileno(fp)) == -1) goto werr; if (fclose(fp) == EOF) goto werr; /* Use RENAME to make sure the DB file is changed atomically only * if the generate DB file is ok. */ if (rename(tmpfile,filename) == -1) {//在这里打一个断点A,待会用来人工控制先后。 redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno)); unlink(tmpfile); return REDIS_ERR; } redisLog(REDIS_NOTICE,"DB saved on disk"); server.dirty = 0; server.lastsave = time(NULL); server.lastbgsave_status = REDIS_OK; return REDIS_OK; }
5. 给从库发送slaveof 127.0.0.1 8379 命令,触发其从master读取数据,并且后来会触发断点readSyncBulkPayload, 从而再将断点打在readSyncBulkPayload 的rename操作位置称之为断点B;
6.接下来只要让断点B先运行完rename,然后再让断点A运行rename,这样文件就会被覆盖,接下来的rdbLoad(server.rdb_filename) 操作就会读到从库备份的无用数据库了。
解决方法
在上一篇文章:“Redis Slave进行数据备份BGSAVE是可能内存突发跑满” 里面说过,由于redis目前允许BGSAVE 和SYNC同时进行,本身就会导致内存可能翻倍的问题。 跟huangz1990 探讨,解决内存翻倍问题的方式就是不允许在做SYNC的时候进行BGSAVE ;
同样的道理,这个数据错乱的bug的方式莫过于 不允许BGSAVE和slave-SYNC同时进行,这样就能很好的解决这个问题。
已经提给开发人员了: [BUG] Slave may mis-overwriten synced server.repl_transfer_tmpfile file with BGSAVE file
@kulv
我们线上用的2.8版本,好像出现了这个问题,所以想问下高版本解决这个问题了吗
redis4.0版本解决这个问题了吗