1. BufferPool
- What is BufferPool?
MySQL InnoDB Buffer Pool,MySQL InnoDB 緩沖池。里面緩存着大量數據(數據頁),使 CPU 讀取或寫入數據時,不直接和低速的磁盤打交道,直接和緩沖區進行交互,從而解決了因為磁盤性能慢導致的數據庫性能差的問題。
- Why need BufferPool?
buffer pool 最主要的功能便是加速讀
和加速寫
。
加速讀就是當需要訪問一個數據頁的時候,如果這個頁已經在緩存池中,那么就不再需要訪問磁盤,直接從緩沖池中就能獲取這個頁面的內容。
加速寫就是當需要修改一個數據頁的時候,先將這個頁在緩沖池中進行修改,記下相關的 redo log,這個頁的修改就算已經完成了。至於這個被修改的頁什么時候真正刷新到磁盤,這個是 buffer pool 后台刷新線程來完成的。
- How implemented?
所有從磁盤加載進內存的數據頁,都會通過這個buffer pool管理起來。對應在代碼的中的結構體為buf_pool_t。在MySQL中通常會有多個buffer pool instance,這是為了減少多線程並發訪問時,buffer pool鎖等待的開銷
。
BufferPool由buf_pool, buf_chunk, buf_block和buf_page組成,結構如下:
- Code?
struct buf_pool_t {
buf_chunk_t *chunks; /*!< buffer pool chunks */
hash_table_t *page_hash; /*!< hash table of buf_page_t or
buf_block_t file pages,
buf_page_in_file() == TRUE,
indexed by (space_id, offset).
page_hash is protected by an
array of mutexes. */
UT_LIST_BASE_NODE_T(buf_page_t) flush_list;
/*!< base node of the modified block
list */
UT_LIST_BASE_NODE_T(buf_page_t) free;
/*!< base node of the free
block list */
UT_LIST_BASE_NODE_T(buf_page_t) LRU;
/*!< base node of the LRU list */
/* ... */
}
struct buf_chunk_t {
ulint size; /*!< size of frames[] and blocks[] */
unsigned char *mem; /*!< pointer to the memory area which
was allocated for the frames */
buf_block_t *blocks; /*!< array of buffer control blocks */
/* ... */
}
struct buf_block_t {
buf_page_t page; /*!< page information; this must
be the first field, so that
buf_pool->page_hash can point
to buf_page_t or buf_block_t */
byte *frame; /*!< pointer to buffer frame which
is of size UNIV_PAGE_SIZE, and
aligned to an address divisible by
UNIV_PAGE_SIZE */
/* ... */
}
class buf_page_t {
public:
/** @name General fields
None of these bit-fields must be modified without holding
buf_page_get_mutex() [buf_block_t::mutex or
buf_pool->zip_mutex], since they can be stored in the same
machine word. */
/* @{ */
/** Page id. */
page_id_t id;
/** Page size. */
page_size_t size;
/** Count of how manyfold this block is currently bufferfixed. */
uint32_t buf_fix_count;
/** type of pending I/O operation. */
buf_io_fix io_fix;
/** Block state. @see buf_page_in_file */
buf_page_state state;
/* ... */
}
2. 頁面管理機制
InnoDB 基於 LRU 算法管理 buffer pool 中的數據頁。一般情況下 list 頭部存放的是熱數據,就是所謂的 young page(最近經常訪問的數據),list 尾部存放的就是 old page(最近不被訪問的數據)。
LRU List:緩存了所有讀入內存的數據頁。包含三類:
- 未修改的頁面,可以從該鏈表中摘除,然后移到 Free List 中;
- 已修改還未刷新到磁盤的頁面;
- 已修改且已經刷新到磁盤的頁面,可並為第一類。
Free List:空閑內存頁列表,需要裝載(緩存)磁盤上數據頁的時候,從此列表取內存塊。
Flush List:在內存中被修改但還沒有刷新到磁盤的數據頁(臟頁)鏈表,內存中的數據跟對應磁盤上的數據不一致,屬於該鏈表的頁同樣存在於 LRU List 中,但反之未必。
- How read a page?
當訪問的頁面在緩存池中命中,則直接從緩沖池中訪問該頁面。如果沒有命中,則需要將這個 page 從磁盤上加載到緩存池,因此需要在緩存池中的 Free List 中找一個空閑的內存頁來緩存這個從磁盤讀入的 page。
但存在空閑內存頁被使用完的情況,不保證一定有空閑的內存頁。假如 Free List 為空,則需要想辦法產生空閑的內存頁。 首先是在 LRU List 中找可以替換的內存頁,查找方向是從列表的尾部開始找,如果找到可以替換的 page,將其從 LRU List 中摘除,加入 Free List,然后再去 Free List 中找空閑的內存頁。第一次查找最多只掃描 100 個 page,循環進行到第二次時,查找深度就是整個 LRU List。這就是 LRU List 的頁面淘汰機制。
如果在 LRU List 中沒有找到可以替換的頁,則進行單頁刷新,將臟頁刷新到磁盤后,再將釋放的內存頁加入到 Free List,最后再去 Free List 取。為什么只做單頁刷新呢?因為它的目的是獲取空閑內存頁,進行臟頁刷新是不得已而為之,所以只會進行一個頁的刷新,目的是為了盡快的獲取空閑內存頁。
因為 Free List 是一個公共的鏈表,所有的用戶線程都可以使用,存在爭用的情況。因此,自己產生的空閑內存頁有可能會剛好被其它線程所使用,因此用戶線程可能會重復執行上面的查找流程,直到找到空閑的內存頁為止。
通過數據頁訪問機制,可以知道當無空閑頁時產生空閑頁就成為了一個必須要做的事情。
如果需要通過刷新臟頁來產生空閑頁或者需要掃描整個 LRU List 來產生空閑頁,查找空閑頁的時間就會延長,這是一個 bad case。
因此,innodb buffer pool 中存在大量可以替換的頁,或者 Free List 中一直存在着空閑內存頁,對快速獲取空閑內存頁就起到了決定性的作用。
而在 innodb buffer pool 的機制中,是采用何種方式來產生空閑內存頁以及可以替換的內存頁呢?這就是下面要講的內容——臟頁刷新策略。
- How to flush a dirty page?
MySQL線程后台會有flush線程,定期地將flush list的臟頁flush到磁盤上,這樣可以減輕check point的開銷,和頁面替換時,那些被替換頁面的flush開銷,而使得讀取頁面時間增長。flush list的頁面根據修改的時間從新到老進行排序,也即是最新的修改,在flush list的頭部,最老的修改在flush list的尾部。當flush時,從尾部取page flush到磁盤上。這樣的邏輯是跟checkpoint保持一致,checkpoint的流程也是從老到新一步步持久化page,所以可以加快checkpoint。
- When to flush dirty page?
- 后台線程定期刷;
- redo log 寫滿了(強制刷);
- 內存不足(強制刷)。
參考: