走进RDS之MySQL内存分配与管理(中)

简介: MySQL内存分配与管理总体上分为上中下三篇介绍,本篇为中篇,主要介绍 InnoDB 的内存构成和使用,代码版本主要基于8.0.25。

1.引言


     InnoDB 是 MySQL 默认的存储引擎,而提到 InnoDB 的内存,就绕不开 Buffer Pool,该结构对性能的影响重大。但事实上 InnoDB 的内存消耗并不只有 BP 而已,其内部还有许多重要的结构(如AHI、Change Buffer、Log buffer)也占据着不可忽视的内存空间。了解 InnoDB 的内存结构和使用特点对于 MySQL 运行期间的内存选型和使用有很大帮助。

图源


2. Buffer Pool


Buffer Pool(后面简称 BP)是 InnoDB 主内存中的一块区域,主要用于缓存数据页、索引页、undo 页、自适应哈希索引、锁信息等。读取数据时,若数据存在于 BP 中则可以直接读取,避免磁盘 IO 而提升性能;写入数据时,先修改 BP 中的数据页,再使用一定的策略进行刷脏,更新磁盘数据。

一般来说,BP size 会配置成机器可用物理内存的 50% 到 75%,数据库在启动时会提前分配好这部分的虚拟内存,而真正的物理内存映射会在实际使用中进行。BP 的内存占用会一直存在,直到数据库实例关闭时统一释放。实际BP size和设定值可能会不同,BP size 始 终会圆整为chunk_size * instances的整数倍。

1.1 数据结构

BP 主要的数据结构包括buf_pool_t、buf_chunk_t、buf_block_t 等,各结构之间的主要关系如下图所示。通过参数 innodb_buffer_pool_instances 可以设置 buf_pool_t 实例的数量,多个 instance 的设计可以减少缓冲池内部的资源竞争以提高引擎整体的性能;每个 BP instance 至少包含 1 个 chunk,每个 chunk 在初始化时会划分出数据页控制体 buf_block_t 和实际的数据页帧 frame。页由 LRU、free、flush 等链表进行管理。

1.1.1 buf_pool_t

buf_pool_t 表示一个 BP instance,该结构中包含了诸多的信息,如实例号、size、chunk 列表、各个链表(free、LRU、flush)及其互斥锁、哈希表及其互斥锁等,还包括了 zip_free 链表数组;此外,buf_pool_t 中还包含了内存分配器 ut_allocator,用于分配 chunks 的内存;xxx_old 用来记录 BP resize 前的旧数据;风险指针 Hp 用于标记链表节点位置。

struct buf_pool_t {
    ...
    ulint                           instance_no;       // 缓冲池实例编号
    ulint                           curr_pool_size;    // 缓冲池实例大小
    buf_chunk_t                     *chunks;           // 缓冲池实例的物理块列表
    hash_table_t                    *page_hash;        // 页哈希表
    hash_table_t                    *zip_hash;         // 伙伴系统分配frame对应的block哈希表
    UT_LIST_BASE_NODE_T(buf_page_t) free;              // 空闲链表
    UT_LIST_BASE_NODE_T(buf_page_t) LRU;               // LRU 链表
    UT_LIST_BASE_NODE_T(buf_page_t) flush_list;        // flush 链表
    UT_LIST_BASE_NODE_T(buf_buddy_free_t) zip_free[BUF_BUDDY_SIZES_MAX]; //伙伴分配系统空闲链表
    BufListMutex                    free_list_mutex;   // 空闲链表的互斥锁
    BufListMutex                    LRU_list_mutex;    // LRU 链表的互斥锁
    BufListMutex                    flush_state_mutex; // flush 链表的互斥锁
    BufListMutex          zip_free_mutex;              // 伙伴分配互斥锁
    BufListMutex          zip_hash_mutex;
    BufListMutex          chunks_mutex;                // chunk mutex链表
    ...
}

1.1.2 buf_chunk_t

chunk 是物理内存分配的基本单位,instance 由一块一块的 chunk 组成。每个 chunk 被切分成 block 串和 frame 串,block 串在前,frame 串在后,两者之间可能存在不圆整的内存碎片。其中 block 为控制块,包含了页面的控制信息;frame 存储了实际的数据,由 bytes 组成。

struct buf_chunk_t {
  ulint               size;           /*!< frames[]/blocks[]的数量 */
  unsigned char       *mem;           /*!< frame内存区域指针 */
  ut_new_pfx_t        mem_pfx;        /*!< 监控信息 */
  buf_block_t         *blocks;        /*!< 控制块数组 */
  uint32_t            chunk_no;       /*! chunk号 */
  UT_LIST_BASE_NODE_T(buf_page_t)   chunk_page_list; /*!< chunk list根结点 */
  ...
};

1.1.3 buf_block_t

页控制块,主要的数据包括 buf_page_t 格式的 page,该对象必须为第一个成员以便进行指针的强制转换;还包括指向实际的数据帧 frame 的指针,frame 指向的内存块大小为 UNIV_PAGE_SIZE(一般为16K);block 中还包含了 frame 和 block 的 mutex,该mutex也负责保护buf_page_t 中的部分数据。

struct buf_block_t {
  buf_page_t    page;  // 放在第一个位置,以便于block和page进行强制转换
  BPageLock     lock;  // frame的读写锁
  byte        *frame;  // 实际数据
  ...
  BPageMutex    mutex; // block锁:state、io_fix、buf_fix_count、accessed
};

1.1.4 buf_page_t

page 包含了 id、size、lsn 等信息,io_fix 和 buf_fix_count 用于控制并发状态,判断该 page 是否处于被访问的状态。page 中还包含了很多 bool 变量,用于判断该 page 是否处于对应链表或哈希表中。

struct buf_page_t {
    ...
    page_id_t      id;                  // page id
    page_size_t    size;                // page 大小
    ib_uint32_t    buf_fix_count;       // 用于并发控制
    buf_io_fix     io_fix;              // 用于并发控制
    buf_page_state state;               // 页状态
    lsn_t          newest_modification; // 最新 lsn,即最近修改的 lsn
    lsn_t          oldest_modification; // 最老 lsn,即第一条修改 lsn
    ...
}

1.2 初始化过程

BP 初始化的过程和上述的各个层级的数据结构紧密相关。初始化的主线过程大致步骤可分为:BP 初始化--- BP isntance 初始化--- chunk 初始化--- block 初始化;此外 AHI、page_hash 等结构的初始化也会在此过程中完成。

1.2.1 buf_pool_init():

这个过程主要做三件事:一是 BP instance 指针数组的初始化(没有分配实际的内存);二是多线程并发使用 buf_pool_create() 去构建实际的内存空间;三是在 BP 初始化完成后开启AHI的初始化。

// 1. 构建BP instance指针数组
buf_pool_ptr =
    (buf_pool_t *)ut_zalloc_nokey(n_instances * sizeof *buf_pool_ptr);
// 2. 多线程并发初始化BP instance
for (ulint id = i; id < n; ++id) {
    threads.emplace_back(std::thread(buf_pool_create, &buf_pool_ptr[id], size,
                                     id, &m, std::ref(errs[id]))); }
// 3. AHI的初始化
btr_search_sys_create(buf_pool_get_curr_size() / sizeof(void *) / 64);

buf_pool_create() 函数负责构建每个BP instance,主要做了几件事:

  1. 初始化各个锁:包括 chunks mutex、LRU 链表锁、free 链表锁、zip_free 链表锁、哈希表锁等;
  2. 计算 chunks 数量,申请 chunk 指针数组;
  3. 初始化上述的链表;
  4. 循环调用 buf_chunk_init() 初始化 chunk;
  5. 设置 instance 相关的参数(size、instance_no 等);
  6. 构建哈希表和相关的锁;
  7. 初始化 flush 相关数据,如 Hp 指针、链表包含关系等。
static void buf_pool_create(buf_pool_t *buf_pool, ulint buf_pool_size,
                            ulint instance_no, std::mutex *mutex,
                            dberr_t &err) {
  ...
  // 1. 构建锁信息
  mutex_create(LATCH_ID_BUF_POOL_CHUNKS, &buf_pool->chunks_mutex);
  mutex_create(LATCH_ID_BUF_POOL_LRU_LIST, &buf_pool->LRU_list_mutex);
  ...
  // 2. 计算chunks数量
  buf_pool->n_chunks = buf_pool_size / srv_buf_pool_chunk_unit;
  chunk_size = srv_buf_pool_chunk_unit;
  buf_pool->chunks = reinterpret_cast<buf_chunk_t *>(
        ut_zalloc_nokey(buf_pool->n_chunks * sizeof(*chunk)));
  ...
  // 3. 初始化各链表
  UT_LIST_INIT(buf_pool->LRU, &buf_page_t::LRU);
  UT_LIST_INIT(buf_pool->free, &buf_page_t::list)
  UT_LIST_INIT(buf_pool->flush_list, &buf_page_t::list);
  ...
  // 4. 初始化chunk
  do {
    if (!buf_chunk_init(buf_pool, chunk, chunk_size, mutex)) {
      ...
  } while (++chunk < buf_pool->chunks + buf_pool->n_chunks);
  ...
  // 5. 设置instance参数
  buf_pool->instance_no = instance_no;
  buf_pool->curr_pool_size = buf_pool->curr_size * UNIV_PAGE_SIZE;
  ...
  // 6. 构建page_hash和locks
  srv_n_page_hash_locks =
      static_cast<ulong>(ut_2_power_up(srv_n_page_hash_locks));
  buf_pool->page_hash =
      ib_create(2 * buf_pool->curr_size, LATCH_ID_HASH_TABLE_RW_LOCK,
                srv_n_page_hash_locks, MEM_HEAP_FOR_PAGE_HASH);
  buf_pool->zip_hash = hash_create(2 * buf_pool->curr_size);
  ...
  // 7. 初始化flush相关信息,如Hp指针等
  for (i = BUF_FLUSH_LRU; i < BUF_FLUSH_N_TYPES; i++) {
    buf_pool->no_flush[i] = os_event_create();
  }
  ...
  new (&buf_pool->flush_hp) FlushHp(buf_pool, &buf_pool->flush_list_mutex);
  ...
}

1.2.2 buf_chunk_init():

该函数主要做了以下几件事:

  1. 通过 ut_allocator + large 的方式分配 chunk 需要的内存;
  2. 分割 chunk,将其分为 blocks 和 frames;
  3. 调用 buf_block_init() 初始化 block(初始化 block/page 中的变量,如 state、chunk_no 还有是否处于链表的各个标志位等,构建mutex 和 rwlock)并将其加入free链表;
  4. 注册 chunk。
static buf_chunk_t *buf_chunk_init(...) 
{
  // 1. ut_allocator + large方式申请内存
  ...
  if (!buf_pool->allocate_chunk(mem_size, chunk)) {
    return (nullptr);
  }
  ...
  // 2. 切分block和frame
  chunk->blocks = (buf_block_t *)chunk->mem;
  frame = (byte *)ut_align(chunk->mem, UNIV_PAGE_SIZE);
  chunk->size = chunk->mem_pfx.m_size / UNIV_PAGE_SIZE - (frame != chunk->mem);
  {
    ulint size = chunk->size;
    while (frame < (byte *)(chunk->blocks + size)) {
      frame += UNIV_PAGE_SIZE;
      size--;
    }
    chunk->size = size;
  }
  ...
  // 3. 初始化页控制体block
  for () {
    buf_block_init(buf_pool, block, frame, chunk, sync_init_nolock);
    UT_LIST_ADD_LAST(buf_pool->free, &block->page);
  }
  // 4. 注册chunk
  buf_pool_register_chunk(chunk);
  ...
}

1.2.3 page_hash初始化

buf_pool->page_hash =
     ib_create(2 * buf_pool->curr_size, LATCH_ID_HASH_TABLE_RW_LOCK,
               srv_n_page_hash_locks, MEM_HEAP_FOR_PAGE_HASH);
buf_pool->page_hash_old = nullptr;
buf_pool->zip_hash = hash_create(2 * buf_pool->curr_size);

这里可以看到 page_hash 和 zip_hash 的构建方式有所差异,前者调用 ib_create(),后者则是直接调用 hash_create()。ib_create() 在hash_create() 的基础上多做了 mem_heap_t 类型结构 heaps 和锁初始化的工作。有关 ib_create() 函数的内容会在第三部分的 AHI 结构中做进一步介绍。

1.3 页面管理链表

BP 中的每个链表都是双向链表,节点类型都是 buf_block_t ,基节点中保存了首尾节点信息和链表长度等,大致结构如下图所示。

Buffer Pool 中的页面使用情况如下图所示。其中每个小方格可视为一个页,free 表示空闲页,即该页面对应的数据为空,随时可以填入新的数据;clean 表示干净页,即该页面存在数据,且该数据未被更新;dirty 表示脏页,即该页面存在数据已被更新。各个类型的页同时存在于 BP 中,它们被各个链表串起来,协同管理。

1.3.1 free list

block 初始化后会直接加入到 free 链表。缓冲池中如果需要使用数据页,直接从空闲链表中获取。当空闲节点不足时,将采用一定的策略从 LRU List 和 flush List 中淘汰节点以补充 free 状态的页面。

1.3.2 LRU list

LRU List 是缓冲池中最重要的数据结构,基本所有读入的数据页都缓冲于其上。LRU 链表根据 Least Recently Used 算法对节点进行淘汰。InnoDB 对 LRU 算法进行了以下优化,解决“预读失效”与“缓冲池污染”的问题。

  • LRU优化

LRU 分为了 Old Sublist 和 New Sublist 两段,加载数据首先会加载到 Old 位置,只有当满足一定的条件时,数据才会从 Old 段转移到 New 段。当发生类似全表扫描的操作时,LRU 的淘汰就不会影响到真正的热点数据,从而保证缓存的热度。

图源

  • 响应时间优化

先设定一个间隔时间 innodb_old_blocks_time,然后将 Old 区域数据页的第一次访问时间在其对应的控制块中记录下来:

  • 如果后续的访问时间与第一次访问的时间小于 innodb_old_blocks_time,则不将该缓存页从 Old 区域移动到 New 区域。
  • 如果后续的访问时间与第一次访问的时间大于 innodb_old_blocks_time,则将该缓存页从 Old 区域移动到 New 区域的头部。

1.3.3 flush list

缓冲池中所有脏页都会挂载在 flush list 中,以等待数据落盘。在数据更改被刷入磁盘前,数据很有可能会被修改多次。数据页控制体中记录了最新修改的 lsn(newset_modification) 和最早修改的 lsn(oldest_modification)。最新加入的数据页放在链表头部,刷数据时从链表尾开始,即优先刷新最早加入的页节点。

1.3.4 zip_free

该结构是由 5 个链表构成的二维数组,分别是 1K、2K、4K、8K 和 16K 的碎片链表,存储从磁盘读入的压缩页,引擎使用伙伴系统来管理该结构。


2. Change Buffer


Change Buffer 是一颗通用 B+ 树,当二级索引页面不在 Buffer Pool 中时会将 DML(insert、update、delete) 操作产生的的变更缓存在其中,并在合适的时机将缓存合并(merge),减少磁盘的 IO 操作。Change Buffer 内存来源是 Buffer Pool,可以通过参数 innodb_change_buffer_max_size 来设置最大的大小占比,默认 25%,最多 50%。

图源

早先的版本中,Change Buffer 只支持 insert 操作,也被称为 Insert Buffer基于这个历史原因,Change Buffer 在代码中的结构为 ibuf_t ,内包含的是基本的 size、max_size、free_list_len、merge 操作次数等信息。全局只有一个 ibuf_t 结构体,在数据库启动的时候构建。Change Buffer 创建和初始化过程在 ibuf_init_at_db_start() 函数中完成,主要包括:

  1. 相关互斥量的构建;
  2. ibuf参数的初始化,包括max_size、index等相关的数据;
  3. root的获取。
void ibuf_init_at_db_start(void) {
  ...
  // 1.互斥量操作
  mutex_create(LATCH_ID_IBUF, &ibuf_mutex);
  ...
    
  // 2.构建root
  {
    buf_block_t *block;
    // IBUF_SPACE_ID = 0, FSP_IBUF_TREE_ROOT_PAGE_NO = 4
    block = buf_page_get(page_id_t(IBUF_SPACE_ID, FSP_IBUF_TREE_ROOT_PAGE_NO),
                          univ_page_size, RW_X_LATCH, &mtr);
    buf_block_dbg_add_level(block, SYNC_IBUF_TREE_NODE);
    // 对应的frame作为Change Buffer B+树的root
    root = buf_block_get_frame(block);
  }
  ...
  // 3. 参数设置
  // CHANGE_BUFFER_DEFAULT_SIZE默认是25
  ibuf->max_size = ((buf_pool_get_curr_size() / UNIV_PAGE_SIZE) *
                    CHANGE_BUFFER_DEFAULT_SIZE) /
                    100;
  ibuf->index =
      dict_mem_index_create("innodb_change_buffer", "CLUST_IND", IBUF_SPACE_ID,
                            DICT_CLUSTERED | DICT_IBUF, 1);
  ibuf->index->id = DICT_IBUF_ID_MIN + IBUF_SPACE_ID;
  ibuf->index->table = dict_mem_table_create("innodb_change_buffer",
                                              IBUF_SPACE_ID, 1, 0, 0, 0, 0);
  ...
}

ibuf_insert()操作底层调用了ibuf_insert_low(),主要做了以下几件事:

  1. 根据数据构建,在数据记录的基础上增加 page.no 等信息;
  2. 选择合适的 block 插入(数据插入在 rec 中,而 block 则包含有 rec 的数据);
  3. 视情况进行 merge。
static MY_ATTRIBUTE((warn_unused_result)) dberr_t
    ibuf_insert_low(ulint mode, ibuf_op_t op, ibool no_counter,
                    const dtuple_t *entry, ulint entry_size,
                    dict_index_t *index, const page_id_t &page_id,
                    const page_size_t &page_size, que_thr_t *thr) {
  ...
  // 1. 构建entry
  ibuf_entry = ibuf_entry_build(op, index, entry, page_id.space(), page_id.page_no(),
                                no_counter ? ULINT_UNDEFINED : 0xFFFF, heap);
  ...
  // 初始化游标
  btr_pcur_open(ibuf->index, ibuf_entry, PAGE_CUR_LE, mode, &pcur, &mtr);
  ...
        
  // 2. 插入操作
  err = btr_cur_optimistic_insert(...);// 也可能是btr_cur_pessimistic_insert
  block = btr_cur_get_block(cursor);
  ...
  // pcur收尾工作,包括rec、block的清空等
  btr_pcur_close(&pcur);
        
  // 3. 视情况进行merge
}

Change Buffer 本身没有很多额外的内存申请,依赖 Buffer Pool 中的 block 进行操作。大部分都是申请一些临时的 mem_heap_t,使用完毕后立即释放,不会在内存中长时间驻留。


3. AHI


InnoDB 的索引组织结构为 btree,当查询的时候会根据条件一直索引到叶子节点。为了减少开销,InnoDB 中引入了自适应哈希索引(Adaptive Hash Index,后文简称AHI)对索引的前缀建立了一个哈希表,用来加速查询。AHI 是为那些频繁被访问的索引页而建立的,可以理解为 btree 上的索引,其中包含了多个 hash_table。初始创建的数组大小为 buf_pool_get_curr_size() / sizeof(void *) / 64,使用 malloc 分配。数组大小最终对应了 hash_table 中 cell/bucket 的总数,这个数量实际上还要进行一个质数化的处理。

3.1 数据结构

struct hash_cell_t {
  void *node; /*!< 哈希链 */
};
/* The hash table structure */
struct hash_table_t {
  enum hash_table_sync_t type; /*!< MUTEX/RW_LOCK/NONE. */
  ibool adaptive;     
  ulint n_cells;      /* 哈希桶数量 */
  hash_cell_t *cells; /*!< bucket数组 */
  ulint n_sync_obj; /* 互斥量、锁的数量 */
  union {
    ib_mutex_t *mutexes; 
    rw_lock_t *rw_locks; 
  } sync_obj;
  mem_heap_t **heaps; // 多个part时,用于分配哈希链的内存数组,个数和n_sync_obj相关,如在page_hash中用到
  mem_heap_t *heap; // 分配哈希链的内存堆
};

3.2 内存初始化

在AHI构建的时候,分成了 8 个 part,每个 part 负责不同的 bucket ,拥有各自部分的锁。构建和初始化主要分为以下几个步骤:

  1. 锁的初始化,锁的数量和 part 数量挂钩;
  2. hash_table 的初始化,底层调用 ib_create(),注意这里传入的 type 是 MEM_HEAP_FOR_BTR_SEARCH,这直接决定了 hash_table 中 heap 的类型,即内存的来源。
void btr_search_sys_create(ulint hash_size) {
  /* Step-1: Allocate latches (1 per part). */
  btr_search_latches = reinterpret_cast<rw_lock_t **>(
      ut_malloc(sizeof(rw_lock_t *) * btr_ahi_parts, mem_key_ahi));
  for (ulint i = 0; i < btr_ahi_parts; ++i) {
    btr_search_latches[i] = reinterpret_cast<rw_lock_t *>(
        ut_malloc(sizeof(rw_lock_t), mem_key_ahi));
    rw_lock_create(btr_search_latch_key, btr_search_latches[i],
                    SYNC_SEARCH_SYS);
  }
  /* Step-2: Allocate hash tablees. */
  btr_search_sys = reinterpret_cast<btr_search_sys_t *>(
      ut_malloc(sizeof(btr_search_sys_t), mem_key_ahi));
  btr_search_sys->hash_tables = reinterpret_cast<hash_table_t **>(
      ut_malloc(sizeof(hash_table_t *) * btr_ahi_parts, mem_key_ahi));
  for (ulint i = 0; i < btr_ahi_parts; ++i) { // 循环调用ib_create()
    btr_search_sys->hash_tables[i] =
        ib_create((hash_size / btr_ahi_parts), LATCH_ID_HASH_TABLE_MUTEX, 0,
                  MEM_HEAP_FOR_BTR_SEARCH);
  ...
}

进一步地,ib_create中主要做2件事:

  • 调用 hash_create() 创建 hash_table

hash_table() 函数将大部分 hash table 结构中的参数初始化为 0/nullptr,最重要的是构建 hash_table->cells,即哈希桶。哈希桶通过malloc & memset 方式进行构建,这也是AHI构建过程中耗时最久的步骤。

  • 初始化table->heap

这里初始化 type 选择 MEM_HEAP_FOR_BTR_SEARCH 类型,heap 的构建为后续的哈希桶指向的哈希链的内存分配做准备。关于 MEM_HEAP_FOR_BTR_SEARCH 类型的说明和使用可以参考上篇


4. 其他


4.1 Log Buffer

Log Buffer 是日志的缓存,大小由参数 innodb_log_buffer_size 指定,一般来说这块内存都比较小,默认是 16M,有 max 和 min 的限制。

Log Buffer 的内存申请/释放底层调用的是 ut_allocate()/ut_free(),参数 srv_log_buffer_size 就是所需的大小。

// 内存申请
static void log_allocate_buffer(log_t &log) {
  ...
  log.buf.create(srv_log_buffer_size);
}
// 内存释放
static void log_deallocate_buffer(log_t &log) { log.buf.destroy(); }

4.2 table cache

MySQL 中对内存中打开表的数量和表结构数量做了限制。open_table 的过程涉及到 Sever 层和引擎层,这里只针对 InnoDB 层涉及的动作展开介绍。

InnoDB 层的开表动作从函数 ha_innobase::open() 开始,主要包括了 dict_table_t 的构建和 row_prebuilt_t 这个结构的建立。ib_table 的获取顺序依次是会话级缓存 session_cache、全局缓存 dict_sys->hash_table、开表 dd_open_table()。在当前缓存中获取 ib_table 失败就会去下一层的缓存中去获取,所有缓存都不命中就执行开表操作。

int ha_innobase::open(const char *name, int, uint open_flags,
                      const dd::Table *table_def) {
  ...
  // session级缓存
  ib_table = thd_to_innodb_session(thd)->lookup_table_handler(norm_name);
  
  ...
  // 全局dict_sys级缓存
  ib_table = dict_table_check_if_in_cache_low(norm_name);
  
  ...
  // 缓存中不存在,直接开表
  ib_table = dd_open_table(client, table, norm_name, table_def, thd);
  
  ...
  // m_prebuilt结构构建
  m_prebuilt = row_create_prebuilt(ib_table, table->s->reclength);
  ...
}

下面分别就 ib_table 的来源内存进行说明

  1. session_table_cache

每个连接都会对应一个 THD 结构,THD 内部保存了 thread_local 的数据,通过该数据可以获取 session 下的 m_open_tables 映射表。该表的插入、删除、查找都是基于 std::map 进行。

class innodb_session_t {
  table_cache_t m_open_tables;
  ...
};
  1. dict_sys->table_hash

dict_sys 可视为全局的缓存。dicy_sys 中 table_hash 的构造是在执行数据字典初始化 dict_init() 的时候完成的,主要包括:lock 的构建、table_LRU 链表的构建、table_hash 的构建,其中 table_hash 也是通过 hash_create() 这个接口进行构建。

void dict_init(void) {
  ...
  dict_sys->table_hash = hash_create(
      buf_pool_get_curr_size() / (DICT_POOL_PER_TABLE_HASH * UNIV_WORD_SIZE));
  dict_sys->table_id_hash = hash_create(
      buf_pool_get_curr_size() / (DICT_POOL_PER_TABLE_HASH * UNIV_WORD_SIZE));
  ...
}
  1. dd_open_table()

在试图获取缓存表失败后,最终会通过 dd_open_table() 接口构造 dict_table_t ,底层的调用是 dict_mem_table_create() ,通过 mem_heap_t 对 dict_table_t 的所有结构进行构造;构造完成后,会把最新的 table 缓存在 dict 的 hash_table 中。

dd_open_table
    |->dd_open_table_one
    |    |->dd_fill_dict_table //create dict_table_t
    |        |->dict_mem_table_create // create
    |           {
    |               // dict_table_t和内部的col、locks等内存都从这个heap上面分配,DICT_HEAP_SIZE=100
    |               heap = mem_heap_create(DICT_HEAP_SIZE);
    |            ...
    |               table = static_cast<dict_table_t *>(mem_heap_zalloc(heap, sizeof(*table)));
    |               ...
    |               table->heap = heap;
    |               table->cols = static_cast<dict_col_t *>(
    |               mem_heap_alloc(heap, table->n_cols * sizeof(dict_col_t)));
    |               table->v_cols = static_cast<dict_v_col_t *>(
    |                  mem_heap_alloc(heap, n_v_cols * sizeof(*table->v_cols)));
    |               table->autoinc_lock =
    |                      static_cast<ib_lock_t *>(mem_heap_alloc(heap, lock_get_size()));
    |               ...
    |           }
    |->dict_table_add_to_cache(m_table, TRUE, heap); // 添加到缓存

Server 层中的总的 table cache 和打开表数量、字段长度都有关系,每个 table cache 占据的内存从几十k ~ 几百k不等。

4.3 lock_sys_t

锁系统也是在 InnoDB start/create 的时候构建的,主要的数据内容包括行锁哈希表、Predicate Locks 哈希表、predicate page locks 哈希表等,主要的构建和销毁操作如下。

void lock_sys_create(ulint n_cells)
{
  ...
  lock_sys->rec_hash = hash_create(n_cells);
  lock_sys->prdt_hash = hash_create(n_cells);
  lock_sys->prdt_page_hash = hash_create(n_cells);
  ...
}
void lock_sys_close(void) {
  ...
  hash_table_free(lock_sys->rec_hash);
  hash_table_free(lock_sys->prdt_hash);
  hash_table_free(lock_sys->prdt_page_hash);
  ...
}

主要的内存消耗都是在 3 个 hash_table 的构造上,并且是直接调用 hash_create() 进行“裸”构造,没有涉及 heap/heaps 的初始化,所有的内存都是通过 malloc 的方式去构造。各个 hash_table 的需要的内存大小和 srv_lock_table_size 相关,其值在 InnoDB 启动时被指定:srv_lock_table_size = 5 * (srv_buf_pool_size / UNIV_PAGE_SIZE)。

4.4 os_event_t

大多数锁、互斥量的构建和初始化最终都会落到 os_event_t 的构造,但是零散的、临时的 mutex 并不会造成很大的内存压力。前文提到的在 buf_block_t 的初始化中就有大量 mutex 和 rw_lock 的初始化,其生命周期和 BP 相当,数量和 buf_block_t 相等,因此会占据很大一部分内存空间。

  • buf_block_init:
/** Initializes a buffer control block when the buf_pool is created. */
static void buf_block_init(
    buf_pool_t *buf_pool, /*!< in: buffer pool instance */
    buf_block_t *block,   /*!< in: pointer to control block */
    byte *frame,          /*!< in: pointer to buffer frame */
    buf_chunk_t *chunk,   /*!< in: pointer to chunk */
    bool sync_init_nolock)
{
  ...
  mutex_create(LATCH_ID_BUF_BLOCK_MUTEX, &block->mutex); // mutex构建
  ...
  rw_lock_create(PFS_NOT_INSTRUMENTED, &block->lock, SYNC_LEVEL_VARYING); // rw_lock构建
  ...
}
  • mutex_create
mutex_create()
    |->mutex_init()
        |->TTASEventMutex::init()
          |->os_event_create() // 构建os_event_t
  • rw_lock_create
rw_lock_create()
    |->pfs_rw_lock_create_func()
    |->rw_lock_create_func()
        |->os_event_create() // 构建os_event_t

os_event_create() 的底层实现是调用了 malloc 的方式,最终由系统分配这部分的内存。

os_event_t os_event_create() {
  os_event_t ret = (UT_NEW_NOKEY(os_event()));
  return ret;
}


5. 总结

最后,对 InnoDB 涉及到的内存使用做了简要概括,汇总如下表所示。可以看到当指定 innodb_buffer_pool_size 时,除了 BP 本身外,还有很多其他部分的内存也被间接确定了,这部分内存如果没有关注到,其具体内存占用不易知晓。

对象

来源

内存结构

大小

分配方式

Buffer Pool

buf_pool_create()

chunk

BP + BP / 16k * 440 (round)

ut_allocator.allocate_large

buf_pool_create()

page_hash

2 * BP / 16k * 8 (prime)

ut_allocator.allocate

buf_pool_create()

zip_hash

2 * BP / 16k * 8 (prime)

ut_allocator.allocate

AHI

buf_pool_init()

hash_tables

BP / 8 / 64 * 8 (prime)

ut_allocator.allocate mem_heap_allocator (from BP)

Log Buffer

log_allocate_buffer()

buf

srv_log_buffer_size

ut_allocator.allocate

DD cache

dict_init()

table_hash

BP / 4096 * 8 (prime)

ut_allocator.allocate

table_id_hash

BP / 4096 * 8 (prime)

ut_allocator.allocate

lock system

lock_sys_create()

rec_hash

5 *  BP / 16k * 8 (prime)

ut_allocator.allocate

rec_hash

5 *  BP / 16k * 8 (prime)

ut_allocator.allocate

rec_hash

5 *  BP / 16k * 8 (prime)

ut_allocator.allocate

osevent

buf_block_init()

mutex

112 * BP / 16k (round)

ut_allocator.allocate

rw_lock

2 * 112 * BP / 16k (round)

ut_allocator.allocate

BP指代innodb_buffer_pool_size,round代表分配的大小需要做圆整对齐处理、prime代表需要做质数化处理。


  • BP 指定的 size 最终体现在 chunk 的内存中,实际内存和指定的 size 可能存在差异。
  • AHI 结构中采用 malloc 的方式申请了 cells ,但在某些情况下数据都会保存在 mem_heap_t 中。由于 MEM_HEAP_FOR_BTR_SEARCH 标志位的设置,AHI 中 mem_heap_t 部分内存将从 BP 中获取。AHI 中存在多个 hash_table ,目前是采用 loop 方式构建,可以考虑并行方式进行初始化,提高初始化效率;理论上减少哈希冲突能够减少 mem_heap_t 的内存使用。
  • 很多内存结构都和 hash_table 相关,hash_table 实际的内存占用需要做质数化处理。
  • 绝大多数的 os_event_t 在 buf_block_t 的初始化中产生,该部分的内存占用较大、生命周期较长。
  • 在实际的内存分配中,除了指定的 BP 大小之外,系统还会产生额外的内存,本节只是列举部分。Oracle 的分配内存的方式对用户更加友好,指定固定的内存,具体的分配在内部完成,可以很好控制内存总量。


上篇在这里【走进RDS】之MySQL内存分配与管理(上)

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

作者介绍
目录

相关产品

  • 云数据库 RDS MySQL 版
  • 云数据库 RDS