为了支持多线程并发刷脏,新实现了以下数据结构:page_cleaner_t, page_cleaner_slot_t 和 page_cleaner_state_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的总数目。

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结构里。

  1. struct page_cleaner_slot_t {
  2. page_cleaner_state_t state; /*!< state of the request.
  3. protected by page_cleaner_t::mutex
  4. if the worker thread got the slot and
  5. set to PAGE_CLEANER_STATE_FLUSHING,
  6. n_flushed_lru and n_flushed_list can be
  7. updated only by the worker thread */
  8. /* This value is set during state==PAGE_CLEANER_STATE_NONE */
  9. ulint n_pages_requested;
  10. /*!< number of requested pages
  11. for the slot */
  12. /* These values are updated during state==PAGE_CLEANER_STATE_FLUSHING,
  13. and commited with state==PAGE_CLEANER_STATE_FINISHED.
  14. The consistency is protected by the 'state' */
  15. ulint n_flushed_lru;
  16. /*!< number of flushed pages
  17. by LRU scan flushing */
  18. ulint n_flushed_list;
  19. /*!< number of flushed pages
  20. by flush_list flushing */
  21. bool succeeded_list;
  22. /*!< true if flush_list flushing
  23. ulint flush_lru_time;
  24. /*!< elapsed time for LRU flushing */
  25. ulint flush_list_time;
  26. /*!< elapsed time for flush_list
  27. flushing */
  28. /*!< count to attempt LRU flushing */
  29. ulint flush_list_pass;
  30. /*!< count to attempt flush_list
  31. flushing */
  32. };

buf_flush_page_cleaner_worker工作线程的主循环启动后就等在page_cleaner_t的is_requested事件上,一旦协调线程通过is_requested唤醒所有等待的工作线程,工作线程就调用pc_flush_slot()函数去完成刷脏动作。

request这个函数的作用主要就是为每个slot代表的缓冲池实例计算要刷脏多少页;然后把每个slot的state设置PAGE_CLEANER_STATE_REQUESTED;把n_slots_requested设置成当前slots的总数,也即缓冲池实例的个数,同时把n_slots_flushing和n_slots_finished清0,然后唤醒等待的工作线程。这个函数只会在协调线程里调用,其核心代码如下:

  1. mutex_enter(&page_cleaner->mutex); //由于page_cleaner是全局的,在修改之前先获取互斥锁
  2. page_cleaner->requested = (min_n > 0); //是否需要对flush_list进行刷脏操作,还是只需要对LRU列表刷脏
  3. page_cleaner->lsn_limit = lsn_limit; // 设置lsn_limit, 只有数据页的oldest_modification小于它的才会刷出去
  4. for (ulint i = 0; i < page_cleaner->n_slots; i++) {
  5. page_cleaner_slot_t* slot = &page_cleaner->slots[i];
  6. //为两种特殊情况设置每个slot需要刷脏的页数,当为ULINT_MAX表示服务器比较空闲,则刷脏线程可以尽可能的把当前的所有脏页都刷出去;而当为0是,表示没有脏页可刷。
  7. if (min_n == ULINT_MAX) {
  8. slot->n_pages_requested = ULINT_MAX;
  9. } else if (min_n == 0) {
  10. slot->n_pages_requested = 0;
  11. }
  12. slot->state = PAGE_CLEANER_STATE_REQUESTED; //在唤醒刷脏工作线程之前,将每个slot的状态设置成requested状态
  13. }
  14. // 协调线程在唤醒工作线程之前,设置请求要刷脏的slot个数,以及清空正在刷脏和完成刷脏的slot个数。只有当完成的刷脏个数等于总的slot个数时,才表示次轮的刷脏结束。
  15. page_cleaner->n_slots_requested = page_cleaner->n_slots;
  16. page_cleaner->n_slots_flushing = 0;
  17. 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事件,通知协调进程所有的工作线程刷脏结束。 已删除流程无关代码代码,其核心代码如下:

  1. os_event_wait(page_cleaner->is_finished); // 协调线程通知工作线程和完成自己的刷脏任务之后,要等在is_finished事件上,知道最后一个完成的工作线程会set这个事件唤醒协调线程
  2. mutex_enter(&page_cleaner->mutex);
  3. for (ulint i = 0; i < page_cleaner->n_slots; i++) {
  4. page_cleaner_slot_t* slot = &page_cleaner->slots[i];
  5. ut_ad(slot->state == PAGE_CLEANER_STATE_FINISHED);
  6. // 统计每个slot分别通过LRU和flush_list队列刷出去的页数
  7. *n_flushed_lru += slot->n_flushed_lru;
  8. *n_flushed_list += slot->n_flushed_list;
  9. all_succeeded &= slot->succeeded_list;
  10. // 把所有slot的状态设置为NONE
  11. slot->state = PAGE_CLEANER_STATE_NONE;
  12. //为每个slot清除请求刷脏的页数
  13. slot->n_pages_requested = 0;
  14. }
  15. // 清零完成的slot刷脏个数,为下一轮刷脏重新统计做准备
  16. page_cleaner->n_slots_finished = 0;
  17. // 清除is_finished事件的通知标志
  18. os_event_reset(page_cleaner->is_finished);
  19. 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等函数实现了多线程的刷脏,提高了刷脏的效率,尽可能的避免用户线程参与刷脏。

MySQL · 引擎特性 · InnoDB Buffer Pool