資料庫是使用記憶體的“大戶”,合理的記憶體分配機制就尤為重要,上一期月報介紹了 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_FAE /* Fatal if any error */
MY_WME /* Write message on error */
MY_ZEROFILL /* Fill array with zero */複製程式碼
MY_FAE 表示記憶體分配失敗就退出整個程式,MY_WME 表示記憶體分配失敗是否需要記錄到日誌中,MY_ZEROFILL 表示分配記憶體後初始化為0。
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;複製程式碼
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;
}
}複製程式碼
找到合適的 Block 之後定位到可用空間的位置就行了,返回之前最後需要判斷 Block 分配之後是否需要移動到 used 連結串列。
- 從空間利用率上來講,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 的統一性和整體性更好。