5. 备份机制
Redis是内存数据库,支持两种方式——snapshot和aof,将数据从内存dump到磁盘。 snapshot是快照式备份,每次将Redis全库dump到磁盘, aof是流水式备份,每次将Redis数据库的修改日志dump到磁盘,并定期整理日志。
5.1. Snapshot
Redis支持Snapshot(快照)式备份。 Redis可以自动检测备份时机,设置每N秒检查,若发生M次以上数据更新操作,则开始Snapshot备份,也可以由客户端发送save或bgsave命令手动启动备份。save是阻塞式备份,备份过程中Redis会停止服务; bgsave是后台备份, Redis将新建一个备份进程负责将全库dump到磁盘。saveCommand()中,首先检查是否有bgsave的后台备份进程,若没有,则执行rdbSave()进行全库备份。bgsaveCommand()中,也会检查是否有bgsave的后台备份进程,若没有,则执行rdbSaveBackground(),启动备份子进程,在后台全库备份。备份子进程最终也是通过rdbSave()备份数据库。rdbSaveBackground()中,首先通过前文所说的waitEmptyIOJobsQueue()检查是否有vm的IO线程在进行数据交换,如果有则阻塞,等待所有vm的IO线程完成工作。然后fork()备份子进程。在父进程中, server.bgsavechildpid记录了备份子进程的pid,并通过updateDictResizePolicy()禁止Hash表自动扩容。在子进程中,如果启用了vm,则打开vm swap文件,然后调用rdbSave()执行备份。因为之前已经检查了所有vm的IO线程是否完成,因此这里打开vm的swap文件并不会和vm的IO线程冲突。
int rdbSaveBackground(char *filename) {
pid_t childpid;
if (server.bgsavechildpid != -1) return REDIS_ERR;
if (server.vm_enabled) waitEmptyIOJobsQueue();
server.dirty_before_bgsave = server.dirty;
if ((childpid = fork()) == 0) {
/* Child */
if (server.vm_enabled) vmReopenSwapFile();
if (server.ipfd > 0) close(server.ipfd);
if (server.sofd > 0) close(server.sofd);
if (rdbSave(filename) == REDIS_OK) {
_exit(0);
} else {
_exit(1);
}
} else {
/* Parent */
if (childpid == -1) {
redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return REDIS_ERR;
}
redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
server.bgsavechildpid = childpid;
updateDictResizePolicy();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
rdbSave()中首先通过waitEmptyIOJobsQueue()检查vm的IO线程,并阻塞直到所有vm的IO线程完成。因为备份过程中,需要备份vm的swap文件,如果有未完成的vm的IO线程,会导致数据不同步。首先建立一个tmp文件,向它写入备份数据。依次循环每个db: server.db,server.db+1, ..., server.db+server.dbnum,通过dict的迭代器dictIterator访问db中的所有数据,依次写入tmp文件。Redis在写备份文件时做了许多优化,例如rdbSaveLen()用于写入长度信息,写入的数字不超过32bit整数。 Redis将长度数字按二进制长度分三类:小于6bit, 6bit至14bit, 14bit至32bit,并用前缀REDIS_RDB_6BITLEN(00)、REDIS_RDB_14BITLEN(01)、 REDIS_RDB_32BITLEN(10)标示数字长度。将前缀与数字一起写入磁盘。这个优化类似于Protobuf里压缩数字的方法。类似的压缩还有很多,再此不一一分析。
int rdbSaveLen(FILE *fp, uint32_t len) {
unsigned char buf[2];
int nwritten;
if (len < (1<<6)) {
/* Save a 6 bit len */
buf[0] = (len&0xFF)|(REDIS_RDB_6BITLEN<<6);
if (rdbWriteRaw(fp,buf,1) == -1) return -1;
nwritten = 1;
} else if (len < (1<<14)) {
/* Save a 14 bit len */
buf[0] = ((len>>8)&0xFF)|(REDIS_RDB_14BITLEN<<6);
buf[1] = len&0xFF;
if (rdbWriteRaw(fp,buf,2) == -1) return -1;
nwritten = 2;
} else {
/* Save a 32 bit len */
buf[0] = (REDIS_RDB_32BITLEN<<6);
if (rdbWriteRaw(fp,buf,1) == -1) return -1;
len = htonl(len);
if (rdbWriteRaw(fp,&len,4) == -1) return -1;
nwritten = 1+4;
}
return nwritten;
}
在备份文件中, Redis写入type/key/value数据,并放弃所有过期的数据。特别地,对于vm中的数据,会先读入内存,再将数据写入备份文件。父进程在serverCron中检查备份进程,如果存在bgsave进程或者bgrewrite进程(AOF备份,下节分析),则wait3()等待进程完成,完成后调用backgroundSaveDoneHandler()或者backgroundRewriteDoneHandler()处理。snapshot式备份中, Redis会停止vm的换入换出操作,停更新LRU策略中的标记信息,但不会影响Redis对内存中的数据的读写。 Redis通过调整snapshot备份的粒度,适应各种应用需求。
5.2. AOF
除了Snapshot备份模式外Redis还支持AOF(流水式)备份,它保存所有操作的commit log,并能自动地进行全库备份和删除无用的commit log。Redis在写commit log时, Redis并不是每次处理请求时都将请求写入磁盘,它会用一段内存Buffer缓存commit log,并在一定时间将缓存中的内容统一写入磁盘。 为了避免磁盘缓存的影响,可以设置三种策略进行fsync():每次写操作后均调用fsync(),每秒调用一次fsync(),从不调用fsync(),三种不同的策略获得不同的性能和一致性。
Redis将所有操作保存在commit log中, commit log的文件是追加写。当commitlog的大小无法承受时,可以手工通过bgrewriteaof命令产生一个快照(这个不同于Snapshot备份),具体构造方式后文分析。
AOF有四种操作:
•feedAppendOnlyFile()-将命令写入AOF。该方法在call()中被调用,用于将
所有Redis处理的命令写入AOF;该方法在Redis发现过期的Keys被调用,记录清除过期Key的操作
call()是Redis中所有命令处理的入口,当启用了AOF且数据有变化时
(server.dirty有变化),将命令用feedAppendOnlyFile()写入AOF,该函数中,将命令编码后写入server.aofbuf,当后台rewriteaof进程存在时,同时将编码后的命令写入server.bgrewritebuf, bgrewritebuf的作用后文解释。
/* Append to the AOF buffer. This will be flushed on disk just before
* of re-entering the event loop, so before the client will get a
* positive reply about the operation performed. */
server.aofbuf = sdscatlen(server.aofbuf,buf,sdslen(buf));
/* If a background append only file rewriting is in progress we want to
* accumulate the differences between the child DB and the current one
* in a buffer, so that when the child process will do its work we
* can append the differences to the new append only file. */
if (server.bgrewritechildpid != -1)
server.bgrewritebuf = sdscatlen(server.bgrewritebuf,buf,sdslen(buf));
•rewriteAppendOnlyFile()-产生快照,并更新aof文件。
rewriteAppendOnlyFile()只在 rewriteAppendOnlyFileBackground()中
被调用。 Redis产生AOF快照主要分为以下几步:
(1) fork()一个rewrite子进程,调用rewriteAppendOnlyFile()产生快照。
(2) 当rewrite进程开始后, rewriteAppendOnlyFile()扫描数据库中所有数
据,将他们编码后写入临时文件。 AOF文件的编码形式为文本,即人肉可读的编码。
(3)rewrite时父进程照常接收请求,并将流水日志写入
server.bgrewriteaofbuf中。
(4)子进程完成工作,父进程在serverCron()中通过wait3()获知其状态后,调用
backgroundRewriteDoneHandler()将server.bgrewriteaofbuf中的内容作为新
的commit log,覆盖原有server.aofbuf。
(5) 父进程将rewrite子进程生成的临时文件改名,作为新的aof文件。用于未来恢
复数据。
Redis的AOF快照机制的基础,是数据库的大小一定小于操作日志的大小,否则产生
的快照文件可能远远超过commit log文件的大小。因此,此处有一点值得考虑,
rewrite过程是否可以只写入aofbuf中改变了的数据的快照?
•flushAppendOnlyFile()-将内存buffer中的commit log写到磁盘上。
Redis在beforeSleep()中,及关闭AOF功能时,将内存buffer中的log写入磁盘。
•loadAppendOnlyFile()-重放AOF文件。该方法在Redis启动时被调用,从AOF
文件中重建数据库。
Redis通过AOF重建时,构造一个虚拟的client(),向自己发送重建的命令。而恢复
数据只需要将AOF快照重新载入数据库,并回放commit log中的操作。