暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PolarDB · 源码分析 · Innodb缓冲池刷脏的多线程实现(二)

Z 2023-07-25
259

工作线程的入口函数 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等函数实现了多线程的刷脏,提高了刷脏的效率,尽可能的避免用户线程参与刷脏。

参考

MySQL · 引擎特性 · InnoDB Buffer Pool

MySQL · 性能优化· 5.7.6 InnoDB page flush 优化

MySQL · 源码分析 · InnoDB LRU List刷脏改进之路

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论