首页 > Redis > Redis从库备份和同步时小概率存在较严重数据错乱的Bug

Redis从库备份和同步时小概率存在较严重数据错乱的Bug

2014年4月11日 发表评论 阅读评论 6904次阅读    

先说一下影响:在主从模式下的从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文件后,准备加载数据库的步骤为:

  1. 将读取回来的临时文件rename放到server.rdb_filename文件名里面;
  2. 然后再清空整个数据库;
  3. 然后调用rdbLoad(server.rdb_filename)将server.rdb_filename 文件加载到内存;
  4. 开始接收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 

Share
分类: Redis 标签:
  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.

注意: 评论者允许使用'@user空格'的方式将自己的评论通知另外评论者。例如, ABC是本文的评论者之一,则使用'@ABC '(不包括单引号)将会自动将您的评论发送给ABC。使用'@all ',将会将评论发送给之前所有其它评论者。请务必注意user必须和评论者名相匹配(大小写一致)。