前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原创|MySQL performance_schema之内存监控

原创|MySQL performance_schema之内存监控

作者头像
腾讯数据库技术
发布2022-10-08 15:10:17
1.5K0
发布2022-10-08 15:10:17
举报

提示:公众号展示代码会自动折行,建议横屏阅读

背景

无论从使用、研发还是运维的角度,内存监控一直是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: 内存字节数的使用范围(最小-最大)。
代码语言:javascript
复制
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()包装一下,在里面做其他的事情:

代码语言:javascript
复制
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来做这件事:

代码语言:javascript
复制
 /** 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()函数为例:

代码语言:javascript
复制
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语法对常用的容器进行了重定向,这样即使开发者忘记指定内存分配器,也不会影响内存统计。

代码语言:javascript
复制
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>>>;

同时,还有对智能指针的实现:

代码语言:javascript
复制
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{});}

那扩展性如何解决呢?上述函数所在的类叫做

代码语言:javascript
复制
Alloc_pfs : public allocator_traits<true>

继承了一个统一的基类allocator_traits。如果以后有需要,还可以扩展出使用其他统计方式的内存分配器,不需要更改上层逻辑,只需要更改内存分配策略即可。

内存分析案例

首先,简单举例一下PFS内存监控的使用方法。

打开performance_schema后,可以通过如下SQL语句获取全局的内存使用情况:

代码语言:javascript
复制
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观察该用户的内存使用情况如下:

代码语言:javascript
复制
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还将推出自研的全局内存精确统计功能,敬请期待。

腾讯数据库研发部数据库技术团队对内支持微信支付、微信红包、腾讯广告、腾讯音乐等公司自研业务,对外在腾讯云上支持 TencentDB 相关产品,如 CynosDB、CDB、TDSQL等。本公众号旨在推广和分享数据库领域专业知识,与广大数据库技术爱好者共同成长。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-09-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯数据库技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 提示:公众号展示代码会自动折行,建议横屏阅读
    • 背景
      • PFS内存监控介绍
        • 8.0.28以前的InnoDB内存监控
          • 重构的内存监控
            • 内存分析案例
              • 总结
              相关产品与服务
              云数据库 SQL Server
              腾讯云数据库 SQL Server (TencentDB for SQL Server)是业界最常用的商用数据库之一,对基于 Windows 架构的应用程序具有完美的支持。TencentDB for SQL Server 拥有微软正版授权,可持续为用户提供最新的功能,避免未授权使用软件的风险。具有即开即用、稳定可靠、安全运行、弹性扩缩等特点。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档