6.2 Async-fork 适配优化
针对找出来的代码位置,可以进行相应优化,针对此处的日志影响,我们可以屏蔽日志或者将日志移动到子进程进行打印,通过同样的分析手段,如果存在其他影响,均可进行对应优化。进行相应适配优化修改后,我们再次进行测试。
● 测试环境
Redis版本:优化后Redis-Server
机器操作系统:Tair专属操作系统镜像
测试数据量:54.38G
127.0.0.1:6680> info memory # Memory used_memory:58385641144 used_memory_human:54.38G
● 现象
在压测过程中执行bgsave,fork耗时和TP100均正常。
使用 info stats 返回上次fork耗时:latest_fork_usec:414
TP100结果如下:
# 压测过程中执行 bgsave [root@xxx Redis]# /usr/bin/Redis-benchmark -d 256 -t set -n 1000000 -a dRedis123456 -p 6680 ====== SET ====== 1000000 requests completed in 7.50 seconds 50 parallel clients 256 bytes payload keep alive: 1 99.99% <= 1 milliseconds 99.99% <= 2 milliseconds 100.00% <= 2 milliseconds 133386.69 requests per second
● 跟踪验证
再次使用strace和perf工具跟踪验证
strace跟踪父进程只看到clone,并且耗时只有378微秒,
# strace -p 14697 -T -tt -o strace04.out 14:42:00.723224 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fa5340d0a50) = 15470 <0.000378>
Perf trace跟踪父进程也只看到clone调用
# perf trace -p 14697 -o trace04.out --max-stack 15 -T 618249694.830 ( 0.423 ms): Redis-server/14697 ... [continued]: clone()) = 15470 (Redis-server) __GI___fork (inlined) rdbSaveBackground (/usr/local/Redis/Redis-server) bgsaveCommand (/usr/local/Redis/Redis-server) call (/usr/local/Redis/Redis-server) processCommand (/usr/local/Redis/Redis-server) processInputBuffer (/usr/local/Redis/Redis-server) aeProcessEvents (/usr/local/Redis/Redis-server) aeMain (/usr/local/Redis/Redis-server) main (/usr/local/Redis/Redis-server)
由于我们的优化是将触发mmap的相关日志修改到子进程中,使用Perf trace跟踪fork产生的子进程,命令为:
strace -p 14697 -T -tt -f -ff -o strace05.out
通过Redis日志文件找到子进程pid为15931;打开对应生成的保存子进程strace信息的文件strace05.out.15931(父进程strace信息保存在文件 strace05.out.14697)
# 以下为子进程 strace 信息 14:47:40.878387 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa5340da000 <0.000008> 14:47:40.878415 write(6, "15931:C 21 Mar 14:47:40.878 * Ba"..., 69) = 69 <0.000015> 14:47:40.878447 close(6) = 0 <0.000006> 14:47:40.878467 munmap(0x7fa5340da000, 4096) = 0 <0.000010> 14:47:40.878494 open("temp-15931.rdb", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 6 <0.000020> 14:47:40.878563 fstat(6, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0 <0.000006> 14:47:40.878584 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa5340da000 <0.000006>
在子进程中看到了mmap调用,子进程中调用不会影响父进程对业务访问的响应。
7. 性能测试
修改Redis代码,针对Async-fork适配优化后,我们针对fork与Async-fork进行了性能对比测试;测试包含不同数据量下fork()命令耗时与fork()操作对压测过程中TP100的影响。
7.1 fork()命令耗时
fork()命令耗时,即针对Redis执行 bgsave 命令后,通过Redis提供的 info stats 命令观察到的 latest_fork_usec 用时。
注:由于fork与Async-fork系统下,fork()操作产生的 latest_fork_usec 数据差距悬殊非常大,使用单纵轴会导致Async-fork的数据在图表中显示不明显,不方便查看,因此, 该图表使用了双纵轴;虽然Async-fork的图表看起来比较高,但是实际右纵轴范围小,所以数据小。
从图表可以看出,使用支持Async-fork的操作系统,fork()操作产生的耗时非常小,不管数据量多大,耗时都非常稳定,基本在200微秒左右;而原生fork产生的耗时会随着数据量增长而增长,而且是从几十毫秒增长到几百毫秒。
7.2 TP100抖动
在使用Redis-benchmark压测过程中,手动执行bgsave命令,触发操作系统fork()操作,观察不同数据量下,fork与Async-fork对Redis压测时TP100的影响。
从图上可以看出,使用支持Async-fork的操作系统,fork()操作对Redis压测产生的性能影响非常小,性能提升非常明显,不管数据量多大,耗时都非常稳定,基本在1-2毫秒左右;而原生fork产生的抖动影响时间会随着数据量增长而增长,TP100从几十毫秒增长到几百毫秒。
8. 总结
通过不同数据量下对比测试,我们可以看到,Async-fork相比原生fork,阻塞时间大大减少,性能提升非常明显。而且阻塞时间非常稳定,不会因为数据量的增长出现倍数级增长。
在单机测试场景下,8G数据量大小下,TP100和 latest_fork_usec 耗时均减少98% 以上。
基于论文中Async-fork的设计思想,Tair专属操作系统镜像已支持该特性,并且将该特性集成在原生fork 中,没有新增系统调用接口,理论上用户只需要使用支持Async-fork的操作系统,程序无需做任何修改,就可以享受到Async-fork特性带来的性能提升。对于Redis而言,我们也只需要对Redis稍加适配就可以获得该技术带来的红利。
在Redis应用场景中,在添加从节点、RDB文件备份、AOF持久化文件重写等场景下,应用支持Async-fork的操作系统,都将极大的减少对业务的影响。
参考资料:
[1] 《Async-fork: Mitigating Query Latency Spikes Incurred by the Fork-based Snapshot Mechanism from the OS Level》