VLDB顶会论文Async-fork解读与Redis在得物的实践(3)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
性能测试 PTS,5000VUM额度
简介: VLDB顶会论文Async-fork解读与Redis在得物的实践

5.2 Async-fork详解

前面提到,每个进程都有自己的虚拟内存空间,Linux使用一组虚拟内存区域VMA来描述进程的虚拟内存空间,每个VMA包含许多页表项。

在默认fork中,父进程遍历每个VMA,将每个VMA复制到子进程,并自上而下地复制该VMA对应的页表项到子进程,对于64位的系统,使用四级分页目录,每个VMA包括PGD、PUD、PMD、PTE,都将由父进程逐级复制完成。在Async-fork中,父进程同样遍历每个VMA,但只负责将PGD、PUD这两级页表项复制到子进程。

随后,父进程将子进程放置到某个CPU上使子进程开始运行,父进程返回到用户态,继续响应用户请求。由子进程负责每个VMA剩下的PMD和PTE两级页表的复制工作。

如果在父进程返回用户态后,子进程复制内存页表期间,父进程需要修改还未完成复制的页表项,怎样避免上述提到的破坏快照一致性问题呢?

image.png

5.2.1 主动同步机制

父进程返回用户态后,父进程的PTE可能被修改。如果在子进程复制内存页表期间,父进程检测到了PTE修改,则会触发主动同步机制,也就是父进程也加入页表复制工作,来主动完成被修改的相关页表复制,该机制用来确保PTE在修改前被复制到子进程。

当一个PTE将被修改时,父进程不仅复制这一个PTE,还同时将位于同一个页表上的所有PTE(一共512个PTE),连同它的父级PMD项复制到子进程。

父进程中的PTE发生修改时,如果子进程已经复制过了这个PTE,父进程就不需要复制了,否则会发生重复复制。怎么区分PTE是否已经复制过?

Async-fork使用PMD项上的RW位来标记是否被复制。具体而言,当父进程第一次返回用户态时,它所有PMD项被设置为写保护(RW=0),代表这个PMD项以及它指向的512个PTE还没有被复制到子进程。当子进程复制一个PMD项时,通过检查这个PMD是否为写保护,即可判断该PMD是否已经被复制到子进程。如果还没有被复制,子进程将复制这个PMD,以及它指向的512个PTE。

在完成PMD及其指向的512个PTE复制后,子进程将父进程中的该PMD设置为可写(RW=1),代表这个PMD项以及它指向的512个PTE已经被复制到子进程。当父进程触发主动同步时,也通过检查PMD项是否为写保护判断是否被复制,并在完成复制后将PMD项设置为可写。同时,在复制PMD项和PTE时,父进程和子进程都锁定PTE表,因此它们不会出现同时复制同一PMD项指向的PTE。

在操作系统中,PTE的修改分为两类:

VMA级的修改。例如,创建、合并、删除VMA等操作作用于特定VMA上,VMA级的修改通常会导致大量的PTE修改,因此涉及大量的PMD。

PMD级的修改。PMD级的修改仅涉及一个PMD。

5.2.2 错误处理

Async-fork在复制页表时涉及到内存分配,难免会发生错误。例如,由于内存不足,进程可能无法申请到新的PTE表。当错误发生时,应该将父进程恢复到它调用Async-fork之前的状态。

在Async-fork中,父进程PMD项目的RW位可能会被修改。因此,当发生错误时,需要将PMD项全部回滚为可写。


6. Redis优化实践


6.1 Async-fork 阻塞现象

在支持Async-fork的操作系统(即Tair专属操作系统镜像)机器上测试,理论上来说,按照文章的预期,用户不需要作任何修改(Async-fork使用了原生fork相同的接口,没有另外新增接口),就可以享受Async-fork优化带来的优势,但是,使用Redis实际测试过程中,结果不符合预期,在Redis压测过程中手动执行bgsave命令触发fork操作,还是观察到了TP100抖动现象。

 测试环境

Redis版本:优化前Redis-Server

机器操作系统:Tair专属操作系统镜像

测试数据量:54.38G

127.0.0.1:6679> info memory
# Memory
used_memory:58385641120
used_memory_human:54.38G

 问题现象

现象:fork耗时正常,但是压测过程中执行bgsave,TP100不正常

在压测过程中执行bgsave,使用 info stats 返回上次fork耗时:latest_fork_usec:426

TP100结果如下:

# 压测过程中执行 bgsave
[root@xxx ~]# /usr/bin/Redis-benchmark -d 256 -t set -n 1000000  -a xxxxxx -p 6679
====== SET ======
  1000000 requests completed in 7.88 seconds
  50 parallel clients
  256 bytes payload
  keep alive: 1
100.00% <= 411 milliseconds
100.00% <= 412 milliseconds
100.00% <= 412 milliseconds
126871.35 requests per second

也就是说,观察到的fork耗时正常,但是压测过程中Redis依然出现了尾延迟,这显然不符合预期。

追踪过程

使用 strace 命令进行分析,结果如下:

$ strace -p 32088 -T -tt -o strace00.out
14:18:12.933441 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f461c0daa50) = 13772 <0.000380>
14:18:12.933884 open("/data1/6679/6679.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 60 <0.000019>
14:18:12.933948 lseek(60, 0, SEEK_END)  = 11484 <0.000013>
14:18:12.933983 stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=556, ...}) = 0 <0.000016>
14:18:12.934032 fstat(60, {st_mode=S_IFREG|0644, st_size=11484, ...}) = 0 <0.000014>
14:18:12.934062 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f461c0e4000 <0.358768>
14:18:13.292883 write(60, "32088:M 21 Mar 14:18:12.933 * Ba"..., 69) = 69 <0.000032>
14:18:13.292951 close(60)               = 0 <0.000014>
14:18:13.292980 munmap(0x7f461c0e4000, 4096) = 0 <0.000019>
$ strace -p 11559 -T -tt -e trace=memory -o trace00.out
14:18:12.934062 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f461c0e4000 <0.358768>
14:18:13.292980 munmap(0x7f461c0e4000, 4096) = 0 <0.000019>

可以观察到,clone耗时380微秒,已经大幅降低,也就fork快速返回了用户态响应用户请求。然而,注意到,紧接着出现了一个mmap耗时358毫秒,与TP100数据接近。

由于mmap系统调用会在当前进程的虚拟地址空间中,寻找一段满足大小要求的虚拟地址,并且为此虚拟地址分配一个虚拟内存区域( vm_area_struct 结构),也就是会触发VMA级虚拟页表变化,也就触发父进程主动同步机制,父进程主动帮助完成相应页表复制动作。VMA级虚拟页表变化,需要将对应的三级和四级所有页目录都复制到子进程,因此,耗时比较高。

那么,这个mmap调用又是哪里来的呢?

 定位问题

perf是Linux下的一款性能分析工具,能够进行函数级与指令级的热点查找。

通过perf trace可以看到响应调用堆栈及耗时,分析结果如下:

$ perf trace -p 11559 -o trace01.out --max-stack 15 -T
616821913.647 (358.740 ms): Redis-server_4/32088 mmap(len: 4096, prot: READ|WRITE, flags: PRIVATE|ANONYMOUS            ) = 0x7f461c0e4000
                                       __mmap64 (/usr/lib64/libc-2.17.so)
                                       __GI__IO_file_doallocate (inlined)
                                       __GI__IO_doallocbuf (inlined)
                                       __GI__IO_file_overflow (inlined)
                                       _IO_new_file_xsputn (inlined)
                                       _IO_vfprintf_internal (inlined)
                                       __GI_fprintf (inlined)
                                       serverLogRaw (/usr/local/Redis/Redis-server)
                                       serverLog (/usr/local/Redis/Redis-server)
                                       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)
616822272.562 ( 0.010 ms): Redis-server_4/32088 munmap(addr: 0x7f461c0e4000, len: 4096                                ) = 0
                                       __munmap (inlined)
                                       __GI__IO_setb (inlined)
                                       _IO_new_file_close_it (inlined)
                                       _IO_new_fclose (inlined)
                                       serverLogRaw (/usr/local/Redis/Redis-server)
                                       serverLog (/usr/local/Redis/Redis-server)
                                       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)

也就可以看到,在bgsave执行逻辑中,有一处打印日志中的fprintf调用了mmap,很显然这应该是fork返回父进程后,父进程中某处调用。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
29天前
|
存储 缓存 NoSQL
深入理解Django与Redis的集成实践
深入理解Django与Redis的集成实践
49 0
|
6月前
|
存储 缓存 NoSQL
蚂蚁金服P7私藏的Redis原理与实践内部笔记
Redis 是完全开源免费的,是一个高性能的key-value类型的内存数据库。整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。
108 1
|
6月前
|
缓存 NoSQL Java
Spring Cache 缓存原理与 Redis 实践
Spring Cache 缓存原理与 Redis 实践
342 0
|
存储 NoSQL Linux
VLDB顶会论文Async-fork解读与Redis在得物的实践(1)
VLDB顶会论文Async-fork解读与Redis在得物的实践
121 0
|
NoSQL 测试技术 Linux
VLDB顶会论文Async-fork解读与Redis在得物的实践(2)
VLDB顶会论文Async-fork解读与Redis在得物的实践
107 0
VLDB顶会论文Async-fork解读与Redis在得物的实践(2)
|
9天前
|
缓存 NoSQL Redis
Redis 缓存使用的实践
《Redis缓存最佳实践指南》涵盖缓存更新策略、缓存击穿防护、大key处理和性能优化。包括Cache Aside Pattern、Write Through、分布式锁、大key拆分和批量操作等技术,帮助你在项目中高效使用Redis缓存。
66 22
|
15天前
|
NoSQL 关系型数据库 MySQL
MySQL与Redis协同作战:百万级数据统计优化实践
【10月更文挑战第21天】 在处理大规模数据集时,传统的单体数据库解决方案往往力不从心。MySQL和Redis的组合提供了一种高效的解决方案,通过将数据库操作与高速缓存相结合,可以显著提升数据处理的性能。本文将分享一次实际的优化案例,探讨如何利用MySQL和Redis共同实现百万级数据统计的优化。
46 9
|
2月前
|
消息中间件 NoSQL Go
PHP转Go系列 | ThinkPHP与Gin框架之Redis延时消息队列技术实践
【9月更文挑战第7天】在从 PHP 的 ThinkPHP 框架迁移到 Go 的 Gin 框架时,涉及 Redis 延时消息队列的技术实践主要包括:理解延时消息队列概念,其能在特定时间处理消息,适用于定时任务等场景;在 ThinkPHP 中使用 Redis 实现延时队列;在 Gin 中结合 Go 的 Redis 客户端库实现类似功能;Go 具有更高性能和简洁性,适合处理大量消息。迁移过程中需考虑业务需求及系统稳定性。
|
5月前
|
网络协议 NoSQL Redis
SMC-R 透明加速 TCP 技术,在 Redis 场景下的应用实践 | 干货推荐
SMC-R 作为一套与 TCP/IP 协议平行、向上兼容 socket 接口、底层使用 RDMA 完成共享内存通信的内核协议栈,其设计意图是为 TCP 应用提供透明的 RDMA 服务,同时保留了 TCP/IP 生态系统中的关键功能。
|
NoSQL 测试技术 Redis
VLDB顶会论文Async-fork解读与Redis在得物的实践(4)
VLDB顶会论文Async-fork解读与Redis在得物的实践
121 0
VLDB顶会论文Async-fork解读与Redis在得物的实践(4)
下一篇
无影云桌面