MYSQL深潜 - 剖析Performance Schema内存管理

简介: 本文主要是通过对PFS引擎的内存管理的源码的阅读,解读PFS内存分配及释放原理,深入剖析其中存在的一些问题,以及一些改进思路。本文源代码分析基于Mysql-8.0.24版本。

image.png

作者 | 之枢
来源 | 阿里技术公众号

一 引言

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内存管理的核心数据结构,整体结构如下图:

image.png

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中默认为256class PFS_buffer_scalable_container {PFS_cacheline_atomic_size_t m_monotonic;            // 单调递增的原子变量,用于无锁选择pagePFS_cacheline_atomic_size_t m_max_page_index;       // 当前已分配的最大page indexsize_t m_max_page_count;                            // 最大page个数,超过后将不再分配新pagestd::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后没有分配成功,如果没有达到上限的话,开始扩容pagewhile (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信息,格式如下:

image.png

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 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 tryingto 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) {// 记录分配成功的indexm_monotonic.m_size_t.store(index);return pfs;} else {// 局部变量递增,避免掉并发累加而跳过某些pagesmonotonic++;}}

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

总结如下:

  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
GitHub - mysql/mysql-server at mysql-8.0.24

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/512322.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于MaxCompute SQL 的半结构化数据处理实践

简介&#xff1a; MaxCompute作为企业级数据仓库服务&#xff0c;集中存储和管理企业数据资产、面向数据应用处理和分析数据&#xff0c;将数据转换为业务洞察。通过与阿里云内、外部服务灵活组合&#xff0c;可构建丰富的数据应用。全托管的数据与分析解决方案&#xff0c;可简…

file_get_contents请求失败处理_SpringCloud Gateway网关处理请求过程中遇到400Bad Request问题解决方案...

大家在使用springcloud自己的gateway作为网关服务时&#xff0c;可能会不小心遇到自定义的Filter处理请求Request报文时出现400的错误&#xff0c;而且这个错误还不是每次请求都必现&#xff0c;额什么意思&#xff1f;难不成你是说请求还时好时坏&#xff1f;bingo&#xff01…

CSDN企业数字化之路 ——「低代码」发展研讨会北京站现场实录大放送

作者 | 千鸟 出品 | CSDN云计算&#xff08;ID&#xff1a;CSDNcloud&#xff09; 2021年底&#xff0c;CSDN面向行业开发者和高校学生开发者&#xff0c;展开了关于“低代码”的开发者调研活动。基于调研数据&#xff0c;CSDN提出了对低代码发展趋势的五大方向。在随后举行的 …

MaxCompute 存储设计

简介&#xff1a; 存储策略该怎么设计 写这篇存储规划的文章主要是想告诉大家该如何给存储做一个规划&#xff0c;在关系数据库的时代存储昂贵且珍惜&#xff0c;掰手指头花钱是存储规划的常态。但是到了大数据时代大家又立即就都变成印美元的美国政府了&#xff0c;感觉存储很…

Serverless Devs 2.0 开箱测评:Serverless 开发最佳实践

简介&#xff1a; 当下&#xff0c;Serverless 概念很火&#xff0c;很多同学被 Serverless 的优势吸引过来&#xff0c;比如它的弹性伸缩&#xff0c;免运维&#xff0c;高可用&#xff0c;资费少。但真正使用起来去落地的时候发现问题很多&#xff0c;大型项目如何组织函数&a…

【CDS技术揭秘系列 总篇】阿里云的云定义存储来了

简介&#xff1a; 全新发布的云定义存储 CDS 和传统的存储阵列、分布式存储、软件定义存储的区别在哪里&#xff1f;阿里云存储团队如何看待将来存储的发展趋势&#xff1f;本文邀请了 CDS 研发团队的核心技术负责人为大家揭开围绕着阿里云 CDS 的种种谜团。 云定义存储&#…

TSDB时序数据库时序数据压缩解压技术浅析

简介&#xff1a; 目前&#xff0c;物联网、工业互联网、车联网等智能互联技术在各个行业场景下快速普及应用&#xff0c;导致联网传感器、智能设备数量急剧增加&#xff0c;随之而来的海量时序监控数据存储、处理问题&#xff0c;也为时序数据库高效压缩、存储数据能力提出了更…

Atmosic推出ATM33新品,全新的ATM33系列性能大升级

为减少各种物联网产品高昂的电池更换成本&#xff0c;以及降低对环境的危害&#xff0c;在上个月举行的媒体发布会中&#xff0c;Atmosic营销及业务拓展副总裁 Srinivas发布了公司的新产品——ATM33&#xff0c;并详细解析了ATM33的技术特性和主要应用领域。 ATM33系列产品可支…

什么是低代码(Low-Code)?

简介&#xff1a; 什么是低代码&#xff1f;我们为什么需要低代码&#xff1f;低代码会让程序员失业吗&#xff1f;本文总结了低代码领域的基本概念、核心价值与行业现状&#xff0c;带你全面了解低代码。 阿里云 云原生应用研发平台EMAS 彭群&#xff08;楚衡&#xff09; 一…

php用wordanalysis抓取姓名_利用vba查询/抓取 外部数据

考虑这么一个excel文件&#xff0c;路径为&#xff1a;"E:dataEdata.xlsx"&#xff0c;样式如封面图片所示想要在其他excel文件中&#xff0c;通过代码直接抓取Edata.xlsx中想要的数据&#xff0c;做法如下&#xff1a;先在Visual Basic中勾选“工具-引用-Microsoft …

如何加速云原生数据应用?这个开源项目备受关注

简介&#xff1a; 自2020年9月Fluid正式对外开源&#xff0c;发展短短一年时间&#xff0c; Fluid 便一次获得两项开源界的重要认可&#xff0c;证明着其所专注的云原生、AI 领域也正在迎来广泛关注。这其中的意义和价值如何&#xff1f;我们尝试管中察豹&#xff0c;从 Fluid …

使用 Cilium 增强 Kubernetes 网络安全

作者 | Addo Zhang来源 | 云原生指北TL;DR在本篇&#xff0c;我们分别使用了 Kubernetes 原生的网络策略和 Cilium 的网络策略实现了 Pod 网络层面的隔离。不同的是&#xff0c;前者只提供了基于 L3/4 的网络策略&#xff1b;后者支持 L3/4、L7 的网络策略。通过网络策略来提升…

内含干货PPT下载|一站式数据管理DMS关键技术解读

简介&#xff1a; 深入解读实时数据流、库仓一体数据处理等核心技术 “数聚云端智驭未来”——阿里云数据库创新上云峰会暨第3届数据库性能挑战赛决赛颁奖典礼已圆满结束&#xff0c;更多干货内容欢迎大家观看峰会直播回放。 峰会直播回放&#x1f4ce;数聚云端 智驭未来——…

好饭不怕晚,扒一下 Redis 的配置文件

作者 | 阿Q来源 | 阿Q说代码在往期的文章中我们已经对Redis的概念和基本命令进行了讲解&#xff0c;今天我们来看下它的配置文件&#xff0c;Redis的配置文件在我们的开发和实际应用中起着非常重要的作用。我们可以在安装目录下找到redis.conf配置文件&#xff0c;通过vim命令进…

ICBU可控文本生成技术详解

简介&#xff1a; 文本生成&#xff08;Text Generation&#xff09;是自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09;领域的一项重要且具有挑战的任务。顾名思义&#xff0c;文本生成任务的目的是生成近似于自然语言的文本序列&#xff0c;…

云拨测助力节卡机器人 全面优化海外网站性能

简介&#xff1a; 【案例分享云拨测】借助云拨测&#xff0c;节卡机器人有效挖掘性能瓶颈&#xff0c;经过优化&#xff0c;提升网站打开速度 50% 以上&#xff0c;提高了运营推广活动的 ROI&#xff0c;帮助节卡为全球用户提供更加优质的服务&#xff01; 作者&#xff5c;白…

分享一个巨好用的 HTTP 命令行宝藏工具

作者 | Eason来源 | 程序员巴士HTTPie是一个命令行 HTTP 客户端。它的目标是使 CLI 与 Web 服务的交互尽可能人性化。HTTPie 设计用于测试、调试以及通常与 API 和 HTTP 服务器交互。http 和 https 的命令允许创建和发送任意 HTTP 请求。HTTPie 整体采用简单自然的语法&#xf…

mysql远程备份工具_innobackupex实现MySQL远程备份

一、了解innobackupex1、mysqldumpmysql逻辑备份工具&#xff0c;作用于服务器本地&#xff0c;不需要额外安装插件可以单表备份&#xff0c;备份为sql文件形式、方便&#xff0c;在多个场景通用可通过shell命令实现定时备份&#xff0c;但备份时如果用户有操作&#xff0c;容易…

技术干货 | Native 页面下如何实现导航栏的定制化开发?

简介&#xff1a; 通过不同实际场景的描述&#xff0c;供大家参考完成 Native 页面的定制化开发。 很多 mPaaS Coder 在接入 H5 容器后都会对容器的导航栏进行深度定制&#xff0c;本文旨在通过不同实际场景的描述&#xff0c;供大家参考完成 Native 页面的定制化开发。 欢迎关…

深入理解云计算OpenAPI体系

简介&#xff1a; 就云计算的API来看&#xff0c;当前并没有类似POSIX这样的API标准&#xff0c;基本上各大厂商各自为政。当然&#xff0c;有一些业界主流标准例如OAS获得多数云厂商的支持&#xff0c;但云厂商本身的API却往往由于历史原因、技术路线原因百花齐放&#xff0c;…