ITPub博客

首页 > 数据库 > 国内数据库 > MySQL内存管理——热璞数据库郝老师

MySQL内存管理——热璞数据库郝老师

原创 国内数据库 作者:xubeibei 时间:2020-08-07 11:50:34 0 删除 编辑

接下来分几个部分,将国庆老师本次直播的干货整理成文字跟大家一起分享

提示: 下面的内容大家结合PPT/ 视频阅更加清晰明了

 

1.  MySQL 内存的分类,我们是按照内存使用的层次来划分的哈,大家都知道 MySQL 分为 Server 层和引擎层。因为这两部分都有内存使用,而且是分开管理的。所以我们也是按照这个进行分类 ;

 

2.  一条sql 语句的内存使用,我们以 delete 为例,来介绍它从发送给 mysql 到执行结束, mysql 内存的分配,使用和释放 ;

 

3.  Mem_root 介绍, Server 层内存管理结构,后续会详细介绍 ;

 

4.  大sql 的内存使用,这个也是为啥会有本次分享的原因,我们热璞是分布式数据库,我的备份工具也是对存储节点进行备份,当还原时,会将备份的数据应逻辑备份的方式,为了备份效率,我们会将数据平成一个大 sql ,还原的时候直接执行这个大 sql ,已实现数据还原,比如 insert into table() values(),()... 等等,然后 MySQL 内存就爆了,这一部分呢,将会详细介绍一条 sql 在不同的阶段使用了哪些内存 ;

 

5. 一个很神奇的问题, free 的内存并没有还给操作系统哦,但是 mysql 已经显式的做了 free 了,那 free 的内存到底去了哪里呢 ?

 

4 & 5 页

我们先看第一部分,MySQL 内存的分类。我们知道 MySQL 中内存的种类很多, sort buffer , join buffer , query_chache , table_cache , innodb_buffer_pool_size 等 。

同时,如前面介绍的,MySQL 是分层的,分为 Server 层以及 engine 层,在 Server 层, MySQL 使用 mem_root 进行管理,在 engine 层,就用大家熟悉的 buffer pool 来进行管理了。

如图,mem_root 如果包括 init_alloc_root , alloc_root , reset_root_defaults, free_alloc 这些函数,用于申请,分配,和回收内存。对内存的使用和释放最终会落到 OS 层的 malloc 和 free 函数。

 

 

Engine 层,我们能看到,使用 buffer_pool 进行管理, buf_pool_init, buf_pool_free,buf_pool_alloc 等等,最终还是落到 OS 的 free 和 malloc 上, buf_pool 不是本次分享的重点哈,因此不做过多解释 。

 

7 页

后面我们看下一条sql 的使用,

当你使用mysql 连接到 mysql server 时,在 handle_connect_per_thread.cc 中当接收到 connect 连接的信息会根据 connect_info 的信息,创建一个 thd 用于一直处理该连接发过来的所有请求,其生命周期是一直到该 connection 关闭, OK ,此时你连上了 mysql 。当你执行一条语句时, thd 中的 mem_root 开始申请内存,用于存储语句,和解析 sql 产生的临时变量。这个时候能看到的就是 MySQL 内存使用量在持续增加。

执行语句,这个顾名思义,server 层拿到 sql 语句,经过语法分析,语义分析等等,最后交给 engine 层执行。这个也是需要大量内存的

执行之后,调用free_root 释放该 thd 中 mem_root 上 保管的内存,同时保留该 thd 哈,用于接收该连接后面的语句

 

--------- 如果此时关闭连接 ---------

Mysql server 层关闭 connection

 

调用thd->release_resources 释放 thd 所有资源

 

删除thd

 

8 页

一条delete 语句的执行流程如上,调用的函数分散在 server 层和 engine 层。因此内存的使用也分散在这两层。我们本次分享呢主要在 server 层的内存使用。因此我们更多的是关注 server 层使用

 

10 页

终于到了我们要介绍的核心 —— Mem_root 。

MySQL Server 层使用 mem_root 来管理内存。我们看下源码, USED_MEM 的结构,包含一个指针,指向下一块儿内存, left 表示本内存块剩余的空间大小。 size 表示该块内存的总大小。 used_mem 是用于管理内存块的。 介绍完 USED_MEM 结构之后,我们来看下 mem_root ,它包含三个链表 free , used , pre_alloc

其中含义如下:

  free 空闲链表块,当需要分配内存时,从该链表读取一块内存,然后划分使用 ;

  used 已使用的内存链表,但是里面内存也不是都占满的哦,会有一些空闲还未使用的区域 ;

  pre_alloc 预先分配的内存,初始化分配,只分配一块,并将其同时加入到 free 链表上 。

 

剩下的有如下变量:

  min_malloc 最小剩余内存阈值,当从一块内存中分配了内存,剩余的内存大小小于该阈值,就有可能会被放入到 used 链表,不在被分配了,这个后面会详细介绍

  block_size 初始化内存大小,以后想 OS 要内存时,都是要 block_size 的整数倍

  block_num   分配内存的次数,这个值是动态变得哦,每分配一次内幕才能,该值 +1

  first_block_usage free 链表第一块内存被使用的次数

 

 

10 & 11 页

 

我们看下初始化函数

 

 

void init_alloc_root(PSI_memory_key key,

                     MEM_ROOT *mem_root, size_t block_size,

     size_t pre_alloc_size MY_ATTRIBUTE((unused)))

{

  DBUG_ENTER("init_alloc_root");

  DBUG_PRINT("enter",("root: 0x%lx", (long) mem_root));

 

  mem_root->free= mem_root->used= mem_root->pre_alloc= 0;  // 清空所有内存链表

  mem_root->min_malloc= 32;   // 设置最小剩余内存阈值为 32

  mem_root->block_size= block_size - ALLOC_ROOT_MIN_BLOCK_SIZE;  // 设置分配内存时分配最小单位, 8192 - 32

  mem_root->error_handler= 0;        // 错误处理函数

  mem_root->block_num= 4;   /* We shift this with >>2 */ // 初始的分配次数个数

  mem_root->first_block_usage= 0;   //

  mem_root->m_psi_key= key;         // 内存类型编号,用于性能计数器使用

  mem_root->max_capacity= 0; //mem_root 最大可分配的内存数量

  mem_root->allocated_size= 0;  // 当前已分配的内存大小

  mem_root->error_for_capacity_exceeded= FALSE;  // 是否分超了

 

#if defined(PREALLOCATE_MEMORY_CHUNKS)

  if (pre_alloc_size) // 如果定义了预分配的大小,则进行预分配

  {

    if ((mem_root->free= mem_root->pre_alloc=

 (USED_MEM*) my_malloc(key,

                               pre_alloc_size+ ALIGN_SIZE(sizeof(USED_MEM)),

       MYF(0))))   // 申请预分配内存 , 同时将分配好的内存加入到 free 链表

    {

      mem_root->free->size= (uint)(pre_alloc_size+ALIGN_SIZE(sizeof(USED_MEM))); // 设置大小

      mem_root->free->left= (uint)pre_alloc_size;   // 设置内存剩余大小

      mem_root->free->next= 0;  // 置尾指针为空

      mem_root->allocated_size+= pre_alloc_size+ ALIGN_SIZE(sizeof(USED_MEM));  // 更新已分配内存大小

    }

  }

#endif

  DBUG_VOID_RETURN;

}

 

以上是mem_root 的初始化操作

 

 

// 分配内存

void *alloc_root(MEM_ROOT *mem_root, size_t length)

{

#if !defined(PREALLOCATE_MEMORY_CHUNKS)

  USED_MEM *next;

  DBUG_ENTER("alloc_root");

  DBUG_PRINT("enter",("root: 0x%lx", (long) mem_root));

 

  DBUG_ASSERT(alloc_root_inited(mem_root));

 

  DBUG_EXECUTE_IF("simulate_out_of_memory",

                  {

                    if (mem_root->error_handler)

                      (*mem_root->error_handler)();

                    DBUG_SET("-d,simulate_out_of_memory");

                    DBUG_RETURN((void*) 0); /* purecov: inspected */

                  });

 

  length+=ALIGN_SIZE(sizeof(USED_MEM));

  if (!is_mem_available(mem_root, length))

  {

    if (mem_root->error_for_capacity_exceeded)

      my_error(EE_CAPACITY_EXCEEDED, MYF(0),

               (ulonglong) mem_root->max_capacity);

    else

      DBUG_RETURN(NULL);

  }

  if (!(next = (USED_MEM*) my_malloc(mem_root->m_psi_key,

                                     length,MYF(MY_WME | ME_FATALERROR))))

  {

    if (mem_root->error_handler)

      (*mem_root->error_handler)();

    DBUG_RETURN((uchar*) 0);   /* purecov: inspected */

  }

  mem_root->allocated_size+= length;

  next->next= mem_root->used;

  next->size= (uint)length;

  next->left= (uint)(length - ALIGN_SIZE(sizeof(USED_MEM)));

  mem_root->used= next;

  DBUG_PRINT("exit",("ptr: 0x%lx", (long) (((char*) next)+

                                           ALIGN_SIZE(sizeof(USED_MEM)))));

  DBUG_RETURN((uchar*) (((char*) next)+ALIGN_SIZE(sizeof(USED_MEM))));

#else

// 这个影响我们看源码,删掉,我们直接看下面的代码

  size_t get_size, block_size;

  uchar* point;

  USED_MEM *next= 0;

  USED_MEM **prev;

  DBUG_ENTER("alloc_root");

  DBUG_PRINT("enter",("root: 0x%lx", (long) mem_root));

  DBUG_ASSERT(alloc_root_inited(mem_root));

 

  DBUG_EXECUTE_IF("simulate_out_of_memory",

                  {

                    /* Avoid reusing an already allocated block */

                    if (mem_root->error_handler)

                      (*mem_root->error_handler)();

                    DBUG_SET("-d,simulate_out_of_memory");

                    DBUG_RETURN((void*) 0); /* purecov: inspected */

                  });

  length= ALIGN_SIZE(length);//double 对齐

  if ((*(prev= &mem_root->free)) != NULL)   //free 非空

  {

    if ((*prev)->left < length && // 剩余大小小于要分配的大小

mem_root->first_block_usage++ >= ALLOC_MAX_BLOCK_USAGE_BEFORE_DROP &&  // 分配的次数大于等于 10 次

(*prev)->left < ALLOC_MAX_BLOCK_TO_DROP)  // 剩余大小小于 4096

    {

      next= *prev;

      *prev= next->next;   /* Remove block from list */ // 挂到 used 链表上

      next->next= mem_root->used;

      mem_root->used= next;

      mem_root->first_block_usage= 0;

    }

    for (next= *prev ; next && next->left < length ; next= next->next)   // 循环继续寻找合适的内存块

      prev= &next->next;

  }

  if (! next)   // 没有分配出来

  {       /* Time to alloc new block */

    block_size= mem_root->block_size * (mem_root->block_num >> 2);   // 大家看到 mem_root->block_size 和 block_num 没, block_num 除以 4 在程序 block_size ,等于要分配的 block_size 的大小

    get_size= length+ALIGN_SIZE(sizeof(USED_MEM));  //get_size 等于 length 加上头长度

    get_size= MY_MAX(get_size, block_size);   // 两个里面取最大值

 

    if (!is_mem_available(mem_root, get_size))

    {

      if (mem_root->error_for_capacity_exceeded)

        my_error(EE_CAPACITY_EXCEEDED, MYF(0),

                 (ulonglong) mem_root->max_capacity);

      else

        DBUG_RETURN(NULL);

    }

    if (!(next = (USED_MEM*) my_malloc(mem_root->m_psi_key,

                                       get_size,MYF(MY_WME | ME_FATALERROR)))) // 调用 my_malloc 分配新内存

    {

      if (mem_root->error_handler)

(*mem_root->error_handler)();

      DBUG_RETURN((void*) 0);                      /* purecov: inspected */

    }

    mem_root->allocated_size+= get_size;  // 更新 mem_root->allocated_size

    mem_root->block_num++;  // 分配次数计数 ++

    next->next= *prev;

    next->size= (uint)get_size;

    next->left= (uint)(get_size-ALIGN_SIZE(sizeof(USED_MEM)));

    *prev=next;

  }

 

  point= (uchar*) ((char*) next+ (next->size-next->left)); // 返回可用的内存地址

  /*TODO: next part may be unneded due to mem_root->first_block_usage counter*/

  if ((next->left-= (uint)length) < mem_root->min_malloc)   // 如果剩余内存小于最小内存阈值,将其挂载到

  {       /* Full block */

    *prev= next->next;     /* Remove block from list */

    next->next= mem_root->used;                                  // 挂载到 used 链表

    mem_root->used= next;

    mem_root->first_block_usage= 0;

  }

  DBUG_PRINT("exit",("ptr: 0x%lx", (ulong) point));

  DBUG_RETURN((void*) point);

#endif

}

 

// 释放内存

void free_root(MEM_ROOT *root, myf MyFlags)

{

  USED_MEM *next,*old;

  DBUG_ENTER("free_root");

  DBUG_PRINT("enter",("root: 0x%lx  flags: %u", (long) root, (uint) MyFlags));

 

  if (MyFlags & MY_MARK_BLOCKS_FREE)   // 如果是 MARK_BLOCKS_FREE

  {

    mark_blocks_free(root);   // 将所有的内存块,初始化一下,并全部挂到 free 链表中

    DBUG_VOID_RETURN;

  }

  if (!(MyFlags & MY_KEEP_PREALLOC))  // 如果不需要 keep prealloc , 直接释放 pre_alloc ,我们介绍过哈, init 的时候,会将 pre_alloc 的内存块挂载到 free 链表上,所以这里直接

    root->pre_alloc=0;                // 将 pre_alloc 置空,不影响后续的释放

 

/*   释放 used 链表的内存块 */

  for (next=root->used; next ;)

  {

    old=next; next= next->next ;

    if (old != root->pre_alloc)

    {

      old->left= old->size;

      TRASH_MEM(old);

      my_free(old);

    }

  }

  /* 释放 free 链表的内存块 */

  for (next=root->free ; next ;)

  {

    old=next; next= next->next;

    if (old != root->pre_alloc)

    {

      old->left= old->size;

      TRASH_MEM(old);

      my_free(old);

    }

  }

  /* 将 free 链表和 used 链表置空 */

  root->used=root->free=0;

  

  if (root->pre_alloc)   // 是否保留 pre_alloc ,如果保留了,则将 pre_alloc 的内存块挂到 free 链表上

  {

    root->free=root->pre_alloc;

    root->free->left=root->pre_alloc->size-(uint)ALIGN_SIZE(sizeof(USED_MEM));

    root->allocated_size= root->pre_alloc->size;

    TRASH_MEM(root->pre_alloc);

    root->free->next=0;

  }

  else

    root->allocated_size= 0;  

  root->block_num= 4;  // 更新 mem_root 的计数信息

  root->first_block_usage= 0;

  DBUG_VOID_RETURN;

}

 

至此Mem_root 的管理:初始化,分配内存,释放内存,就都介绍完了。

 

14 页

下面我们看下大sql 对 MySQL 的影响,图 1 可以看到启动时,内存的使用量只有 400 多 M ,当我们执行一条 150M 的内存之后,我们发现内存的使用量已经飙升到约 4.2 GB 了。

15 页

这个我们通过mysql 的 performance_schema 也能看出来,比如启动时,我们能看到内幕才能的使用量, main_mem_root 还有 151KB , NET::buff 0 KB ,当我们执行大 sql 的时候,我们再查一下性能计数器,

会发现,内存的使用量已经飚上去了,main_mem_root 4.15 GB , NET::buf 也到了 150MB 。

 

16 页

这个时候我们就比较疑惑了,内存到底消耗在哪儿了呢。当时我们的恢复程序在执行时,是执行的bulk insert ,我们查表中的数据,发现我们的查询语句并没有被阻塞,所以我们怀疑这条大 sql 根本没有到

engine 层,所有的内存消耗都在 MySQL Server 层。为此,我们用脚本生成了一条 150M 的 delete 语句,该 delete 语句操作的表不存在,因此他对 innodb 的数据页修改是 0 ,所以可以帮助我们确认是不是 server 层

的内存使用过高导致被OOM 。

我们把server 层程序的执行堆栈打出来,我们来确定到底在那个函数导致内存使用量上去的

 

17 页

通过昨天的图,我们可以看到,

在MYSQLparse 之前, allocated_size = 315563856 在 MYSQLparse 之后,我们可以看到 allocated_size= 370903056 新增的 allocated_size 为 3393499200 ,当前的 allocated_size 加上 MySQL 刚启动时 461500 K 内存,约等于目前的内存使用量 4277196 KB 。

 

至此我们可以确定,还原时内存增大是因为bulk insert 的 sql 太大, MYSQLpase 生成语法树的时候消耗了过多的内存因为 OOM 被 OS kill 掉

 

由上可知,线上的生产环境,最好不要有太大的sql ,一条 150M 的 sql ,可能会导致 MySQL 额外消耗三四个 GB 的内存来运行,这还不包括 buffer pool 的内存消耗

 

19 页

出了上面内存消耗过多的问题,我们在跟源码的时候,也发现另外一个问题,就是MySQL 调用 free_root 释放了内存,如右图执行 free_root 之前, main_mem_root 显示申请了 3709063056 bytes 内存, free_root 之后,只有 8208bytes 的内存了,按理说, MySQL 的使用内存应该从 4 个多 G 降低到 400 多 M 才对,实际呢,并没有。

 

20 页

我们看下这两个图,free_root 之前, MySQL 占用了 4.2 GB 内存, free_root 执行之后呢, MySQL 还占了 3.2 GB ,但是我们通过上一页能够算出来, mem_root 实际释放了 (3709063056 – 8208)bytes 的内存。

但是实际OS 认为只释放了 (4239588-3207996) KB 的内存。这中间出现差错的内存到底去了哪里了呢。这里不得不提 Linux 中内存申请和释放的 malloc 和 free 了。 malloc 和 free 用来向系统申请和释放内存的。但是 free 的内存是否就立即返回给系统呢?这个从我们测试的结果来看就不确定了。这个问题留给大家研究,我们下次在详细分享。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69961799/viewspace-2709936/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
精通分布式事务数据库产品原理,曾参与全国金融标准化技术委员会《分布式数据库技术金融应用技术架构标准》、工信部信息通信研究院《金融分布式事务数据库标准》和《关系型云数据库应用白皮书》的编写,为行业标准的制定作出重大贡献,并多次受邀作为嘉宾出席各类大会,发表精彩演讲。

注册时间:2020-02-19

  • 博文量
    71
  • 访问量
    9240