MySQL · 源碼分析 · 內存分配機製
前言
內存資源由操作係統管理,分配與回收操作可能會執行係統調用(以 malloc 算法為例,較大的內存空間分配接口是 mmap, 而較小的空間 free 之後並不歸還給操作係統 ),頻繁的係統調用必然會降低係統性能,但是可以最大限度的把使用完畢的內存讓給其它進程使用,相反長時間占有內存資源可以減少係統調用次數,但是內存資源不足會導致操作係統頻繁換頁,降低服務器的整體性能。
數據庫是使用內存的“大戶”,合理的內存分配機製就尤為重要,上一期月報介紹了 PostgreSQL 的內存上下文,本文將介紹在 MySQL 中又是怎麼管理內存的。
基礎接口封裝
MySQL 在基本的內存操作接口上麵封裝了一層,增加了控製參數 my_flags
void *my_malloc(size_t size, myf my_flags)
void *my_realloc(void *oldpoint, size_t size, myf my_flags)
void my_free(void *ptr)
my_flags 的值目前有:
MY_FAE /* Fatal if any error */
MY_WME /* Write message on error */
MY_ZEROFILL /* Fill array with zero */
MY_FAE 表示內存分配失敗就退出整個進程,MY_WME 表示內存分配失敗是否需要記錄到日誌中,MY_ZEROFILL 表示分配內存後初始化為0。
MEM_ROOT
基本結構
在 MySQL 的 Server 層中廣泛使用 MEM_ROOT 結構來管理內存,避免頻繁調用封裝的基礎接口,也可以統一分配和管理,防止發生內存泄漏。不同的 MEM_ROOT 之間互相沒有影響,不像 PG 中不同的內存上下文之間還有關聯。這可能得益於 MySQL Server 層是麵向對象的代碼,MEM_ROOT 作為類中的一個成員變量,伴隨著對象的整個生命周期。比較典型的類有: THD,String, TABLE, TABLE_SHARE, Query_arena, st_transactions 等。
MEM_ROOT 分配內存的單元是 Block,使用 USED_MEM 結構體來描述。結構比較簡單,Block 之間相互連接形成內存塊鏈表,left 和 size 表示對應 Block 還有多少可分配的空間和總的空間大小。
typedef struct st_used_mem
{ /* struct for once_alloc (block) */
struct st_used_mem *next; /* Next block in use */
unsigned int left; /* memory left in block */
unsigned int size; /* size of block */
} USED_MEM;
而 MEM_ROOT 結構體負責管理 Block 鏈表 :
typedef struct st_mem_root
{
USED_MEM *free; /* blocks with free memory in it */
USED_MEM *used; /* blocks almost without free memory */
USED_MEM *pre_alloc; /* preallocated block */
/* if block have less memory it will be put in 'used' list */
size_t min_malloc;
size_t block_size; /* initial block size */
unsigned int block_num; /* allocated blocks counter */
/*
first free block in queue test counter (if it exceed
MAX_BLOCK_USAGE_BEFORE_DROP block will be dropped in 'used' list)
*/
unsigned int first_block_usage;
void (*error_handler)(void);
} 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 大小策略有關。
void init_alloc_root(MEM_ROOT *mem_root, size_t block_size,
size_t pre_alloc_size __attribute__((unused)))
{
mem_root->free= mem_root->used= mem_root->pre_alloc= 0;
mem_root->min_malloc= 32;
mem_root->block_size= block_size - ALLOC_ROOT_MIN_BLOCK_SIZE;
mem_root->error_handler= 0;
mem_root->block_num= 4; /* We shift this with >>2 */
mem_root->first_block_usage= 0;
if (pre_alloc_size)
{
if ((mem_root->free= mem_root->pre_alloc=
(USED_MEM*) my_malloc(pre_alloc_size+ ALIGN_SIZE(sizeof(USED_MEM)),
MYF(0))))
{
mem_root->free->size= pre_alloc_size+ALIGN_SIZE(sizeof(USED_MEM));
mem_root->free->left= pre_alloc_size;
mem_root->free->next= 0;
rds_update_query_size(mem_root, mem_root->free->size, 0);
}
}
DBUG_VOID_RETURN;
}
初始化完成就可以調用 alloc_root 進行內存申請,整個分配流程並不複雜,代碼也不算長,為了方便閱讀貼出來,也可以略過直接看分析。
void *alloc_root( MEM_ROOT *mem_root, size_t length )
{
size_t get_size, block_size;
uchar * point;
reg1 USED_MEM *next = 0;
reg2 USED_MEM **prev;
length = ALIGN_SIZE( length );
if ( (*(prev = &mem_root->free) ) != NULL ) // 判斷 free 鏈表是否為空
{
if ( (*prev)->left < length &&
mem_root->first_block_usage++ >= ALLOC_MAX_BLOCK_USAGE_BEFORE_DROP &&
(*prev)->left < ALLOC_MAX_BLOCK_TO_DROP ) // 優化策略
{
next = *prev;
*prev = next->next; /* Remove block from list */
next->next = mem_root->used;
mem_root->used = next;
mem_root->first_block_usage = 0;
}
// 找到一個空閑空間大於申請內存空間的 Block
for ( next = *prev; next && next->left < length; next = next->next )
prev = &next->next;
}
if ( !next ) // free 鏈表為空,或者沒有滿足可分配條件 Block
{ /* Time to alloc new block */
block_size = mem_root->block_size * (mem_root->block_num >> 2);
get_size = length + ALIGN_SIZE( sizeof(USED_MEM) );
get_size = MY_MAX( get_size, block_size );
if ( !(next = (USED_MEM *) my_malloc( get_size, MYF( MY_WME | ME_FATALERROR ) ) ) )
{
if ( mem_root->error_handler )
(*mem_root->error_handler)();
DBUG_RETURN( (void *) 0 ); /* purecov: inspected */
}
mem_root->block_num++;
next->next = *prev;
next->size = get_size;
next->left = get_size - ALIGN_SIZE( sizeof(USED_MEM) );
*prev = next; // 新申請的 Block 放到 free 鏈表尾部
}
point = (uchar *) ( (char *) next + (next->size - next->left) );
if ( (next->left -= length) < mem_root->min_malloc ) // 分配完畢後,Block 是否還能在 free 鏈表中繼續分配
{ /* Full block */
*prev = next->next; /* Remove block from list */
next->next = mem_root->used;
mem_root->used = next;
mem_root->first_block_usage = 0;
}
}
首先判斷 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 分配的內存空間,是統一歸還的。mark_blocks_free
不真正的歸還 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 的統一性和整體性更好。
最後更新:2017-08-21 09:03:03