Innodb記憶體管理解析[轉載]

darren__chan發表於2021-04-16


本文轉自    《》

本文主要介紹innodb的記憶體管理,涉及基礎的記憶體分配結構、演算法以及buffer pool的實現細節,提及change buffer、自適應hash index和log buffer的基本概念和記憶體基本配比,側重點在記憶體的分配和管理方式。本文所述內容基於mysql8.0版本。

基礎記憶體分配

在5.6以前的版本中,innodb內部實現了除buffer pool外的額外記憶體池,那個時期lib庫中的分配器在效能和擴充套件性上表現比較差,缺乏針對多核系統優化的記憶體分配器,像linux下最通用的ptmalloc的前身是Doug Lea Malloc,也是因為不支援多執行緒而被棄用了。所以innodb自己實現了記憶體分配器,使用額外的記憶體池來響應那些原本要發給系統的記憶體請求,使用者可以通過設定引數 innodb_use_sys_malloc 來選擇使用innodb的分配器還是系統分配器,使用 innodb_additional_mem_pool_size引數設定額外記憶體池的大小。隨著多核系統的發展,一些分配器對內部實現進行了優化和擴充套件,已經可以很好的支援多執行緒,相較於innodb特定的記憶體分配器可以提供更好的效能和擴充套件性,所以這個實現在5.6版本已經棄用,5.7版本刪除。本文所討論的內容和涉及到的程式碼基於mysql8.0版本。

innodb內部封裝了基礎分配釋放方式malloc,free,calloc,new,delete等,在開啟pfs模式下,封裝內部加入了記憶體追蹤資訊,使用者可傳入對應的key值來記錄某個event或者模組的記憶體分配資訊,這部分資訊通過pfs內部表來對外展示,以便分析記憶體洩漏、記憶體異常等問題。

innodb內部也提供了對系統基礎分配釋放函式封裝的allocator,用於std::*容器內部的記憶體分配,可以讓這些容器內部的隱式分配走innodb內部封裝的介面,以便於記憶體資訊的追蹤。基礎的malloc/calloc等封裝後也會走allocator的介面。 基礎封裝:

非UNIV_PFS_MEMORY編譯模式#define UT_NEW(expr, key) ::new (std::nothrow) expr#define UT_NEW_NOKEY(expr) ::new (std::nothrow) expr#define UT_DELETE(ptr) ::delete ptr#define UT_DELETE_ARRAY(ptr) ::delete[] ptr#define ut_malloc(n_bytes, key) ::malloc(n_bytes)#define ut_zalloc(n_bytes, key) ::calloc(1, n_bytes)#define ut_malloc_nokey(n_bytes) ::malloc(n_bytes)
...
開啟UNIV_PFS_MEMORY#define UT_NEW(expr, key)                                                \
  ::new (ut_allocator<byte>(key).allocate(sizeof expr, NULL, key, false, \
                                          false)) expr#define ut_malloc(n_bytes, key)                         \
  static_cast<void *>(ut_allocator<byte>(key).allocate( \
      n_bytes, NULL, UT_NEW_THIS_FILE_PSI_KEY, false, false))#define ut_zalloc(n_bytes, key)                         \
  static_cast<void *>(ut_allocator<byte>(key).allocate( \
      n_bytes, NULL, UT_NEW_THIS_FILE_PSI_KEY, true, false))#define ut_malloc_nokey(n_bytes)               \
  static_cast<void *>(                         \
      ut_allocator<byte>(PSI_NOT_INSTRUMENTED) \
          .allocate(n_bytes, NULL, UT_NEW_THIS_FILE_PSI_KEY, false, false))
  ...

可以看到在非UNIV_PFS_MEMORY編譯模式下,直接呼叫系統的分配函式,忽略傳入的key,而UNIV_PFS_MEMORY編譯模式下使用ut_allocator分配,下面有ut_allocator的介紹,比較簡單的封裝。

memory heap

主要管理結構為mem_heap_t,8.0中的實現比較簡單,內部維護block塊的連結串列,包含指向連結串列開始和尾部的指標可以快速找到連結串列頭部和尾部的節點,每個節點都是mem_heap_t的結構。mem_heap在建立的時候會初始一塊記憶體作為第一個block,大小可由使用者指定。mem_heap_alloc響應基本的記憶體分配請求,先嚐試從block中切分出滿足請求大小的記憶體,如果不能滿足則建立一個新的block,新的block size至少為上一個block的兩倍(last block),直到達到規定的上限值,新建立的block總是鏈到連結串列的尾部。mem_heap_t中記錄了block的size和連結串列中block的總size以及分配模式(type)等資訊,基本結構如下圖: 

 innodb在內部定義了三種分配block模式供選擇:

  1. MEM_HEAP_DYNAMIC 使用malloc動態分配,比較通用的分配模式
  2. MEM_HEAP_BUFFER  size滿足一定條件,使用buffer pool中的記憶體塊
  3. MEM_HEAP_BTR_SEARCH  保留額外的記憶體,地址儲存在free_block中 
    一般的使用模式為MEM_HEAP_DYNAMIC,也可以使用b|c的模式,在某些情況下使用free_block中的記憶體。mem heap連結串列中的block在最後統一free,按照分配模式走不同的free路徑。

我理解基本思想也是使用一次性分配大記憶體塊,再從大記憶體塊中切分來響應小記憶體分配請求,以避免多次呼叫malloc/free,減少overhead。實現上比較簡單,內部只是將多個大小可能不一的記憶體塊使用連結串列鏈起來,大多場景只有一個block。沒有記憶體歸還、複用和合並等機制,使用過程中不會將記憶體free,會有一定程度的記憶體浪費,但有效減少了記憶體碎片,比較適用於短週期多次分配小記憶體的場景。

基礎allocator

ut_allocator

innodb內部提供ut_allocator用來為std::* 容器分配記憶體,內部封裝了基礎的記憶體分配釋放函式,以便於記憶體追蹤和統一管理。 ut_allocator提供基本的allocate/deallocate的分配釋放函式,用於分配n個物件所需記憶體,內部使用malloc/free。另外也提供了大記憶體塊的分配,開啟LINUX_LARGE_PAGES時使用HugePage,在伺服器記憶體比較大的情況可以減少頁表條目提高檢索效率,未開啟時使用mmap/munmap記憶體對映的方式。 更多細節見如下程式碼:

/** Allocator class for allocating memory from inside std::* containers. */template <class T>class ut_allocator {	// 分配n個elements所需記憶體大小,內部使用malloc/calloc分配
  // 不開啟PFS_MEMORY分配n_elements * sizeof(T)大小的記憶體
  // 開啟PFS_MEMORY多分配sizeof(ut_new_pfx_t)大小的記憶體用於資訊統計
  pointer allocate(size_type n_elements, const_pointer hint = NULL,
                   PSI_memory_key key = PSI_NOT_INSTRUMENTED,                   bool set_to_zero = false, bool throw_on_error = true);                   
  // 釋放allocated()分配的記憶體,內部使用free釋放                  
  void deallocate(pointer ptr, size_type n_elements = 0)
  
  // 分配一塊大記憶體,如果開啟了LINUX_LARGE_PAGES使用HugePage
  // 否則linux下使用mmap
  pointer allocate_large(size_type n_elements, ut_new_pfx_t *pfx)
  // 對應allocate_large,linux下使用munmap
  void deallocate_large(pointer ptr, const ut_new_pfx_t *pfx)
  
  // 提供顯示構造/析構func
  void construct(pointer p, const T &val) { new (p) T(val); }  void destroy(pointer p) { p->~T(); }
  
  開啟PFS_MEMORY場景下提供更多可用func:  pointer reallocate(void *ptr, size_type n_elements, PSI_memory_key key);  // allocate+construct的封裝, 分配n個單元的記憶體並構造相應物件例項
  pointer new_array(size_type n_elements, PSI_memory_key key);  // 同上,deallocate+destroy的封裝
  void delete_array(T *ptr);
}

mem_heap_allocator

mem_heap_t的封裝,可以作為stl allocator來使用,內部使用mem_heap_t的記憶體管理方式。

/** A C++ wrapper class to the mem_heap_t routines, so that it can be used
as an STL allocator */template <typename T>class mem_heap_allocator {
  mem_heap_allocator(mem_heap_t *heap) : m_heap(heap) {}  pointer allocate(size_type n, const_pointer hint = 0) {    return (reinterpret_cast<pointer>(mem_heap_alloc(m_heap, n * sizeof(T))));
  }  void deallocate(pointer p, size_type n) {}  // 提供顯示構造/析構func
  void construct(pointer p, const T &val) { new (p) T(val); }  void destroy(pointer p) { p->~T(); }  
 private:
  mem_heap_t *m_heap;
}

buddy

innodb支援建立壓縮頁以減少資料佔用的磁碟空間,支援1K, 2K, 4K 、8K和16k大小的page。buddy allocator用於管理壓縮page的記憶體空間,提高記憶體使用率和效能。

buffer pool中:
UT_LIST_BASE_NODE_T(buf_buddy_free_t) zip_free[BUF_BUDDY_SIZES_MAX];/** Struct that is embedded in the free zip blocks */struct buf_buddy_free_t {  union {
    ulint size; /*!< size of the block */
    byte bytes[FIL_PAGE_DATA];
  } stamp;
  buf_page_t bpage; /*!< Embedded bpage descriptor */
  UT_LIST_NODE_T(buf_buddy_free_t) list;
};

夥伴系統也是比較經典的記憶體分配演算法,也是linux核心用於解決外部碎片的一種手段。innodb的實現在演算法上與buddy的基本實現並無什麼區別,所支援最小的記憶體塊為1k(2^10),最大為16k,每種記憶體塊維護一個連結串列,多種記憶體塊連結串列組成了zip_free連結串列。 分配入口在 buf_buddy_alloc_low,先嚐試從zip_free[i]中獲取所需大小的記憶體塊,如果當前連結串列中沒有,則嘗試從更大的記憶體塊連結串列中獲取,獲取成功則進行切分,一部分返回另一塊放入對應free連結串列中,實際上是buf_buddy_alloc_zip的一個遞迴呼叫,只是傳入的i不斷增加。如果一直遞迴到16k的塊都沒法滿足,則從buffer pool中新申請一塊大記憶體塊,並將其按照夥伴關係進行(比如現分配了16,需要2k,先切分8k,8k,再將其中一個8k切分為4k,4k,再將其中4k切分為2k,2k)切分直到滿足分配請求。 釋放入口在 buf_buddy_free_low,為了避免碎片在釋放的時候多做了一些事情。在釋放一個記憶體塊的時候沒有直接放回對應連結串列中,而是先檢視其夥伴是不是free的,如果是則進行合併,再嘗試對合並後的記憶體塊進行合併。如果其夥伴是在USED的狀態,這裡做了一次relocate操作,將其內容拷貝到其它free的block塊上,再進行對它合併。這種做法有效減少了碎片的存在,但拷貝這種操作也降低了效能。

buffer pool

buffer pool是innodb主記憶體中一塊區域,用於快取主表和索引中的資料,讀執行緒可以直接從buffer pool中讀取相應資料從而避免io提升讀取效能,當一個頁面需要修改時,先在buffer pool中進行修改,另有後臺執行緒來負責刷髒頁。一般在專用伺服器中,會將80%的記憶體分配給buffer pool使用。資料庫啟動時就會將記憶體分配給buffer pool,不過記憶體有延遲分配的優化,這部分記憶體在未真正使用前是沒有進行物理對映的,所以只會影響虛存大小。buffer pool的記憶體在執行期間不會收縮還給系統,在資料庫關閉時將這部分記憶體統一釋放。可以設定多個buffer pool例項。

buffer pool中使用chunk記憶體塊來管理記憶體,每個buffer pool例項包含一個或多個chunk,chunks在buffer pool初始化時使用mmap分配,並初始化為多個block。每個block地址相差UNIV_PAGE_SIZE,UNIV_PAGE_SIZE一般是16kb,這塊記憶體包含了page相關控制資訊和真正的資料page兩部分,之後將這些page加入free list中供使用。這裡直接使用了mmap而非malloc,是因為在glibc的ptmalloc記憶體分配器中,大於MMAP_THRESHOLD閥值的記憶體請求也是使用mmap,MMAP_THRESHOLD預設值是128k,buffer pool的配置大小一般會遠大於128k。

資料結構

buffer pool進行記憶體管理的主要資料結構。

buf_pool_t

控制buffer pool的主結構,內部包含多種邏輯連結串列以及相關鎖資訊、統計資訊、hash table、lru和flush演算法相關等資訊。

struct buf_pool_t {	//鎖相關 
  BufListMutex chunks_mutex;    /*!< protects (de)allocation of chunks*/
  BufListMutex LRU_list_mutex;  /*!< LRU list mutex */
  BufListMutex free_list_mutex; /*!< free and withdraw list mutex */
  BufListMutex zip_free_mutex;  /*!< buddy allocator mutex */
  BufListMutex zip_hash_mutex;  /*!< zip_hash mutex */
  ib_mutex_t flush_state_mutex; /*!< Flush state protection mutex */
  BufPoolZipMutex zip_mutex;    /*!< Zip mutex of this buffer */
  // index、各種size、數量統計
  ulint instance_no;            /*!< Array index of this buffer pool instance */
  ulint curr_pool_size;         /*!< Current pool size in bytes */
  ulint LRU_old_ratio;          /*!< Reserve this much of the buffer pool for "old" blocks */#ifdef UNIV_DEBUG
  ulint buddy_n_frames; 
#endif
  ut_allocator<unsigned char> allocator; // 用於分配chunks
  volatile ulint n_chunks;     /*!< number of buffer pool chunks */
  volatile ulint n_chunks_new; /*!< new number of buffer pool chunks */
  buf_chunk_t *chunks;         /*!< buffer pool chunks */
  buf_chunk_t *chunks_old;    
  ulint curr_size;             /*!< current pool size in pages */
  ulint old_size;              /*!< previous pool size in pages */
  
  // hash table, 用於索引相關資料頁
  page_no_t read_ahead_area;   
  hash_table_t *page_hash;    
  hash_table_t *page_hash_old; 
  hash_table_t *zip_hash;     
  
  // 統計資訊相關
  ulint n_pend_reads;         
  ulint n_pend_unzip;         
  time_t last_printout_time;
  buf_buddy_stat_t buddy_stat[BUF_BUDDY_SIZES_MAX + 1];
  buf_pool_stat_t stat;     /*!< current statistics */
  buf_pool_stat_t old_stat; /*!< old statistics */
  
  // flush相關
  BufListMutex flush_list_mutex; 
  FlushHp flush_hp;             
  UT_LIST_BASE_NODE_T(buf_page_t) flush_list;
  ibool init_flush[BUF_FLUSH_N_TYPES];
  ulint n_flush[BUF_FLUSH_N_TYPES];
  os_event_t no_flush[BUF_FLUSH_N_TYPES];
  ib_rbt_t *flush_rbt;   
  ulint freed_page_clock; 
  ibool try_LRU_scan;     
  lsn_t track_page_lsn; /* Pagge Tracking start LSN. */
  lsn_t max_lsn_io;
  UT_LIST_BASE_NODE_T(buf_page_t) free;
  UT_LIST_BASE_NODE_T(buf_page_t) withdraw;
  ulint withdraw_target; 
  
  // lru 相關
  LRUHp lru_hp;
  LRUItr lru_scan_itr;
  LRUItr single_scan_itr;
  UT_LIST_BASE_NODE_T(buf_page_t) LRU;
  buf_page_t *LRU_old; 
  ulint LRU_old_len;   
  UT_LIST_BASE_NODE_T(buf_block_t) unzip_LRU;#if defined UNIV_DEBUG || defined UNIV_BUF_DEBUG
  UT_LIST_BASE_NODE_T(buf_page_t) zip_clean;#endif /* UNIV_DEBUG || UNIV_BUF_DEBUG */
  UT_LIST_BASE_NODE_T(buf_buddy_free_t) zip_free[BUF_BUDDY_SIZES_MAX];
  buf_page_t *watch;
};

buf_page_t

資料頁的控制資訊,包含資料頁的大部分資訊,page_id、size、引用計數(io,buf),access_time(用於lru調整),page_state以及壓縮頁的一些資訊。

class buf_page_t { public:
  page_id_t id;
  page_size_t size;  uint32_t buf_fix_count;
  buf_io_fix io_fix;
  buf_page_state state;
  the flush_type.  @see buf_flush_t */  unsigned flush_type : 2;  unsigned buf_pool_index : 6;
  page_zip_des_t zip; 
#ifndef UNIV_HOTBACKUP
  buf_page_t *hash; 
#endif              /* !UNIV_HOTBACKUP */#ifdef UNIV_DEBUG
  ibool in_page_hash; /*!< TRUE if in buf_pool->page_hash */
  ibool in_zip_hash;  /*!< TRUE if in buf_pool->zip_hash */#endif                /* UNIV_DEBUG */
  UT_LIST_NODE_T(buf_page_t) list;#ifdef UNIV_DEBUG
  ibool in_flush_list; 
  ibool in_free_list;  
#endif                 /* UNIV_DEBUG */
  FlushObserver *flush_observer; /*!< flush observer */
  lsn_t newest_modification;
  lsn_t oldest_modification;
  UT_LIST_NODE_T(buf_page_t) LRU;#ifdef UNIV_DEBUG
  ibool in_LRU_list; 
#endif               /* UNIV_DEBUG */#ifndef UNIV_HOTBACKUP
  unsigned old : 1;               
  unsigned freed_page_clock : 31; 
  unsigned access_time; 
#ifdef UNIV_DEBUG
  ibool file_page_was_freed;#endif /* UNIV_DEBUG */#endif /* !UNIV_HOTBACKUP */};

邏輯連結串列

free list

包含空閒的pages,當需要從buffer pool中分配空閒塊時從free list中摘取,當free list為空時需要從LRU、unzip_LRU或者flush list中進行淘汰或刷髒以填充free list。buffer pool初始化時建立多個chunks,劃分的pages都加入free list中待使用。

LRU list

最重要的連結串列,也是快取佔比最高的連結串列。buffer pool使用lru演算法來管理快取的資料頁,所有從磁碟讀入的資料頁都會先加入到lru list中,也包含壓縮的page。新的page預設加入到連結串列的3/8處(old list),等待下次讀取並滿足一定條件後再從old list加入到young list中,以防止全表掃描汙染lru list。需要淘汰時從連結串列的尾部進行evict。

unzip_LRU list

是LRU list的一個子集,每個節點包含一個壓縮的page和指向對應解壓後的page的指標。

flush list 

已修改過還未寫到磁碟的page list,按修改時間排序,帶有最老的修改的page在連結串列的最尾部。當需要刷髒時,從flush list的尾部開始遍歷。

zip_clean list

包含從磁碟讀入還未解壓的page,page一旦被解壓就從zip_clean list中刪除並加入到unzip_LRU list中。

zip_free

用於buddy allocator的空閒block list,buddy allcator是專門用於壓縮的page(buf_page_t)和壓縮的資料頁的分配器。

主要操作

初始化buffer pool

buffer pool在db啟動時呼叫buffer_pool_create進行初始化,使用mmap建立配置大小的虛擬記憶體,並劃分為多個chunks,其它欄位基本使用calloc分配(memset)。

buf_page_get_gen

從buffer pool中讀取page,比較重要的操作。通過page_id獲取對應的page,如果buffer pool中有這個page則封裝一些資訊後返回,如果沒有則需要從磁碟中讀入。讀取模式分為以下7種:

NORMAL

在page hash table中查詢(hash_lock s模式),如果找到增加bufferfix cnt並釋放hash_lock,如果該page不在buffer pool中則以sync模式從磁碟中讀取,並加入對應的邏輯連結串列中,判讀是否需要線性預讀。如果第一次訪問buffer pool中的該page,設定訪問時間並判斷是否需要線性預讀。判斷是否需要加入到young list中。

SCAN

如果page不在buffer pool中使用非同步讀取磁碟的模式,不做隨機預讀和線性預讀,不設定訪問時間不加入young list,其它與normal一樣。

IF_IN_POOL

只在buffer pool中查詢page,如果沒有找到則返回NOT_FOUND。

PEEK_IF_IN_POOL

這種模式僅僅用來drop 自適應hash index,跟IF_IN_POOL類似,只是不加入young list,不做線性預讀。

NO_LATCH

讀取並設定buffefix,但是不加鎖。

IF_IN_POOL_OR_WATCH

與IF_IN_POOL類似,只在buffer pool中查詢此page,如果沒有則設定watch。

POSSIBLY_FREED

與normal類似,只是允許執行過程中page被釋放。

buf_LRU_get_free_block

從buffer pool的free list中摘取一個空閒頁,如果free list為空,移除lru連結串列尾部的block到free list中。這個函式主要是用於使用者執行緒需要一個空閒block來讀取資料頁。具體操作如下:

  1. 如果free list不為空,從中摘取並返回。否則轉下面的操作。
  2. 如果buffer pool設定了try_LRU_scan,遍歷lru連結串列嘗試從尾部釋放一個空閒塊加入free list中。如果unzip_LRU連結串列不為空,則先嚐試從unzip_LRU連結串列中釋放。如果沒有找到再從lru連結串列中淘汰。
  3. 如果沒有找到則嘗試從lru中flush dirty page並加入到free list中。
  4. 沒有找到,設定scan_all重複上述過程,與第一遍不同的地方在於需要scan整個lru連結串列。
  5. 如果遍歷了整個lru連結串列依然沒有找到可以淘汰的block,則sleep 10s等待page cleaner執行緒做一批淘汰或者刷髒。
  6. 重複上述過程直到找到一個空閒block。超過20遍設定warn資訊。

page cleaner

系統執行緒,負責定期清理空閒頁放入free list中和flush dirty page到磁碟上,dirty page指那些已經被修改但是還未寫到磁碟上的資料頁。使用 innodb_page_cleaners 引數設定page cleaner的執行緒數,預設是4。通過特定引數控制dirty page所佔buffer pool的空間比維持在一定水位下,預設是10%。

flush list

上面章節提到過flush_list是包含dirty page並按修改時間有序的連結串列,在刷髒時選擇從連結串列的尾部進行遍歷淘汰,程式碼主體在 buf_do_flush_list_batch中。這裡不得不提的一個巧妙的操作,叫作Hazard Pointer,buf_pool_t中的flush_hp,將整體遍歷複雜度由最差O(n n)降到了O(n)。之所以複雜度最差會變為O(nn)是由於flush list允許多個執行緒併發刷髒,每次從連結串列尾部進行遍歷,使用非同步io的方式刷盤,在io完成後將page從連結串列中摘除,每次提交非同步io後從連結串列尾部再次掃描,在刷盤速度比較慢的情況下,可能每次都需要跳過之前已經flush過的page,最差會退化為O(n*n)。  flush_hp: 用於刷髒遍歷flush list過程,flush_list_mutex保護下修改,在處理當前page之前,將hazard pointer設定為下一個要遍歷的buf_page_t的指標,為執行緒指定下一個需要處理的page。當前page刷盤時會釋放flush_list_mutex,刷盤完成後重新獲得鎖,處理flush_hp指向的page,無論這中間發生過什麼,連結串列如何變動,flush_hp總是被設定成為下一個有效的buf_page_t指標。所以複雜度總能保證為O(n)。 刷髒的過程中也做了一些優化,程式碼在 buf_flush_page_and_try_neighbors可以將當前page相鄰的dirty page頁也一起刷盤,目的是將多個隨機io轉為順序io減少overhead,這在傳統HHD的裝置上比較有用,在SSD上seek time已經不是顯著的影響因素。可以使用引數 innodb_flush_neighbors進行設定和關閉:

  1. 為0則關閉flush neighbors的優化
  2. 預設值為1,flush與當前page相同extent(1M)上的連續dirty page
  3. 為2則flush與當前page相同extent上的dirty page

    flush LRU list

    buf_flush_LRU_list主要完成兩件事:

  4. 將lru list尾部的可以移除的pages放入到free list中
  5. 將lru list尾部的dirty page刷到磁碟。

同一時刻只會有一個page cleaner執行緒對同一個LRU list操作。lru list遍歷的深度由動態引數 innodb_lru_scan_depth決定,用於優化io敏感的場景,預設值1024,設定比預設值小的值可以適應大多數work load,設定過大會影響效能,尤其是在buffer pool足夠大的情況。操作過程在LRU_list_mutex的保護下,程式碼主體在 buf_do_LRU_batch其中涉及到unzip_LRU list和LRU list中的淘汰,unzip_LRU list不一定每次都會做淘汰操作,衡量記憶體大小和負載情況,只有在size超出buffer pool的1/10以及當前負載為io bound的情況才會做。程式碼主體在 buf_free_from_unzip_LRU_list_batch中, 將uncompressed page移除到free list中,並不會將任何資料刷盤,只是將解壓縮的frames與壓縮page分離。 如果在上述操作後仍無法達到需要釋放的page數量(遍歷深度),則繼續從lru list尾部進行遍歷,操作在 buf_flush_LRU_list_batchlru list的遍歷同樣使用了Hazard Pointer,buf_pool_t中的lru_hp。當前page如果是clear並且沒有被io fixed和buffer fixed,則從lru list中移除並加入free list中,否則如果page是已經修改過的並且滿足flush的條件則對其進行刷髒。

小結

innodb設定了buffer pool的總大小,空閒page不夠用時會將lru連結串列中可替換的頁面移到free list中,根據統計資訊估計負載情況來決定淘汰的策略。所有的block在幾種狀態之間進行轉換,unzip_LRU、flush list設定一定的上限,設定多個影響淘汰和刷髒策略的引數,以達到不同負載不同buffer pool size下的效能和記憶體之間的平衡。

change buffer

change buffer是用於快取不在buffer pool中的二級索引頁改動的資料結構,insert、update或者delete這些DML操作會引起buffer的改變,page被讀入時與change buffer中的修改合併加入buffer pool中。引入buffer pool的目的主要減少隨機io,對於二級索引的更新經常是比較隨機的,當頁面不在buffer pool中時將其對應的修改快取在change buffer中可有效地減少磁碟的隨機訪問。可以通過引數  innodb_change_buffering 設定對應快取的操作:all, none, inserts, deletes, changes(inserts+deletes), purges。change buffer的記憶體也是buffer pool中的一部分,可以通過引數 innodb_change_buffer_max_size來設定記憶體佔比,預設25%,最多50%。 

Adaptive Hash Index

innodb的索引組織結構為btree,當查詢的時候會根據條件一直索引到葉子節點,為了減少尋路的開銷,AHI使用索引鍵的字首建立了一個雜湊索引表,在實現上就是多個個hash_tables(分片)。雜湊索引是為那些頻繁被訪問的索引頁而建立的,可以理解為btree上的索引。看程式碼初始建立的陣列大小為 buf_pool_get_curr_size() /  sizeof( void *) / 64,其實是比較小的一塊記憶體,使用malloc分配。

log buffer

log buffer是日誌未寫到磁碟的快取,大小由引數 innodb_log_buffer_size指定,一般來說這塊記憶體都比較小,預設是16M。在有大事務的場景下,在事務未commited之前可以將redo日誌資料一直快取,避免多次寫磁碟,可以將log buffer調大。

參考資料:
https://dev.mysql.com/doc/refman/5.6/en/innodb-performance-use_sys_malloc.html  https://dev.mysql.com/doc/refman/8.0/en/innodb-in-memory-structures.html


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29863023/viewspace-2768336/,如需轉載,請註明出處,否則將追究法律責任。

相關文章