杭州职称评审系统网站,自己如何制作一个网页,番禺网站开发哪家专业,wordpress 分页简介#xff1a; 本文主要是通过对PFS引擎的内存管理的源码的阅读#xff0c;解读PFS内存分配及释放原理#xff0c;深入剖析其中存在的一些问题#xff0c;以及一些改进思路。本文源代码分析基于Mysql-8.0.24版本。 作者 | 之枢 来源 | 阿里技术公众号
一 引言
MYSQL Pe…简介 本文主要是通过对PFS引擎的内存管理的源码的阅读解读PFS内存分配及释放原理深入剖析其中存在的一些问题以及一些改进思路。本文源代码分析基于Mysql-8.0.24版本。 作者 | 之枢 来源 | 阿里技术公众号
一 引言
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中默认为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_monotonic0.
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个状态FREEDIRTY, ALLOCATEDFREE表示空闲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位无符号整型来保存versionstate信息格式如下 state 低2位字节表示分配状态。
state PFS_LOCK_FREE 0x00 state PFS_LOCK_DIRTY 0x01 state PFS_LOCK_ALLOCATED 0x11version
初始version为0每分配成功一次加1version就能表示该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修改为dirtyatomic_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;}}
另外一点每次查找都是从最近一次分配成功的位置开始这样必然导致并发访问的冲突因为大家都从同一个位置开始找起始查找位置应该加入一定的随机性这样可以避免大量的冲突重试。
总结如下
每次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.wztalibaba-inc.com期待与您共同打造世界一流的下一代云原生分布式关系型数据库。
参考
[1] MySQL Performance SchemaMySQL :: 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.24GitHub - mysql/mysql-server at mysql-8.0.24
原文链接 本文为阿里云原创内容未经允许不得转载。