天天看點

Innodb緩沖池刷髒的多線程實作Innodb緩沖池刷髒的多線程實作

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版本為例,分析具體多線程刷髒的源碼實作。

提煉要點:

  • 5.6版本以前,刷髒任務交由master-Thread處理
  • 5.62,引入Page cleaner thread,負責緩沖池刷髒頁的工作
  • 5.7.4,提升為多個page cleaner線程,進行并行刷髒,這些線程都不與緩沖池綁定。
  • 由一個協調線程與多個工作線程進行處理,工作隊列長度為緩沖池執行個體的個數,使用一個全局slot數組表示。

以下列出部分源碼進行解析:(涉及的資料結構:page_cleaner_t, page_cleaner_slot_t 和 page_cleaner_state_t)

//page_cleaner_t,
/** 這個結構體是由所有的刷髒線程共用的 */
struct page_cleaner_t {
  //修改任何資訊都要先擷取互斥鎖mutex字段
  (暫時猜是由協調線程持有這個鎖,才能進行管理)
    ib_mutex_t      mutex;      /*!< mutex to protect whole of
                        page_cleaner_t struct and
                        page_cleaner_slot_t slots. */
  //is_requested和is_finished event是分别
  //用來喚醒工作線程和最後一個完成刷髒的工作線程通知協調線程這次的刷髒完成;
    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; 
  //用來表示刷髒協調線程是否有髒頁需要寫到磁盤上
    bool            requested; 
  //lsn_limit表示需要重新整理到lsn的位置,頁的最早修改lsn必須小于這個值,它才能被刷出到磁盤上
    lsn_t           lsn_limit; 
  //需要刷髒的執行個體個數
    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 */
  //每一個slot就是一個page_cleaner_slot_t結構; 記錄着刷髒狀态;
    page_cleaner_slot_t*    slots;      /*!< pointer to the slots */
    bool            is_running; /*!< false if attempt
                        to shutdown */
};
           

總結:

  • 這個資料結構是實作多刷髒線程的核心結構,包含了所有刷髒線程所需要的資訊,以及刷髒協調線程和刷髒工作線程之間同步所需要的同步事件。
  • 這個結構體是由所有的刷髒線程共用的,修改任何資訊都要先擷取互斥鎖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_slot_t

/** Page cleaner request state for each buffer pool instance */
//這個結構體代表着slot的狀态,而slot其實就是一個pool,一個pool有多個page;這個狀态在上層進行統籌。
struct page_cleaner_slot_t {
  //state 用來記錄對緩沖池刷髒狀态的記錄,這個slot表示的緩沖池執行個體是否已經發起了刷髒請求(PAGE_CLEANER_STATE_REQUESTED)、是否正在刷髒(PAGE_CLEANER_STATE_FLUSHING)以及這輪的刷髒處理是否已經完成(PAGE_CLEANER_STATE_FINISHED)
    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' */
  //次輪重新整理從LRU list刷出的頁數
    ulint           n_flushed_lru;
                    /*!< number of flushed pages
                    by LRU scan flushing */
  //從flush list刷出的頁數
    ulint           n_flushed_list;
                    /*!< number of flushed pages
                    by flush_list flushing */
  //刷髒是否成功
  //若是次輪要刷髒的資料頁成功的放到IO的隊列上則表示成功了,否則傳回false
    bool            succeeded_list;
                 
    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 */
};
           

state 用來記錄對緩沖池刷髒狀态的記錄,這個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結構裡。

實作刷髒多線程支援的關鍵函數

//刷髒協調線程的入口函數
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 */
  		  //協調線程在這裡調用pc_flush_slot(),與其它工作線程并行刷髒。
            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這三個核心函數的實作

pc_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中的re..的值在這裡進行設定;判斷是否有髒頁需要沖刷
    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事件,通知協調程序所有的工作線程刷髒結束。 已删除流程無關代碼代碼,其核心代碼如下:

//以下這個周遊其實就是在找出需要刷髒的slot的索引;      
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 */
	  //刷LRU隊列,并且記錄LRU沖刷的頁數
        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);
           

總結

  • 刷髒的多線程實作由一個協調線程主導,工作線程刷髒,當然,協調線程也具有刷髒的能力。
  • pc_flush_slot()函數是真正執行刷髒工作的地方。
  • 總而言之,就是維護幾個資料結構,這些資料結構中包含着必要的資訊,而協調線程進行刷髒工作的安排與自處理,工作線程在接收到協調線程的event喚醒之後,就會進行對應的刷髒工作。