MYSQLPrformancschma(PFS)是mysql提供的强大的性能监控诊断工具,提供了一种能够在运行时检查srvr内部执行情况的特方法。PFS通过监视srvr内部已注册的事件来收集信息,一个事件理论上可以是srvr内部任何一个执行行为或资源占用,比如一个函数调用、一个系统调用wait、SQL查询中的解析或排序状态,或者是内存资源占用等。
PFS将采集到的性能数据存储在prformanc_schma存储引擎中,prformanc_schma存储引擎是一个内存表引擎,也就是所有收集的诊断信息都会保存在内存中。诊断信息的收集和存储都会带来一定的额外开销,为了尽可能小的影响业务,PFS的性能和内存管理也显得非常重要了。
本文主要是通过对PFS引擎的内存管理的源码的阅读,解读PFS内存分配及释放原理,深入剖析其中存在的一些问题,以及一些改进思路。本文源代码分析基于Mysql-8.0.24版本。
二内存管理模型PFS内存管理有几个关键特点:
内存分配以Pag为单位,一个Pag内可以存储多条cord系统启动时预先分配部分pags,运行期间根据需要动态增长,但pag是只增不回收的模式cord的申请和释放都是无锁的1核心数据结构
PFS_buffr_scalabl_containr是PFS内存管理的核心数据结构,整体结构如下图:
Containr中包含多个pag,每个pag都有固定个数的cords,每个cord对应一个事件对象,比如PFS_thad。每个pag中的cords数量是固定不变的,但pag个数会随着负载增加而增长。
2Allocat时Pag选择策略
PFS_buffr_scalabl_containr是PFS内存管理的核心数据结构
涉及内存分配的关键数据结构如下:
PFS_PAGE_SIZE//每个pag的大小,global_thad_containr中默认为PFS_PAGE_COUNT//pag的最大个数,global_thad_containr中默认为classPFS_buffr_scalabl_containr{PFS_cachlin_atomic_siz_tm_monotonic;//单调递增的原子变量,用于无锁选择pagPFS_cachlin_atomic_siz_tm_max_pag_indx;//当前已分配的最大pagindxsiz_tm_max_pag_count;//最大pag个数,超过后将不再分配新pagstd::atomicarray_typ*m_pags[PFS_PAGE_COUNT];//pag数组nativ_mutx_tm_critical_sction;//创建新pag时需要的一把锁}
首先m_pags是一个数组,每个pag都可能有f的cords,也有可能整个pag都是busy的,Mysql采用了比较简单的策略,轮训挨个尝试每个pag是否有空闲,直到分配成功。如果轮训所有pags依然没有分配成功,这个时候就会创建新的pag来扩充,直到达到pag数的上限。
轮训并不是每次都是从第1个pag开始寻找,而是使用原子变量m_monotonic记录的位置开始查找,m_monotonic在每次在pag中分配失败是加1。
核心简化代码如下:
valu_typ*allocat(pfs_dirty_stat*dirty_stat){curnt_pag_count=m_max_pag_indx.m_siz_t.load();monotonic=m_monotonic.m_siz_t.load();monotonic_max=monotonic+curnt_pag_count;whil(monotonicmonotonic_max){indx=monotonic%curnt_pag_count;array=m_pags[indx].load();pfs=array-allocat(dirty_stat);if(pfs){//分配成功返回turnpfs;}ls{//分配失败,尝试下一个pag,//因为m_monotonic是并发累加的,这里有可能本地monotonic变量并不是线性递增的,有可能是从1直接变为3或更大,//所以当前whil循环并不是严格轮训所有pag,很大可能是跳着尝试,换者说这里并发访问下大家一起轮训所有的pag。//这个算法其实是有些问题的,会导致某些pag被跳过忽略,从而加剧扩容新pag的几率,后面会详细分析。monotonic=m_monotonic.m_siz_t++;}}//轮训所有Pag后没有分配成功,如果没有达到上限的话,开始扩容pagwhil(curnt_pag_countm_max_pag_count){//因为是并发访问,为了避免同时去创建新pag,这里有一个把同步锁,也是整个PFS内存分配唯一的锁nativ_mutx_lock(m_critical_sction);//拿锁成功,如果array已经不为null,说明已经被其它线程创建成功array=m_pags[curnt_pag_count].load();if(array==nullptr){//抢到了创建pag的责任m_allocator-alloc_array(array);m_pags[curnt_pag_count].sto(array);++m_max_pag_indx.m_siz_t;}nativ_mutx_unlock(m_critical_sction);//在新的pag中再次尝试分配pfs=array-allocat(dirty_stat);if(pfs){//分配成功并返回turnpfs;}//分配失败,继续尝试创建新的pag直到上限}}
我们再详细分析下轮训pag策略的问题,因为m_momotonic原子变量的累加是并发的,会导致一些pag被跳过轮训它,从而加剧了扩容新pag的几率。
举一个极端一些的例子,比较容易说明问题,假设当前一共有4个pag,第1、4个pag已满无可用cord,第2、3个pag有可用cord。
当同时来了4个线程并发Allocat请求,同时拿到了的m_monotonic=0.
monotonic=m_monotonic.m_siz_t.load();
这个时候所有线程尝试从第1个pag分配cord都会失败(因为第1个pag是无可用cord),然后累加去尝试下一个pag
monotonic=m_monotonic.m_siz_t++;
这个时候问题就来了,因为原子变量++是返回最新的值,4个线程++成功是有先后顺序的,第1个++的线程后monotonic值为2,第2个++的线程为3,以次类推。这样就看到第3、4个线程跳过了pag2和pag3,导致3、4线程会轮训结束失败进入到创建新pag的流程里,但这个时候pag2和pag3里是有空闲cord可以使用的。
虽然上述例子比较极端,但在Mysql并发访问中,同时申请PFS内存导致跳过一部分pag的情况应该还是非常容易出现的。
3Pag内Rcord选择策略
PFS_buffr_dfault_array是每个Pag维护一组cords的管理类。
关键数据结构如下:
classPFS_buffr_dfault_array{PFS_cachlin_atomic_siz_tm_monotonic;//单调递增原子变量,用来选择f的cordsiz_tm_max;//cord的最大个数T*m_ptr;//cord对应的PFS对象,比如PFS_thad}
每个Pag其实就是一个定长的数组,每个cord对象有3个状态FREE,DIRTY,ALLOCATED,FREE表示空闲cord可以使用,ALLOCATED是已分配成功的,DIRTY是一个中间状态,表示已被占用但还没分配成功。
Rcord的选择本质就是轮训查找并抢占状态为f的cord的过程。
核心简化代码如下:
valu_typ*allocat(pfs_dirty_stat*dirty_stat){//从m_monotonic记录的位置开始尝试轮序查找monotonic=m_monotonic.m_siz_t++;monotonic_max=monotonic+m_max;whil(monotonicmonotonic_max){indx=monotonic%m_max;pfs=m_ptr+indx;//m_lock是pfs_lock结构,f/dirty/allocatd三状态是由这个数据结构来维护的//后面会详细介绍它如何实现原子状态迁移的if(pfs-m_lock.f_to_dirty(dirty_stat)){turnpfs;}//当前cord不为f,原子变量++尝试下一个monotonic=m_monotonic.m_siz_t++;}}
选择cord的主体主体流程和选择pag基本相似,不同的是pag内cord数量是固定不变的,所以没有扩容的逻辑。
当然选择策略相同,也会有同样的问题,这里的m_monotonic原子变量++是多线程并发的,同样如果并发大的场景下会有cord被跳过选择了,这样导致pag内部即便有f的cord也可能没有被选中。
所以也就是pag选择即便是没有被跳过,pag内的cord也有几率被跳过而选不中,雪上加霜,更加加剧了内存的增长。
4pfs_lock
每个cord都有一个pfs_lock,来维护它在pag中的分配状态(f/dirty/allocatd),以及vrsion信息。
关键数据结构:
structpfs_lock{std::atomicm_vrsion_stat;}
pfs_lock使用1个32位无符号整型来保存vrsion+stat信息,格式如下:
stat低2位字节表示分配状态。
statPFS_LOCK_FREE=0x00statPFS_LOCK_DIRTY=0x01statPFS_LOCK_ALLOCATED=0x11
vrsion
初始vrsion为0,每分配成功一次加1,vrsion就能表示该cord被分配成功的次数主要看一下状态迁移代码:
//下面3个宏主要就是用来位操作的,方便操作stat或vrsion#dfinVERSION_MASK0xFFFFFFFC#dfinSTATE_MASK0x#dfinVERSION_INC4boolf_to_dirty(pfs_dirty_stat*copy_ptr){uint32old_val=m_vrsion_stat.load();//判断当前stat是否为FREE,如果不是,直接返回失败if((old_valSTATE_MASK)!=PFS_LOCK_FREE){turnfals;}uint32nw_val=(old_valVERSION_MASK)+PFS_LOCK_DIRTY;//当前stat为f,尝试将stat修改为dirty,atomic_