天天看点

[MySQL学习] Innodb change buffer(2) 相关函数及流程A.相关结构体B.何时决定使用ibuf:C.何时进行ibuf 合并

简单的代码跟踪,顺便弄清了之前一直困惑的bp->watch的用途。。。。

////////////////////////////////

在介绍ibuf在innodb中的使用前,我们先介绍下相关的结构体及全局变量。

我们知道通过ibuf可以缓冲多种操作类型,每种操作类型,在内部都有一个宏与之对应:

ibuf_op_insert

ibuf_op_delete_mark

ibuf_op_delete

至于对update操作的缓冲,由于二级索引记录的更新是先delete-mark,再insert,因此其ibuf实际有两条记录ibuf_op_delete_mark+ibuf_op_insert

ibuf是全局对象,用于控制change buffer的控制对象,从ibuf_struct结构体来看,其中存储了ibuf索引树信息和其他一些统计信息。

ibuf_flush_count计数器,计算调用ibuf_should_try的次数

ibuf_use用于内部标示当前使用的change buffer 类型

由于从5.5开始扩展了change buffer缓冲的操作类型,因此在ibuf记录的格式也需要做变化,需要记录在同一个page上的操作计数器并标示操作类型

ibuf entry的格式在ibuf0ibuf.c文件头部的注释中有详细描述:

4字节

space id

1字节,marker (0)

区分老版本

page no

类型信息,包括:

5.5特有的counter(2字节)

、操作类型(1字节)

、flags1(1字节,

值为ibuf_rec_compact.

剩下的是实际数据

当我们更新一条数据的时候,首先是更新聚集索引记录,然后再更新二级索引,当通过聚集索引记录寻找搜索二级索引btree时,会做判断是否可以进行ibuf,判断函数为ibuf_should_try

row_update_for_mysql->row_upd_step->row_upd->row_upd_sec_step->row_upd_sec_index_entry->row_search_index_entry->btr_pcur_open_func->btr_cur_search_to_nth_level->ibuf_should_try

而对于二级索引purge操作的缓冲,则调用如下backtrace:

row_purge->row_purge_del_mark->row_purge_remove_sec_if_poss->row_purge_remove_sec_if_poss_leaf->row_search_index_entry->btr_pcur_open_func->btr_cur_search_to_nth_level->ibuf_should_try

可以看到最终的backtrace都汇总到row_search_index_entry->btr_pcur_open_func->btr_cur_search_to_nth_level->ibuf_should_try

因此以下我们也不区分对待这两种backtrace类型

 ibuf_should_try作为基础判断是否使用ibuf,其判断逻辑为:

1.打开了change buffer(即ibuf_use != ibuf_use_none)

2.不是聚集索引,聚集索引不可以做ibuf

3.对于唯一索引,不缓存插入操作(btr_insert_op)

当判断可以缓存时,对ibuf_flush_count++,每四次(ibuf_flush_count % 4 == 0),调用一次buf_lru_try_free_flushed_blocks,尝试去把buffer pool中lru链表上干净的block(已经和磁盘同步)转移到free list上。这样做的目的是尽量把干净的block放到free list,防止在希望使用ibuf时,依然能读到该二级索引页并进行修改,这就达不到使用ibuf的目的。脏页的增加会加重io线程的负担。

但从5.6的代码来看,这一步是被移除掉的。这么做有什么优化么?值得测试。

以上步骤都是在从btree上检索到叶子节点时,才会去做判断,因为根节点和非叶子节点不可以做ibuf。

当判断可以使用ibuf时,根据btr_op判断使用什么样的buf_mode,然后作为参数传递给buf_page_get_gen,这样就可以在从buffer pool中读取page时,决定是否从磁盘读取文件页。

            buf_mode = btr_op == btr_delete_op

                ? buf_get_if_in_pool_or_watch

                : buf_get_if_in_pool;

如果是purge操作(btr_delete_op),buf_mode为buf_get_if_in_pool_or_watch,其他类型的可ibuf的操作为buf_get_if_in_pool

对于不可ibuf的操作,buf_mode值为buf_get

这几个宏变量分别代表如下意义:

buf_get

总是要获取到文件page,如果bp没有,则从磁盘读进来

buf_get_if_in_pool

只从bp读取文件page

buf_peek_if_in_pool

只从bp读取文件page,并且不在lru链表中置其为young

buf_get_no_latch

和buf_get类似,但不在page上加latch

buf_get_if_in_pool_or_watch

只从bp读取文件page,如果没有的话,则在这个page上设置一个watch

buf_get_possibly_freed

和buf_get类似,但不care这个page是否已经被释放了

其他的倒还好理解,这里的watch是个神马东东呢?从buf_page_get_gen来看,当从buffer pool的page hash中找不到对应的block时,会做如下处理:

        if (mode == buf_get_if_in_pool_or_watch) {

            block = (buf_block_t*) buf_pool_watch_set(

                space, offset, fold);

            if (univ_likely_null(block)) {

                block_mutex = buf_page_get_mutex((buf_page_t*)block);

                ut_a(block_mutex);

                ut_ad(mutex_own(block_mutex));

                goto got_block;

            }    

        } 

在每个buffer pool的控制结构体中,有一个成员buf_pool->watch[buf_pool_watch_size],该数组类型为buf_page_t,修改或访问该数组需要持有bp->mutex锁或者bp->zip_mutex。

当前buf_pool_watch_size值为1,而在5.6中这个值为purge线程数加1。

我们来看看函数buf_pool_watch_set干啥了。

首先从page hash中根据指定的space id 和page no查找page,如果查找到了,说明可能已经有线程把这个page读到了bp中,如果这个bpage不属于bp->watch数组中的一员,就直接返回这个page。

 如果在page hash中没有的话,就查看bp->watch数组成员的状态,在5.5中只有一个成员。

如果bp->watch[]的state是buf_block_pool_watch,则将当前请求的page进行进行赋值:

            bpage->state = buf_block_zip_page;

            bpage->space = space;

            bpage->offset = offset;

            bpage->buf_fix_count = 1; 

            bpage->buf_pool_index = buf_pool_index(buf_pool);

            ut_d(bpage->in_page_hash = true);

            hash_insert(buf_page_t, hash, buf_pool->page_hash,

                    fold, bpage);

bp->watch[]的状态被设置为buf_block_zip_page,这样可以保证一次只会设置一个watch的page,然后把请求的page no和space id都赋值给page,并将其插入到page hash中。

 如果bp->watch[]的state为buf_block_zip_page的话,就不做插入。

在设置为bp->wath[]后就直接返回null.

在从磁盘读入文件块的时候,会调用buf_page_init_for_read->buf_page_init初始化一个block,这时候会做一个判断,如果将被读入的page被设置为sentinel(在watch数组中被设置),则调用buf_pool_watch_remove将其从page hash中移除,并对bp->watch进行重置,但block->page的buf_fix_count会被设置+1,以防止这个page被替换出去。

buf_pool_watch_occurred函数用于检测当前page是否依然被watch住。我们可以看到,它是在ibuf_insert_low被调用到。

    if (op == ibuf_op_delete

        && (min_n_recs < 2

        || buf_pool_watch_occurred(space, page_no))) {

op == ibuf_op_delete 表示该操作类型是purge操作,如果purge操作会导致page为空,或者刚刚被设置为watch的页面被读入了bp,那么就走实际的记录purge流程,不做purge的缓冲操作。

我们继续回到函数btr_cur_search_to_nth_level,如果二级索引page不在bp中,那么就开始真正的ibuf记录创建流程,针对不同的操作,为函数ibuf_insert传递不同的参数。对于purge操作略有不同,在调用ibuf_insert之前要先判断该二级索引记录是否可以被purge(row_purge_poss_sec,当该二级索引记录对应的聚集索引记录没有delete mark并且其trx id比当前的purge view还旧时,不可以做purge操作);当完成ibuf_insert后,还需要移除watch的page(buf_pool_watch_unset)

ibuf_insert是创建ibuf entry的接口函数,

a.首先检查对应操作的ibuf是否已经开启(由参数innodb_change_buffering决定)

对于ibuf_op_insert/ibuf_op_delete_mark操作,需要做一些额外的检查(goto check_watch),检查page hash中是否已经有该page(刚刚被读入bp或者被一个purge操作设置为watch),如果存在,则直接返回false,不走ibuf

这么做的原因是,如果在purge线程进行的过程中,一条insert/delete_mark操作尝试缓存同样page上的操作时,purge不应该被缓存,因为他可能移除一条随后被重新插入的记录。简单起见,在有一个purge pending的请求时,我们让随后对该page的ibuf操作都失效。

如果这里的insert/delete_mark的ibuf操作失效,那么随后必然要去读取相应的二级索引页,这可以保证之前pending的purge操作先被合并掉。

因此bp->watch还有个作用,就是告诉其他用户线程,对这个page上已经有一个purge被缓存了。

b.检查操作的记录是否大于空page可用空间的1/2,如果大于的话,也不可以使用ibuf,返回false.

c.调用ibuf_insert_low插入ibuf entry,这里和普通的insert的乐观/悲观插入类似,也根据是否产生ibuf btree分裂分为两种情况:

    err = ibuf_insert_low(btr_modify_prev, op, no_counter,

                  entry, entry_size,

                  index, space, zip_size, page_no, the);

    if (err == db_fail) {

        err = ibuf_insert_low(btr_modify_tree, op, no_counter,

                      entry, entry_size,

                      index, space, zip_size, page_no, the);

    }  

d.ibuf_insert_low

–>首先判断ibuf->size >= ibuf->max_size + ibuf_contract_do_not_insert,这表明当前change buffer太大了,需要

ibuf->max_size是一个常量(在函数ibuf_init_at_db_start中进行初始化),表示一半的buffer pool大小所容纳的ibuf page数。

ibuf->size表示当前的ibuf page数,当这个值过大时,需要做一次同步ibuf tree收缩(ibuf_contract->ibuf_contract_ext),随机的选取一个ibuf tree上的叶子节点上(btr_pcur_open_at_rnd_pos),每次最多选择8个(ibuf_max_n_pages_merged) 二级索引页进行merge(ibuf_get_merge_page_nos),然后将选择的page读入内存中(buf_read_ibuf_merge_pages),在读入的时候,会进行merge操作。

至于具体如何合并,下一篇再议。

–>然后构建ibuf entry

    ibuf_entry = ibuf_entry_build(

        op, index, entry, space, page_no,

        no_counter ? ulint_undefined : 0xffff, heap);

如果需要对ibuf的btree进行pessimistic insert(mode == btr_modify_tree),还需要保证ibuf btree上有足够的page(ibuf_data_enough_free_for_insert),如果不够,则需要扩展空闲块(ibuf_add_free_page)

然后开启一个mini transaction(ibuf_mtr_start),并将cursor定位到ibuf btree的对应位置:btr_pcur_open(ibuf->index, ibuf_entry, page_cur_le, mode, &pcur, &mtr);

–> 计算已经为该page分配的ibuf entry大小

    buffered = ibuf_get_volume_buffered(&pcur, space, page_no,

                        op == ibuf_op_delete

                        ? &min_n_recs

                        : null, &mtr);

min_n_recs表示在为当前page应用所有的ibuf entry后还剩下的记录数。

–>当当前操作为purge操作(ibuf_op_delete)且操作的二级索引page上只剩下一条记录或者操作的page被读入了bp中(buf_pool_watch_occurred),则不进行buffer操作,

–>读入操作page对应的ibuf bitmap page

    bitmap_page = ibuf_bitmap_get_map_page(space, page_no,

                           zip_size, &bitmap_mtr);

如果该page读入了bp或者该page上有显示锁,不进行buffer操作(何时会发生呢?)

对于insert操作,需要去检查该page是否能够满足插入空间大小,从bitmap_page中找到当前二级索引page对应的bit位(ibuf_bitmap_page_get_bits),获得该page上还能写入的空闲空间(ibuf_index_page_calc_free_from_bits),如果新记录加不上的话,则需要对该page上的ibuf entry进行合并,然后退出,不进行buffer操作

–>设置当前ibuf的counter(ibuf_set_entry_counter),如果只用到了insert的ibuf,则无需设置counter

在设置完counter后,需要更新bitmap_page上对应二级索引页的ibuf_bitmap_buffered为true,表名这个page上缓存的ibuf entry.

–>在完成上述流程后,调用btr_cur_optimistic_insert/btr_cur_pessimistic_insert向ibuf btree中插入记录。

如果使用的是悲观插入,还需要更新ibuf(ibuf_size_update),并在后面进行一次ibuf收缩(ibuf_contract_after_insert)

相应的,该二级索引页的max trx id也需要更新(page_update_max_trx_id)

以上介绍的比较粗略,还有很多细节需要深入。

ibuf merge可以在多个地方发生,在用户线程中,当发现ibuf tree的空闲空间不够,或者发生ibuf tree分裂时,会去做合并,以收缩ibuf,防止过于膨胀。

在master线程中,也可能去做ibuf merge srv_master_thread->ibuf_contract_for_n_pages,每10秒必有一次merge,在系统空闲时,也会去尝试做ibuf merge。通过唤醒异步io线程读入page,异步io线程在读入page后,会进行merge操作io_handler_thread->fil_aio_wait->buf_page_io_complete

buf_merge_or_delete_for_page是进行change buffer 合并的核心函数,先来看看在什么地方会调用这个函数:

在三个地方会调用ibuf_merge_or_delete_for_page

1.buf_page_create

       ibuf_merge_or_delete_for_page(null, space, offset, zip_size, true);

2.buf_page_get_gen  //针对压缩表

        if (univ_likely(!recv_no_ibuf_operations)) {

            ibuf_merge_or_delete_for_page(block, space, offset,

                              zip_size, true);

        }    

这里主要针对压缩表,在对文件页进行解压后,会调用ibuf_merge_or_delete_for_page。

这里存在过度调用ibuf_merge_or_delete_for_page的问题(http://bugs.mysql.com/bug.php?id=65886)

对于非压缩页,在buf_page_io_complete中会调用函数做ibuf merge。

对于压缩页,则在解压后做ibuf merge。

这里存在的问题有两个,一个是压缩页的解压页可能会被驱逐(只在内存中保留压缩页),如果下次要用,就需要去解压;

另外一种情况是,压缩页被预读到内存中(read ahead),只在用到的时候才解压。

根据ibuf merge的规则,只有在第一次从磁盘读取对应文件页到内存时,才需要去合并ibuf。

因此第一种情况实际上是无需去进行ibuf merge的,在io-bound的场景下,这可能会比较频繁,从而影响到性能,因为当io吃紧时,压缩表优先选择释放解压页

3.buf_page_io_complete           //为非压缩页merge ibuf

        if (uncompressed && !recv_no_ibuf_operations) {

            ibuf_merge_or_delete_for_page(

这里也是主要的合并ibuf的方式,在读入一个page的io完成后,进行ibuf entry的合并。

说起change buffer,就不得不提到一个有名的bug,在2011年report的bug61104(http://bugs.mysql.com/bug.php?id=61104),在crash recovery时,合并ibuf中的delete操作时,发现二级索引page上记录为空,导致断言失败(没有记录,怎么做delete合并呢?)

直到去年(2012)年percona的alexey kopytov提交了bug#66819(http://bugs.mysql.com/bug.php?id=66819),指出change buffer并不是crash-safe的。特别是对于delete操作,在执行完delete操作(mtr commit)才会去删除ibuf记录。

以下代码选自ibuf_merge_or_delete_for_page:

首先读取ibuf记录,进行merge操作,针对不同的操作类型,走不同的分支,ibuf_ip_delete有些特殊,它这里直接commit了mtr 

            case ibuf_op_delete:

                ibuf_delete(entry, block, dummy_index, &mtr);

                /* because ibuf_delete() will latch an

                insert buffer bitmap page, commit mtr

                before latching any further pages.

                store and restore the cursor position. */

                ut_ad(rec == btr_pcur_get_rec(&pcur));

                ut_ad(page_rec_is_user_rec(rec));

                ut_ad(ibuf_rec_get_page_no(&mtr, rec) 

                      == page_no);

                ut_ad(ibuf_rec_get_space(&mtr, rec) == space);

                btr_pcur_store_position(&pcur, &mtr);

                ibuf_btr_pcur_commit_specify_mtr(&pcur, &mtr);

我们知道,在innodb中,被提交的mtr日志也就是redo 日志,如果被其他线程刷到了磁盘,实际上就相当于对这个page的一次ibuf merge的完成。

随后,会去尝试删除ibuf记录,如下:

        /* delete the record from ibuf */

        if (ibuf_delete_rec(space, page_no, &pcur, search_tuple,

                    &mtr)) {

            /* deletion was pessimistic and mtr was committed:

            we start from the beginning again */

在函数ibuf_delete_rec中,如果btr_cur_optimistic_delete失败,会先把mtr提交,然后再做btr_cur_pessimistic_delete。

在mtr提交和做btr_cur_pessimistic_delete之间crash的话,ibuf记录和实际数据就可能处于不一致状态。如果crash之前删除的记录后,page上只剩下最后一条记录,在crash recovery时,ibuf记录还在的话,就会调用ibuf_delete去继续重复的apply ibuf记录,触发断言错误,如下:

ibuf_delete:

        /* refuse to delete the last record. */

        ut_a(page_get_n_recs(page) > 1);   

断言的目的是在函数ibuf_insert_low中能够确保索引页中至少有一条记录,因为change buffer在生成ibuf entry时已经保证了这一点。

官方已经放出了patch:

http://bazaar.launchpad.net/~mysql/mysql-server/5.5/revision/3979

从官方的修复来看,在ibuf_delete_rec中,在进行btr_cur_pessimistic_delete之前,先把ibuf记录设置为标记删除;这样如果发生crash,重启后就不会再应用这条ibuf.

不过目前官方的fix还不完整,没有修复ibuf_op_delete操作,期望下一个版本修复这个问题,目前线上设置为inserts。

继续阅读