Redis持久化 - AOF 篇

一. AOF简介

AOF( append only file )持久化以独立日志的方式记录每次写命令,并在 Redis 重启时在重新执行 AOF 文件中的命令以达到恢复数据的目的。AOF 的主要作用是解决数据持久化的实时性。


二. AOF相关配置

# 是否开启AOF默认关闭no
appendonly yes

# 指定 AOF 文件名
appendfilename appendonly.aof

# Redis支持三种不同的刷写模式
# appendfsync always #每次收到写命令就立即强制写入磁盘是最有保证的完全的持久化但速度也是最慢的一般不推荐使用
appendfsync everysec #每秒钟强制写入磁盘一次在性能和持久化方面做了很好的折中是受推荐的方式
# appendfsync no     #完全依赖OS的写入一般为30秒左右一次性能最好但是持久化最没有保证不被推荐

#在日志重写时不进行命令追加操作而只是将其放在缓冲区里避免与命令的追加造成DISK IO上的冲突
#设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入默认为no
no-appendfsync-on-rewrite no 

#当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时自动启动新的日志重写过程指定百分比为0将禁用aof自动重写功能
auto-aof-rewrite-percentage 100

#当前AOF文件启动新的日志重写过程的最小值避免刚刚启动Reids时由于文件尺寸较小导致频繁的重写
auto-aof-rewrite-min-size 64mb


三. AOF实践

我的redis aof 相关配置

appendonly yes
appendfilename 7000_appendonly.aof
appendfsync everysec

启动redis ,执行 tail -f 监控 7000_appendonly.aof文件变化

[root@localhost 7000]# tail -1000f 7000_appendonly.aof

redis 客户端 设置值 , AOF文件变化如下:

https://pic2.zhimg.com/v2-ad4da5aa5450da9440b3ad42af727f71_b.jpg

AOF内容解释

*3     #### 此条命令有3个参数
$3     #### 第一个参数 3个字节    set
set    #### 第一个参数内容
$6     #### 第二个参数 6个字节    HadLuo
HadLuo  #### 第二个参数内容
$7     ###....
OnePice

当执行del 时:

127.0.0.1:7000> del HadLuo
(integer) 1

###############AOF内容变化####################
*2
$3
del
$6
HadLuo

当执行查询时:

127.0.0.1:7000> get HadLuo
"OnePice1"

###############AOF内容变化####################
无变化

当执行expire时:

127.0.0.1:7000> EXPIRE hadluo 122

###############AOF内容变化####################
*3
$9
PEXPIREAT             # PEXPIREAT 以毫秒记时
$6
hadluo
$13
1611294370239

阶段总结:

  • AOF会把修改,新增,删除的key 记录到AOF文件里面。
  • AOF不会记录查询的操作。


四. AOF原理

当我们set key 的时候, 观察redis 的 系统调用 情况

2468  12:02:59.778708 read(9, "*3\r\n$3\r\nset\r\n$6\r\nhadluo\r\n$7\r\nOne"..., 16384) = 38
2468  12:02:59.778792 write(8, "*3\r\n$3\r\nset\r\n$6\r\nhadluo\r\n$7\r\nOne"..., 38) = 38
2468  12:02:59.778821 futex(0x6fc3f4, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x6fc3f0, FUTEX_OP_SET<<28|0<<12|FUTEX_OP_CMP_GT<<24|0x1 <unfinished ...>
2470  12:02:59.778842 <... futex resumed>) = 0
2468  12:02:59.778850 <... futex resumed>) = 1
2470  12:02:59.778863 futex(0x6fc488, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
2468  12:02:59.778874 futex(0x6fc488, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
.....
2470  12:02:59.779048 fdatasync(8 <unfinished ...>

不会看系统调用的请看我上篇文章RDB解析:

罗政:Redis 持久化 RDB 原理

简单解析下几个过程:

  • read : 从控制台 读取 set hadluo One.. 命令。
  • write : 向 文件描述符为8 的文件 写内容,很明显 写的就是我们 7000_appendonly.aof 文件, 但是 为什么没有 open (打开文件)这个函数调用呢? 难道redis-server一启动就占着这个文件了? 8又是从哪里来的?

为了找出2的疑问?我们来看下redis-server打开了哪些文件描述符

[root@localhost 7000]# ps -ef |grep redis
root      2468     1  0 11:46 ?        00:00:03 redis-server 0.0.0.0:7000

[root@localhost 7000]# ll /proc/2468/fd
总用量 0
lrwx------. 1 root root 64 1  22 13:19 0 -> /dev/null
lrwx------. 1 root root 64 1  22 13:19 1 -> /dev/null
lrwx------. 1 root root 64 1  22 13:19 2 -> /dev/null
lr-x------. 1 root root 64 1  22 13:19 3 -> pipe:[27837]
l-wx------. 1 root root 64 1  22 13:19 4 -> pipe:[27837]
lrwx------. 1 root root 64 1  22 13:19 5 -> anon_inode:[eventpoll]
lrwx------. 1 root root 64 1  22 13:19 6 -> /dev/pts/2
lrwx------. 1 root root 64 1  22 13:19 7 -> socket:[27838]
l-wx------. 1 root root 64 1  22 13:19 8 -> /usr/local/app/redis-cluster/7000/7000_appendonly.aof
lrwx------. 1 root root 64 1  22 13:19 9 -> socket:[27907]

果不其然,redis-server一直占着我们的 7000_appendonly.aof 文件,且文件描述符值为8 ,可能是redis服务一启动就open了这个文件。

3. fdatasync : 将write的数据确认同步到磁盘,为什么write没有确认是写到磁盘的原因,在下面这篇文章有讲。注意 : fdatasync 函数为阻塞的,直到刷盘成功。

罗政:Redis 持久化 RDB 原理

redis写aof文件要理解为两步:

  • 调用write函数去写到操作系统的页缓存(没有真正落盘,等待fdatasync函数去刷页缓存)。

2. 调用fdatasync 去写到磁盘。(fdatasync函数调用时间跟write调用无关系,不能简单理解为write 后就 立马fdatasync )

AOF流程

  • Redis在处理一条命令时,并不立即调用write写AOF文件,只是将数据先写入到aof_buf缓冲队列的末尾。

Redis事件循环会检测aof_buf,有数据就进行write操作。然后判断配置, appendfsync always :立刻执行fdatasync 去落盘。 appendfsync everysec : 创建 fsync bio线程任务 去消费(类似生产者消费者),让消费者去执行 fdatasync 。

appendfsync 三个配置的优缺点

appendfsync always

每次执行完write函数后,都要执行 fdatasync 去真正落盘。fdatasync 会阻塞直到落盘成功。不建议这种方式,首先fdatasync是系统调用,需要切换到内核空间,然后就是fdatasync 会阻塞直到落盘成功。

always策略最多只丢失一次事件循环的数据

appendfsync everysec

每隔一秒执行 fdatasync 去真正落盘。跟write执不执行无关。建议这种方式,首先是在bio线程去调用 fdatasync 不会影响主进程。而且1秒时间,内核空间切换 和 数据丢失率也能接受。

everysecond策略最多只丢失一秒数据

appendfsync no

redis自己不执行appendfsync ,等操作系统的updater定时器去执行(可能是30s执行一次)。这种效率很高,因为都不涉及到内核空间切换,但是数据可能会丢失(30s)哦。


五. AOF重写

AOF重写的目的 :因为很多命令在执行多次后(文件体积很大),可以进行合并,比如以下场景就需要重写:

set a 123
...(时隔多日后执行)
set a 789

重写的话就只留下
set a 789

重写过程就是对 现有reids主进程的所有数据,生成相对应的命令(并没有很牛x的合并算法),覆盖之前的AOF文件,这样就是最新的数据命令了。


先看一个实操,redis.conf配置如下:

# 设置为0 禁止AOF重写
auto-aof-rewrite-percentage 0

我把redis当前内存值调到2G ,总共是3G多

127.0.0.1:7000> INFO Memory
# Memory
used_memory:2153565984
used_memory_human:2.01G
used_memory_rss:2198642688
used_memory_rss_human:2.05G
.............

[root@localhost 7000]# du -h 7000_appendonly.aof 
2.2G	7000_appendonly.aof

执行 BGREWRITEAOF 手动重写,观察系统情况

我们发现内核态CPU占的挺多的,也是fork了一个redis子进程(原理跟上面说的RDB一样)。 内存并不是拷贝了2份,而是共享的主进程的内存。

继续看下系统调用情况

########## 与主进程通信管道
2708  09:58:57.715623 pipe([16, 17])    = 0
########## fork子进程
2708  09:53:13.788244 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f6d92e0ba10) = 3847
########## 打开临时文件
3847  09:53:13.799179 open("temp-rewriteaof-3847.aof", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 7
3847  09:53:13.799340 fstat(7, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
3847  09:53:13.799368 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6d92e15000
########## 往临时文件写内容
3847  09:53:13.799407 write(7, "*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n*3\r\n$3\r\nS"..., 4096) = 4096
3847  09:53:13.799436 write(7, "\346\211\200\345\244\247\350\220\250\350\276\276\346\211\200\345\244\232A\346\210\221\347\232\204\345\244\247\350\220\250\350"..., 86016) = 86016
3847  09:53:13.799495 read(8, 0x7ffcea466940, 65536) = -1 EAGAIN (资源暂时不可用)
3847  09:53:13.799523 write(7, "\250\350\276\276\346\211\200\345\244\247\350\220\250\350\276\276\346\211\200\345\244\232A\346\210\221\347\232\204\345\244\247"..., 4096) = 4096
3847  09:53:13.799550 write(7, "\276\276\346\211\200\345\244\247\350\220\250\350\276\276\346\211\200\345\244\232A\346\210\221\347\232\204\345\244\247\350\220"..., 98304) = 98304
3847  09:53:13.799605 read(8, 0x7ffcea466940, 65536) = -1 EAGAIN (资源暂时不可用)
....
....
########## 重命名临时文件
3859  09:59:12.264694 rename("temp-rewriteaof-3859.aof", "temp-rewriteaof-bg-3859.aof") = 0
########## 通过管道通知主进程 AOF完毕
3859  09:59:12.277177 write(17, "\1\0\0\0\0\0\0\0\0\20c\0\0\0\0\0xV4\22z\332}\301", 24) = 24
3859  09:59:12.277241 exit_group(0)     = ?

我们可以总结得到如下结果

  • 开启子进程进行AOF,且先打开临时文件。
  • 分页读取主进程数据,然后执行write ,这样交替进行。
  • 主进程数据读取完后,rename临时文件,之前的AOF就被覆盖了。
  • AOF重写完成后,会通知redis主进程,做一些事情。(主要包括:将 AOF 重写缓存中的内容全部写入到新 AOF 文件中)

AOF 重写缓存

子进程在进行 AOF 重写期间, 主进程还需要继续处理命令, 而新的命令可能对 现有的数据进行修改 , 这会让当前数据库的数据和重写后的 AOF 文件中的数据不一致。

为了解决这个问题, Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用, Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外, 还会追加到这个缓存中。

当AOF执行完成后,会将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。这样就保证数据正确性了。


AOF重写导致的内存问题实例

业务上接到报警提示服务器内存爆了,登录查看发现机器剩余内存还很多,怀疑是被OOM了,查看/var/log/messages:

kernel: [25918282.632003] Out of memory: Kill process 18665 (redis-server) score 919 or sacrifice child
kernel: [25918282.637201] Killed process 18665 (redis-server) total-vm:17749556kB, anon-rss:14373204kB, file-rss:1236kB
kernel: [25918791.441427] redis-server invoked oom-killer: gfp_mask=0x24280ca, order=0, oom_score_adj=0

发现redis-server被oom kill了,但是登录查看发现redis-server并没有down掉.既然redis-server并没有被kill,那被kill的有可能是redis的子进程。

进入redis的data目录查看:

-rw-rw-r-- 1 myuser myuser 18044223152 4   8 12:01 appendonly.aof
-rw-rw-r-- 1 myuser myuser  3603981186 4   8 12:01 temp-rewriteaof-25595.aof
-rw-rw-r-- 1 myuser myuser  4083774382 4   8 11:46 temp-rewriteaof-18665.aof
-rw-rw-r-- 1 myuser myuser  4326578230 4   8 11:21 temp-rewriteaof-8116.aof

发现有好几个temp-rewriteaof文件,这是redis在进行aofrewrite时产生的临时文件。

注意看其中一个的名字: temp-rewriteaof-18665.aof ,后面的18665即rewrite子进程的pid,上面被oom kill的进程ID也是18665,说明是redis的aofrewrite子进程被kill了。

而多个temp文件,而且时间都是最近的,说明redis已经尝试了多次rewrite,都因为内存不足被中途kill。

为什么aof重写会导致内存爆涨?

上面原理中可以了解到,如果在重写过程中redis的写入很频繁或写入量很大,就会导致占用大量的AOF重写缓冲区,导致内存爆涨。

事后,登录Redis使用monitor监控了一段时间的访问,记录到文件中:

time redis-cli -p 6379 monitor > monitor.log

通过查看monitor.log发现,存在这样一条语句:

1523351418.461744 [0 10.10.10.10:6379] "SET" "xx_xx_id_17791" 
"[615249,615316,615488,616246,616498,616580,617117,617291,617510,617879,
618052,618377,618416,619010,619185,619603,619816,620190,620230,620387,
620445,620524,621012,621214,621219,621589,621596,621616,621623,621669,
621670,621682,621683,621820,621994,622168,622207,622245,622384,622442,
622450,622608,622644,622654,622658,622704,622784,622785,622786,622810,
622834,622876,622887,622934,622936,622937,622939,622943,622967,......]"

原文出自

Redis AOF重写导致的内存问题


更多Redis文章:

JAVA架构师修炼

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

Java架构师修炼

支付宝打赏 微信打赏

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