
在MySQL 8.0.28中,官方引入了内存限制的新特性WL#13458--Global and session memory allocation limits,从内核层面对服务运行期间的内存使用进行限制,降低OOM的风险。本文将围绕该项工作的改动、设计实现等方面展开介绍。

1.内存限制的功能改动
1.1 变量添加
内存限制新特性添加了4个新的变量,具体的含义和取值如下表所示。

MySQL [(none)]> show variables where variable_name in ('global_connection_memory_limit', 'connection_memory_limit', 'connection_memory_chunk_size', 'global_connection_memory_tracking'); # 内存限制变量+-----------------------------------+----------------------+| Variable_name | Value |+-----------------------------------+----------------------+| connection_memory_chunk_size | 8912 || connection_memory_limit | 18446744073709551615 || global_connection_memory_limit | 18446744073709551615 || global_connection_memory_tracking | OFF |+-----------------------------------+----------------------+MySQL [(none)]> show status like "Global_connection_memory"; # 内存使用量+--------------------------+-------+| Variable_name | Value |+--------------------------+-------+| Global_connection_memory | 0 |+--------------------------+-------+
1.2 数据结构变更
该特性对部分已有的数据结构做了变更,新增了与内存使用量统计信息相关的成员对象,修改了PFS_thread、PSI_thread_service_v5、THD这3个类型。
class PFS_thread {...+ THD *m_cnt_thd // 用于更新内存计数器的THD...};// -------------------------------------------------------------struct PSI_thread_service_v5 {...+ set_mem_cnt_THD_v1_t set_mem_cnt_THD;};typedef void (*set_mem_cnt_THD_v1_t)(THD *thd, THD **backup_thd);// -------------------------------------------------------------class THD {...+ Thd_mem_cnt *mem_cnt; // 内存计数器对象+ bool enable_mem_cnt(); // 启用内存计数器+ void disable_mem_cnt(); // 关闭内存计数器...};
● 接口set_mem_cnt_THD()是协助完成THD转换的函数,分别对m_thd和m_cnt_thd进行设置,大部分情况下两者是相同的。
// Thd_mem_cnt_conn创建thd_prepare_connection()| thd->enable_mem_cnt() {| | Thd_mem_cnt *tmp_mem_cnt = new Thd_mem_cnt_conn(this);| | mem_cnt = tmp_mem_cnt;| }// Thd_mem_cnt_conn释放~THD()| THD::release_resources()| | disable_mem_cnt() {| | | mem_cnt->flush(); // 清空当前THD的内存计数信息并扣除对应的gloabl数据| | | delete mem_cnt;| | }
1.3 数据结构添加
如前文所述,内存计数器对象Thd_mem_cnt是该WL中引入的最重要的数据结构,负责保存、更新相关的内存使用信息。在内存限制的过程中,真正起到作用的是其子类Thd_mem_cnt_conn,下面对此展开进一步介绍。
enum enum_mem_cnt_mode {MEM_CNT_DEFAULT = 0U, // 不计数MEM_CNT_UPDATE_GLOBAL_COUNTER = (1U << 0), // 更新global信息MEM_CNT_GENERATE_ERROR = (1U << 1), // 产生OOM错误信息MEM_CNT_GENERATE_LOG_ERROR = (1U << 2) // 产生OOM错误信息写入日志};

Thd_mem_cnt_conn的关键数据结构如上图所示,mem_count、max_conn_mem、glob_mem_counter分别指代connection已申请的内存、connection的最大内存(该值并不是一个指定值,会随mem_count变化)和该连接传递给global计数器的值。参数的联系和变化过程如下图所示,mem_count和连接实际的内存使用量相关,glob_mem_counter则是单位化增长,增长的值和connection_memory_chunk_size这个参数相关。


mode参数是enum_mem_cnt_mode中的组合,例如SUPER用户在连接建立时的mode是MEM_CNT_UPDATE_GLOBAL_COUNTER,而普通的用户的mode则是MEM_CNT_UPDATE_GLOBAL_COUNTER|MEM_CNT_GENERATE_ERROR | MEM_CNT_GENERATE_LOG_ERROR。在进行内存计数时会使用这个判断位,决定是否产生错误并kill connection。换言之,SUPER用户在执行查询等操作时是不会受到limit参数的限制的,而普通用户则会受这些参数的影响。
static void prepare_new_connection_state(THD *thd) {...thd->mem_cnt->set_orig_mode(is_admin_conn ? MEM_CNT_UPDATE_GLOBAL_COUNTER // 根据身份设置mode: (MEM_CNT_UPDATE_GLOBAL_COUNTER |MEM_CNT_GENERATE_ERROR |MEM_CNT_GENERATE_LOG_ERROR));...}
● 修改mem_counter、max_conn_mem和glob_mem_counter。max_conn_mem随mem_counter更新,glob_mem_counter以lazy方式添加到全局内存计数器中(只有满足max_conn_mem > glob_mem_counter才会插值delta到全局计数器)。由于访问全局计数器需要加一把全局的大锁,这样的操作可以减少变更和加锁的次数。
bool Thd_mem_cnt_conn::alloc_cnt(size_t size) {mem_counter += size;max_conn_mem = std::max(max_conn_mem, mem_counter);// connection级别的报错if (mem_counter > m_thd->variables.conn_mem_limit) {(void)generate_error(ER_DA_CONN_LIMIT, m_thd->variables.conn_mem_limit,mem_counter);}// 三个条件分别指代:开启全局更新、开启内存追踪、存量大于提前量if ((curr_mode & MEM_CNT_UPDATE_GLOBAL_COUNTER) &&m_thd->variables.conn_global_mem_tracking &&max_conn_mem > glob_mem_counter) {// 控制全局计数器更新频率const ulonglong curr_mem =(max_conn_mem m_thd->variables.conn_mem_chunk_size + 1) *m_thd->variables.conn_mem_chunk_size;ulonglong delta = curr_mem - glob_mem_counter;ulonglong global_conn_mem_counter_save;ulonglong global_conn_mem_limit_save;{MUTEX_LOCK(lock, &LOCK_global_conn_mem_limit);global_conn_mem_counter += delta;global_conn_mem_counter_save = global_conn_mem_counter;global_conn_mem_limit_save = global_conn_mem_limit;}glob_mem_counter = curr_mem;max_conn_mem = std::max(max_conn_mem, glob_mem_counter);// global级别的报错if (global_conn_mem_counter_save > global_conn_mem_limit_save) {(void)generate_error(ER_DA_GLOBAL_CONN_LIMIT, global_conn_mem_limit_save,global_conn_mem_counter_save);}}return true;}
free_cnt()
和统计信息增加的方式不同,该函数的功能单一,只对connection级别的mem_counter做减法。那全局的计数信息如何减少呢?全局数据的修改在reset()函数中完成,这样做的目的同样是为了减少全局资源的竞用。显而易见,大多数情况下,全局的计数信息将会滞后于连接计数信息。
void Thd_mem_cnt_conn::free_cnt(size_t size) {mem_counter -= size;}
● 重置mode,此前的一些操作可能会将计数器的mode进行修改(例如在prepare connection阶段),这里要确保更新前counter处于正确的模式,避免出现不同权限操作出错(如此前的SUPER和普通用户等)。
● 更新三个计数数据,当glob_mem_counter > mem_counter时,表明此前有free_cnt操作减少了mem_counter,此处对glob_mem_counter和全局数据进行更新;反之表明存在未加入全局内存统计的连接级别内存信息,也需要将差值补全。在reset()过程中也可能出现内存不足的情况,同样需要调用错误产生函数对错误信息进行报告,对THD设置killed标志。
int Thd_mem_cnt_conn::reset() {// 重置moderestore_mode();max_conn_mem = mem_counter;// 更新计数数据信息if (m_thd->variables.conn_global_mem_tracking &&(curr_mode & MEM_CNT_UPDATE_GLOBAL_COUNTER)) {ulonglong delta;ulonglong global_conn_mem_counter_save;ulonglong global_conn_mem_limit_save;if (glob_mem_counter > mem_counter) {delta = glob_mem_counter - mem_counter;MUTEX_LOCK(lock, &LOCK_global_conn_mem_limit);assert(global_conn_mem_counter >= delta);global_conn_mem_counter -= delta;global_conn_mem_counter_save = global_conn_mem_counter;global_conn_mem_limit_save = global_conn_mem_limit;} else {delta = mem_counter - glob_mem_counter;MUTEX_LOCK(lock, &LOCK_global_conn_mem_limit);global_conn_mem_counter += delta;global_conn_mem_counter_save = global_conn_mem_counter;global_conn_mem_limit_save = global_conn_mem_limit;}glob_mem_counter = mem_counter;if (is_connection_stage &&(global_conn_mem_counter_save > global_conn_mem_limit_save))return generate_error(ER_DA_GLOBAL_CONN_LIMIT, global_conn_mem_limit_save,global_conn_mem_counter_save);}if (is_connection_stage && (mem_counter > m_thd->variables.conn_mem_limit))return generate_error(ER_DA_CONN_LIMIT, m_thd->variables.conn_mem_limit,mem_counter);is_connection_stage = false;return 0;}
flush()
void Thd_mem_cnt_conn::flush() {max_conn_mem = mem_counter = 0;if (glob_mem_counter > 0) {MUTEX_LOCK(lock, &LOCK_global_conn_mem_limit);global_conn_mem_counter -= glob_mem_counter;}glob_mem_counter = 0;}
2. 内存限制的过程
2.1 执行流程
以最简单的handle_connection为例(非线程池模型),连接建立、执行语句和连接关闭过程对应的内存限制操作如下图所示:

...if (thd_prepare_connection(thd))handler_manager->inc_aborted_connects();else {while (thd_connection_alive(thd)) {if (do_command(thd)) break;}end_connection(thd);}close_connection(thd, 0, false, false);...
2.2 关键函数
● 构建内存块头部,其中保存了size、magic、psi_memory_key等信息;
void *my_malloc(PSI_memory_key key, size_t size, myf flags) {// malloc出一块包含header信息的内存块my_memory_header *mh;size_t raw_size;raw_size = PSI_HEADER_SIZE + size;mh = (my_memory_header *)my_raw_malloc(raw_size, flags);// 对header数据结构初始化,调用pfs_memory_alloc_vc对head->key进行赋值if (likely(mh != nullptr)) {void *user_ptr;mh->m_magic = PSI_MEMORY_MAGIC;mh->m_size = size;// 调用服务mh->m_key = PSI_MEMORY_CALL(memory_alloc)(key, raw_size, &mh->m_owner);user_ptr = HEADER_TO_USER(mh);MEM_MALLOCLIKE_BLOCK(user_ptr, size, 0, (flags & MY_ZEROFILL));return user_ptr;}return nullptr;}
这个函数是计数数据增加的入口,主要工作如下:
● 返回key值,若启用计数器,此时的key值是经过PSI_MEM_CNT_BIT(1 << 31)标记的。
PSI_memory_key pfs_memory_alloc_vc(PSI_memory_key key, size_t size,PSI_thread **owner) {// 获取key对应的PFS_memory_classPSI_memory_key result_key = key;...PFS_memory_class *klass = find_memory_class(key);// 启动thread监控维度、非全局监控模式if (flag_thread_instrumentation && !klass->is_global()) {PFS_thread *pfs_thread = my_thread_get_THR_PFS();// 判断是否启用计数器,在PFS_memory_class初始化阶段if (klass->has_memory_cnt()) {if (pfs_thread->m_cnt_thd != nullptr && pfs_thread->mem_cnt_alloc(size)) // 内存信息增加的入口// 标志位,标记key是否经过计数器的处理result_key |= PSI_MEM_CNT_BIT;}// 统计信息更新...*owner_thread = pfs_thread;} else {// 统计信息更新...*owner_thread = nullptr;}return result_key;}
3.内存限制的简单测试
3.1 测试准备
创建普通用户RDS_test; 构建大数据记录; 设置较小的connection_memory_limit。
create user RDS_test identified by 'RDS_test';grant select on test.* to RDS_test;use test;create table t(id int primary key, c longtext);insert t values (1, lpad('RDS', 6000000, 'test'));set global connection_memory_limit=1024 * 1024 * 2;
3.2 测试
普通用户执行
ySQL [test]> show variables like "connection_memory_limit";+-------------------------+---------+| Variable_name | Value |+-------------------------+---------+| connection_memory_limit | 2097152 |+-------------------------+---------+MySQL [test]> select count(c) from t group by c;ERROR 4082 (HY000): Connection closed. Connection memory limit 2097152 bytes exceeded. Consumed 7079568 bytes.
SUPER用户执行
MySQL [test]> show variables like "connection_memory_limit";+-------------------------+---------+| Variable_name | Value |+-------------------------+---------+| connection_memory_limit | 2097152 |+-------------------------+---------+MySQL [test]> select count(c) from t group by c;+----------+| count(c) |+----------+| 1 |+----------+
3.3 测试结果
引入了这个功能后,普通用户的内存使用受到限制,超出限定值直接被kill,但SUPER用户还是不受限制的。
4.总结展望
● 对于SUPER用户和普通用户,内存限制有差别,前者不做限制,可能会引发OOM。
● global数据的更新总是滞后于connection,减少了全局锁的争用。内存统计数据增加阶段,使用connection_memory_chunk_size来控制更新频率;内存统计数据减少阶段,connection和global级别的信息分别通过free_cnt()和reset()完成更新。
● connection_memory_chunk_size设置的较大的情况下,容易提前报告OOM错误导致connection被kill;设置的较小则可能会导致全局锁的频繁访问。
参考







点击「阅读原文」了解云数据库RDS MySQL更多内容







