前言
Redis持久化RDB(Redis Database Backup)将所有数据在特定时刻以一种形式写入到一个专用的二进制文件中,通常给人感觉是“全量拷贝”。
RDB实现原理
我们一起来解析代码rdb.c,Redis通过定时或者规则触发fork子进程,子进程把当前数据拷贝到RDB文件,主进程继续服务客户端,避免阻塞,这是通过rdbSaveBackground函数实现的。
RDB系统过程
- 首先进入rdbSaveBackground,首先检查是否已有子进程运行
if (hasActiveChildProcess()) return C_ERR;
如果Redis当前已有子进程,就不再fork,会直接返回失败返回。
- 记录当前dirty和启动时间
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
dirty是什么,它其实是Redis的写操作计数器(dirty keys),主要用来判断bgsave是否真正保存了数据。
3.创建父子进程管道(info pipe)
openChildInfoPipe();
为父子进程共享 COW(copy-on-write)信息提供通道。
4.调用fork创建子进程
if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
redisFork是对fork的封装,CHILD_TYPE_RDB表示本次子进程主要作用是RDB保存。
redisFork的定义如下:
int childpid;
long long start = ustime();
if ((childpid = fork()) == 0) {
/* Child */
server.in_fork_child = purpose;
setOOMScoreAdj(CONFIG_OOM_BGCHILD);
setupChildSignalHandlers();
updateDictResizePolicy();
closeClildUnusedResourceAfterFork();
} else {
/* Parent */
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
return -1;
}
}
return childpid;
}
- 子进程逻辑(childpid == 0)
redisSetProcTitle("redis-rdb-bgsave");
redisSetCpuAffinity(server.bgsave_cpulist);
retval = rdbSave(filename,rsi);
redisSetProcTitle(“redis-rdb-bgsave”)就是用于修改Redis运行时进程名(process title)函数,目的是在系统工具(ps,htop,top,pgrep等)中更直观地看到Redis当前的运行状态或者任务。我们来看看它的源码。
void redisSetProcTitle(char *title) {
// 编译时是否开启启用了 USE_SETPROCTITLE(宏定义,通常依赖于系统支持)来决定是否执行 setproctitle()
#ifdef USE_SETPROCTITLE
char *server_mode = "";
if (server.cluster_enabled) server_mode = " [cluster]";
else if (server.sentinel_mode) server_mode = " [sentinel]";
// <进程类型> <绑定地址>:<端口号> [cluster|sentinel]
setproctitle("%s %s:%d%s",
title,
server.bindaddr_count ? server.bindaddr[0] : "*",
server.port ? server.port : server.tls_port,
server_mode);
#else
UNUSED(title);
#endif
}
这样,如果调用redisSetProcTitle(“redis-rdb-bgsave”),子进程会显示为redis-rdb-bgsave *:6379。
那么,redisSetCpuAffinity(server.bgsave_cpulist)则是设置子进程的CPU的亲和性。
retval = rdbSave(filename,rsi)执行实际的保存逻辑。
紧接着
if (retval == C_OK) {
sendChildCOWInfo(CHILD_TYPE_RDB, "RDB");
}
exitFromChild((retval == C_OK) ? 0 : 1);
保存成功后收集内存copy-on-write信息。那么,父进程逻辑在做什么呢?我们继续往下看。
serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
updateDictResizePolicy();
return C_OK;`
父进程会记录子进程 pid,更新一些状态变量,同时继续提供服务,等待 SIGCHLD 处理子进程退出回调。
难点:怎么检测父子进程的COW?
首先,我们要理解一点,COW(copy-on-write)是什么时候发生的,通过代码我们能看到它其实发生在fork调用之后,父子进程共享同一块内存,但是一旦某个进程(通常是父进程)对这块共享内存执行“写操作”,内核就会给它单独复制一份,这个内存被称为COW页面。代码中调用fork创建子进程开始共享父进程内存。同时,父进程继续处理客户端请求,比如。
SET key foo
HSET hash field value
RPUSH list item
那么这些写操作会触发COW,导致对应的页被复制一份给父进程。子进程则继续保持原样执行rdbSave(),读取原始数据写入RDB文件。所以在子进程执行sendChildCOWInfo其实是在子进程即将推出时调用,其目的是用 zmalloc_get_private_dirty(-1) 检测本进程产生了多少“私有页”(即 COW 页),把这些信息记录到日志,然后sendChildInfo(ptype) 把这些信息发送给父进程。
zmalloc_get_private_dirty 是怎么检测 COW 的?
在Linux中,它是读取/proc/self/smaps,统计字段如下:
Private_Dirty: xxx kB
举个例子,如果你的Redis日志中看到如下
RDB: 38 MB of memory used by copy-on-write。说明这次bgsave共触发了38MB内存的COW。这部分内存实际上是子进程复制出来的,不再与父进程共享。