AOF 和 RDB

https://app.diagrams.net/#G1ZI0SeLxjvF7EnIbyF7G0HyBjzioMod6b#%7B%22pageId%22%3A%22C5RBs43oDa-KdzZeNtuy%22%7D

Redis 3.0

由上图可以看到,Redis从整体来看其实就是一个while死循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
while(true){
//阻塞,直到被唤醒
//文件处理器
events = epoll_wait()
//处理每一个事件
for(event: events){
//这里的读事件是指 从网卡读 要拿取数据了
//这里的写事件是指 往网卡里写 要发送数据了
if(读事件){
//从socket缓冲区拿出数据
readQueryFormClient() ;
// while 循环处理 客户端缓冲区 只要构成一个完整的Redis协议命令
//在这也是pipeline机制实现原理
processInputBuffer();
//容易阻塞点!!!
//1、检查命令合法性,参数合法性
//2、客户端是否认证,集群转向ask、moved
//3、是否maxMemory,slave节点拒绝写命令
//4、订阅模式命令,发布模式命令
processCommand();
//1、执行命令 ---> 执行结果放到输出缓冲区,然后注册可写事件
// 什么是可写事件:客户端tcp窗口没满,就是可写
//2、慢命令 --> show log
//3、propagate Command --> feedAppendOnlyFile 把命令写入AOF缓冲区
// --> rePlicationFeedSlave 把命令发送给从服务器
call()
//epoll:水平触发,边缘触发。Redis这里属于水平触发
//epoll的坑
/**
水平模式:多次触发
边缘模式:一次没触发后就不会触发了
*/

}
else if(写事件){
/**
输出缓冲区:
buf静态缓冲区,固定大小
replay动态缓冲区,大响应时用
*/
//将输出缓冲区里的数据write回 客户端的socket(就是网卡缓冲区)
//如果write出错了,就返回(如果tcp窗口满了)
//如果缓冲区写完了,那么就卸载可写事件
sendReplyToClient()

}
}
//时间处理器
severcorn(100ms); //低频处理

//每次循环都会执行,跟epoll_wait() 被唤醒的频率有关
beforeSleep()
}
severcorn(){
watch dog 机制
更新信息 时钟 内存峰值 rss内存 关闭信号
客户端操作(超时,缓冲区)
清理过期件,渐进式hash
(易阻塞)重写aof rdb
辅助性aof落盘
客户端异步关闭
主从操作
集群操作
sentinel操作

}

beforeSleep(){
过期键清理
副本ack同步
(易阻塞)aof缓冲区落盘
集群维护 故障转移
}

aof和rdb调用关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 客户端的命令
lastsave
saveCommand -> rdbSave()
bgsaveCommand -> rdbSaveBackground() -> rdbSave()
bgrewriteaofCommand -> rewriteAppendOnlyFileBackground() fork子进程 -> rewriteAppendOnlyFile() 读取内存数据到临时文件

//低频调用
serverCron(){
//如果 BGSAVE 和 BGREWRITEAOF 都没有在执行,并且有一个 BGREWRITEAOF 在等待
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 && server.aof_rewrite_scheduled) {
rewriteAppendOnlyFileBackground(); //手动触发才会走到这里来
}
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1){
backgroundSaveDoneHandler() //
backgroundRewriteDoneHandler()
}else{
//BGSAVE 和 BGREWRITEAOF 都没有在执行
rdbSaveBackground() // save 60 10000 参数格式,及默认
rewriteAppendOnlyFileBackground()
}
}

//高频调用
beforeSleep(){
//aof刷盘
flushAppendOnlyFile(0); //server.aof_buf 写到文件
}

aof

aof包含刷盘和重写两大块

aof 流程及刷盘

每执行一个redis写命令,会存到aof缓冲区。然后在文件事件结束后,依赖** beforeSleep()** 函数执行写入动作。

写命令 -> aof 缓冲区 -> write数据 -> 刷盘策略

flushAppendOnlyFile() 函数被 beforeSleep() 函数高频调用,每次调用都会调用 write函数(主线程执行),然后根据策略刷盘。

aof_fsync 的含义,每次都会调用write函数,但是刷盘时机可以配置

always 模式,也可能丢失数据,而且高频的调用 aof_fsync 会导致redis吞吐量下降。

PS:为什么高频调用会导致Redis,因为always模式下flush是在主线程,而不是跟everysec一样是异步线程

everysec 模式下,数据丢失风险 不止1秒!若刷盘上次刷盘不及时,或者文件事件执行过长(高并发时,且有阻塞性的大key导致)

no 依赖操作系统刷盘

思考:如果设置的刷盘策略为每秒,为什么write数据还是在主线程,而刷盘在异步线程。为什么不把write也放到异步线程呢?

:::color4
1.write是轻量级的,就是将用户态的数据拷贝到page cache,而flush需要等到IO事件结束,耗时长,更适合异步化

2.write的时候涉及 aof_buf,如果是异步的话,在多线程情况下,就需要引入锁机制

:::

重写aof流程

为什么

手动

自动重写aof时机

:::color4
此时没有进行rdb和重写aof(判断是否存在子进程)

aof当前体积 > 最小重写阈值

配置了自动重写百分比,且现在的体积 较于上次aof体积 增长率 大于 配置的值

:::

aof重写逻辑流程

:::color4

  1. fork 一个子进程 ,记录fork子进程时间,关闭字典rehash
    子进程:

1.关闭网络连接

2.创建临时文件名 tempfile = temp-rewriteaof-bg-%d.aof**** ,这里就是相当于在内存里创建了一个字符串

3.创建临时文件temp-rewriteaof-%d.aof

4.遍历所有db,写入键值对以及过期信息

5.temp-rewriteaof-%d aof 原子改名为 temp-rewriteaof-bg-%d.aof

6.unlink temp0-rewriteaof-%d.aof

//unlink:删除文件名 file1.txt。 将 inode 的链接计数器减 1,变为 1。

7.向父进程发送退出信号

  1. 在n个循环后子进程执行完毕,打开temp-rewrite-bg-%d aof
  2. 遍历aof重写缓冲区,数据追加到 temp-rewrite-bg-%d.aof
  3. 打开appendonl.aof,temp-rewrite-bg-%d.aof 改名为 appendonly.aof
  4. 更新Server aof_filename 等于tmpfile的 fd引用,并刷盘
  5. 异步关闭旧的aof文件fd

:::

伪代码:

if(xxx = fork() == 0){

}

else{

//关闭网络连接

}

整个过程中会有三个文件

appendonly.aof

temp-rewrite-bg-%d aof

temp-rewrite-%d aof

Redis异步线程

rewriteAppendOnlyFileBackground() -> bioCreateBackgroundJob()

redis3.0 使用异步线程的地方:

1,异步刷盘( fsync)

2,重写aof时,异步关闭文件

版本 关键异步任务 技术实现 演进意义
Redis 3.0 AOF持久化(fsync) bioCreateBackgroundJob创建后台线程 首次引入多线程,解决fsync阻塞主线程问题
Redis 4.0 大Key异步删除、数据库清空(UNLINK/FLUSHDB ASYNC) 独立BIO线程池(3个线程:关闭文件、AOF刷盘、惰性删除) 主线程与耗时操作解耦,提升稳定性
Redis 6.0 网络I/O读写、协议解析 I/O线程组(可配置数量) 网络吞吐提升300%,单节点性能突破20W QPS
Redis 7.0 RDB生成、AOF重写、集群任务分发 动态线程池(负载自适应) 优化后台任务并行性,减少尾延迟65%
Redis 7.4+ 哈希字段过期、向量数据处理(AI优化) 字段级异步过期线程

rdb流程

自动处理的默认配置

save xx xx

save xx xx

save xx xx

只要满足了以上一个就触发

1,记录开始的时间,fork子进程

子进程:

1,关闭网络fd

2,创建临时文件 temp-%d.rdb

3,遍历16个db的所有数据,写入到临时文件,刷盘并关闭fd

4,把临时文件重命名为dump.rdb

5,向父进程发送信号

2,计算fork所花时间,记录子进程id,关闭自动rehash

3,在n个循环后,收到子进程信号,更新一些信息

4,处理正在等待 BGSAVE 完成的那些 slave

cow 和 fork

cow内存计算逻辑

1个g,fork子进程之后 子进程和父进程同一块物理内存,总内存还是一个g,父进程还会写

1g内存

–> fork 子进程后,因为共享

还是1g内存

–> 父进程修改一遍 全部内存后 父进程持有1g内存

–> 子进程修改一遍 全部内存后 子进程持有1g内存

1 + 1 + 1=3?

但好像不是

共享内存都很小了

fork的内存计算问题

fork阻塞问题

主进程fork子进程本身也是有延迟的。父进程fork 超过5g内存,会有一定延迟,超过200ms

fork性能问题堪忧

LostPigxx

很好奇一点,fork对开源redis可以说是阿喀琉斯之踵了,我看antirez在twitter上也说尽可能的利用sharding来规避这个问题。
从技术上真的没办法解决fork的影响吗?

2020-12-24

logoV5

其实解决fork影响是os内核要做的,甚至os内核认为这点影响是理所当然,在它看来未必要做啥优化。
而使用者是应该认识到并避免它的。Redis之所以无法避免,还是因为它本身是个缓存,又想做持久化。

2020-12-24