
1、为什么需要持久化?
Redis的数据是存放在服务器的内存中,试想Redis的进程异常退出,数据不会写入到磁盘就丢失掉,为了解决这个问题,Redis提供几种选项进行持久化,包括RDB快照,AOF文件,混合RDB和AOF。无论哪种方式,Redis主进程都调用linux库函数fork(),创建一个子进程来进行持久化操作,然后主进程就离开去处理其他请求,详见上图。
2、如何触发RDB持久化?
自动触发
配置文件"3600 1 300 100 60 10000":3600内至少有1个key变更,300秒内至少有100个key变更,60秒内至少有1000个key变更,满足任意条件动触发rdbSaveBackground。
请问如何关闭save参数功能?
可以设置 config set save ""关闭自动触发bgsave的功能。
手动触发
save命令:saveCommand(),主进程会阻塞其他请求来处理数据写入磁盘操作。
bgsave命令:bgsaveCommand(),主进程会fork一个子进程来单独处理数据写入磁盘操作。

3、理解RDB持久化的流程
RDB触发和执行流程图

GDB调试执行bgsave命令的堆栈
redis-cli -p 6379 bgsave #手动执行bgsave
Thread 2 hit Breakpoint 1, rdbSaveBackground (filename=0x100404290 "dump.rdb", rsi=0x0) at Users/wanzhao/CLionProjects/redis/src/rdb.c:13331333 if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;#0 rdbSaveBackground (filename=0x100404290 "dump.rdb", rsi=0x0) at Users/wanzhao/CLionProjects/redis/src/rdb.c:1333#1 0x00000001000407b5 in bgsaveCommand (c=0x101809200) at Users/wanzhao/CLionProjects/redis/src/rdb.c:2505#2 0x0000000100007818 in call (c=0x101809200, flags=15) at Users/wanzhao/CLionProjects/redis/src/server.c:2479#3 0x00000001000086d0 in processCommand (c=0x101809200) at Users/wanzhao/CLionProjects/redis/src/server.c:2781
GDB调试执行写入命令触发rdb的堆栈
redis-benchmark -t set -n 10000 -r 10000 -c 5 #执行写入命令
21649:M 26 Jun 2021 10:46:00.838 * 100 changes in 300 seconds. Saving...
Thread 2 hit Breakpoint 1, rdbSaveBackground (filename=0x100404290 "dump.rdb", rsi=0x0) at Users/wanzhao/CLionProjects/redis/src/rdb.c:13331333 if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;#0 rdbSaveBackground (filename=0x100404290 "dump.rdb", rsi=0x0) at Users/wanzhao/CLionProjects/redis/src/rdb.c:1333#1 0x0000000100004323 in serverCron (eventLoop=0x1004334c0, id=0, clientData=0x0) at Users/wanzhao/CLionProjects/redis/src/server.c:1296#2 0x000000010000e9e0 in processTimeEvents (eventLoop=0x1004334c0) at Users/wanzhao/CLionProjects/redis/src/ae.c:331#3 0x000000010000e58a in aeProcessEvents (eventLoop=0x1004334c0, flags=11) at Users/wanzhao/CLionProjects/redis/src/ae.c:469
4、阅读源码来解析RDB的持久化和加载过程
这里直接给出rdbSaveBackground的代码,因为手动和自动触发rdb持久化都调用该函数。
//后台线程执行rdb的核心函数int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {pid_t childpid;long long start;//如果有正在运行的aof或rdb,返回C_ERR=-1if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;server.dirty_before_bgsave = server.dirty;server.lastbgsave_try = time(NULL);//打开子进程的信息管道,进程之间是不共享内存空间的,通过管道来通信openChildInfoPipe();start = ustime();if ((childpid = fork()) == 0) {//进入子进程分支处理int retval;/* Child */closeClildUnusedResourceAfterFork();redisSetProcTitle("redis-rdb-bgsave");retval = rdbSave(filename,rsi);if (retval == C_OK) {//计算copy on write是占用的内存大小,当父进程被写入新数据时,触发copy-on-writesize_t private_dirty = zmalloc_get_private_dirty(-1);if (private_dirty) {serverLog(LL_NOTICE,"RDB: %zu MB of memory used by copy-on-write",private_dirty/(1024*1024));}server.child_info_data.cow_size = private_dirty;sendChildInfo(CHILD_INFO_TYPE_RDB);}exitFromChild((retval == C_OK) ? 0 : 1);} else {/* Parent *///父进程继续处理//计算fork所花费的时间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) {closeChildInfoPipe();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 %d,%d",childpid,server.stat_fork_rate);server.rdb_save_time_start = time(NULL);server.rdb_child_pid = childpid;server.rdb_child_type = RDB_CHILD_TYPE_DISK;//关闭resize哈希表,因为resize操作会拷贝很多内存updateDictResizePolicy();return C_OK;}return C_OK; /* unreached */}
//将rdb文件加载到内存中int rdbLoad(char *filename, rdbSaveInfo *rsi) {FILE *fp;rio rdb;int retval;//打开文件只读方式if ((fp = fopen(filename,"r")) == NULL) return C_ERR;//初始化load rdb的实例状态startLoading(fp);//初始化一个文件流对象并赋值给fp指针rioInitWithFile(&rdb,fp);//从rio流中加载RDB文件,成功返回C_OKretval = rdbLoadRio(&rdb,rsi,0);fclose(fp);stopLoading();return retval;}
5、做RDB持久化会遇到什么问题呢?
Background saving terminated by signal N
1个Redis节点占用10gb内存,手动执行bgsave命令,ps -ef会看到redis-rdb-bgsave进程号19055,然后执行redis-benchmark -p 6379 -c 20 -t set -n 1000000 -d 2000000 -r 10000写入数据,会发现redis-rdb-bgsave进程因为服务器内存不足oom被操作系统kill:
Redis日志:18486:M 27 Jun 2021 11:56:01.138 * Background saving started by pid 1905518486:M 27 Jun 2021 11:56:05.939 # Background saving terminated by signal 9/var/log/messages日志:Jun 27 11:56:04 iZ2zeetwpf5smee40rgm2zZ kernel: Out of memory: Kill process 18486 (redis-server) score 734 or sacrifice childJun 27 11:56:04 iZ2zeetwpf5smee40rgm2zZ kernel: Killed process 19055 (redis-server), UID 0, total-vm:1741308kB, anon-rss:1387532kB, file-rss:396kB, shmem-rss:0kB
RDB: 137 MB of memory used by copy-on-write
1个Redis节点占用10gb内存,执行bgsave命令时fork操作耗时300ms,这个耗时是阻塞的时间,对于请求量高的情况请求会排队。当父进程被写入数据时,kernel会对需要修改的内存页复制出一份副本给父进程来完成写操作,可以看到copy-on-write时占用内存137 MB。
latest_fork_usec:296251rdb_last_cow_size:144547840
为什么Redis启动时提示要关闭大页(huge pages)?
# WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis。
HugePageSize默认是2M,如果开启大页,copy-on-write期间,当有页面要修改,那么就要拷贝N个2MB的大页面,如果父进程写入量很大,会增加内存复制数量,子进程会消耗更多内存甚至超过used_memory。
Redis实例的碎片率mem_fragmentation_ratio竟然会小于1?
如果启用了swap分区,当服务器内存不足的时候,操作系统把Redis的内存数据交换到磁盘,此时发现used_memory内存竟然大于used_memory_rss,因为磁盘速度太慢了,会严重影响Redis的性能。
Can't save in background: fork: Cannot allocate memory
6、什么场景使用RDB持久化比较好?
实时持久化不适合使用RDB备份,因为我们通过前文知道执行bgsave需要fork子进程,消耗资源较大,频繁执行成本太高。但是RDB的备份压缩率高,恢复数据时load速度很快,可以用于以下场景比较合适:
1、对于业务不经常修改的数据,读请求占比写请求要高;
2、业务使用Redis做为缓存时,当内存达到max_memory数据淘汰后可以接受的场景
参考文档:
Redis5配置:https://raw.githubusercontent.com/redis/redis/5.0/redis.conf




