简介: 本文主要是通过对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中默认为256
PFS_PAGE_COUNT // page的最大个数,global_thread_container中默认为256

class 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的record
size_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 = 0x00

state PFS_LOCK_DIRTY = 0x01

state PFS_LOCK_ALLOCATED = 0x11

version

初始version为0,每分配成功一次加1,version就能表示该record被分配成功的次数主要看一下状态迁移代码:

// 下面3个宏主要就是用来位操作的,方便操作state或version
#define VERSION_MASK 0xFFFFFFFC
#define STATE_MASK 0x00000003
#define VERSION_INC 4

bool 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++;
}
}

另外一点,每次查找都是从最近一次分配成功的位置开始,这样必然导致并发访问的冲突,因为大家都从同一个位置开始找,起始查找位置应该加入一定的随机性,这样可以避免大量的冲突重试。

总结如下:

  1. 每次Allocate是从最近一次分配成功的index开始查找,或者随机位置开始查找
  2. 每个Allocate严格轮训所有pages或records

四  内存释放的优化

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

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

主要有如下几点需要考虑:

  1. 释放肯定是要以page为单位的,也就是释放的page内的所有records都必须保证都为free,而且要保证待free的page不会再被分配到
  2. 内存分配是随机的,整体上内存是可以回收的,但可能每个page都有一些busy的,如何更优的协调这种情况
  3. 释放的阈值怎么定,也要避免频繁分配+释放的问题

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

五  关于我们

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

参考:

[1] MySQL Performance Schema

MySQL :: MySQL 8.0 Reference Manual :: 27 MySQL Performance Schema

[2] MySQL · 最佳实践 · 今天你并行了吗?---洞察PolarDB 8.0之并行查询

MySQL · 最佳实践 · 今天你并行了吗?---洞察PolarDB 8.0之并行查询

[3] Source code mysql / mysql-server 8.0.24

https://github.com/mysql/mysql-server/tree/mysql-8.0.24

原文链接

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

MySQL深潜|剖析Performance Schema内存管理的更多相关文章

  1. [MySQL Reference Manual] 23 Performance Schema结构

    23 MySQL Performance Schema 23 MySQL Performance Schema 23.1 性能框架快速启动 23.2 性能框架配置 23.2.1 性能框架编译时配置 2 ...

  2. linux0.11内核源码剖析:第一篇 内存管理、memory.c【转】

    转自:http://www.cnblogs.com/v-July-v/archive/2011/01/06/1983695.html linux0.11内核源码剖析第一篇:memory.c July  ...

  3. MySQL 调优基础(二) Linux内存管理

    进程的运行,必须使用内存.下图是Linux中进程中的内存的分布图: 其中最重要的 heap segment 和 stack segment.其它内存段基本是大小固定的.注意stack是向低地址增长的, ...

  4. CLR via C#深解笔记七 - 自动内存管理(垃圾回收)

    每个应用程序都要使用这样或者那样的资源,比如文件.内存缓冲区.屏幕空间.网络连接.数据库资源等.事实上,在面向对象的环境中,每个类型都代表可供程序使用的一种资源. 要使用这些资源,必须为代表资源的类型 ...

  5. MySQL 调优基础:Linux内存管理 Linux文件系统 Linux 磁盘IO Linux网络

    http://www.cnblogs.com/digdeep/category/739915.html

  6. spark 源码分析之十五 -- Spark内存管理剖析

    本篇文章主要剖析Spark的内存管理体系. 在上篇文章 spark 源码分析之十四 -- broadcast 是如何实现的?中对存储相关的内容没有做过多的剖析,下面计划先剖析Spark的内存机制,进而 ...

  7. MySQL调优性能监控之performance schema

    一.performance_schema的介绍 performance:性能 schema:图(表)示,以大纲或模型的形式表示计划或理论. MySQL的performance schema 用于监控M ...

  8. MySQL Performance Schema详解

    MySQL的performance schema 用于监控MySQL server在一个较低级别的运行过程中的资源消耗.资源等待等情况. 1 performance schema特点 提供了一种在数据 ...

  9. Python 源码剖析(六)【内存管理机制】

    六.内存管理机制 1.内存管理架构 2.小块空间的内存池 3.循环引用的垃圾收集 4.python中的垃圾收集 1.内存管理架构 Python内存管理机制有两套实现,由编译符号PYMALLOC_DEB ...

  10. MySQL 5.7 Performance Schema 详解

    refman mysql 5.7 MySQL Performance Schema  用于监视MySQL服务器,且运行时消耗很少的性能.Performance Schema 收集数据库服务器性能参数, ...

随机推荐

  1. DialogFragment源码分析

    目录介绍 1.最简单的使用方法 1.1 官方建议 1.2 最简单的使用方法 1.3 DialogFragment做屏幕适配 2.源码分析 2.1 DialogFragment继承Fragment 2. ...

  2. 关于三维模型OBJ格式轻量化压缩必要性探讨

    关于三维模型OBJ格式轻量化压缩必要性探讨 三维模型的OBJ格式轻量化压缩在当前的计算机图形学和虚拟现实应用中具有重要的必要性.以下是对三维模型OBJ格式轻量化压缩必要性的分析: 1.提高加载和传输效 ...

  3. 记录--vue脚手架

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.vue脚手架 1.简介 Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统. 2.命令行操作步骤 npm install ...

  4. Python 汇总列数据到行

    Python汇总Excel列数据到行(方法一) import pandas as pd # 读取Excel文件 df = pd.read_excel('C:\\Users\\liuchunlin2\\ ...

  5. FFmpeg开发笔记(七)欧拉系统编译安装FFmpeg

    FFmpeg支持Linux.macOS.Windows.Android等操作系统,其中Linux系列包括Ubuntu.Debian.Mint.CentOS.RHEL.Fedora等分支.FFmpeg官 ...

  6. Scala数值类型转换、算数运算符、关系(比较)运算符和逻辑运算符

    原则 强制类型转换 Java : int num = (int)2.5Scala : var num : Int = 2.7.toInt 数值类型和String类型之间的转换 (1)基本类型转 Str ...

  7. js实现多列排序-存在问题

    js实现多列排序 根据业务逻辑调整 sortData 的数据. 排序的规则是按照第一列排序,第一列相同按照第二列排序,依次类推 // 要排序的数据 const array = [{ name: '甲' ...

  8. #前缀和,后缀和#洛谷 4280 [AHOI2008]逆序对

    题目传送门 分析 首先填的数字单调不降,感性理解 那可以维护\([a_1\sim a_{i-1}]\)的\(cnt\)后缀和以及 \([a_{i+1}\sim a_n]\)的\(cnt\)前缀和,那可 ...

  9. OTP/HOTP/TOTP的资料

    参考资料 [加解密]动态令牌-(OTP,HOTP,TOTP)-基本原理 每天一个小知识:HOTP HOTP和TOTP算法图解 RFC HMAC: Keyed-Hashing for Message A ...

  10. JDK10的新特性:var和匿名类

    目录 简介 匿名类中自定义变量 lambda表达式中的匿名类 总结 简介 匿名类相信大家都用过了,学过JDK8中的lambda表达式之后,可以发现有些匿名类是可以用lambda表达式来替代的,能够被替 ...