為了說明清楚這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.