暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

原创|MySQL performance_schema之内存监控

1197

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


背景

无论从使用、研发还是运维的角度,内存监控一直是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还将推出自研的全局内存精确统计功能,敬请期待。


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


                    文章转载自腾讯数据库技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论