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_ALLOCCOUNT_FREE: 调用内存分配器进行内存分配和释放的次数。

  • SUM_NUMBER_OF_BYTES_ALLOCSUM_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_USEDHIGH_COUNT_USED: 内存block的使用范围(最小-最大)。

  • LOW_NUMBER_OF_BYTES_USEDHIGH_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: 0CURRENT_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还将推出自研的全局内存精确统计功能,敬请期待。

 

posted @ 2022-09-29 21:05  papering  阅读(540)  评论(0编辑  收藏  举报