MySQL 原始碼分析 Innodb緩衝池刷髒的多執行緒實現
簡介
為了提高效能,大多數的資料庫在操作資料時都不會直接讀寫磁碟,而是中間經過緩衝池,將要寫入磁碟的資料先寫入到緩衝池裡,然後在某個時刻後臺執行緒把修改的資料刷寫到磁碟上。MySQL的InnoDB引擎也使用緩衝池來快取從磁碟讀取或修改的資料頁,如果當前資料庫需要操作的資料集比緩衝池中的空閒頁面大的話,當前緩衝池中的資料頁就必須進行髒頁淘汰,以便騰出足夠的空閒頁面供當前的查詢使用。如果資料庫負載太高,對於空閒頁面的需求超出了page cleaner的淘汰能力,這時候是否能夠快速獲取空閒頁面,會直接影響到資料庫的處理能力。5.6版本以前,髒頁的清理工作交由master執行緒的;Page cleaner thread是5.6.2引入的一個新執行緒,它實現從master執行緒中卸下緩衝池刷髒頁的工作;為了進一步提升擴充套件性和刷髒效率,在5.7.4版本里引入了多個page cleaner執行緒,從而達到並行刷髒的效果。目前Page cleaner並未和緩衝池繫結,有一個協調執行緒 和 多個工作執行緒,協調執行緒本身也是工作執行緒。工作佇列長度為緩衝池例項的個數,使用一個全域性slot陣列表示。 下面以MySQL 5.7的5.7.23版本為例,分析具體多執行緒刷髒的原始碼實現。
核心資料結構
為了支援多執行緒併發刷髒,新實現了以下資料結構:page_cleaner_t, page_cleaner_slot_t 和 page_cleaner_state_t。
page_cleaner_t 結構體
這個資料結構是實現多刷髒執行緒的核心結構。它包含了所有刷髒執行緒所需要的資訊,以及刷髒協調執行緒和刷髒工作執行緒之間同步所需要的同步事件。因為這個結構體是由所有的刷髒執行緒共用的,修改任何資訊都要先獲取互斥鎖mutex欄位;is_requested和is_finished event是分別用來喚醒工作執行緒和最後一個完成刷髒的工作執行緒通知協調執行緒這次的刷髒完成;n_workers表示刷髒工作執行緒的數目;requested用來表示刷髒協調執行緒是否有髒頁需要寫到磁碟上,若是沒有的話,刷髒執行緒只需要對LRU列表中的頁回收到空閒列表中;lsn_limit表示需要重新整理到lsn的位置,頁的最早修改lsn必須小於這個值,它才能被刷出到磁碟上;n_slots表示這些刷髒執行緒需要刷髒的緩衝池例項的個數;另外還有一個比較重要的欄位slots,它用來記錄刷髒執行緒對緩衝池刷髒的當前狀態,每一個slot就是一個page_cleaner_slot_t結構; n_slots_requested/n_slots_flushing/n_slots_finished主要用在刷髒過程中記錄所有刷髒執行緒處在各個階段的執行緒數目,當一開始刷髒時協調執行緒會把n_slots_requested設定成當前slots的總數,也即緩衝池例項的個數,而會把n_slots_flushing和n_slots_finished清0。每當一個刷髒執行緒完成一個緩衝池例項的刷髒n_slots_requested會減1、n_slots_finished會加1。所有的刷髒執行緒完成後,n_slots_requested會為0,n_slots_finished會為slots的總數目。
/** Page cleaner structure common for all threads */ struct page_cleaner_t { ib_mutex_t mutex; /*!< mutex to protect whole of page_cleaner_t struct and page_cleaner_slot_t slots. */ os_event_t is_requested; /*!< event to activate worker os_event_t is_finished; /*!< event to signal that all slots were finished. */ volatile ulint n_workers; /*!< number of worker threads in existence */ bool requested; /*!< true if requested pages to flush */ lsn_t lsn_limit; /*!< upper limit of LSN to be flushed */ ulint n_slots; /*!< total number of slots */ ulint n_slots_requested; /*!< number of slots in the state PAGE_CLEANER_STATE_REQUESTED */ ulint n_slots_flushing; /*!< number of slots in the state PAGE_CLEANER_STATE_FLUSHING */ ulint n_slots_finished; /*!< number of slots in the state PAGE_CLEANER_STATE_FINISHED */ ulint flush_time; /*!< elapsed time to flush requests for all slots */ ulint flush_pass; /*!< count to finish to flush requests for all slots */ page_cleaner_slot_t* slots; /*!< pointer to the slots */ bool is_running; /*!< false if attempt to shutdown */ };
page_cleaner_slot_t資料結構
tate 用來記錄對緩衝池刷髒狀態的記錄,這個slot表示的緩衝池例項是否已經發起了刷髒請求(PAGE_CLEANER_STATE_REQUESTED)、是否正在刷髒(PAGE_CLEANER_STATE_FLUSHING)以及這輪的刷髒處理是否已經完成(PAGE_CLEANER_STATE_FINISHED);n_pages_requested則記錄次輪刷髒要對這個緩衝池例項刷髒的頁數,在發起刷髒前由協調執行緒設定;而其餘的各個欄位都是被刷髒的工作執行緒返回前所設定的。n_flushed_lru和n_flushed_list 分別表示次輪重新整理從LRU list刷出的頁數和從flush list刷出的頁數,也就是分別從函式buf_flush_LRU_list和buf_flush_do_batch返回的處理的頁數;succeeded_list用來表示是否對髒頁list(flush_list)刷髒成功;若是次輪要刷髒的資料頁成功的放到IO的佇列上則表示成功了,否則返回false;flush_lru_time和flush_list_time則分別表示重新整理LRU list和flush list所用的時間;flush_lru_pass和flush_list_pass分別表示嘗試對LRU list和flush list頁進行刷髒的次數。當所有的刷髒執行緒完成後,對於每個slot的這些統計資訊會統一計算到全域性的page_cleaner_t結構裡。
/** Page cleaner request state for each buffer pool instance */
struct page_cleaner_slot_t {
page_cleaner_state_t state; /*!< state of the request.
protected by page_cleaner_t::mutex
if the worker thread got the slot and
set to PAGE_CLEANER_STATE_FLUSHING,
n_flushed_lru and n_flushed_list can be
updated only by the worker thread */
/* This value is set during state==PAGE_CLEANER_STATE_NONE */
ulint n_pages_requested;
/*!< number of requested pages
for the slot */
/* These values are updated during state==PAGE_CLEANER_STATE_FLUSHING,
and commited with state==PAGE_CLEANER_STATE_FINISHED.
The consistency is protected by the 'state' */
ulint n_flushed_lru;
/*!< number of flushed pages
by LRU scan flushing */
ulint n_flushed_list;
/*!< number of flushed pages
by flush_list flushing */
bool succeeded_list;
/*!< true if flush_list flushing
succeeded. */
ulint flush_lru_time;
/*!< elapsed time for LRU flushing */
ulint flush_list_time;
/*!< elapsed time for flush_list
flushing */
ulint flush_lru_pass;
/*!< count to attempt LRU flushing */
ulint flush_list_pass;
/*!< count to attempt flush_list
flushing */
};
實現刷髒多執行緒支援的關鍵函式
刷髒協調執行緒的入口函式buf_flush_page_cleaner_coordinator
buf_flush_page_cleaner_coordinator協調執行緒的主迴圈主執行緒以最多1s的間隔或者收到buf_flush_event事件就會觸發進行一輪的刷髒。協調執行緒首先會呼叫pc_request()函式,這個函式的作用就是為每個slot代表的緩衝池例項計算要刷髒多少頁,然後把每個slot的state設定PAGE_CLEANER_STATE_REQUESTED, 喚醒等待的工作執行緒。由於協調執行緒也會和工作執行緒一樣做具體的刷髒操作,所以它在喚醒工作執行緒之後,會呼叫pc_flush_slot(),和其它的工作執行緒並行去做刷髒頁操作。一但它做完自己的刷髒操作,就會呼叫pc_wait_finished()等待所有的工作執行緒完成刷髒操作。完成這一輪的刷髒之後,協調執行緒會收集一些統計資訊,比如這輪刷髒所用的時間,以及對LRU和flush_list佇列刷髒的頁數等。然後會根據當前的負載計算應該sleep的時間、以及下次刷髒的頁數,為下一輪的刷髒做準備。在主迴圈執行緒跳過與多執行緒刷髒不相關的部分,主迴圈的核心主要就集中在pc_request()、pc_flush_slot()以及pc_wait_finished()三個函式的呼叫上。精簡後的部分程式碼如下:
while (srv_shutdown_state == SRV_SHUTDOWN_NONE) {
......
ulint n_to_flush;
lsn_t lsn_limit = 0;
/* Estimate pages from flush_list to be flushed */
if (ret_sleep == OS_SYNC_TIME_EXCEEDED) {
last_activity = srv_get_activity_count();
n_to_flush =
page_cleaner_flush_pages_recommendation(
&lsn_limit, last_pages);
} else {
n_to_flush = 0;
}
/* Request flushing for threads */
pc_request(n_to_flush, lsn_limit);
/* Coordinator also treats requests */
while (pc_flush_slot() > 0) {
/* No op */
}
......
pc_wait_finished(&n_flushed_lru, &n_flushed_list);
......
}
工作執行緒的入口函式 buf_flush_page_cleaner_worker
buf_flush_page_cleaner_worker工作執行緒的主迴圈啟動後就等在page_cleaner_t的is_requested事件上,一旦協調執行緒通過is_requested喚醒所有等待的工作執行緒,工作執行緒就呼叫pc_flush_slot()函式去完成刷髒動作。
pc_request、pc_flush_slot以及pc_wait_finished這三個核心函式的實現
request這個函式的作用主要就是為每個slot代表的緩衝池例項計算要刷髒多少頁;然後把每個slot的state設定PAGE_CLEANER_STATE_REQUESTED;把n_slots_requested設定成當前slots的總數,也即緩衝池例項的個數,同時把n_slots_flushing和n_slots_finished清0,然後喚醒等待的工作執行緒。這個函式只會在協調執行緒裡呼叫,其核心程式碼如下:
mutex_enter(&page_cleaner->mutex); //由於page_cleaner是全域性的,在修改之前先獲取互斥鎖
page_cleaner->requested = (min_n > 0); //是否需要對flush_list進行刷髒操作,還是隻需要對LRU列表刷髒
page_cleaner->lsn_limit = lsn_limit; // 設定lsn_limit, 只有資料頁的oldest_modification小於它的才會刷出去
for (ulint i = 0; i < page_cleaner->n_slots; i++) {
page_cleaner_slot_t* slot = &page_cleaner->slots[i];
//為兩種特殊情況設定每個slot需要刷髒的頁數,當為ULINT_MAX表示伺服器比較空閒,則刷髒執行緒可以儘可能的把當前的所有髒頁都刷出去;而當為0是,表示沒有髒頁可刷。
if (min_n == ULINT_MAX) {
slot->n_pages_requested = ULINT_MAX;
} else if (min_n == 0) {
slot->n_pages_requested = 0;
}
slot->state = PAGE_CLEANER_STATE_REQUESTED; //在喚醒刷髒工作執行緒之前,將每個slot的狀態設定成requested狀態
}
// 協調執行緒在喚醒工作執行緒之前,設定請求要刷髒的slot個數,以及清空正在刷髒和完成刷髒的slot個數。只有當完成的刷髒個數等於總的slot個數時,才表示次輪的刷髒結束。
page_cleaner->n_slots_requested = page_cleaner->n_slots;
page_cleaner->n_slots_flushing = 0;
page_cleaner->n_slots_finished = 0;
os_event_set(page_cleaner->is_requested);
mutex_exit(&page_cleaner->mutex);
pc_flush_slot是刷髒執行緒真正做刷髒動作的函式,協調執行緒和工作執行緒都會呼叫。由於刷髒執行緒和slot並不是事先繫結對應的關係。所以工作執行緒在刷髒時首先會找到一個未被佔用的slot,修改其狀態,表示已被排程,然後對該slot所對應的緩衝池instance進行操作。直到所有的slot都被消費完後,才進入下一輪。通過這種方式,多個刷髒執行緒實現了併發刷髒緩衝池。一旦找到一個未被佔用的slot,則需要把全域性的page_cleaner裡的n_slots_rqeusted減1、把n_slots_flushing加1,同時這個slot的狀態從PAGE_CLEANER_STATE_REQUESTED狀態改成PAGE_CLEANER_STATE_FLUSHING。然後分別呼叫buf_flush_LRU_list() 和buf_flush_do_batch() 對LRU和flush_list刷髒。刷髒結束把n_slots_flushing減1,把n_slots_finished加1,同時把這個slot的狀態從PAGE_CLEANER_STATE_FLUSHING狀態改成PAGE_CLEANER_STATE_FINISHED狀態。同時若這個工作執行緒是最後一個完成的,則需要通過is_finished事件,通知協調程序所有的工作執行緒刷髒結束。 已刪除流程無關程式碼程式碼,其核心程式碼如下:
for (i = 0; i < page_cleaner->n_slots; i++) { //由於slot和刷髒執行緒不是事先定好的一一對應關係,所以在每個工作執行緒開始要 先找到一個未被處理的slot
slot = &page_cleaner->slots[i];
if (slot->state == PAGE_CLEANER_STATE_REQUESTED) {
break;
}
}
buf_pool_t* buf_pool = buf_pool_from_array(i); // 根據找到的slot,對應其緩衝池的例項
page_cleaner->n_slots_requested--; // 表明這個slot開始被處理,將未被處理的slot數減1
page_cleaner->n_slots_flushing++; //這個slot開始刷髒,將flushing加1
slot->state = PAGE_CLEANER_STATE_FLUSHING; // 把這個slot的狀態設定為flushing狀態
if (page_cleaner->n_slots_requested == 0) { //若是所有的slot都處理了,則清楚is_requested的通知標誌
os_event_reset(page_cleaner->is_requested);
}
/* Flush pages from end of LRU if required */
slot->n_flushed_lru = buf_flush_LRU_list(buf_pool); // 開始刷LRU佇列
/* Flush pages from flush_list if required */
if (page_cleaner->requested) { // 刷flush_list佇列
slot->succeeded_list = buf_flush_do_batch(
buf_pool, BUF_FLUSH_LIST,
slot->n_pages_requested,
page_cleaner->lsn_limit,
&slot->n_flushed_list);
} else {
slot->n_flushed_list = 0;
slot->succeeded_list = true;
}
page_cleaner->n_slots_flushing--; // 刷髒工作執行緒完成次輪刷髒後,將flushing減1
page_cleaner->n_slots_finished++; //刷髒工作執行緒完成次輪刷髒後,將完成的slot加一
slot->state = PAGE_CLEANER_STATE_FINISHED; // 設定此slot的狀態為FINISHED
if (page_cleaner->n_slots_requested == 0
&& page_cleaner->n_slots_flushing == 0) {
os_event_set(page_cleaner->is_finished); // 當所有的工作執行緒都完成了刷髒,要通知協調程序,本輪刷髒完成
}
pc_wait_finished函式的主要由協調執行緒呼叫,它主要用來收集每個工作執行緒分別對LRU和flush_list列表刷髒的頁數。以及為每個slot清0次輪請求刷髒的頁數和重置它的狀態為NONE。
os_event_wait(page_cleaner->is_finished); // 協調執行緒通知工作執行緒和完成自己的刷髒任務之後,要等在is_finished事件上,知道最後一個完成的工作執行緒會set這個事件喚醒協調執行緒
mutex_enter(&page_cleaner->mutex);
for (ulint i = 0; i < page_cleaner->n_slots; i++) {
page_cleaner_slot_t* slot = &page_cleaner->slots[i];
ut_ad(slot->state == PAGE_CLEANER_STATE_FINISHED);
// 統計每個slot分別通過LRU和flush_list佇列刷出去的頁數
*n_flushed_lru += slot->n_flushed_lru;
*n_flushed_list += slot->n_flushed_list;
all_succeeded &= slot->succeeded_list;
// 把所有slot的狀態設定為NONE
slot->state = PAGE_CLEANER_STATE_NONE;
//為每個slot清除請求刷髒的頁數
slot->n_pages_requested = 0;
}
// 清零完成的slot刷髒個數,為下一輪刷髒重新統計做準備
page_cleaner->n_slots_finished = 0;
// 清除is_finished事件的通知標誌
os_event_reset(page_cleaner->is_finished);
mutex_exit(&page_cleaner->mutex);
總結
在MySQL 5.7中,Innodb通過定義page_cleaner_t, page_cleaner_slot_t 和 page_cleaner_state_t等資料結構,以及pc_request、pc_flush_slot和pc_wait_finished等函式實現了多執行緒的刷髒,提高了刷髒的效率,儘可能的避免使用者執行緒參與刷髒