内存统计信息的聚合
内存统计信息的聚合总共有5个维度,也分别对应以下5张表,分别是:
- MEMORY_SUMMARY_BY_ACCOUNT_BY_EVENT_NAME
- MEMORY_SUMMARY_BY_HOST_BY_EVENT_NAME
- MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME
- MEMORY_SUMMARY_BY_USER_BY_EVENT_NAME
- MEMORY_SUMMARY_GLOBAL_BY_EVENT_NAME
其包括内存分配、释放、聚合的涉及的入口函数及架构如下:
可以看到最基础的维度为thread,然后再往父bucket中进行聚合。
聚合的“主要”逻辑堆栈如下:
|PSI_THREAD_CALL(delete_current_thread) |--pfs_delete_current_thread_vc |----aggregate_thread |------aggregate_thread_memory |--------aggregate_all_memory_with_reassign |----------memory_full_aggregate_with_reassign |--------aggregate_all_memory |----------memory_full_aggregate
在线程退出时,会调用PSI_THREAD_CALL(delete_current_thread)接口进行该连接相关的内存统计信息的聚合,并释放pfs thread对象。
其核心逻辑函数为aggregate_thread_memory,代码如下:
void aggregate_thread_memory(bool alive, PFS_thread *thread, PFS_account *safe_account, PFS_user *safe_user, PFS_host *safe_host) { if (thread->read_instr_class_memory_stats() == nullptr) { return; } if (likely(safe_account != nullptr)) { /* Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME to MEMORY_SUMMARY_BY_ACCOUNT_BY_EVENT_NAME. */ aggregate_all_memory_with_reassign( alive, thread->write_instr_class_memory_stats(), safe_account->write_instr_class_memory_stats(), global_instr_class_memory_array); return; } if ((safe_user != nullptr) && (safe_host != nullptr)) { /* Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME to: - MEMORY_SUMMARY_BY_USER_BY_EVENT_NAME - MEMORY_SUMMARY_BY_HOST_BY_EVENT_NAME in parallel. */ aggregate_all_memory_with_reassign( alive, thread->write_instr_class_memory_stats(), safe_user->write_instr_class_memory_stats(), safe_host->write_instr_class_memory_stats(), global_instr_class_memory_array); return; } if (safe_user != nullptr) { /* Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME to: - MEMORY_SUMMARY_BY_USER_BY_EVENT_NAME - MEMORY_SUMMARY_GLOBAL_BY_EVENT_NAME in parallel. */ aggregate_all_memory_with_reassign( alive, thread->write_instr_class_memory_stats(), safe_user->write_instr_class_memory_stats(), global_instr_class_memory_array, global_instr_class_memory_array); return; } if (safe_host != nullptr) { /* Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME to MEMORY_SUMMARY_BY_HOST_BY_EVENT_NAME, directly. */ aggregate_all_memory_with_reassign( alive, thread->write_instr_class_memory_stats(), safe_host->write_instr_class_memory_stats(), global_instr_class_memory_array); return; } /* Aggregate MEMORY_SUMMARY_BY_THREAD_BY_EVENT_NAME to MEMORY_SUMMARY_GLOBAL_BY_EVENT_NAME. */ aggregate_all_memory(alive, thread->write_instr_class_memory_stats(), global_instr_class_memory_array); }
根据代码可知,thread维度最为基础维度,通常情况下用户线程的内存统计信息都会从thread维度汇聚到account维度;MySQL后台线程或插件等会被聚合到global维度。所以这里我们主要关注account和global维度的具体聚合逻辑。
account维度的聚合实现主要通过aggregate_all_memory_with_reassign(bool alive,
PFS_memory_safe_stat *from_array,
PFS_memory_shared_stat *to_array,
PFS_memory_shared_stat *global_array)函数实现,其中alive总是为false,from_array为thread维度的instruments内存统计信息数组指针,to_array为account维度的instruments内存统计信息数据指针,global_array为global维度的instruments内存统计信息数组指针。具体实现代码如下:
void aggregate_all_memory_with_reassign(bool alive, PFS_memory_safe_stat *from_array, PFS_memory_shared_stat *to_array, PFS_memory_shared_stat *global_array) { PFS_memory_safe_stat *from; PFS_memory_safe_stat *from_last; PFS_memory_shared_stat *to; from = from_array; from_last = from_array + memory_class_max; to = to_array; if (alive) { for (; from < from_last; from++, to++) { memory_partial_aggregate(from, to); } } else { PFS_memory_shared_stat *global; global = global_array; for (; from < from_last; from++, to++, global++) { memory_full_aggregate_with_reassign(from, to, global); from->reset(); } } }
在for循环中依次迭代from_array中的instrument内存统计信息往父bucket中进行聚合,其聚合实现代码如下:
void memory_full_aggregate_with_reassign(const PFS_memory_safe_stat *from, PFS_memory_shared_stat *stat, PFS_memory_shared_stat *global) { if (!from->m_used) { return; } stat->m_used = true; size_t alloc_count = from->m_alloc_count; size_t free_count = from->m_free_count; size_t alloc_size = from->m_alloc_size; size_t free_size = from->m_free_size; size_t net; size_t capacity; if (likely(alloc_count <= free_count)) { /* Nominal path */ stat->m_alloc_count += alloc_count; stat->m_free_count += free_count; stat->m_alloc_count_capacity += from->m_alloc_count_capacity; stat->m_free_count_capacity += from->m_free_count_capacity; } else { global->m_used = true; stat->m_alloc_count += free_count; /* base */ stat->m_free_count += free_count; /* Net memory contributed affected to the global bucket directly. */ net = alloc_count - free_count; global->m_alloc_count += net; stat->m_alloc_count_capacity += from->m_alloc_count_capacity; size_t free_count_capacity; free_count_capacity = from->m_free_count_capacity; capacity = std::min(free_count_capacity, net); free_count_capacity -= capacity; /* Corresponding low watermark split between the parent and global bucket. */ stat->m_free_count_capacity += free_count_capacity; global->m_free_count_capacity += capacity; } if (likely(alloc_size <= free_size)) { /* Nominal path. */ stat->m_alloc_size += alloc_size; stat->m_free_size += free_size; stat->m_alloc_size_capacity += from->m_alloc_size_capacity; stat->m_free_size_capacity += from->m_free_size_capacity; } else { /* Global net alloc. */ global->m_used = true; stat->m_alloc_size += free_size; /* base */ stat->m_free_size += free_size; net = alloc_size - free_size; global->m_alloc_size += net; stat->m_alloc_size_capacity += from->m_alloc_size_capacity; size_t free_size_capacity; free_size_capacity = from->m_free_size_capacity; capacity = std::min(free_size_capacity, net); free_size_capacity -= capacity; stat->m_free_size_capacity += free_size_capacity; global->m_free_size_capacity += capacity; } }
通过代码可知,通常情况下是直接将thread维度的统计信息属性值与account维度的统计信息属性值进行相加来进行聚合,即(alloc_count <= free_count)。
但还有一种情况,即父线程调用子线程进行一些工作,通常有两种场景,比如1.event调度,调度线程调用worker线程进行工作 2. main线程接受用户连接进行工作。这两种情况会产生内存分配与释放在同一线程的不均衡,比如内存的分配是在X线程进行的,但是内存的释放却是在Y线程进行的。这里的情况就对应函数中的另一半逻辑,即(alloc_count > free_count),此时可以理解为一个用户连接分配了一些global类型的内存(内存claim),在用户连接退出时,会将global类型的这部分内存直接聚合到global级别。
通过以上的逻辑分析,我们可以得出一个结论:内存统计信息是从thread维度往父bucket中进行聚合,其中通常会聚合到account维度和global维度,而在进行account维度的聚合时,通常情况下就是将thread维度的统计信息属性值直接与account维度的统计信息属性值相加,如果存在内存claim的情况,则会将额外分配的global类型的内存聚合到global级别。
从thread维度直接聚合到global维度的主要实现函数为memory_full_aggregate,具体实现代码如下:
void memory_full_aggregate(const PFS_memory_safe_stat *from, PFS_memory_shared_stat *stat) { if (!from->m_used) { return; } stat->m_used = true; stat->m_alloc_count += from->m_alloc_count; stat->m_free_count += from->m_free_count; stat->m_alloc_size += from->m_alloc_size; stat->m_free_size += from->m_free_size; stat->m_alloc_count_capacity += from->m_alloc_count_capacity; stat->m_free_count_capacity += from->m_free_count_capacity; stat->m_alloc_size_capacity += from->m_alloc_size_capacity; stat->m_free_size_capacity += from->m_free_size_capacity; }
这一部分的代码相对比较简单,直接就是将thread维度的统计信息属性值与global维度的统计信息属性值相加即可。
统计信息与OS内存使用不一致的问题
现象1:统计信息大于OS使用量
当前实例使用内存为1.4G
Buffer Pool设置为1G
相关内存统计信息为1G
此时将Buffer Pool调整为2G
如果此时分配的内存都被使用了,那么在OS上看到的实例使用的内存应该至少是大于2G的,但此时实际使用量却是1.6G。
此时产生的现象就是在OS层面观测到的实例实际使用的内容量与MySQL内存统计信息里看到的不一致。
问题产生的原因主要是Linux内存主要是通过虚拟内存文件映射的方式来管理的,当MySQL向Linux申请内存时,分配的都是虚拟内存,并没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,会发生缺页中断,操作系统再进行物理内存的分配,然后建立虚拟内存和物理内存之间的映射关系。
现象2:内存统计信息出现负值
在上一章节关于内存统计信息聚合的介绍中,我们看到在memory_full_aggregate_with_reassign函数中,内存聚合有两个路径,其中一个是(likely(alloc_size <= free_size)),从这个条件中我们可以看到alloc_size是有可能会小于free_size的,在PS表中,CURRENT_NUMBER_OF_BYTES_USED的值是通过alloc_size-free-size计算出来的,所以CURRENT_NUMBER_OF_BYTES_USED的值就也可能会出现负值。
而出现负值显然是不合理的,此种情况就属于是Bug缺陷了。在MariaDB的官方jira中,也找到了相关类似的issue:
https://jira.mariadb.org/browse/MDEV-23936,不过看记录已经过去三年了,官方也没有给出相关解释或解决方案。
还有一点,由于sys.memory_global_total表是performance_schema.memory_summary_global_by_event_name的视图,而total_allocated是通过sum(CURRENT_NUMBER_OF_BYTES_USED)得到的,那么既然CURRENT_NUMBER_OF_BYTES_USED的值是有可能会出现负值,所有total_allocated这个总计值就也不可信了。还有一点,我们之前讲过,内存统计信息的聚合是有几个不同的维度的,并不是所有的统计信息最后都会被聚合到Global级别,所有将sys.memory_global_total的total_allocated值作为内存分配量的统计值是一点不准确的。
现象3:存在未被统计的内存操作
在MySQL相关代码中,明确定义了只有这些文件中的内存分配会被PFS监控,这里这列出了一部分的MySQL代码文件,所以一定存在未被统计的内存。
/** List of filenames that allocate memory and are instrumented via PFS. */ static constexpr const char *auto_event_names[] = { /* Keep this list alphabetically sorted. */ "api0api", "api0misc", "btr0btr", "btr0bulk", "btr0cur", "btr0pcur", "btr0sea", "btr0types", "buf", "buf0buddy", "buf0buf", "buf0checksum", "buf0dblwr", "buf0dump", "buf0flu", "buf0lru", "buf0rea", "buf0stats", "buf0types", "checksum", "crc32", "create", "data0data", "data0type", "data0types", "db0err", "dict", "dict0boot", "dict0crea", "dict0dd", "dict0dict", "dict0load", "dict0mem", "dict0priv", "dict0sdi", "dict0stats", "dict0stats_bg", "dict0types", "dyn0buf", "dyn0types", "eval0eval", "eval0proc", "fil0fil", "fil0types", "file", "fsp0file", "fsp0fsp", "fsp0space", "fsp0sysspace", "fsp0types", "fts0ast", "fts0blex", "fts0config", "fts0fts", "fts0opt", "fts0pars", "fts0plugin", "fts0priv", "fts0que", "fts0sql", "fts0tlex", "fts0tokenize", "fts0types", "fts0vlc", "fut0fut", "fut0lst", "gis0geo", "gis0rtree", "gis0sea", "gis0type", "ha0ha", "ha0storage", "ha_innodb", "ha_innopart", "ha_prototypes", "handler0alter", "hash0hash", "i_s", "ib0mutex", "ibuf0ibuf", "ibuf0types", "lexyy", "lob0lob", "lock0iter", "lock0lock", "lock0prdt", "lock0priv", "lock0types", "lock0wait", "log0log", "log0recv", "log0write", "mach0data", "mem", "mem0mem", "memory", "mtr0log", "mtr0mtr", "mtr0types", "os0atomic", "os0event", "os0file", "os0numa", "os0once", "os0proc", "os0thread", "page", "page0cur", "page0page", "page0size", "page0types", "page0zip", "pars0grm", "pars0lex", "pars0opt", "pars0pars", "pars0sym", "pars0types", "que0que", "que0types", "read0read", "read0types", "rec", "rem0cmp", "rem0rec", "rem0types", "row0ext", "row0ftsort", "row0import", "row0ins", "row0log", "row0merge", "row0mysql", "row0purge", "row0quiesce", "row0row", "row0sel", "row0types", "row0uins", "row0umod", "row0undo", "row0upd", "row0vers", "sess0sess", "srv0conc", "srv0mon", "srv0srv", "srv0start", "srv0tmp", "sync0arr", "sync0debug", "sync0policy", "sync0sharded_rw", "sync0rw", "sync0sync", "sync0types", "trx0i_s", "trx0purge", "trx0rec", "trx0roll", "trx0rseg", "trx0sys", "trx0trx", "trx0types", "trx0undo", "trx0xa", "usr0sess", "usr0types", "ut", "ut0byte", "ut0counter", "ut0crc32", "ut0dbg", "ut0link_buf", "ut0list", "ut0lock_free_hash", "ut0lst", "ut0mem", "ut0mutex", "ut0new", "ut0pool", "ut0rbt", "ut0rnd", "ut0sort", "ut0stage", "ut0ut", "ut0vec", "ut0wqueue", "zipdecompress", };
我们找一个未被统计的案例,可以看到,在下图的堆栈中,在内存跟踪接口函数pfs_memory_alloc_vc中,在尝试进行内存分配跟踪时,由于找不到相关的Performance Schema Key,则函数直接return,不再进行后续的内存分配跟踪。(通过堆栈可以看到,该内存分配相关动作为后台master线程在进行主循环):
- 中文(简体)
- 中文(繁体)
- 丹麦语
- 乌克兰语
- 乌尔都语
- 亚美尼亚语
- 俄语
- 保加利亚语
- 克罗地亚语
- 冰岛语
- 加泰罗尼亚语
- 匈牙利语
- 卡纳达语
- 印地语
- 印尼语
- 古吉拉特语
- 哈萨克语
- 土耳其语
- 威尔士语
- 孟加拉语
- 尼泊尔语
- 布尔语(南非荷兰语)
- 希伯来语
- 希腊语
- 库尔德语
- 德语
- 意大利语
- 拉脱维亚语
- 挪威语
- 捷克语
- 斯洛伐克语
- 斯洛文尼亚语
- 旁遮普语
- 日语
- 普什图语
- 毛利语
- 法语
- 波兰语
- 波斯语
- 泰卢固语
- 泰米尔语
- 泰语
- 海地克里奥尔语
- 爱沙尼亚语
- 瑞典语
- 立陶宛语
- 缅甸语
- 罗马尼亚语
- 老挝语
- 芬兰语
- 英语
- 荷兰语
- 萨摩亚语
- 葡萄牙语
- 西班牙语
- 越南语
- 阿塞拜疆语
- 阿姆哈拉语
- 阿尔巴尼亚语
- 阿拉伯语
- 韩语
- 马尔加什语
- 马拉地语
- 马拉雅拉姆语
- 马来语
- 马耳他语
- 高棉语