简介: 本文主要是通过对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. 你是怎么理解ES6中Proxy的?使用场景?

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.介绍 定义: 用于定义基本操作的自定义行为 本质: 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta pro ...

  2. [ROS串口通信]报错:IO Exception (13): Permission denied, file /tmp/binarydeb/ros-noetic-serial-1.2.1/src/impl/unix.cc, line 151. [ERROR] [1705845384.528602780]: Unable to open port

    ROS在串口通信时,当我们插入USB后,catkin_make之后,报错: IO Exception (13): Permission denied, file /tmp/binarydeb/ros- ...

  3. KingabseES 构造常量数据表的方式 union, values, array

    背景 通用报表系统中,如果过滤条件是多选数据项,需要动态构造虚拟数据表,这里也会成为查询性能的痛点. 构造方式与执行计划 构造1000行数据的虚拟表. SQL UNION 组合多个查询的结果,需要解析 ...

  4. 如何修改 ARM Linux 系统的启动画面

    [如何修改 ARM Linux 系统的启动画面 Table of Contents U-Boot 的启动画面 内核的启动画面 PPM 图片格式 显示启动画面的过程 更换启动画面 动态修改启动画面 一些 ...

  5. Python列表list 分片实例

    1 a = list(range(10)) 2 print(a[::]) #复制一个列表 3 print(a[::2]) #每隔2个取一次 4 print(a[::3]) #每隔3个取一次 5 6 p ...

  6. 知名压缩软件 xz 被植入后门,黑客究竟是如何做到的?

    昨天,Andres Freund 通过电子邮件告知 oss-security@ 社区,他在 xz/liblzma 中发现了一个隐藏得非常巧妙的后门,这个后门甚至影响到了 OpenSSH 服务器的安全. ...

  7. 小师妹学JavaIO之:File copy和File filter

    目录 简介 使用java拷贝文件 使用File filter 总结 简介 一个linux命令的事情,小师妹非要让我教她怎么用java来实现,哎,摊上个这么杠精的小师妹,我也是深感无力,做一个师兄真的好 ...

  8. Go 语言学习:了解 const 关键字及常量声明

    如果一个变量应该有一个固定的.不能改变的值,你可以使用const关键字. const关键字将变量声明为"常量",这意味着它是不可改变和只读的. 语法 const CONSTNAME ...

  9. 深度解析 Spring Security:身份验证、授权、OAuth2 和 JWT 身份验证的完整指南

    Spring 安全框架 Spring Security 是一个用于保护基于 Java 的应用程序的框架.它是一个功能强大且高度可定制的身份验证和访问控制框架,可以轻松地集成到各种应用程序中,包括 We ...

  10. Linux 使用 crontab 定时拆分日志、清理过期文件

    @ 目录 前言 简介 一.准备工作 1.1 创建写入脚本 1.2 设置执行权限 1.3 添加定时任务 1.4 配置生效 二.Tomcat日志 按每天分割 2.1 创建一个 sh文件 2.2 设置执行权 ...