MySQL • 原始碼分析 • 記憶體分配機制

姬子玉發表於2017-12-13

前言 記憶體資源由作業系統管理,分配與回收操作可能會執行系統呼叫(以 malloc 演算法為例,較大的記憶體空間分配介面是 mmap, 而較小的空間 free 之後並不歸還給作業系統 ),頻繁的系統呼叫必然會降低系統效能,但是可以最大限度的把使用完畢的記憶體讓給其它程式使用,相反長時間佔有記憶體資源可以減少系統呼叫次數,但是記憶體資源不足會導致作業系統頻繁換頁,降低伺服器的整體效能。

前言

記憶體資源由作業系統管理,分配與回收操作可能會執行系統呼叫(以 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 是否歸還。

總結

原文連結


相關文章