数据库是使用内存的“大户”,合理的内存分配机制就尤为重要,上一期月报介绍了 PostgreSQL 的内存上下文,本文将介绍在 MySQL 中又是怎么管理内存的。

MySQL 在基本的内存操作接口上面封装了一层,增加了控制参数 my_flags

my_flags 的值目前有:

  1. MY_WME /* Write message on error */
  2. MY_ZEROFILL /* Fill array with zero */

MY_FAE 表示内存分配失败就退出整个进程,MY_WME 表示内存分配失败是否需要记录到日志中,MY_ZEROFILL 表示分配内存后初始化为0。

MEM_ROOT 分配内存的单元是 Block,使用 USED_MEM 结构体来描述。结构比较简单,Block 之间相互连接形成内存块链表,left 和 size 表示对应 Block 还有多少可分配的空间和总的空间大小。

而 MEM_ROOT 结构体负责管理 Block 链表 :

  1. typedef struct st_mem_root
  2. {
  3. USED_MEM *free; /* blocks with free memory in it */
  4. USED_MEM *used; /* blocks almost without free memory */
  5. USED_MEM *pre_alloc; /* preallocated block */
  6. /* if block have less memory it will be put in 'used' list */
  7. size_t min_malloc;
  8. size_t block_size; /* initial block size */
  9. unsigned int block_num; /* allocated blocks counter */
  10. /*
  11. first free block in queue test counter (if it exceed
  12. MAX_BLOCK_USAGE_BEFORE_DROP block will be dropped in 'used' list)
  13. */
  14. unsigned int first_block_usage;
  15. void (*error_handler)(void);
  16. } MEM_ROOT;

整体结构就是两个 Block 链表,free 链表管理所有的仍然存在可分配空间的 Block,used 链表管理已经没有可分配空间的所有 Block。pre_alloc 类似于 PG 内存上下文中的 keeper,在初始化 MEM_ROOT 的时候就可以预分配一个 Block 放到 free 链表中,当 free 整个 MEM_ROOT 的时候可以通过参数控制,选择保留 pre_alloc 指向的 Block。min_malloc 控制一个 Block 剩余空间还有多少的时候从 free 链表移除,加入到 used 链表中。block_size 表示初始化 Block 的大小。block_num 表示 MEM_ROOT 管理的 Block 数量。first_block_usage 表示 free 链表中第一个 Block 不满足申请空间大小的次数,是一个调优的参数。err_handler 是错误处理函数。

分配流程

使用 MEM_ROOT 首先需要初始化,调用 init_alloc_root, 通过参数可以控制初始化的 Block 大小和 pre_alloc_size 的大小。其中比较有意思的点是 min_block_size 直接指定一个值 32,个人觉得不太灵活,对于小内存的申请可能会有比较大的内存碎片。另一个是 block_num 初始化为 4,这个和决定新分配的 Block 大小策略有关。

  1. void *alloc_root( MEM_ROOT *mem_root, size_t length )
  2. size_t get_size, block_size;
  3. uchar * point;
  4. reg1 USED_MEM *next = 0;
  5. length = ALIGN_SIZE( length );
  6. if ( (*(prev = &mem_root->free) ) != NULL ) // 判断 free 链表是否为空
  7. {
  8. if ( (*prev)->left < length &&
  9. mem_root->first_block_usage++ >= ALLOC_MAX_BLOCK_USAGE_BEFORE_DROP &&
  10. (*prev)->left < ALLOC_MAX_BLOCK_TO_DROP ) // 优化策略
  11. {
  12. next = *prev;
  13. *prev = next->next; /* Remove block from list */
  14. next->next = mem_root->used;
  15. mem_root->used = next;
  16. mem_root->first_block_usage = 0;
  17. }
  18. // 找到一个空闲空间大于申请内存空间的 Block
  19. for ( next = *prev; next && next->left < length; next = next->next )
  20. prev = &next->next;
  21. }
  22. if ( !next ) // free 链表为空,或者没有满足可分配条件 Block
  23. { /* Time to alloc new block */
  24. block_size = mem_root->block_size * (mem_root->block_num >> 2);
  25. get_size = length + ALIGN_SIZE( sizeof(USED_MEM) );
  26. {
  27. if ( mem_root->error_handler )
  28. (*mem_root->error_handler)();
  29. DBUG_RETURN( (void *) 0 ); /* purecov: inspected */
  30. }
  31. mem_root->block_num++;
  32. next->next = *prev;
  33. next->size = get_size;
  34. next->left = get_size - ALIGN_SIZE( sizeof(USED_MEM) );
  35. *prev = next; // 新申请的 Block 放到 free 链表尾部
  36. }
  37. point = (uchar *) ( (char *) next + (next->size - next->left) );
  38. if ( (next->left -= length) < mem_root->min_malloc ) // 分配完毕后,Block 是否还能在 free 链表中继续分配
  39. { /* Full block */
  40. *prev = next->next; /* Remove block from list */
  41. next->next = mem_root->used;
  42. mem_root->used = next;
  43. mem_root->first_block_usage = 0;
  44. }
  45. }

首先判断 free 链表是否为空,如果不为空,按逻辑应该遍历整个链表,找到一个空闲空间足够大的 Block,但是看代码是先执行了一个判断语句,这其实是一个空间换时间的优化策略,因为free 链表大多数情况下都是不为空的,几乎每次分配都需要从 free 链表的第一个 Block 开始判断,我们当然希望第一个 Block 可以立刻满足要求,不需要再扫描 free 链表,所以根据调用端的申请趋势,设置两个变量:ALLOC_MAX_BLOCK_USAGE_BEFORE_DROP 和 ALLOC_MAX_BLOCK_TO_DROP,当 free 链表的第一个 Block 申请次数超过 ALLOC_MAX_BLOCK_USAGE_BEFORE_DROP 而且剩余的空闲空间小于 ALLOC_MAX_BLOCK_TO_DROP,就把这个 Block 放到 used 链表里,因为它已经一段时间无法满足调用端的需求了。

如果在 free 链表中没有找到合适的 Block,就需要调用基础接口申请一块新的内存空间,新的内存空间大小当然至少要满足这次申请的大小,同时预估的新 Block 大小是 : mem_root->block_size * (mem_root->block_num >> 2) 也就是初始化的 Block 大小乘以当前 Block 数量的 1/4,所以初始化 MEM_ROOT 的 block_num 至少是 4。

找到合适的 Block 之后定位到可用空间的位置就行了,返回之前最后需要判断 Block 分配之后是否需要移动到 used 链表。

归还内存空间的接口有两个:mark_blocks_free(MEM_ROOT *root)free_root(MEN_ROOT *root,myf MyFlags) ,可以看到两个函数的参数不像基础封装的接口,没有直接传需要归还空间的指针,传入的是 MEM_ROOT 结构体指针,说明对于 MEM_ROOT 分配的内存空间,是统一归还的。 不真正的归还 Block,而是放到 free 链表中标记可用。free_root 真正归还空间给操作系统,MyFlages 可以控制是否和标记删除的函数行为一样,也可以控制 pre_alloc 指向的 Block 是否归还。

  • 从空间利用率上来讲,MEM_ROOT 的内存管理方式在每个 Block 上连续分配,内部碎片基本在每个 Block 的尾部,由 min_malloc 成员变量和参数 ALLOC_MAX_BLOCK_USAGE_BEFORE_DROP,ALLOC_MAX_BLOCK_TO_DROP 共同决定和控制,但是 min_malloc 的值是在代码中写死的,有点不够灵活,可以考虑写成可配置的,同时如果写超过申请长度的空间,就很有可能会覆盖后面的数据,比较危险。但相比 PG 的内存上下文,空间利用率肯定是会高很多的。
  • 从时间利用率上来讲,不提供 free 一个 Block 的操作,基本上一整个 MEM_ROOT 使用完毕才会全部归还给操作系统,可见 MySQL 在内存上面还是比较“贪婪”的。
  • 从使用方式上来讲,因为 MySQL 拥有多个存储引擎,引擎之上的 Server 层是面向对象的 C++ 代码,MEM_ROOT 常常作为对象中的一个成员变量,在对象的生命周期内分配内存空间,在对象析构的时候回收,引擎的内存申请使用封装的基本接口。相比之下 MySQL 的使用方式更加多元,PG 的统一性和整体性更好。