为了说明清楚这3个参数的作用,我们有必要先从temptable引擎的内存分配器说起,这样能够更自然的带出这3个参数的确切含义,同时能够理解其内存分配的基本方式。
一、综述
在temptable引擎中包含了一个内存分配器Allocator,这个分配器主要目的是为了高效的管理内存,并且为temptable中的各个容器提供所需的内存,譬如临时表的索引插入数据就需要分配内存,每个线程都会初始化一个内存分配器。
总的来看这个内存分配器主要是以Block为单位进行分配的,并且每次需要分配的内存以Chunk为单位存放在Block中,这就减少了内存分配的次数,并且Block的分配是以1M,2M,4M,最大512M 成指数上升的,也就意味着如果需要大量的内存,分配的方式也会改变,因此比较灵活。而我们常见的 temptable_max_mmap/temptable_max_ram/tmp_table_size 参数就是在这一层生效的。
二、Block和Chunk
容器在分配内存的时候都是以Chunk为单位的,但是实际物理内存的分配是以Block为单位的,也就是说内存实际上实在Block中分配,如果不够才会到OS层面去获取,而一个Block到底多大下面在分析。每个Block有自己的metadata(在Block header中)如下,主要包含4个元素:
- 物理内存类型 8字节,类型为分配的来自MMAP还是RAM
- 当前block大小 8字节
- chunk数量 8字节
- first_pristine_offset 8字节,当前Block使用的总量包含了元数据,用m_offset(block 起点offset)+first_pristine_offset(Block使用总量) 就可以得到Block中下一个可用的位置(也叫做slot,但和上面的含义不同)。
而每个Chunk也有自己的metadata,主要存放的是本次分配chunk前first_pristine_offset的位置。我们大概将一个Block的表示如下,假定这个Block已经分配了2个Chunk,
block(block header)
block metadata chunk1 offset chunk2 offset
|---- ---- ---- ---- | chunk1 header---data- |chunk2 header---data-|--------------------------------------------------------------------
| | |\
| | |
next_available_slot
Block offset pristine_offset = offset + pristine_offset
metadata 4*8 定位新的内存点
+chunk 1 size
+chunk 2 size
保存的是偏移量
chunk1 header(8字节) = metadata 4*8 大小
chunk2 header(8字节) = metadata 4*8+chunk 1 size
并且通过Chunk的起点地址很容易的就能反推到Block起点地址,只需要使用Chunk内存的起点位置减去其元数据中存储的pristine_offset就能够快速反推Block的起点位置了如下,
inline uint8_t *Chunk::block() const { return m_offset - offset(); }
三、关于物理内存分配的策略
只要需要向OS申请内存,总是一次申请一个Block,temptable的内存分配主要包含2种策略,在8036中用到的是
- Exponential_policy:size策略,主要是按照指数的方式增长内存,避免过多的物理内存分配影响性能,比如前面说的每个Block 1M,2M,4M 最大512M就是这个size 策略进行判断的。
- Prefer_RAM_over_MMAP_policy_obeying_per_table_limit:source策略,首先会判断参数tmp_table_size是否超过,超过则直接报Result::RECORD_FILE_FULL,然后根据参数的设置temptable_max_mmap/temptable_max_ram,先考虑使用ram分配,不够在进行mmap分配。如果都满了则报Result::RECORD_FILE_FULL, 报错后转为Innodb物理临时表。
如果涉及到物理内存分配和释放的时候总是是调用两个函数如下
- static temptable::allocate_from :从MMAP或者RAM中分配内存,并且返回实际的地址,返回的地址会存储在Block的m_offset中。
- static temptable::deallocate_from:释放内存,根据Block的m_offset就可以释放这一片内存。
这里面包含了实际的分配方法,可以自行参考,需要注意的是MMAP分配内存的时候可能会出现一些包含以开头mysql_temptable的临时文件如下,
inline void *Memory<Source::MMAP_FILE>::fetch(size_t bytes) {
File f = create_temp_file(file_path, mysql_tmpdir, "mysql_temptable.", mode,
UNLINK_FILE, MYF(MY_WME));
下面我们来看看分配的过程。
四、从全局block_pool中获取一个slot
这个block_pool中主要包含了各个线程的第一个temptable block所在的位置,本质是一个容器数组其中每条数据只包含一个block属性,并且为全局共享,lock free的静态变量如下
- static Lock_free_shared_block_pool<SHARED_BLOCK_POOL_SIZE> shared_block_pool;
其中每一个元素位置叫做slot,当线程需要建立temptable 临时表的时候都会通过类方法去获取一个有效的slot,如下,
temptable::Handler::Handler
m_shared_block(shared_block_pool.try_acquire(thd_thread_id(ha_thd()))),
调用方法
*try_acquire(size_t thd_id)
在线程退出的时候释放最后一个Block(也就是建立的第一个Block),但是其他Block会在临时表使用中或者使用后释放,如下
try_release(size_t thd_id)
获取到虽然获取了Block,但是并没有实际的分配内存。
五、Allocator的初始化和内存分配
当某个线程建立临时表的时候,就需要对Allocator进行初始化,分配器中包含3个重要的元素,
- m_shared_block:这个就来自前面说的block_pool中获取的某个slot对应的block,这代表的是某个线程第一个block
- m_state:这里面包含了一个当前分配的block的一个计数器和当前指向的block(current_block)
- m_table_resource_monitor:当前表的内存统计,这是为了实现参数tmp_table_size所做的。
Allocator实现了2个必须要实现的功能就是内存分配和释放,分别叫做,当分配内存的时候需要调用,
- temptable::Allocator::allocate:从Block中分配Chunk需要的内存,首先需要查看的m_shared_block是否为空,如果为空则需要分配第一个Block,这个肯定是从OS内存中获取一个1M的空间,如果不是第一分配,可能在第一个Block中存在剩余的空间则不需要再次从OS中获取内存直接分配即可,如果第一个Block分配完了就需要新分配一个Block,并且从OS中获取2M的空间了,并且由current_block指向,接下来可能Chunk需要的内存就在current_block分配了,如果current_block也满了就再分配4M的Block,并且由current_block指向,依次类推。
- temptable::Allocator::deallocate:从Block中删除Chunk的内存,这部分基本和上面是相反的,先从Block中删除这个Chunk,然后判断Block Chunk的数量,如果为0了则这个Block整体从OS中释放。但是需要注意的是第一个Block的内存不会释放会持续到线程退出如下,
if (m_shared_block && (block == *m_shared_block)) { //如果m_shared_block存在 则不做任何事情,保留最后一个block
// Do nothing. Keep the last block alive.