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

【走进RDS】之MySQL内存分配与管理(下篇)

阿里云数据库 2023-01-16
1057

《MySQL的内存分配与管理》上篇中篇,介绍了MySQL的内存分配、使用和管理。在实际使用的过程中,控制好内存的用量、降低OOM风险十分重要。本篇为下篇,主要对MySQL内存限制特性进行解读,代码基于8.0.28。

在MySQL 8.0.28中,官方引入了内存限制的新特性WL#13458--Global and session memory allocation limits,从内核层面对服务运行期间的内存使用进行限制,降低OOM的风险。本文将围绕该项工作的改动、设计实现等方面展开介绍。

1.内存限制的功能改动

1.1 变量添加

内存限制新特性添加了4个新的变量,具体的含义和取值如下表所示。

    MySQL [(none)]> show variables where variable_name in ('global_connection_memory_limit', 'connection_memory_limit', 'connection_memory_chunk_size', 'global_connection_memory_tracking'); # 内存限制变量
    +-----------------------------------+----------------------+
    | Variable_name | Value |
    +-----------------------------------+----------------------+
    | connection_memory_chunk_size | 8912 |
    | connection_memory_limit | 18446744073709551615 |
    | global_connection_memory_limit | 18446744073709551615 |
    | global_connection_memory_tracking | OFF |
    +-----------------------------------+----------------------+


    MySQL [(none)]> show status like "Global_connection_memory"; # 内存使用量
    +--------------------------+-------+
    | Variable_name | Value |
    +--------------------------+-------+
    | Global_connection_memory | 0 |
    +--------------------------+-------+

    1.2 数据结构变更

    该特性对部分已有的数据结构做了变更,新增了与内存使用量统计信息相关的成员对象,修改了PFS_thread、PSI_thread_service_v5、THD这3个类型。

      class PFS_thread {
      ...
      + THD *m_cnt_thd // 用于更新内存计数器的THD
      ...
      };


      // -------------------------------------------------------------


      struct PSI_thread_service_v5 {
      ...
      + set_mem_cnt_THD_v1_t set_mem_cnt_THD;
      };
      typedef void (*set_mem_cnt_THD_v1_t)(THD *thd, THD **backup_thd);


      // -------------------------------------------------------------


      class THD {
      ...
      + Thd_mem_cnt *mem_cnt; // 内存计数器对象
      + bool enable_mem_cnt(); // 启用内存计数器
      + void disable_mem_cnt(); // 关闭内存计数器
      ...
      };
      ● THD可以简单理解为一个连接的上下文信息,m_cnt_thd是负责更新内存计数信息的THD对象,在组提交等操作中存在THD转换的问题,该成员可以确保转换时内存统计信息的正确性。

      ● 接口set_mem_cnt_THD()是协助完成THD转换的函数,分别对m_thd和m_cnt_thd进行设置,大部分情况下两者是相同的。

      ● Thd_mem_cnt是内存计数器对象,THD结构中新增的mem_cnt成员在初始化时为Thd_mem_cnt_noop(空操作计数器),在connnection建立的prepare阶段通过调用enable_mem_cnt()创建为Thd_mem_cnt_conn(真正具备计数功能);在THD析构阶段调用disable_mem_cnt()释放该计数器。Thd_mem_cnt_noop和Thd_mem_cnt_conn都是Thd_mem_cnt的子类。
        // Thd_mem_cnt_conn创建
        thd_prepare_connection()
        | thd->enable_mem_cnt() {
        | | Thd_mem_cnt *tmp_mem_cnt = new Thd_mem_cnt_conn(this);
        | | mem_cnt = tmp_mem_cnt;
        | }


        // Thd_mem_cnt_conn释放
        ~THD()
        | THD::release_resources()
        | | disable_mem_cnt() {
        | | | mem_cnt->flush(); // 清空当前THD的内存计数信息并扣除对应的gloabl数据
        | | | delete mem_cnt;
        | | }

        1.3 数据结构添加

        如前文所述,内存计数器对象Thd_mem_cnt是该WL中引入的最重要的数据结构,负责保存、更新相关的内存使用信息。在内存限制的过程中,真正起到作用的是其子类Thd_mem_cnt_conn,下面对此展开进一步介绍。

        1.3.1 数据结构

        在介绍Thd_mem_cnt_conn之前,首先需要知道引入的计数模式,不同的模式下,计数操作和错误处理是有差异的;通过位运算可以实现多种模式的组合。
          enum enum_mem_cnt_mode {
          MEM_CNT_DEFAULT = 0U, // 不计数
          MEM_CNT_UPDATE_GLOBAL_COUNTER = (1U << 0), // 更新global信息
          MEM_CNT_GENERATE_ERROR = (1U << 1), // 产生OOM错误信息
          MEM_CNT_GENERATE_LOG_ERROR = (1U << 2) // 产生OOM错误信息写入日志
          };

          Thd_mem_cnt_conn的关键数据结构如上图所示,mem_count、max_conn_mem、glob_mem_counter分别指代connection已申请的内存、connection的最大内存(该值并不是一个指定值,会随mem_count变化)和该连接传递给global计数器的值。参数的联系和变化过程如下图所示,mem_count和连接实际的内存使用量相关,glob_mem_counter则是单位化增长,增长的值和connection_memory_chunk_size这个参数相关。


          Q&A

          问:为什么还需要一个glob_mem_counter呢,直接将当前的mem_count累加到全局内存计数器不可以吗?
          答:全局计数信息的变更需要保证原子性,频繁地变更会造成全局计数器锁的争用,影响并发度。参数connection_memory_chunk_size的意义是每次汇总到全局内存计数器的内存数据是chunk_size的整数倍,也就是说glob_mem_counter = connection_memory_chunk_size * n,并且以connection_memory_chunk_size * m的大小增加。每个连接提前传递足够多数量的内存计数到global中可以减少变更全局计数器的次数,避免每次增加零散内存数量带来的全局数据的频繁改动。只有mem_count > glob_mem_counter时才对global数据进行写入,同时将glob_mem_counter加上connection_memory_chunk_size的整数倍。因此说,connection_memory_chunk_size能够控制全局计数器更新的频率。connection_memory_chunk_size设置的较大时,每次汇总到全局计数器的内存信息数据就会很大,会被误认为有OOM的风险,提前引发OOM的报错,因此connection_memory_chunk_size不宜设置的太大;但这个值也不能设置的太小,否则就会导致全局计数器频繁更新。

          mode参数是enum_mem_cnt_mode中的组合,例如SUPER用户在连接建立时的mode是MEM_CNT_UPDATE_GLOBAL_COUNTER,而普通的用户的mode则是MEM_CNT_UPDATE_GLOBAL_COUNTER|MEM_CNT_GENERATE_ERROR | MEM_CNT_GENERATE_LOG_ERROR。在进行内存计数时会使用这个判断位,决定是否产生错误并kill connection。换言之,SUPER用户在执行查询等操作时是不会受到limit参数的限制的,而普通用户则会受这些参数的影响

            static void prepare_new_connection_state(THD *thd) {
            ...
            thd->mem_cnt->set_orig_mode(is_admin_conn ? MEM_CNT_UPDATE_GLOBAL_COUNTER // 根据身份设置mode
            : (MEM_CNT_UPDATE_GLOBAL_COUNTER |
            MEM_CNT_GENERATE_ERROR |
            MEM_CNT_GENERATE_LOG_ERROR));
            ...
            }



            1.3.2 关键接口

            alloc_cnt()
            该函数的功能是更新(增加)connection和global级别的内存计数信息,伴随内存申请被调用,主要做了以下几件事:

            ● 修改mem_counter、max_conn_mem和glob_mem_counter。max_conn_mem随mem_counter更新,glob_mem_counter以lazy方式添加到全局内存计数器中(只有满足max_conn_mem > glob_mem_counter才会插值delta到全局计数器)。由于访问全局计数器需要加一把全局的大锁,这样的操作可以减少变更和加锁的次数。

            ● 产生错误信息,包括connection级别的和global级别的错误信息。generate_error会根据错误信息,给THD设置THD::KILL_CONNECTION状态,随后连接会在状态检测的位点killed。连接被kill后,物理内存下降、THD析构(THD上的计数器对象析构),统计信息随之更新。
              bool Thd_mem_cnt_conn::alloc_cnt(size_t size) {
              mem_counter += size;
              max_conn_mem = std::max(max_conn_mem, mem_counter);


              // connection级别的报错
              if (mem_counter > m_thd->variables.conn_mem_limit) {
              (void)generate_error(ER_DA_CONN_LIMIT, m_thd->variables.conn_mem_limit,
              mem_counter);
              }
              // 三个条件分别指代:开启全局更新、开启内存追踪、存量大于提前量
              if ((curr_mode & MEM_CNT_UPDATE_GLOBAL_COUNTER) &&
              m_thd->variables.conn_global_mem_tracking &&
              max_conn_mem > glob_mem_counter) {
              // 控制全局计数器更新频率
              const ulonglong curr_mem =
              (max_conn_mem m_thd->variables.conn_mem_chunk_size + 1) *
              m_thd->variables.conn_mem_chunk_size;
              ulonglong delta = curr_mem - glob_mem_counter;
              ulonglong global_conn_mem_counter_save;
              ulonglong global_conn_mem_limit_save;
              {
              MUTEX_LOCK(lock, &LOCK_global_conn_mem_limit);
              global_conn_mem_counter += delta;
              global_conn_mem_counter_save = global_conn_mem_counter;
              global_conn_mem_limit_save = global_conn_mem_limit;
              }
              glob_mem_counter = curr_mem;
              max_conn_mem = std::max(max_conn_mem, glob_mem_counter);


              // global级别的报错
              if (global_conn_mem_counter_save > global_conn_mem_limit_save) {
              (void)generate_error(ER_DA_GLOBAL_CONN_LIMIT, global_conn_mem_limit_save,
              global_conn_mem_counter_save);
              }
              }
              return true;
              }

              free_cnt()

              和统计信息增加的方式不同,该函数的功能单一,只对connection级别的mem_counter做减法。那全局的计数信息如何减少呢?全局数据的修改在reset()函数中完成,这样做的目的同样是为了减少全局资源的竞用。显而易见,大多数情况下,全局的计数信息将会滞后于连接计数信息。

                void Thd_mem_cnt_conn::free_cnt(size_t size) {
                mem_counter -= size;
                }
                reset()
                free_cnt()操作只是减去了连接级别的内存计数,全局的计数数据更新在reset()函数中完成,该函数保证了当前global处于最新的状态。主要做了以下几件事:

                ● 重置mode,此前的一些操作可能会将计数器的mode进行修改(例如在prepare connection阶段),这里要确保更新前counter处于正确的模式,避免出现不同权限操作出错(如此前的SUPER和普通用户等)。

                ● 更新三个计数数据,当glob_mem_counter > mem_counter时,表明此前有free_cnt操作减少了mem_counter,此处对glob_mem_counter和全局数据进行更新;反之表明存在未加入全局内存统计的连接级别内存信息,也需要将差值补全。在reset()过程中也可能出现内存不足的情况,同样需要调用错误产生函数对错误信息进行报告,对THD设置killed标志。

                  int Thd_mem_cnt_conn::reset() {
                  // 重置mode
                  restore_mode();
                  max_conn_mem = mem_counter;


                  // 更新计数数据信息
                  if (m_thd->variables.conn_global_mem_tracking &&
                  (curr_mode & MEM_CNT_UPDATE_GLOBAL_COUNTER)) {
                  ulonglong delta;
                  ulonglong global_conn_mem_counter_save;
                  ulonglong global_conn_mem_limit_save;
                  if (glob_mem_counter > mem_counter) {
                  delta = glob_mem_counter - mem_counter;
                  MUTEX_LOCK(lock, &LOCK_global_conn_mem_limit);
                  assert(global_conn_mem_counter >= delta);
                  global_conn_mem_counter -= delta;
                  global_conn_mem_counter_save = global_conn_mem_counter;
                  global_conn_mem_limit_save = global_conn_mem_limit;
                  } else {
                  delta = mem_counter - glob_mem_counter;
                  MUTEX_LOCK(lock, &LOCK_global_conn_mem_limit);
                  global_conn_mem_counter += delta;
                  global_conn_mem_counter_save = global_conn_mem_counter;
                  global_conn_mem_limit_save = global_conn_mem_limit;
                  }
                  glob_mem_counter = mem_counter;
                  if (is_connection_stage &&
                  (global_conn_mem_counter_save > global_conn_mem_limit_save))
                  return generate_error(ER_DA_GLOBAL_CONN_LIMIT, global_conn_mem_limit_save,
                  global_conn_mem_counter_save);
                  }
                  if (is_connection_stage && (mem_counter > m_thd->variables.conn_mem_limit))
                  return generate_error(ER_DA_CONN_LIMIT, m_thd->variables.conn_mem_limit,
                  mem_counter);
                  is_connection_stage = false;
                  return 0;
                  }

                  flush()

                  该函数清空当前连接的内存计数,同时扣除全局的内存计数。在删除计数器对象前,必须要先调用此函数,确保计数信息正确。
                    void Thd_mem_cnt_conn::flush() {
                    max_conn_mem = mem_counter = 0;
                    if (glob_mem_counter > 0) {
                    MUTEX_LOCK(lock, &LOCK_global_conn_mem_limit);
                    global_conn_mem_counter -= glob_mem_counter;
                    }
                    glob_mem_counter = 0;
                    }

                    2. 内存限制的过程

                    2.1 执行流程

                    以最简单的handle_connection为例(非线程池模型),连接建立、执行语句和连接关闭过程对应的内存限制操作如下图所示:

                      ...
                      if (thd_prepare_connection(thd))
                      handler_manager->inc_aborted_connects();
                      else {
                      while (thd_connection_alive(thd)) {
                      if (do_command(thd)) break;
                      }
                      end_connection(thd);
                      }
                      close_connection(thd, 0, false, false);
                      ...


                      2.2 关键函数

                      计数器的构建、销毁以及计数信息的更新等操作在前文中做了说明,此处针对内存申请的处理逻辑进行说明。在connection建立和query执行的过程涉及的内存基本通过my_malloc()(结构数据、sort buffer等)和allocate_from()(临时表)这两个接口进行,对应的释放函数为my_free()和deallocate_from()。两种内存申请方式中对于计数器的处理逻辑是相同的,这里以my_malloc()、my_free()为例对计数器操作逻辑做进一步说明。



                      2.2.1 my_malloc()



                      my_malloc()中主要做了两件事:

                      ● 构建内存块头部,其中保存了size、magic、psi_memory_key等信息;

                      ● 调用PSI_thread_service_v5服务中的pfs_memory_alloc_vc()接口对key进行赋值,实际的计数器更新就在这个接口中进行。
                        void *my_malloc(PSI_memory_key key, size_t size, myf flags) {
                        // malloc出一块包含header信息的内存块
                        my_memory_header *mh;
                        size_t raw_size;
                        raw_size = PSI_HEADER_SIZE + size;
                        mh = (my_memory_header *)my_raw_malloc(raw_size, flags);


                        // 对header数据结构初始化,调用pfs_memory_alloc_vc对head->key进行赋值
                        if (likely(mh != nullptr)) {
                        void *user_ptr;
                        mh->m_magic = PSI_MEMORY_MAGIC;
                        mh->m_size = size;
                        // 调用服务
                        mh->m_key = PSI_MEMORY_CALL(memory_alloc)(key, raw_size, &mh->m_owner);
                        user_ptr = HEADER_TO_USER(mh);
                        MEM_MALLOCLIKE_BLOCK(user_ptr, size, 0, (flags & MY_ZEROFILL));
                        return user_ptr;
                        }
                        return nullptr;
                        }

                        2.2.2 pfs_memory_alloc_vc()

                        这个函数是计数数据增加的入口,主要工作如下:

                        ● 根据key找到对应的PFS_memory_class;
                        ● 获取PFS_thread,在启用了计数器的情况下,对统计数据进行更新。PFS_memory_key类型只要执行了注册memory_class逻辑(register_memory_class),就会启用计数器对象;

                        ● 返回key值,若启用计数器,此时的key值是经过PSI_MEM_CNT_BIT(1 << 31)标记的。

                          PSI_memory_key pfs_memory_alloc_vc(PSI_memory_key key, size_t size,
                          PSI_thread **owner) {
                          // 获取key对应的PFS_memory_class
                          PSI_memory_key result_key = key;
                          ...
                          PFS_memory_class *klass = find_memory_class(key);


                          // 启动thread监控维度、非全局监控模式
                          if (flag_thread_instrumentation && !klass->is_global()) {
                          PFS_thread *pfs_thread = my_thread_get_THR_PFS();
                          // 判断是否启用计数器,在PFS_memory_class初始化阶段
                          if (klass->has_memory_cnt()) {
                          if (pfs_thread->m_cnt_thd != nullptr && pfs_thread->mem_cnt_alloc(size)) // 内存信息增加的入口
                          // 标志位,标记key是否经过计数器的处理
                          result_key |= PSI_MEM_CNT_BIT;
                          }
                          // 统计信息更新
                          ...
                          *owner_thread = pfs_thread;
                          } else {
                          // 统计信息更新
                          ...
                          *owner_thread = nullptr;
                          }
                          return result_key;
                          }

                          2.2.3 my_free() && pfs_memory_free_vc()

                          和上述的两个函数功能相反,my_free()中首先调用pfs_memory_free_vc()对key进行释放,包括了计数器的信息的扣除更新等,然后对包含header在内的整块内存区域进行释放。
                          Q&A

                          问:哪些内存会被计数器进行统计呢?
                          答:在psi_memory_key.cc中,新特性引入的PSI_FLAG_MEM_COLLECT标志位,对all_server_memory数组中需要进行限制的内存打上了标签。

                          3.内存限制的简单测试

                          3.1 测试准备

                          • 创建普通用户RDS_test;
                          • 构建大数据记录;
                          • 设置较小的connection_memory_limit。
                            create user RDS_test identified by 'RDS_test';
                            grant select on test.* to RDS_test;


                            use test;
                            create table t(id int primary key, c longtext);
                            insert t values (1, lpad('RDS', 6000000, 'test'));


                            set global connection_memory_limit=1024 * 1024 * 2;

                            3.2 测试

                            • 普通用户执行

                              ySQL [test]> show variables like "connection_memory_limit";
                              +-------------------------+---------+
                              | Variable_name | Value |
                              +-------------------------+---------+
                              | connection_memory_limit | 2097152 |
                              +-------------------------+---------+


                              MySQL [test]> select count(c) from t group by c;
                              ERROR 4082 (HY000): Connection closed. Connection memory limit 2097152 bytes exceeded. Consumed 7079568 bytes.
                              • SUPER用户执行

                                MySQL [test]> show variables like "connection_memory_limit";
                                +-------------------------+---------+
                                | Variable_name | Value |
                                +-------------------------+---------+
                                | connection_memory_limit | 2097152 |
                                +-------------------------+---------+


                                MySQL [test]> select count(c) from t group by c;
                                +----------+
                                | count(c) |
                                +----------+
                                | 1 |
                                +----------+

                                3.3 测试结果

                                引入了这个功能后,普通用户的内存使用受到限制,超出限定值直接被kill,但SUPER用户还是不受限制的。

                                4.总结展望

                                MySQL 8.0.28中带来的内存限制新特性,总结来说有以下几点:

                                ● 对于SUPER用户和普通用户,内存限制有差别,前者不做限制,可能会引发OOM。

                                ● global数据的更新总是滞后于connection,减少了全局锁的争用。内存统计数据增加阶段,使用connection_memory_chunk_size来控制更新频率;内存统计数据减少阶段,connection和global级别的信息分别通过free_cnt()和reset()完成更新。

                                ● connection_memory_chunk_size设置的较大的情况下,容易提前报告OOM错误导致connection被kill;设置的较小则可能会导致全局锁的频繁访问。

                                ● 内存统计和限制操作依赖于PFS_thread,计数器数据的更新首先通过该对象传递。
                                结合上篇和中篇,不难发现,InnoDB中的内存基本可控,大多数的内存都有指定的size进行控制,额外产生的内存也能粗略的推断出。在MySQL服务过程中,还有许多无法准确估量的内存损耗,如果没有很好地对其进行控制,可能就会引发OOM。官方引入的connection/global的内存使用限制对这个情况进行了优化,降低了发生OOM的风险,但OOM的问题还无法完全避免,有待进一步优化。在最新的MySQL 8.0.31的release notes中可以看到,官方对于内存限制增加了一些监控信息,相信在后续也会推出相关的新功能、新特性,RDS MySQL内核团队也会持续对OOM问题进行优化。
                                此外,MySQL中的net_buffer、join_buffer、sort_buffer等结构会在运行中占据不小内存,同时在Server启动的阶段也会产生许多临时性的内存如recovery、初始化等所需的内存,都值得研究和讨论。

                                参考

                                [1]https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-28.html
                                [2]https://segmentfault.com/a/1190000041506588
                                [3]https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-31.html#mysqld-8-0-31-performance-schema


                                作者信息:
                                宋华雄(桦雄)来自RDS MySQL内核组,从事RDS MySQL内核研发工作。有任何问题和咨询请发邮件至:vogts.wangt@alibaba-inc.com
                                  / End /  
                                推荐阅读


                                点击「阅读原文」了解云数据库RDS MySQL更多内容

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

                                评论