简介: 本文次要是通过对PFS引擎的内存治理源码的浏览,解读PFS内存调配及开释原理,深刻分析其中存在的一些问题,以及一些改良思路。

一 引言

MySQL Performance schema(PFS)是MySQL提供的弱小的性能监控诊断工具,提供了一种可能在运行时查看server外部执行状况的特办法。PFS通过监督server外部已注册的事件来收集信息,一个事件实践上能够是server外部任何一个执行行为或资源占用,比方一个函数调用、一个零碎调用wait、SQL查问中的解析或排序状态,或者是内存资源占用等。

PFS将采集到的性能数据存储在performance_schema存储引擎中,performance_schema存储引擎是一个内存表引擎,也就是所有收集的诊断信息都会保留在内存中。诊断信息的收集和存储都会带来肯定的额定开销,为了尽可能小的影响业务,PFS的性能和内存治理也显得十分重要了。本文次要是通过对PFS引擎的内存治理的源码的浏览,解读PFS内存调配及开释原理,深刻分析其中存在的一些问题,以及一些改良思路。本文源代码剖析基于MySQL-8.0.24版本。

二 内存治理模型

PFS内存治理有几个要害特点:

内存调配以Page为单位,一个Page内能够存储多条record

系统启动时事后调配局部pages,运行期间依据须要动静增长,但page是只增不回收的模式

record的申请和开释都是无锁的

1 外围数据结构

PFS_buffer_scalable_container是PFS内存治理的外围数据结构,整体构造如下图:

Container中蕴含多个page,每个page都有固定个数的records,每个record对应一个事件对象,比方PFS_thread。每个page中的records数量是固定不变的,但page个数会随着负载减少而增长。

2 Allocate时Page抉择策略

PFS_buffer_scalable_container是PFS内存治理的外围数据结构
波及内存调配的要害数据结构如下:

PFS_PAGE_SIZE  // 每个page的大小, global_thread_container中默认为256PFS_PAGE_COUNT // page的最大个数,global_thread_container中默认为256class PFS_buffer_scalable_container {  PFS_cacheline_atomic_size_t m_monotonic;            // 枯燥递增的原子变量,用于无锁抉择page  PFS_cacheline_atomic_size_t m_max_page_index;       // 以后已调配的最大page index  size_t m_max_page_count;                            // 最大page个数,超过后将不再调配新page  std::atomic<array_type *> m_pages[PFS_PAGE_COUNT];  // page数组  native_mutex_t m_critical_section;                  // 创立新page时须要的一把锁}

首先m_pages是一个数组,每个page都可能有free的records,也有可能整个page都是busy的,Mysql采纳了比较简单的策略,轮训挨个尝试每个page是否有闲暇,直到调配胜利。如果轮训所有pages仍然没有调配胜利,这个时候就会创立新的page来裁减,直到达到page数的下限。

轮训并不是每次都是从第1个page开始寻找,而是应用原子变量m_monotonic记录的地位开始查找,m_monotonic在每次在page中调配失败是加1。

外围简化代码如下:

value_type *allocate(pfs_dirty_state *dirty_state) {  current_page_count = m_max_page_index.m_size_t.load();    monotonic = m_monotonic.m_size_t.load();  monotonic_max = monotonic + current_page_count;  while (monotonic < monotonic_max) {    index = monotonic % current_page_count;    array = m_pages[index].load();    pfs = array->allocate(dirty_state);    if  (pfs) {      // 调配胜利返回      return pfs;    } else {      // 调配失败,尝试下一个page,       // 因为m_monotonic是并发累加的,这里有可能本地monotonic变量并不是线性递增的,有可能是从1 间接变为 3或更大,      // 所以以后while循环并不是严格轮训所有page,很大可能是跳着尝试,换者说这里并发拜访下大家一起轮训所有的page。      // 这个算法其实是有些问题的,会导致某些page被跳过疏忽,从而加剧扩容新page的几率,前面会详细分析。      monotonic = m_monotonic.m_size_t++;    }  }    // 轮训所有Page后没有调配胜利,如果没有达到下限的话,开始扩容page  while (current_page_count < m_max_page_count) {    // 因为是并发拜访,为了防止同时去创立新page,这里有一个把同步锁,也是整个PFS内存调配惟一的锁    native_mutex_lock(&m_critical_section);    // 拿锁胜利,如果array曾经不为null,阐明曾经被其它线程创立胜利    array = m_pages[current_page_count].load();    if (array == nullptr) {      // 抢到了创立page的责任      m_allocator->alloc_array(array);      m_pages[current_page_count].store(array);      ++m_max_page_index.m_size_t;    }    native_mutex_unlock(&m_critical_section);        // 在新的page中再次尝试调配    pfs = array->allocate(dirty_state);    if (pfs) {      // 调配胜利并返回      return pfs;    }    // 调配失败,持续尝试创立新的page直到下限  }}

咱们再详细分析下轮训page策略的问题,因为m_momotonic原子变量的累加是并发的,会导致一些page被跳过轮训它,从而加剧了扩容新page的几率。

举一个极其一些的例子,比拟容易阐明问题,假如以后一共有4个page,第1、4个page已满无可用record,第2、3个page有可用record。

当同时来了4个线程并发Allocate申请,同时拿到了的m_monotonic=0.

monotonic = m_monotonic.m_size_t.load();

这个时候所有线程尝试从第1个page调配record都会失败(因为第1个page是无可用record),而后累加去尝试下一个page

monotonic = m_monotonic.m_size_t++;

这个时候问题就来了,因为原子变量++是返回最新的值,4个线程++胜利是有先后顺序的,第1个++的线程后monotonic值为2,第2个++的线程为3,以次类推。这样就看到第3、4个线程跳过了page2和page3,导致3、4线程会轮训完结失败进入到创立新page的流程里,但这个时候page2和page3里是有闲暇record能够应用的。

尽管上述例子比拟极其,但在Mysql并发拜访中,同时申请PFS内存导致跳过一部分page的状况应该还是非常容易呈现的。

3 Page内Record抉择策略

PFS_buffer_default_array是每个Page保护一组records的治理类。

要害数据结构如下:

class PFS_buffer_default_array {PFS_cacheline_atomic_size_t m_monotonic;      // 枯燥递增原子变量,用来抉择free的recordsize_t m_max;                                 // record的最大个数T *m_ptr;                                     // record对应的PFS对象,比方PFS_thread}

每个Page其实就是一个定长的数组,每个record对象有3个状态FREE,DIRTY, ALLOCATED,FREE示意闲暇record能够应用,ALLOCATED是已调配胜利的,DIRTY是一个中间状态,示意已被占用但还没调配胜利。

Record的抉择实质就是轮训查找并抢占状态为free的record的过程。

外围简化代码如下:

value_type *allocate(pfs_dirty_state *dirty_state) {  // 从m_monotonic记录的地位开始尝试轮序查找  monotonic = m_monotonic.m_size_t++;  monotonic_max = monotonic + m_max;  while (monotonic < monotonic_max) {    index = monotonic % m_max;    pfs = m_ptr + index;      // m_lock是pfs_lock构造,free/dirty/allocated三状态是由这个数据结构来保护的    // 前面会具体介绍它如何实现原子状态迁徙的    if (pfs->m_lock.free_to_dirty(dirty_state)) {      return pfs;    }    // 以后record不为free,原子变量++尝试下一个    monotonic = m_monotonic.m_size_t++;  }}

抉择record的主体主体流程和抉择page根本类似,不同的是page内record数量是固定不变的,所以没有扩容的逻辑。

当然抉择策略雷同,也会有同样的问题,这里的m_monotonic原子变量++是多线程并发的,同样如果并发大的场景下会有record被跳过抉择了,这样导致page外部即使有free的record也可能没有被选中。

所以也就是page抉择即使是没有被跳过,page内的record也有几率被跳过而选不中,雪上加霜,更加加剧了内存的增长。

4 pfs_lock

每个record都有一个pfs_lock,来保护它在page中的调配状态(free/dirty/allocated),以及version信息。

要害数据结构:

struct pfs_lock {std::atomic m_version_state;}

pfs_lock应用1个32位无符号整型来保留version+state信息,格局如下:

state

低2位字节示意调配状态。

state PFS_LOCK_FREE = 0x00state PFS_LOCK_DIRTY = 0x01state PFS_LOCK_ALLOCATED = 0x11

version

初始version为0,每调配胜利一次加1,version就能示意该record被调配胜利的次数次要看一下状态迁徙代码:

// 上面3个宏次要就是用来位操作的,不便操作state或version#define VERSION_MASK 0xFFFFFFFC#define STATE_MASK 0x00000003#define VERSION_INC 4bool free_to_dirty(pfs_dirty_state *copy_ptr) {  uint32 old_val = m_version_state.load();  // 判断以后state是否为FREE,如果不是,间接返回失败  if ((old_val & STATE_MASK) != PFS_LOCK_FREE) {    return false;  }  uint32 new_val = (old_val & VERSION_MASK) + PFS_LOCK_DIRTY;  // 以后state为free,尝试将state批改为dirty,atomic_compare_exchange_strong属于乐观锁,多个线程可能同时  // 批改该原子变量,但只有1个批改胜利。  bool pass =      atomic_compare_exchange_strong(&m_version_state, &old_val, new_val);  if (pass) {    // free to dirty 胜利    copy_ptr->m_version_state = new_val;  }  return pass;}void dirty_to_allocated(const pfs_dirty_state *copy) {  /* Make sure the record was DIRTY. */  assert((copy->m_version_state & STATE_MASK) == PFS_LOCK_DIRTY);  /* Increment the version, set the ALLOCATED state */  uint32 new_val = (copy->m_version_state & VERSION_MASK) + VERSION_INC +                   PFS_LOCK_ALLOCATED;  m_version_state.store(new_val);}

状态迁徙过程还是比拟好了解的, 由dirty_to_allocated和allocated_to_free的逻辑是更简略的,因为只有record状态是free时,它的状态迁徙是存在并发多写问题的,一旦state变为dirty,以后record相当于曾经被某一个线程占有,其它线程不会再尝试操作该record了。

version的增长是在state变为PFS_LOCK_ALLOCATED时

5 PFS内存开释

PFS内存开释就比较简单了,因为每个record都记录了本人所在的container和page,调用deallocate接口,最终将状态置为free就实现了。

最底层都会进入到pfs_lock来更新状态:

struct pfs_lock {  void allocated_to_free(void) {    /*      If this record is not in the ALLOCATED state and the caller is trying      to free it, this is a bug: the caller is confused,      and potentially damaging data owned by another thread or object.    */    uint32 copy = copy_version_state();    /* Make sure the record was ALLOCATED. */    assert(((copy & STATE_MASK) == PFS_LOCK_ALLOCATED));    /* Keep the same version, set the FREE state */    uint32 new_val = (copy & VERSION_MASK) + PFS_LOCK_FREE;    m_version_state.store(new_val);  }}

三 内存调配的优化

后面咱们剖析到无论是page还是record都有几率呈现跳过轮训的问题,即使是缓存中有free的成员也会呈现调配不胜利,导致创立更多的page,占用更多的内存。最次要的问题是这些内存一旦调配就不会被开释。

为了晋升PFS内存命中率,尽量避免上述问题,有一些思路如下:

while (monotonic < monotonic_max) {    index = monotonic % current_page_count;    array = m_pages[index].load();    pfs = array->allocate(dirty_state);    if  (pfs) {       // 记录调配胜利的index       m_monotonic.m_size_t.store(index);      return pfs;    } else {      // 局部变量递增,防止掉并发累加而跳过某些pages      monotonic++;    }  }

另外一点,每次查找都是从最近一次调配胜利的地位开始,这样必然导致并发拜访的抵触,因为大家都从同一个地位开始找,起始查找地位应该退出肯定的随机性,这样能够防止大量的抵触重试。

总结如下:

  • 每次Allocate是从最近一次调配胜利的index开始查找,或者随机地位开始查找
  • 每个Allocate严格轮训所有pages或records

四 内存开释的优化

PFS内存开释的最大的问题就是一旦创立出的内存就得不到开释,直到shutdown。如果遇到热点业务,在业务顶峰阶段调配了很多page的内存,在业务低峰阶段仍然得不到开释。

要实现定期检测回收内存,又不影响内存调配的效率,实现一套无锁的回收机制还是比较复杂的。

次要有如下几点须要思考:

  • 开释必定是要以page为单位的,也就是开释的page内的所有records都必须保障都为free,而且要保障待free的page不会再被调配到
  • 内存调配是随机的,整体上内存是能够回收的,但可能每个page都有一些busy的,如何更优的协调这种状况
  • 开释的阈值怎么定,也要防止频繁调配+开释的问题

针对PFS内存开释的优化,PolarDB曾经开发并提供了定期回收PFS内存的个性,鉴于本篇幅的限度,留在后续再介绍了。

五 对于咱们

PolarDB 是阿里巴巴自主研发的云原生分布式关系型数据库,于2020年进入Gartner寰球数据库Leader象限,并取得了2020年中国电子学会颁发的科技进步一等奖。PolarDB 基于云原生分布式数据库架构,提供大规模在线事务处理能力,兼具对简单查问的并行处理能力,在云原生分布式数据库畛域整体达到了国内领先水平,并且失去了宽泛的市场认可。在阿里巴巴团体外部的最佳实际中,PolarDB还全面撑持了2020年天猫双十一,并刷新了数据库解决峰值记录,高达1.4亿TPS。欢送有志之士退出咱们,简历请投递到zetao.wzt@alibaba-inc.com,期待与您独特打造世界一流的下一代云原生分布式关系型数据库。

参考:
[1] MySQL Performance Schema
https://dev.mysql.com/doc/ref...

[2] MySQL · 最佳实际 · 明天你并行了吗?---洞察PolarDB 8.0之并行查问
http://mysql.taobao.org/month...

[3] Source code mysql / mysql-server 8.0.24
https://github.com/mysql/mysq...

原文链接
本文为阿里云原创内容,未经容许不得转载。