redis源码学习-持久化rdb篇
0 条评论redis源码学习-持久化rdb篇
redis数据持久化有多种方案,不落盘的方式可以使用主从,落盘的方式有rdb,aof或者rdb+aof
本篇详细介绍rdb持久化方式
rdb处理流程
在redis中,rdb为快照持久化方式。特点比较明显,即在某一时间点将当前数据全部保存下来。
rdb主要也分为两种,主动快照或被动快照。
主动触发
主要通过save与bgsave两条命令。bg指的是background,bgsave为异步的save命令。redis在数据处理层面上依然为单线程,因此简单的save在备份时会导致redis节点不可用,这也是bgsave的由来。无论生产或测试环境都推荐bgsave不使用save,save命令也建议通过rename进行屏蔽。对于线上环境来说,save的危险性不亚于keys *。
被动触发
主要通过conf文件中的save inta intb。指的是每过inta秒,数据变化大于intb时自动调用bgsave。同时save支持多行写入,例如写两行 save 10 20 save 900 1 ,当redis满足任意一条save条件时都会调用bgsave进行持久化。
如上所述rdb的处理流程,可见redis的rdb机制本身是定时备份的大框架。在下一个备份周期到来时,数据的可靠性是没有保障的。
bgsave处理流程
- 判断是否在save的过程中,若则跳过此次
- fork出子进程
- 打开一个临时文件
- 将当前redis的内存信息写入到这个临时文件中
- 将文件写入磁盘中
- 将临时文件改名为正式的RDB文件
- 记录 dirty 和 lastsave等状态
1-7步不为完整的事务状态,当rdb正式改名后redis发生故障,dirty与lastsave数据不对时,仍可通过第四步的rdb文件进行数据的恢复。同时也可看出,每个bgsave的过程中都互不干涉。对于大数据量节点的情况下,较为耗时的为4,5步,若save配置太过频繁,导致redis每时刻都在save的过程中,对系统负载有所影响。但同时save配置频率太低,也会导致数据的不可靠性增加,因此在进行redis扩容或缩容时,也要根据相应的数据量调整redisRDB的参数设置。
rdb源码分析
rio
struct _rio {
/* Backend functions.
* Since this functions do not tolerate short writes or reads the return
* value is simplified to: zero on error, non zero on complete success. */
size_t (*read)(struct _rio *, void *buf, size_t len);
size_t (*write)(struct _rio *, const void *buf, size_t len);
off_t (*tell)(struct _rio *);
int (*flush)(struct _rio *);
/* The update_cksum method if not NULL is used to compute the checksum of
* all the data that was read or written so far. The method should be
* designed so that can be called with the current checksum, and the buf
* and len fields pointing to the new block of data to add to the checksum
* computation. */
void (*update_cksum)(struct _rio *, const void *buf, size_t len);
/* The current checksum and flags (see RIO_FLAG_*) */
uint64_t cksum, flags;
/* number of bytes read or written */
size_t processed_bytes;
/* maximum single read or write chunk size */
size_t max_processing_chunk;
/* Backend-specific vars. */
union {
/* In-memory buffer target. */
struct {
sds ptr;
off_t pos;
} buffer;
/* Stdio file pointer target. */
struct {
FILE *fp;
off_t buffered; /* Bytes written since last fsync. */
off_t autosync; /* fsync after 'autosync' bytes written. */
} file;
/* Connection object (used to read from socket) */
struct {
connection *conn; /* Connection */
off_t pos; /* pos in buf that was returned */
sds buf; /* buffered data */
size_t read_limit; /* don't allow to buffer/read more than that */
size_t read_so_far; /* amount of data read from the rio (not buffered) */
} conn;
/* FD target (used to write to pipe). */
struct {
int fd; /* File descriptor. */
off_t pos;
sds buf;
} fd;
} io;
};
rio为redis对io的一层包装,可以理解为redisio,抽象出read,write等方法,在rdb或aof时通过rio真正对操作系统的io进行操作。
rdbSave
/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
int rdbSave(char *filename, rdbSaveInfo *rsi) {
//临时文件,用于库函数snprintf
char tmpfile[256];
//找到当前工作目录
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
FILE *fp = NULL;
rio rdb;
int error = 0;
//库函数,用于生成临时文件,以temp-开头,整体完成后再进行改名
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
//打开文件
fp = fopen(tmpfile,"w");
//打开失败时写日志,固定格式Failed opening the RDB file......
if (!fp) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Failed opening the RDB file %s (in server root dir %s) "
"for saving: %s",
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
return C_ERR;
}
//初始化rio数据结构,用来与操作系统进行io交互
rioInitWithFile(&rdb,fp);
startSaving(RDBFLAGS_NONE);
//参数判断,通过分批将数据fsync到硬盘,用来缓冲io,建议开启
if (server.rdb_save_incremental_fsync)
rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);
if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
errno = error;
goto werr;
}
/* Make sure data will not remain on the OS's output buffers */
if (fflush(fp)) goto werr;
if (fsync(fileno(fp))) goto werr;
if (fclose(fp)) { fp = NULL; goto werr; }
fp = NULL;
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
//重命名操作,修改temp-*.rdb为真正的.rdb文件
if (rename(tmpfile,filename) == -1) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
//日志写入,固定格式DB saved on disk
serverLog(LL_NOTICE,"DB saved on disk");
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
stopSaving(1);
return C_OK;
werr:
//异常写入日志,固定格式Write error saving DB on disk
serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
if (fp) fclose(fp);
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
rdbSave为真正的落盘操作,在bgsave中被rdbSaveBackground作为子流程完整执行
rdbSaveBackground
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
if (hasActiveChildProcess()) return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
//fock子进程,redis仅作包装,真正的fork还是依托于操作系统
if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
int retval;
/* Child */
//子进程操作
redisSetProcTitle("redis-rdb-bgsave");
redisSetCpuAffinity(server.bgsave_cpulist);
//将rdbSave完整的作为子流程
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB");
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent */
if (childpid == -1) {
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,"Background saving started by pid %ld",(long) childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
return C_OK;
}
return C_OK; /* unreached */
}
主要依托操作系统的fork,在子进程中完整调用rdbSave进行持久化
serverCron(自动rdb持久化核心函数,在server.c中)
核心代码
if (!hasActiveChildProcess() &&
server.rdb_bgsave_scheduled &&
(server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == C_OK))
{
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK)
server.rdb_bgsave_scheduled = 0;
}
可以看到,自动rdb调用的为rdbSaveBackground,在bgsave前会先判断是否正在bgsave,防止出现大量fork子进程导致灾难。
rdb参数
rdb_save_incremental_fsync
yes时redis通过分批将数据fsync到硬盘,用来缓冲io,保证每次fsync数据量在32m以内。建议开启,redis5之后支持
stop-writes-on-bgsave-error
当bgsave快照操作出错时停止写数据到磁盘。建议为no
rdbcompression
是否进行rdb压缩
rdbchecksum
是否检查rdb
相对于数据库来说,rdb的持久化方式相对太过粗糙,fork进程时也总会让人担心是否会造成redis的卡顿,但在数据恢复时相对来说较为友好。相对于aof的方式,rdb的数据安全性得不到更好的保障,在bgsave时也需额外内存进行数据复制。对于线上来说,在意数据安全性的话不建议只开rdb,推荐aof或aof+rdb的方式,至少aof的类binlog记录方式对于故障溯源相对友好。
同时rdb文件的可读性也较差,需通过od -A -x -t xlc -v dump.rdb将rdb文件转化为16进制后才能进行阅读。