MySQL performance_schema之内存监控
https://mp.weixin.qq.com/s/fKA8j53DAOrJcw56wjs7nA
原创|MySQL performance_schema之内存监控
提示:公众号展示代码会自动折行,建议横屏阅读
背景
无论从使用、研发还是运维的角度,内存监控一直是MySQL的重点之一。完善的内存监控手段有很多作用,包括但不限于:
-
发现内存泄漏,避免MySQL实例内存耗尽
-
对实例的运行状态进行定量分析
-
资源管控和优化
但内存监控想要“完善”并不是那么简单的事。
PFS内存监控介绍
在PFS中,一共有五张内存相关的监控表,每张表会从不同维度收集和聚合内存事件。
-
memory_summary_by_account_by_event_name
: 从用户和连接host的角度统计内存信息。 -
memory_summary_by_host_by_event_name
: 从host角度统计内存信息。 -
memory_summary_by_thread_by_event_name
: 从线程角度统计内存信息。 -
memory_summary_by_user_by_event_name
: 从用户角度统计内存信息。 -
memory_summary_global_by_event_name
: 从Memory Event(内存事件)本身,统计全局的内存信息。
每张表内,内存相关的列如下:
-
COUNT_ALLOC
,COUNT_FREE:
调用内存分配器进行内存分配和释放的次数。 -
SUM_NUMBER_OF_BYTES_ALLOC
,SUM_NUMBER_OF_BYTES_FREE:
总共分配和释放内存的字节数。 -
CURRENT_COUNT_USED:
COUNT_ALLOC
−COUNT_FREE
. -
CURRENT_NUMBER_OF_BYTES_USED:
前正在使用的内存字节数。它等于目
SUM_NUMBER_OF_BYTES_ALLOC
−SUM_NUMBER_OF_BYTES_FREE
. -
LOW_COUNT_USED
,HIGH_COUNT_USED:
内存block的使用范围(最小-最大)。 -
LOW_NUMBER_OF_BYTES_USED
,HIGH_NUMBER_OF_BYTES_USED:
内存字节数的使用范围(最小-最大)。
mysql> SELECT *
FROM performance_schema.memory_summary_global_by_event_name
WHERE EVENT_NAME = 'memory/sql/TABLE'\G
*************************** 1. row ***************************
EVENT_NAME: memory/sql/TABLE
COUNT_ALLOC: 1381
COUNT_FREE: 924
SUM_NUMBER_OF_BYTES_ALLOC: 2059873
SUM_NUMBER_OF_BYTES_FREE: 1407432
LOW_COUNT_USED: 0
CURRENT_COUNT_USED: 457
HIGH_COUNT_USED: 461
LOW_NUMBER_OF_BYTES_USED: 0
CURRENT_NUMBER_OF_BYTES_USED: 652441
HIGH_NUMBER_OF_BYTES_USED: 669269
8.0.28以前的InnoDB内存监控
最简单的内存监控,就是把malloc()和free()包装一下,在里面做其他的事情:
void *traced_malloc(size_t size, const char *user) {
void *ptr = malloc(size);
// record the allocation in some ways
// trace(size,user)
return ptr;
}
void traced_free(void *ptr) {
// obtain the allocation information in some ways
// information = get_trace(ptr)
free(ptr);
}
上面代码的意思是,在执行真正的内存分配/释放操作之前,通过某些手段记录这次“内存事件”,随后再执行真正的分配/释放,从而能够统计内存的使用情况。
因为我们在讨论C++,所以也可以把new/delete包一层,做同样的事情。
具体到InnoDB的代码上,InnoDB通过allocate_trace和deallocate_trace来做这件事:
/** Trace a memory allocation.
@param[in] size number of bytes that were allocated
@param[in] key Performance Schema key
@param[out] pfx placeholder to store the info which will be
needed when freeing the memory */
void allocate_trace(size_t size, PSI_memory_key key, ut_new_pfx_t *pfx) {
if (m_key != PSI_NOT_INSTRUMENTED) {
key = m_key;
}
pfx->m_key = PSI_MEMORY_CALL(memory_alloc)(key, size, &pfx->m_owner);
pfx->m_size = size;
}
/** Trace a memory deallocation.
@param[in] pfx info for the deallocation */
void deallocate_trace(const ut_new_pfx_t *pfx) {
PSI_MEMORY_CALL(memory_free)(pfx->m_key, pfx->m_size, pfx->m_owner);
}
但是,这个内存监控已经很老了,有一些显而易见的缺点:
-
对于STL容器内的Allocator没有实现,如std::vector<>内的元素无法统计到
-
对于新的语法(如C++17引入的std::align_val_t等)无法支持统计
-
对于智能指针的支持不到位(如make_unique(), make_shared())
-
强耦合PFS,扩展性不高
在8.0.28,MySQL官方把内存监控彻底重构,解决了上述问题。
重构的内存监控
InnoDB引入了一个新的内存区段,叫做PFS元数据。所有通过performance_schema追踪内存使用的allocator都会使用该统一的元数据结构。
结构大概长这样:
该PFS元数据由内部分配器分配额外的长度储存,并将用户申请的真实内存指针贴在后面。也就是这个实现细节是对上层应用隐藏的,在分配/释放的时候,通过指针计算,获取该元数据的偏移量来统计内存事件。
一个内存元数据由三部分组成:
-
申请的线程(所有者)
-
申请的内存长度
-
PFS Memory Key,用于分类别统计内存
来看一个具体实现,以operator new的allocate()函数为例:
static inline void *alloc(std::size_t size,
pfs_metadata::pfs_memory_key_t key) {
const auto total_len = size + Alloc_pfs::metadata_len;
auto mem = Alloc_fn::alloc<Zero_initialized>(total_len);
if (unlikely(!mem)) return nullptr;
// The point of this allocator variant is to trace the memory allocations
// through PFS (PSI) so do it.
pfs_metadata::pfs_owning_thread_t owner;
key = PSI_MEMORY_CALL(memory_alloc)(key, total_len, &owner);
// To be able to do the opposite action of tracing when we are releasing the
// memory, we need right about the same data we passed to the tracing
// memory_alloc function. Let's encode this it into our allocator so we
// don't have to carry and keep this data around.
pfs_metadata::pfs_owning_thread(mem, owner); //所有者
pfs_metadata::pfs_datalen(mem, total_len); //内存长度
pfs_metadata::pfs_key(mem, key); //PFS Memory Key
pfs_metadata::pfs_metaoffset(mem, Alloc_pfs::metadata_len); //PFS偏移量
return static_cast<uint8_t *>(mem) + Alloc_pfs::metadata_len;
}
在申请内存之前,MySQL首先通过metadata_len计算出额外所需的内存大小,然后根据总和申请内存。
申请内存后,根据元数据结构的定义,依次将内存所有者,内存长度,PFS Key,偏移量写入额外的内存空间。
最后,通过指针计算出返回值的内存偏移,将真实的内存返回给上层(隐藏了额外的内容)。
同样,在释放内存时,根据上层传入的指针,逆向计算出整块内存的起始地址,并取出元数据后,再释放所有内存。
实现内存分配器后,InnoDB在头文件中使用using语法对常用的容器进行了重定向,这样即使开发者忘记指定内存分配器,也不会影响内存统计。
template <typename T>
using vector = std::vector<T, ut::allocator<T>>;
/** Specialization of list which uses ut_allocator. */
template <typename T>
using list = std::list<T, ut::allocator<T>>;
/** Specialization of set which uses ut_allocator. */
template <typename Key, typename Compare = std::less<Key>>
using set = std::set<Key, Compare, ut::allocator<Key>>;
template <typename Key>
using unordered_set =
std::unordered_set<Key, std::hash<Key>, std::equal_to<Key>,
ut::allocator<Key>>;
/** Specialization of map which uses ut_allocator. */
template <typename Key, typename Value, typename Compare = std::less<Key>>
using map =
std::map<Key, Value, Compare, ut::allocator<std::pair<const Key, Value>>>;
同时,还有对智能指针的实现:
template <typename T,
typename Deleter = detail::Array_deleter<std::remove_extent_t<T>>>
std::enable_if_t<detail::is_bounded_array_v<T>, std::shared_ptr<T>> make_shared(
PSI_memory_key_t key) {
return std::shared_ptr<T>(
ut::new_arr_withkey<std::remove_extent_t<T>>(
key, ut::Count{detail::bounded_array_size_v<T>}),
Deleter{});
}
那扩展性如何解决呢?上述函数所在的类叫做
Alloc_pfs : public allocator_traits<true>
继承了一个统一的基类allocator_traits。如果以后有需要,还可以扩展出使用其他统计方式的内存分配器,不需要更改上层逻辑,只需要更改内存分配策略即可。
内存分析案例
首先,简单举例一下PFS内存监控的使用方法。
打开performance_schema后,可以通过如下SQL语句获取全局的内存使用情况:
mysql> select event_name,current_alloc from sys.memory_global_by_current_bytes limit 10;
+-----------------------------------------------------------------------------+---------------+
| event_name | current_alloc |
+-----------------------------------------------------------------------------+---------------+
| memory/innodb/buf_buf_pool | 1.05 GiB |
| memory/performance_schema/events_statements_summary_by_digest | 40.28 MiB |
| memory/innodb/ut0link_buf | 24.00 MiB |
| memory/innodb/log_buffer_memory | 16.00 MiB |
| memory/performance_schema/events_statements_history_long | 14.19 MiB |
| memory/performance_schema/events_errors_summary_by_thread_by_error | 12.70 MiB |
| memory/performance_schema/events_statements_summary_by_thread_by_event_name | 11.04 MiB |
| memory/performance_schema/events_statements_summary_by_digest.digest_text | 9.77 MiB |
| memory/performance_schema/events_statements_history_long.digest_text | 9.77 MiB |
| memory/performance_schema/events_statements_history_long.sql_text | 9.77 MiB |
+-----------------------------------------------------------------------------+---------------+
这句话的意思是,获取整个实例的前10内存消耗量的元素。可以看到,排第一的是InnoDB Buffer Pool。
接下来,我们来了解一个线上用户的实际案例。
某线上用户实例频繁OOM。通过PFS观察该用户的内存使用情况如下:
mysql> select * from memory_by_thread_by_current_bytes ;
+-----------+--------------------------------------+--------------------+-------------------+-------------------+-------------------+-----------------+
| thread_id | user | current_count_used | current_allocated | current_avg_alloc | current_max_alloc | total_allocated |
+-----------+--------------------------------------+--------------------+-------------------+-------------------+-------------------+-----------------+
| 55 | root@localhost | 364315 | 1.76 GiB | 5.06 KiB | 1.75 GiB | 8.33 GiB |
mysql> select event_name,current_alloc from sys.memory_global_by_current_bytes limit 10;
+-----------------------------------------------------------------------------+---------------+
| event_name | current_alloc |
+-----------------------------------------------------------------------------+---------------+
| memory/sql/user_var_entry::value | 1.92 GiB |
| memory/innodb/buf_buf_pool | 1.05 GiB |
| memory/performance_schema/events_statements_summary_by_digest | 40.28 MiB |
| memory/innodb/ut0link_buf | 24.00 MiB |
| memory/innodb/log_buffer_memory | 16.00 MiB |
| memory/performance_schema/events_statements_history_long | 14.19 MiB |
| memory/performance_schema/events_errors_summary_by_thread_by_error | 12.70 MiB |
| memory/performance_schema/events_statements_summary_by_thread_by_event_name | 11.04 MiB |
| memory/performance_schema/events_statements_summary_by_digest.digest_text | 9.77 MiB |
| memory/performance_schema/events_statements_history_long.sql_text | 9.77 MiB |
+-----------------------------------------------------------------------------+---------------+
可以看到,thread_id为55的用户占用内存较多(这里只截取了部分),且全局内存使用中有一项memory/sql/user_var_entry::value 异常增大。
通过PSI Memory Key定位到代码,发现该用户的一个存储过程存在死循环,并且在循环中频繁更改一个变量的值。由于用户开启了Binlog,所有的变量修改都会记录一份“历史记录”,在生成Binlog Event事件时一并写入。但因为存储过程死循环,此时并没有DML执行,因此“历史记录”在内存中堆积,堆积过多就引发了OOM现象。
排查清楚后,联系用户修改了存储过程代码,后来没有再复现。
总结
8.0.28中,InnoDB重构的内存分配器能够更加精准的跟踪模块的内存使用情况,无论在开发还是运维的角度,无疑都提供了很多便利。
后续TXSQL还将推出自研的全局内存精确统计功能,敬请期待。