首页 > Redis, 系统架构 > Redis Slave进行数据备份BGSAVE时可能内存突发跑满

Redis Slave进行数据备份BGSAVE时可能内存突发跑满

2014年4月10日 发表评论 阅读评论 29244次阅读    

前几天突然线上的redis slave 把64G的内存给占用满了,本来机器上的数据正常只会占用55.5%左右35G的内存的。
查看进程情况,当时redis正好在做BGSAVE操作,所以有2个Redis进程存在,初步怀疑是因为这个原因,但是理论上redis的BGSAVE是fork出来的进程,他们刚开始是共用物理内存的(Redis源码学习-AOF数据持久化原理分析(0)),除非主进程有数据修改,其实就是利用了操作系统的COW机制,巧妙的对redis做了一个数据快照。但明显线上系统不可能短时间内触发大部分数据的修改,所以排除这个的原因。

然后查看redis的日志发现了问题:

[2187] 08 Apr 17:11:49 * MASTER <-> SLAVE sync started //由于网络抖动,到master的连接断开,需要重连接
[2187] 08 Apr 17:11:49 * Non blocking connect for SYNC fired the event.
[2187] 08 Apr 17:28:23 * MASTER <-> SLAVE sync: receiving 35120836844 bytes from master //开始去接收35G的数据到本地;
[2187] 09 Apr 00:10:19 * 1 changes in 43200 seconds. Saving...      //悲剧的事还么有SYNC完,就正好撞到了一次BGSAVE备份操作
[2187] 09 Apr 00:10:20 * Background saving started by pid 20456
[2187] 09 Apr 00:10:24 * MASTER <-> SLAVE sync: Loading DB in memory        //备份操作还没完成的时候,从master的数据读取完毕,准备要加载数据到内存了。,,这个时候就出问题了。
[2187] 09 Apr 00:45:42 * MASTER <-> SLAVE sync: Finished with success    //完成SYNC,但此时机器物理内存占用满了,系统进入频繁的swap的颠簸期
[20456] 09 Apr 04:45:07 * DB saved on disk
[2187] 09 Apr 04:45:14 * Background saving terminated with success //4个小时才备份完,本来只需要10分支的。

从上面可以看出,本来很正常的BGSAVE被主从同步给搞垮了, 因为SYNC主从同步会彻底删掉内存中的数据,然后加载从master读取回来的整个RDB文件,这个操作是非常重的,可能会阻塞花费几十分钟甚至上小时。

看一下代码就知道了,具体看这里:(Redis主从同步源码浅析-Slave端):

void readSyncBulkPayload(aeEventLoop *el, int fd, void *privdata, int mask) {
//读取master发过来的RDB大小以及文件内容保存到本地文件中;
//如果读取完毕,那么调用rdbLoad加载文件内容。并考虑重新启动startAppendOnly
    if (server.repl_transfer_read == server.repl_transfer_size) {//看看是否文件全部接收完毕,如果完毕,GOOD
        if (rename(server.repl_transfer_tmpfile,server.rdb_filename) == -1) {
            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: Loading DB in memory");
        signalFlushedDb(-1);
        emptyDb();//清空整个数据库,这个操作非常重,如果当前正在做BGSAVE,那么会导致快照的COW写时复制机制失效,严重耗费物理内存。
        /* 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);//上面注释说了,避免循环进入。
        if (rdbLoad(server.rdb_filename) != REDIS_OK) {//开始加载RDB文件到内存数据结构中,这个要花费不少时间的。
            redisLog(REDIS_WARNING,"Failed trying to load the MASTER synchronization DB from disk");
            replicationAbortSyncTransfer();
            return;
        }
        /* Final setup of the connected slave <- master link */
        zfree(server.repl_transfer_tmpfile);
        close(server.repl_transfer_fd);
        server.master = createClient(server.repl_transfer_s);//重新注册可读事件毁掉为readQueryFromClient
        server.master->flags |= REDIS_MASTER;
        server.master->authenticated = 1;
        server.repl_state = REDIS_REPL_CONNECTED;
		//当切换server.repl_state 为 REDIS_REPL_CONNECTED的时候,新来的查询请求就能够被处理了,
		//在processCommand里面就不会过滤非STALE请求,同时本slave也能接受下一级slave的SYNC指令了。
        redisLog(REDIS_NOTICE, "MASTER <-> SLAVE sync: Finished with success");
//···
    }

这里面有2个很重的操作:emptyDb() 和 rdbLoad(server.rdb_filename), 前者会清空整个数据库,这样势必导致会扫遍所有申请的物理内存并释放;后者加载整个RDB文件就不用说了,重新申请内存,并且一定会申请那么多物理内存的,因为会访问。

本来巧妙的BGSAVE快照能利用COW, 但此时却被emptyDb 和rdbLoad 给失效了,从而导致本来只占用55%物理内存的redis这下需要110%的物理内存,于是没办法只能swap,引起系统进入颠簸状态。

建议:

理解其实在做SYNC的时候,是不需要做BGSAVE的,因为没用,等SYNC完成后,自然就会将同步回来的RDB文件覆盖BGSAVE的文件的:rename(server.repl_transfer_tmpfile,server.rdb_filename), 所以BGSAVE等于白做了,甚至更严重的还会导致如下时序竞争条件:

1. SYNC将同步回来的RDB文件临时文件rename成server.rdb_filename,并加载到内存;

2.BGSAVE完成备份到临时文件后,又将其过期的老的备份文件覆盖了相对更新的server.rdb_filename文件,从而就给后面挖坑了····。

当然slave做BGSAVE肯定是最安全的,但有SYNC操作在的时候可以不用做BGSAVE的。

Share
分类: Redis, 系统架构 标签:

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