閱讀222 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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

  上一篇:go  PgSQL · 源碼分析 · PG 優化器中的pathkey與索引在排序時的使用
  下一篇:go  MySQL · 源碼分析 · mysql認證階段漫遊