Redis 持久化 RDB 原理

一. RDB 简介

Redis的RDB是用来将redis内存数据持久化到磁盘的一种机制,以此来防止redis数据全量丢失。

二. RDB 相关配置

# 900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化)  save "" 关闭RDB功能
save 900 1  
save 300 10  
save 60 10000 

# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes

#是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes

#RDB文件名称
dbfilename dump.rdb  

# 文件路径目录
dir ./  

三. RDB 恢复实操

此节可酌情跳过。我们有台redis,配置如下

dir /usr/local/app/redis-cluster/7000/
dbfilename 7000_dump.rdb
save 5 1

5s内有一个key发生改变就执行持久化。然后执行set命令

127.0.0.1:7000> set hadluo OnePice
OK

然后我们停止redis-server,改名7000_dump.rdb文件。重启redis-server

[root@localhost 7000]# ps -aux |grep redis
root      7461  0.0  0.1 142016  2168 ?        Ssl  20:06   0:00 redis-server 0.0.0.0:7000
root      7469  0.0  0.0  11932  1148 pts/2    S+   20:06   0:00 redis-cli -p 7000
root      7624  0.0  0.0 112824   976 pts/1    R+   20:08   0:00 grep --color=auto redis
[root@localhost 7000]# kill -9 7461
[root@localhost 7000]# mv 7000_dump.rdb 0000_dump.rdb
[root@localhost 7000]# redis-server redis.conf

发现我们key 丢失了

127.0.0.1:7000> get hadluo
(nil)

然后将rdb改名回来。重启redis,数据又回来了。


四. RDB 原理

我先弄了个已经使用了700M内存的redis。由于执行save 和 bgsave 命令会主动进行RDB操作

127.0.0.1:7000> save
OK
(1.57s)

数据多了发现还是很耗时的,同时观察CPU已经达到了40%多。我们看下RDB过程中监控下系统调用情况:

[root@localhost 7000]# ps -ef |grep redis
root       371  5245  0 14:15 pts/1    00:00:00 grep --color=auto redis
root     30969     1  1 13:42 ?        00:00:25 redis-server 0.0.0.0:7000
root     31617  5434  0 13:55 pts/2    00:00:00 redis-cli -p 7000

[root@localhost 7000]# strace -tt -f -o output.log -p 30969

strace命令会把 redis-server进程执行的系统调用给输出到output.log 中,然后我们在执行save命令,观察输出:

30969 14:05:43.842462 read(8, "*1\r\n$4\r\nsave\r\n", 16384) = 14
30969 14:05:43.842663 open("temp-30969.rdb", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 10
30969 14:05:43.842955 fstat(10, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
30969 14:05:43.843127 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7f74a25000
30969 14:05:43.843457 write(10, "REDIS0008\372\tredis-ver\0064.0.10\372\nred"..., 4096) = 4096
30969 14:05:43.843848 write(10, "127.1711.1781.2321\303\22D\21\1AA\340\377\0\340\377\0\340"..., 4096) = 4096
30969 14:05:43.844230 write(10, "\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0\340\377"..., 4096) = 4096
30969 14:05:43.844547 write(10, "\340 \0\1AA\0\020127.231"..., 4096) = 4096
30969 14:05:43.844894 write(10, "71.721\303\fA\204\1AA\340\377\0\340o\0\1AA\0\020127.1691"..., 4096) = 4096
30969 14:05:43.845259 write(10, "\36GS\1AA\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0\340\16\0\1A"..., 4096) = 4096
30969 14:05:43.845591 write(10, ".1251.1801\303\36Gf\1AA\340\377\0\340\377\0\340\377\0\340\377\0\340\377\0"..., 4096) = 4096
....
....
30969 14:05:45.271929 fsync(10)         = 0
30969 14:05:45.372375 close(10)         = 0
30969 14:05:45.372606 munmap(0x7f7f74a25000, 4096) = 0
30969 14:05:45.372821 rename("temp-30969.rdb", "7000_dump.rdb") = 0
30969 14:05:45.373023 open("/usr/local/app/redis-cluster/7000/redis.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 10
30969 14:05:45.373221 lseek(10, 0, SEEK_END) = 1117342
30969 14:05:45.373381 stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=528, ...}) = 0
30969 14:05:45.373665 fstat(10, {st_mode=S_IFREG|0644, st_size=1117342, ...}) = 0
30969 14:05:45.373819 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7f74a25000
30969 14:05:45.374029 write(10, "30969:M 20 Jan 14:05:45.373 * DB"..., 47) = 47
30969 14:05:45.374196 close(10)         = 0
30969 14:05:45.374363 munmap(0x7f7f74a25000, 4096) = 0

从上面日志我们分析save执行RDB的几个重要的步骤:

  • read save : 读入save命令 。
  • open temp-30969.rdb : 打开一个临时文件。
  • fstat : 获取临时文件的详细信息。
  • mmap : 将进程的一段地址 映射到 文件上,这样直接改进程的内存就直接反映到文件上了。
  • write : 写redis数据到临时文件,每次写4096个字节 (4KB)
  • fsync : 将上面write的数据实际写入磁盘, 这个 fsync 需要好好讲一讲。下面会有介绍。
  • close : 关闭文件。
  • 将步骤4的 映射解除。
  • rename : 重命名临时文件为 你指定的RDB文件名称。
  • open :写一些日志到日志文件。发现写的内容是 30969:M 20 Jan 14:05:45.373 * DB...

上面是save命令的结果,save是同步的,是需要阻塞主进程的(线上不要用), bgsave据说是开的子进程来执行RDB的,我们来看下bgsave 的系统调用过程:

30969 15:54:54.071907 read(8, "*1\r\n$6\r\nBGSAVE\r\n", 16384) = 16
30969 15:54:54.071955 pipe([9, 10])     = 0
30969 15:54:54.071989 fcntl(9, F_GETFL) = 0 (flags O_RDONLY)
30969 15:54:54.072014 fcntl(9, F_SETFL, O_RDONLY|O_NONBLOCK) = 0
30969 15:54:54.072044 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7f74a1ba10) = 5246
30969 15:54:54.072559 open("/usr/local/app/redis-cluster/7000/redis.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 11
30969 15:54:54.072602 lseek(11, 0, SEEK_END) = 1117483
30969 15:54:54.072632 stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=528, ...}) = 0
30969 15:54:54.072672 fstat(11, {st_mode=S_IFREG|0644, st_size=1117483, ...}) = 0
30969 15:54:54.072697 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7f74a25000
30969 15:54:54.072729 write(11, "30969:M 20 Jan 15:54:54.072 * Ba"..., 68) = 68
30969 15:54:54.072758 close(11)         = 0
30969 15:54:54.073053 write(8, "+Background saving started\r\n", 28) = 28
30969 15:54:54.073169 epoll_wait(5,  <unfinished ...>
5246  15:54:54.073311 set_robust_list(0x7f7f74a1ba20, 24) = 0
5246  15:54:54.073435 close(7)          = 0
5246  15:54:54.073514 open("temp-5246.rdb", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 7
5246  15:54:54.073630 fstat(7, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
5246  15:54:54.073658 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7f74a25000
5246  15:54:54.074319 write(7, "REDIS0008\372\tredis-ver\0064.0.10\372\nred"..., 4096) = 4096
..... (这里是写redis数据到临时文件)
.....
.....
5246  15:54:54.807921 write(7, "\0\1AA\0\017127.01.371.1611\303'J\241\1AA\340\377\0\340"..., 2637) = 2637
5246  15:54:54.807952 fsync(7 <unfinished ...>
5246  15:54:54.925819 close(7)          = 0
5246  15:54:54.925904 munmap(0x7f7f74a25000, 4096) = 0
5246  15:54:54.925969 rename("temp-5246.rdb", "7000_dump.rdb") = 0
5246  15:54:54.926044 open("/usr/local/app/redis-cluster/7000/redis.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 7
5246  15:54:54.926122 lseek(7, 0, SEEK_END) = 1117551
。。。。。(这里是写日志我就不列出来了
5246  15:54:54.927601 close(7)          = 0
5246  15:54:54.927623 munmap(0x7f7f74a25000, 4096) = 0
5246  15:54:54.927651 write(10, "\0\0\0\0\0\0\0\0\0@\242\0\0\0\0\0xV4\22z\332}\301", 24) = 24
5246  15:54:54.927681 exit_group(0)     = ?
5246  15:54:54.927888 +++ exited with 0 +++

从上面日志我们分析bgsave执行RDB的几个重要的步骤:

  • read save : 读入BGSAVE命令 。
  • pipe : 建立与redis主进程通信的管道,9是读管道,10是写管道。
  • fcntl(9, F_GETFL) : 获取文件描述符状态,flags O_RDONLY 是只读状态。
  • fcntl(9, F_SETFL, O_RDONLY|O_NONBLOCK) : 设置文件描述符状态,nonblocking非阻塞状态。也就是设置读管道为非阻塞。
  • clone : 克隆一个子进程执行RDB,pid为5246 。
  • open ~ close(11) : 这一块就是写日志。
  • write(8) : 8是第一步read的输出重定向,也就是向客户端输出
127.0.0.1:7000> BGSAVE
Background saving started

8. open ~ fsync : 打开临时文件 temp-5246.rdb , 写入 redis 所有key ,value 。

9. rename : 重命名临时文件7000_dump.rdb , 也就是你指定RDB配置的文件。

10. write 10 : 10为我们第二步的主进程写管道,也就是向redis主进程发送 \0\0\0\0\0\0\0\0\0@\242\0\0\0\0\0xV4\22z\332}\301 , 我估计是结束标志。

11. exit_group : 退出子进程中的所有线程 。

redis是使用clone来建子进程

clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7f74a1ba10)=5246

总结一下RDB bgsave过程,也是网上的言论:

  • Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
  • 父进程执行fork操作创建子进程,fork操作过程中父进程被阻塞。
  • 父进程fork完成后,bgsave命令返回“* Background saving started by pid xxx”信息,并不再阻塞父进程,可以继续响应其他命令。
  • 父进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。根据lastsave命令可以获取最近一次生成RDB的时间,对应info Persistence中的rdb_last_save_time。
  • 子进程发送信号给父进程表示完胜,父进程更新统计信息。

为什么我发现是clone ,网上都是说fork ,哪位知道的请留言给我解惑,谢谢~


fork与clone的区别

Linux提供两种方式复制子进程:一个是fork(),另外一个是clone()。fork()函数复制时将父进程的所有资源都通过复制 数据结构 进行了复制,然后传递给子进程。

http://lib.csdn.net/base/datastructure

clone()函数则是将 部分父进程的资源 的数据结构进行复制,复制哪些资源是可选择的,这个可以通过参数设定。

fsync详解

传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存, 大多数磁盘I/O都通过缓冲进行

当将数据写入文件时,内核通常先将该数据复制到一个缓冲区,如果该缓冲区尚未写满,则 并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时, 再将该缓冲排入到输出队列,然后待其到达队首时,才进行实际的I/O操作。

这样的好处 减少了磁盘读写的次数,但是降低了文件内容的更新速度,所以就会在一段时间内文件数据没有写到磁盘上, 为了保证磁盘上实际文件系统与缓冲区中内容保持一致,unix提供了sync、fsync、fdatasync三个函数

sync : sync函数会强制将内核中的所有修改过的缓冲区刷新,并立刻返回,不会等到实际的I/O操作完成后再返回。所以sync函数并不能保证数据一定写入到了磁盘中。

fsync : fsync函数会强制将内核中与fd文件相关的缓冲区刷新,并等待到实际I/O操作结束后再返回,如果实际I/O操作未结束,那么函数将一直处于阻塞状态。所以fsync函数可以保证数据一定被写入到磁盘中。

fdatasync : 与fsync 差不多。

Redis的RDB 就是通过 write 和 fsync 技术 来写dump快照文件的。

RDB子进程内存大揭秘

我作了个实验,redis最大内存3G ,且key已经占满了3G,下面是bgsave执行前情况

执行bgsave时情况

我们发现确实开了个子进程1953 , 内存也是跟redis server主进程一样,总内存占用基本不变(还是有点变化),所以一个结论: redis rdb时内存并没有翻倍,而是共享的主进程的内存 。 ( 但是cpu占了76%,严重会影响主进程度写性能)

分页拷贝思想

子进程是按照 数据页 进行复制。在持久化过程中,子进程将主进程数据段的 数据页 复制一份出来,然后对原数据页进行存储;同一时间,主进程对那份复制出来的数据进行操作。通过复制小数据量数据页(通常每个数据页只有 4KB)的方式,保证了主进程修改数据不会影响子进程存储,子进程存储的数据,还是子进程产生一瞬间的数据。

通过我们上面的系统调用也能看出来(每次写一页4096字节数据)

如果父进程for后 ,有数据修改的话,就使用 copy on write技术。

copy on write

fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会 把触发的异常的页复制一份 ,于是父子进程各自持有独立的一份(只是发生变化的数据)。

RDB缺点

1. 线上应该不能频繁RDB, 所有的RDB都应该放在从库 。redis数据内存占用越大,RDB耗的CPU贼高,而且内存也不是没变化,for子进程只是共享一段内存而已。

2. RDB fork子进程时也会短暂阻塞,数据量越大,阻塞时间越长。

3. RDB 不是那么及时, 数据不是实时持久化,不能靠它来保证每时每刻数据不丢失。


更多Redis文章:

Redis

强烈推荐一个 进阶 JAVA架构师 的博客

Java架构师修炼

支付宝打赏 微信打赏

如果文章对您有帮助,您可以鼓励一下作者