前几天突然线上的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文件,这个操作是非常重的,可能会阻塞花费几十分钟甚至上小时。
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
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的。