GaussDB(DWS) mmap锁分析以及优化
一,mmap锁介绍
mmap_sem锁本质是一个进程级别的读写锁:
写锁保护VMA的修改,例如:mmap分配虚拟内存,munmap释放虚拟内存;
读锁保护VMA不被并发修改,例如:page_fault时VMA到物理内存的映射;
二、mmap锁产生的问题
mmap_sem读写锁设计上可以实现并发的多线程读访问。但这个并发是有条件的。
如下图场景,线程1和线程3的读锁无法并发访问,线程3需要等待线程2的写锁释放后才能加读锁。
即有大量写锁时,该锁其实就变成互斥锁了,所以应用实现中应该尽可能避免触发mmap写锁。
page fault问题案例:cgroup限额CPU排队mmap读写锁阻塞
如下图,在小资源场景,与CPU调度互相等待,产生进程级阻塞
0,某现网环境配置限额小资源池,仅绑定两个CPU核,但有2000+业务线程只在CPU1/2上调度。
1,新分配内存的首次读写和释放需要使用持有mmap读锁,大量mmap读锁申请,如果没有写锁,完全可以并行。
2,pm启动pg子线程需要持有mmap的写锁,将阻塞在这之后的mmap读锁(R7-R9无法与R1-R6并行)。
3,同样,R7-R8的读锁又会阻塞后续的写锁W2
4,R7-R9中有大量的mmap读锁任务,但因为大多限制在CPU1/2上调度,在CPU调度队列上排队,进一步阻塞W1。
5,pm启动pg子线程需要持有mmap的写锁,此时要等待R7-R9中的所有mmap读锁任务被CPU调度完成。
6,pm启动pg子线程阻塞在mmap写锁,最终60s超时报错,无法对外提供服务。
cache回收阻塞问题案例:高IO业务导致集群hang死
如下图,在高IO压力场景,cache回收阻塞后,导致进程mmap锁阻塞
1,高IO业务读取大量的磁盘文件,产生大量的磁盘IO和cache,同时消耗大量内存
2,pm线程fork pg线程时调用mmap因内存不足触了阻塞式直接内存回收
3,直接内存回收需要将脏页刷盘,刷盘时被业务的磁盘IO阻塞,影响回收性能
4,pm最终60s超时报错,无法对外提供服务
Linux开发人员也知道这把锁罪大恶极,做了很多优化,但都没法根治
|
优化点 |
说明 |
kernel |
mmap_lock: add tracepoints around lock acquisition |
独立mmap_lock函数,并增加trace点,增强DFX分析 |
kernel |
Speculative page faults(投机性缺页,Android已合入,TencentOS已应用) |
写锁只修改引用计数,读操作过程中引用计数没有修改则无需加锁,反之浪费一次操作 |
kernel |
per-VMA lock(未正式合入) |
每一块VMA单独一把锁,减少锁粒度 |
阿里 |
VLDB :《Async-fork》 |
将 fork 调用过程中最耗时的页表拷贝部分从父进程移动到子进程 |
三,DWS mmap锁的使用场景
如上图所示,从代码层面上分析,DWS使用mmap的场景主要是子进程启动,子线程启动,LLVM,jemalloc预热等阶段。
使用BPF trace工具,调用命令以下命令,可以在运行态对mmap写锁进行调用栈抓取与分析
./trace -p gaussdb进程号 -KU -T 'down_write_killable(struct rw_semaphore *sem)(sem == &($task->mm->mmap_sem))'
TPCDS并发场景下使用BCC可以观察到写锁热点如下:
总次数 |
子线程 |
jemalloc |
llvm |
子进程 |
73277 |
29554 |
23181 |
20552 |
手动触发 |
DWS由于是多线程架构,进程级别的mmap锁一但出现阻塞,轻则性能下降,重则进程hang死。
四,DWS mmap锁优化策略
1,子进程优化
子进程启动时持有mmap写锁主要是在copy_mm阶段(子进程复制父进程的VMA虚拟内存地址),这些子进程基本都是调用简单的外部命令获取一些信息,无需父进程的全量内存信息。
通过使用vfork对fork的替换,实现fork/system/popen的重新封装,绕开copy_mm,不再持有mmap写锁。
2,子线程优化
子线程启动时持有mmap写锁主要是在栈内存空间的mmap调用上。
通过在用户态提前mmap内存,并对栈内存进行池化管理,调用pthread_attr_setstack使用用户态栈内存,实现pthread_create不再调用mmap,规避mmap写锁。
3,其他优化
LLVM需要临时mmap分配内存生成可执行代码,执行完成后又需要munmap释放,都需要加mmap写锁,需要对LLVM内存进行池化管理
jemalloc在预热阶段,mmap内存需要加锁写,释放内存时使用madvise(DONTNEED),只需要加读锁,且并不会归还虚拟内存,预热完成后,将不再需要调用mmap申请更多的虚拟内存,即不再需要加mmap写锁
优化后使用BPF trace工具观察,mmap写锁已经基本消除
TPCDS 20分钟 |
优化前首轮 |
优化前次轮 |
优化后首轮 |
优化后次轮 |
mm写锁次数 |
66484 |
65617 |
464 |
29 |
- 点赞
- 收藏
- 关注作者
评论(0)