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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 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返回父进程后,父进程中某处调用。

目录
相关文章
|
12月前
|
存储 缓存 NoSQL
深入理解Django与Redis的集成实践
深入理解Django与Redis的集成实践
376 0
|
存储 缓存 NoSQL
蚂蚁金服P7私藏的Redis原理与实践内部笔记
Redis 是完全开源免费的,是一个高性能的key-value类型的内存数据库。整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。
201 1
|
缓存 NoSQL Java
Spring Cache 缓存原理与 Redis 实践
Spring Cache 缓存原理与 Redis 实践
516 0
|
25天前
|
消息中间件 缓存 NoSQL
Redis各类数据结构详细介绍及其在Go语言Gin框架下实践应用
这只是利用Go语言和Gin框架与Redis交互最基础部分展示;根据具体业务需求可能需要更复杂查询、事务处理或订阅发布功能实现更多高级特性应用场景。
158 86
|
1月前
|
存储 缓存 监控
Redis分区的核心原理与应用实践
Redis分区通过将数据分散存储于多个节点,提升系统处理高并发与大规模数据的能力。本文详解分区原理、策略及应用实践,涵盖哈希、范围、一致性哈希等分片方式,分析其适用场景与性能优势,并探讨电商秒杀、物联网等典型用例,为构建高性能、可扩展的Redis集群提供参考。
75 0
|
3月前
|
存储 缓存 NoSQL
Redis 核心知识与项目实践解析
本文围绕 Redis 展开,涵盖其在项目中的应用(热点数据缓存、存储业务数据、实现分布式锁)、基础数据类型(string 等 5 种)、持久化策略(RDB、AOF 及混合持久化)、过期策略(惰性 + 定期删除)、淘汰策略(8 种分类)。 还介绍了集群方案(主从复制、哨兵、Cluster 分片)及主从同步机制,分片集群数据存储的哈希槽算法。对比了 Redis 与 Memcached 的区别,说明了内存用完的情况及与 MySQL 数据一致性的保证方案。 此外,详解了缓存穿透、击穿、雪崩的概念及解决办法,如何保证 Redis 中是热点数据,Redis 分布式锁的实现及问题解决,以及项目中分布式锁
|
7月前
|
缓存 NoSQL Java
Redis应用—6.热key探测设计与实践
热key问题在高并发系统中可能导致数据层和服务层的严重瓶颈,如Redis集群瘫痪和用户体验下降。为解决此问题,京东开发了JdHotkey热key探测框架,具备实时性、准确性、集群一致性和高性能等特点。该框架由etcd集群、Client端jar包、Worker端集群和Dashboard控制台组成,通过分布式计算快速识别热key并推送至应用内存,有效减轻数据层负载,提升服务性能。JdHotkey适用于多种场景,安装部署简便,支持毫秒级热key探测和集群一致性维护。
329 61
Redis应用—6.热key探测设计与实践
|
5月前
|
缓存 NoSQL Java
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
111 5
Redis:现代服务端开发的缓存基石与电商实践-优雅草卓伊凡
|
8月前
|
存储 缓存 NoSQL
Redis哈希结构在提升数据检索速度中的实践应用
本文详细介绍了 Redis 哈希结构的特点、常见使用场景以及如何在实际应用中利用哈希结构提升数据检索速度。通过合理使用 Redis 哈希结构,可以显著提高系统的性能和响应速度。在实际开发中,结合具体业务需求,灵活运用 Redis 提供的多种数据结构,构建高效的缓存和数据存储解决方案。希望本文能帮助您更好地理解和应用 Redis 哈希结构,提升数据检索速度。
199 18
|
9月前
|
缓存 NoSQL JavaScript
Vue.js应用结合Redis数据库:实践与优化
将Vue.js应用与Redis结合,可以实现高效的数据管理和快速响应的用户体验。通过合理的实践步骤和优化策略,可以充分发挥两者的优势,提高应用的性能和可靠性。希望本文能为您在实际开发中提供有价值的参考。
190 11