MySQL · 引擎特性 · Innodb change buffer介绍

一 序

    之前整理了undo logredo log以及InnoDB如何崩溃恢复来实现数据ACID的相关知识。本篇继续整理InnoDB change buffer。 Change buffer的主要目的是将对二级索引的数据操作缓存下来,以此减少二级索引的随机IO,并达到操作合并的效果。

  官网的解释比较详细了。5.7版本的截取如下:

 Change Buffer

The change buffer is a special data structure that caches changes to secondary index pages when affected pages are not in the buffer pool. The buffered changes, which may result from INSERTUPDATE, or DELETE operations (DML), are merged later when the pages are loaded into the buffer pool by other read operations.

Unlike clustered indexes, secondary indexes are usually nonunique, and inserts into secondary indexes happen in a relatively random order. Similarly, deletes and updates may affect secondary index pages that are not adjacently located in an index tree. Merging cached changes at a later time, when affected pages are read into the buffer pool by other operations, avoids substantial random access I/O that would be required to read-in secondary index pages from disk.

Periodically, the purge operation that runs when the system is mostly idle, or during a slow shutdown, writes the updated index pages to disk. The purge operation can write disk blocks for a series of index values more efficiently than if each value were written to disk immediately.

Change buffer merging may take several hours when there are numerous secondary indexes to update and many affected rows. During this time, disk I/O is increased, which can cause a significant slowdown for disk-bound queries. Change buffer merging may also continue to occur after a transaction is committed. In fact, change buffer merging may continue to occur after a server shutdown and restart (see Section 14.21.2, “Forcing InnoDB Recovery” for more information).

In memory, the change buffer occupies part of the InnoDB buffer pool. On disk, the change buffer is part of the system tablespace, so that index changes remain buffered across database restarts.

     在MySQL5.5之前的版本中,由于只支持缓存insert操作,所以最初叫做insert buffer,只是后来的版本中支持了更多的操作类型缓存,才改叫change buffer,这也是为什么代码中有大量的ibuf前缀开头的函数或变量。

    好了,即使你英语很烂,也能明白个大概。再看个图加深理解:(图片来自geaozhang)

       对图可见将对索引的更新记录存入insert buffer中,而不是直接调入索引页进行更新;择机进行merge insert buffer的操作,将insert buffer中的记录合并(merge)到真正的辅助索引中,解决了insert表数据产生过多物理读的问题。

   下面主要分为两个部分来看change buffer:1 对应的数据结构,2 对应的函数.

二 数据结构

2.1 ibuf btree

    change buffer的物理上是一颗普通的btree,存储在ibdata系统表空间中,根页为ibdata的第4个page(FSP_IBUF_TREE_ROOT_PAGE_NO)。一条ibuf 记录大概包含如下列:(图片来自taobao.mysql)

      ibuf btree通过三列(space id, page no , counter)作为主键来唯一决定一条记录,其中counter是一个递增值,目的是为了维持不同操作的有序性,例如可以通过counter来保证在merge时执行如下序列时的循序和用户操作顺序是一致的:INSERT x, DELETE-MARK x, INSERT x。

      在插入ibuf记录前我们是不知道counter的值的,因此总是先将对应tuple的counter设置为0xFFFF,然后将cursor以模式PAGE_CUR_LE定位到小于等于(space id, page no, 0xFFFF)的位置,新记录的counter为当前位置记录counter值加1。

     ibuf btree最大默认为buffer pool size的25%,当超过25%时,可能触发用户线程同步缩减ibuf btree。为何要将ibuf btree的大小和buffer pool大小相关联呢 ? 一个比较重要的原因是防止ibuf本身占用过多的buffer pool资源。change buffer会占用buffer pool,并且在非聚集索引很少时,并不总是必要的,反而会降低buffer pool做data cache的能力。

2.2 ibuf bitmap

     当文件页在buffer pool中时,就直接操作文件页,而不会去考虑ibuf;当文件页从磁盘读入内存时,总会去尝试做Merge。在表空间中,有ibuf bitmap来跟踪记录每个Page空闲空间的范围,以避免Page溢出或被清空。

     在FSP_HDR页随后是ibuf_bitmap页,其中FSP_HDR在表空间的第一个Page上。每个FSP_HDR只能管理256 个extent的信息(也就是16384个Page),因此每隔16384个page,会有一个类似FSP_HDR的Page来描述随后的extend信息,相应的每个ibuf_bitmap也可以管理16384个page,每个page的空闲空间用一个字节来表示。
从Jeremy Cole大神博客上扣的图

下图是来自taobao.mysql的图,介绍ibuf bitmap page的结构图、

使用4个bit来描述每个page的change buffer信息。

Macro bits Desc
IBUF_BITMAP_FREE 2 使用2个bit来描述page的空闲空间范围:0(0 bytes)、1(512 bytes)、2(1024 bytes)、3(2048 bytes)
IBUF_BITMAP_BUFFERED 1 是否有ibuf操作缓存
IBUF_BITMAP_IBUF 1 该Page本身是否是Ibuf Btree的节点

2.3 操作类型

    InnoDB change buffer可以对三种类型的操作进行缓存:INSERT、DELETE-MARK 、DELETE操作,前两种对应用户线程操作,第三种则由purge操作触发。
用户可以通过参数innodb_change_buffering来控制缓存何种操作:

/** Allowed values of innodb_change_buffering */
static const char* innobase_change_buffering_values[IBUF_USE_COUNT] = {
        "none",         /* IBUF_USE_NONE */
        "inserts",      /* IBUF_USE_INSERT */
        "deletes",      /* IBUF_USE_DELETE_MARK */
        "changes",      /* IBUF_USE_INSERT_DELETE_MARK */
        "purges",       /* IBUF_USE_DELETE */
        "all"           /* IBUF_USE_ALL */
};

     innodb_change_buffering默认值为all,表示缓存所有操作。注意由于在二级索引上的更新操作总是先delete-mark,再insert新记录,因此其ibuf实际有两条记录IBUF_OP_DELETE_MARK+IBUF_OP_INSERT。

三 操作流程

    这里举两个流程,对于insert,mysql 5.7与5.6流程不太一致。

insert

插入聚集索引 // row_ins_clust_index_entry_low 与5.6不同没有看到, 插入二级索引 // row_ins_sec_index_entry_low 里面有。

write_row -> ha_innobase::write_row -> row_insert_for_mysql -> … -> row_ins_step ->...->row_ins_sec_index_entry_low->btr0cur.cc::btr_cur_search_to_nth_level ->->ibuf_should_try...

再看更新过程:

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

sql/handler.cc:handler::ha_update_row->ha_innobase::update_row->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都汇总到btr_cur_search_to_nth_level->ibuf_should_try

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

该函数用于定位到btree上满足条件的记录,大概的判断条件如下:

  1. 用户设置了选项innodb_change_buffering;(即ibuf_use != IBUF_USE_NONE)
  2. 只有叶子节点才会去考虑是否使用ibuf;
  3. 对于聚集索引,不可以缓存操作;
  4. 对于唯一二级索引(unique key),由于索引记录具有唯一性,因此无法缓存插入操作,但可以缓存删除操作;
  5. 表上没有flush 操作,例如执行flush table for export时,不允许对表进行 ibuf 缓存 (通过dict_table_t::quiesce 进行标识)

源码为:innobase/include/ibuf0ibuf.ic

ibuf_should_try(
/*============*/
	dict_index_t*	index,			/*!< in: index where to insert */
	ulint		ignore_sec_unique)	/*!< in: if != 0, we should
						ignore UNIQUE constraint on
						a secondary index when we
						decide */
{
	return(ibuf_use != IBUF_USE_NONE
	       && ibuf->max_size != 0
	       && !dict_index_is_clust(index)
	       && !dict_index_is_spatial(index)
	       && index->table->quiesce == QUIESCE_NONE
	       && (ignore_sec_unique || !dict_index_is_unique(index))
	       && srv_force_recovery < SRV_FORCE_NO_IBUF_MERGE);
}

    下面的分析主要代码是btr_cur_search_to_nth_level。源码在innobase/btr/btr0cur.cc,可以结合update的流程图来看。

    当判断可以使用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;

把buf_mode传入buf_page_get_gen,源码在innobase/buf/buf0buf.cc

如果是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是否已经被释放了

       这里不做展开,可以关注下BUF_GET_IF_IN_POOL_OR_WATCH的实现。buf_page_get_gen的作用就是读取页面,若叶页面不在buffer pool中,同时可以进行insert buffer,则返回NULL。

我们继续回到函数btr_cur_search_to_nth_level,如果二级索引page不在bp中,那么就开始真正的ibuf记录创建流程。

ibuf_insert -> ibuf_insert_low -> ibuf_entry_build ->
btr_pcur_open(btr_pcur_open_func – > btr_cur_search_to_nth_level) ->
ibuf_get_volume_bufferd ->
ibuf_bitmap_get_map_page -> ibuf_bitmap_page_get_bits -> ibuf_index_page_calc_free_from_bits -> btr_cur_optimistic_insert ->

ibuf_insert是创建ibuf entry的接口函数,源码在/innobase/ibuf/ibuf0ibuf.cc
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操作都失效。

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, page_id, page_size, thr);
	if (err == DB_FAIL) {
		err = ibuf_insert_low(BTR_MODIFY_TREE | BTR_LATCH_FOR_INSERT,
				      op, no_counter, entry, entry_size,
				      index, page_id, page_size, thr);
	}

d 接下来看看ibuf_insert_low,源码在innobase/ibuf/ibuf0ibuf.cc

首先是判断ibuf->size >= ibuf->max_size + IBUF_CONTRACT_DO_NOT_INSERT,这表明当前change buffer太大了,

if (ibuf->size >= ibuf->max_size + IBUF_CONTRACT_DO_NOT_INSERT) {
		/* Insert buffer is now too big, contract it but do not try
		to insert */


#ifdef UNIV_IBUF_DEBUG
		fputs("Ibuf too big\n", stderr);
#endif
		ibuf_contract(true);

		return(DB_STRONG_FAIL);
	}

然后构建ibuf entry ,ibuf_entry_build:

ibuf_entry = ibuf_entry_build(
		op, index, entry, page_id.space(), page_id.page_no(),
		no_counter ? ULINT_UNDEFINED : 0xFFFF, heap);

构造insert buffer中的记录,记录组织结构如下:
  4 bytes:space_id
  1 byte: marker = 0
  4 bytes:page number
type info:
   2 bytes:counter,标识当前记录属于同一页面中的第几条insert buffer记录
   1 byte: 操作类型:IBUF_OP_INSERT; IBUF_OP_DELETE_MARK; IBUF_OP_DELETE;
   1 byte: Flags. 当前只能是IBUF_REC_COMPACT
   entry fields:之后就是索引记录
    由于前9个字节[space_id, marker, page_numer, counter]组合,前三个字段,相同页面是一样的,这也保证了相同页面的记录,一定是存储在一起。第四个字段,标识页面中的第几次更新,保证同一页面buffer的操作,按照顺序存储。

	if (BTR_LATCH_MODE_WITHOUT_INTENTION(mode) == BTR_MODIFY_TREE) {
		for (;;) {
			mutex_enter(&ibuf_pessimistic_insert_mutex);
			mutex_enter(&ibuf_mutex);

			if (UNIV_LIKELY(ibuf_data_enough_free_for_insert())) {

				break;
			}

			mutex_exit(&ibuf_mutex);
			mutex_exit(&ibuf_pessimistic_insert_mutex);

			if (!ibuf_add_free_page()) {

				mem_heap_free(heap);
				return(DB_STRONG_FAIL);
			}
		}
	}

	ibuf_mtr_start(&mtr);

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

接下来开启mtr事务。ibuf_mtr_start,并将cursor定位到ibuf btree的对应位置

btr_pcur_open(ibuf->index, ibuf_entry, PAGE_CUR_LE, mode, &pcur, &mtr);

计算已经为该page分配的ibuf entry大小:ibuf_get_volume_buffered

buffered = ibuf_get_volume_buffered(&pcur,
					    page_id.space(),
					    page_id.page_no(),
					    op == IBUF_OP_DELETE
					    ? &min_n_recs
					    : NULL, &mtr);

在insert buffer中已存在的项,同时返回这些项占用的空间大小buffered。首先遍历当前页面的前页面,比较前页面中的项,若[space_id, page_num]相同,则增加buffered;然后遍历当前页面的后页面,同样增加相同页面的项。

if (op == IBUF_OP_DELETE
	    && (min_n_recs < 2 || buf_pool_watch_occurred(page_id))) {
		/* The page could become empty after the record is
		deleted, or the page has been read in to the buffer
		pool.  Refuse to buffer the operation. */

		/* The buffer pool watch is needed for IBUF_OP_DELETE
		because of latching order considerations.  We can
		check buf_pool_watch_occurred() only after latching
		the insert buffer B-tree pages that contain buffered
		changes for the page.  We never buffer IBUF_OP_DELETE,
		unless some IBUF_OP_INSERT or IBUF_OP_DELETE_MARK have
		been previously buffered for the page.  Because there
		are buffered operations for the page, the insert
		buffer B-tree page latches held by mtr will guarantee
		that no changes for the user page will be merged
		before mtr_commit(&mtr).  We must not mtr_commit(&mtr)
		until after the IBUF_OP_DELETE has been buffered. */

fail_exit:
		if (BTR_LATCH_MODE_WITHOUT_INTENTION(mode) == BTR_MODIFY_TREE) {
			mutex_exit(&ibuf_mutex);
			mutex_exit(&ibuf_pessimistic_insert_mutex);
		}

		err = DB_STRONG_FAIL;
		goto func_exit;
	}

     如果当前操作为Purge操作(IBUF_OP_DELETE)且操作的二级索引page上只剩下一条记录或者操作的page被读入了bp中(buf_pool_watch_occurred),则不进行buffer操作,为啥呢?由于ibuf 缓存的操作都是针对某个具体page的,因此在缓存操作时必须保证该操作不会导致空page 或索引分裂。这段代码的逻辑就是针对第一种情况,即避免空page,主要是对purge线程而言,因为只有purge线程才会去真正的删除二级索引上的物理记录。在准备插入类型为IBUF_OP_DELETE的操作缓存时,会预估在apply完该page上所有的ibuf entry后还剩下多少记录(ibuf_get_volume_buffered),如果只剩下一条记录,则拒绝本次purge操作缓存,改走正常的读入物理页逻辑。

读入操作page对应的ibuf bitmap page:

	bitmap_page = ibuf_bitmap_get_map_page(page_id, page_size,
					       &bitmap_mtr);

这里要与下面结合来看。

if (op == IBUF_OP_INSERT) {
		ulint	bits = ibuf_bitmap_page_get_bits(
			bitmap_page, page_id, page_size, IBUF_BITMAP_FREE,
			&bitmap_mtr);

		if (buffered + entry_size + page_dir_calc_reserved_space(1)
		    > ibuf_index_page_calc_free_from_bits(page_size, bits)) {
			/* Release the bitmap page latch early. */
			ibuf_mtr_commit(&bitmap_mtr);

			/* It may not fit */
			do_merge = TRUE;

			ibuf_get_merge_page_nos(FALSE,
						btr_pcur_get_rec(&pcur), &mtr,
						space_ids,
						page_nos, &n_stored);

			goto fail_exit;
		}
	}

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

   这里就是针对第二种情况(缓存操作时必须保证该操作不会导致索引分裂),InnoDB通过一种特殊的page(ibuf bitmap page,前面已经介绍过页面结构)来维护每个数据页的空闲空间大小,由于只有INSERT操作才可能导致page记录满,因此只需要对IBUF_OP_INSERT类型的操作进行判断。

  • 其中ibuf_bitmap_page_get_bits函数根据space id 和page no 获取对应的bitmap page,找到空闲空间描述信息;如果本次插入操作可能超出限制,则从当前cursor位置附近开始,触发一次异步的ibuf merge,目的是尽量将当前page的缓存操作做一次合并。 在正常的对物理页的DML过程中,如果page内空间发生了变化,总是需要去更新对应的IBUF_BITMAP_FREE值。参考函数:btr_compress、btr_cur_optimistic_insert。
  • IBUF_BITMAP_BUFFERED:用于表示该page是否有操作缓存,在ibuf_insert_low函数中,准备插入ibuf btree前设置成true。二级索引物理页读入内存时会根据该标记位判断是否需要进行ibuf merge操作。
  • IBUF_BITMAP_IBUF:表示该数据页是否是ibuf btree的一部分,该标记位主要用于异步AIO读操作。InnoDB专门为change buffer模块分配了一个后台AIO线程,如果page属于change buffer的b树,则使用该线程做异步读,参考函数:ibuf_page_low

如果只用到了INSERT的ibuf,则无需设置counter if (!no_counter)
在设置完counter后,需要更新bitmap_page上对应二级索引页的IBUF_BITMAP_BUFFERED为TRUE,表名这个page上缓存的ibuf entry.

err = btr_cur_optimistic_insert(
			BTR_NO_LOCKING_FLAG | BTR_NO_UNDO_LOG_FLAG,
			cursor, &offsets, &offsets_heap,
			ibuf_entry, &ins_rec,
			&dummy_big_rec, 0, thr, &mtr);

		if (err == DB_FAIL) {
			err = btr_cur_pessimistic_insert(
				BTR_NO_LOCKING_FLAG | BTR_NO_UNDO_LOG_FLAG,
				cursor, &offsets, &offsets_heap,
				ibuf_entry, &ins_rec,
				&dummy_big_rec, 0, thr, &mtr);
		}

		mutex_exit(&ibuf_pessimistic_insert_mutex);
		ibuf_size_update(root);
		mutex_exit(&ibuf_mutex);

在完成上述流程后,调用btr_cur_optimistic_insert/btr_cur_pessimistic_insert向ibuf btree中插入记录。
还需要更新ibuf(ibuf_size_update)
相应的,该二级索引页的max trx id也需要更新(page_update_max_trx_id)。

以上只是粗略介绍流程,能够与前面的结构对应起来。很多细节没有展开看。

四 purge操作缓存

      上面的第三节流程中,主要针对的是buffer insert操作。在5.5之后,Insert Buffer不仅能够buffer insert操作,并且能够buffer delete mark/purge等操作。提到delete mark操作,不得不简单说一下InnoDB的多版本。为了实现多版本,InnoDB的索引在进行delete操作时,并不是直接将记录从索引中删除,而是仅仅将记录标识为delete状态(delete mark),每条记录上,都有一个delete bit。
      记录何时被真正删除,要等到InnoDB的purge线程,根据redo log,回收索引上被标识为delete bit的项。
     从以上的简单描述可以看出,delete mark操作并不会删除记录,因此也不会对索引页面的利用率产生影响。但是purge操作却是真正的删除数据,会减少索引页面的利用率,甚至将页面删空。空页面会导致索引进行SMO(索引页分裂的时候,Structure Modification Operation,SMO)操作,而Insert Buffer是不支持SMO的,因此,必须能够监控这种情况。也就我们第三节提到的第一种情况。另外,第三节提到ibuf_should_try: 当满足ibuf缓存条件时,去尝试获取数据页有一种情况:  BUF_GET_IF_IN_POOL_OR_WATCH:如果数据页在内存中,则获取page并返回,否则为请求的page设置一个`sentinel`(buf_pool_watch_set),相当于标记这个page,表示这个page上的记录正在被purge。

下面看看purge操作的缓存流程:

     如何设置sentinel 当purge线程尝试读入page时,若数据页不在buffer pool中,则调用函数buf_pool_watch_set,分为两步:

  • Step1: 首先检查page hash,如果存在于page hash中:1)若未被设置成sentinel (别的线程将数据页读入内存时会清理掉对应标记),返回数据页;2)否则返回NULL;
  • Step2: 若page hash中不存在,则从buf_pool_t::watch数组中找到一个空闲的(状态为BUF_BLOCK_POOL_WATCH)page控制结构体对象buf_page_t,将其状态设置为BUF_BLOCK_ZIP_PAGE,初始化相关变量,并插入到page hash中。buf_pool_t::watch数组的大小为purge线程的个数,这意味着即使所有purge线程同时访问同一个buffer pool instance,总会拥有一个空闲的watch数组对象。

判断是否可以缓存purge操作 当设置sentinel并返回后,在决定缓存purge之前,需要去判断是否有别的线程对同一条记录缓存了新的操作,举个简单的例子:

  • Step 1: delete-mark X (sec index) //session 1
  • Step 2: insert X (clust index) //session 1
  • Step 3: delete X(sec index) //purge thread
  • Step 4: insert X (sec index) //session 1

       如果二级索引页在内存中,那么Step 3 和Step4必然是有序的,因为需要获取block锁才能进行数据变更操作。但数据页不在内存时,就需要确保Step 4在Step 3之后执行。因此在缓存purge操作之前,需要根据当前要清理的记录,找到对应的聚集索引记录,并检查相比当前purge线程的readview是否有新版本的聚集索引记录(即有新的插入操作发生)。
       如果检查到有新的插入,则本次purge操作直接放弃。因为当符合一定条件时,Step 4的操作可以直接把Step1产生的记录删除标记清除掉,重用物理空间。
      参考函数:row_purge_poss_sec: 但是注意上述检查流程结束时,会在函数row_purge_poss_sec中将mtr提交掉,对应的聚集索引页的Latch会被释放掉,这意味着可能出现如下序列:

  • Step 1: delete-mark X;
  • Step 2: delete X,purge线程为其设置watch,并完成在函数row_purge_poss_sec中的检查,准备插入ibuf
  • Step 3: insert X,索引页不在内存,准备插入ibuf

在函数ibuf_insert中,针对IBUF_OP_INSERT和IBUF_OP_DELETE_MARK操作,会去检查是否对应的二级索引页被设置成sentinel(buf_page_get_also_watch),如果是的话,表明当前有一个pending的purge操作,目前的处理逻辑是放弃insert和delete-mark的缓存操作,转而读取物理页。

      综上,如果purge操作先进入ibuf_insert,则对应二级索引页的watch必然被设置,insert操作将放弃缓存,转而尝试读入索引页;如果insert先进入ibuf_insert,则purge操作的缓存放弃。
         即使Purge线程完成一系列检查,进入缓存阶段,这时候用户线程依旧可能会去读入物理页;有没有可能导致purge操作丢失呢 ?答案是否定的!因为purge线程在缓存操作时先将cursor定位到ibuf btree上,对应的ibuf page已将加上latch;而用户线程如果读入物理页,为了merge ibuf entry,也需要请求page latch;当purge线程在拿到latch后,会再检查一次看看物理页是否已读入内存(buf_pool_watch_occurred),如果是的话,则放弃本次缓存。
何时清理sentinel 有两种情况会清理sentinel:
        第一种情况是purge操作完成缓存后(或者判断无法进行purge缓存)进行清理;
       第二种情况是从磁盘读入文件块的时候,会调用buf_page_init_for_read->buf_page_init初始化一个page对象。这时候会做一个判断,如果将被读入的page被设置为sentinel(在watch数组中被设置),则调用buf_pool_watch_remove将其从page hash中移除,对应bp->watch的数据元素被重置成空闲状态。

五 ibuf merge

在merge insert buffer之前,insert buffer数据是存在内存中,为了防止数据库意外宕机导致数据丢失,系统会周期性将insert buffer数据写入共享表空间中。有以下几种场景会触发ibuf merge操作:

  1. 用户线程选择二级索引进行数据查询,这时候必须要读入二级索引页,相应的ibuf entry需要merge到Page中。
  2. 当尝试缓存插入操作时,如果预估page的空间不足,可能导致索引分裂,则定位到尝试缓存的page no在ibuf btree中的位置,最多merge 8个(IBUF_MERGE_AREA) page,merge方式为异步,即发起异步读索引页请求。 参考函数:ibuf_insert_low —> ibuf_get_merge_page_nos_func
  3. 若当前ibuf tree size 超过ibuf->max_size + 10(IBUF_CONTRACT_DO_NOT_INSERT)时,执行一次同步的ibuf merge(ibuf_contract),merge的page no为随机定位的cursor,最多一次merge 8个page,同时放弃本次缓存。 其中ibuf->max_size默认为25% * buffer pool size,百分比由参数innodb_change_buffer_max_size控制,可动态调整。 参考函数:ibuf_insert_low —> ibuf_contract

  4. 若本次插入ibuf操作可能产生ibuf btree索引分裂(BTR_MODIFY_TREE)时:
    • 当前ibuf->size < ibuf->max_size, 不做处理;
    • 当前ibuf->size >= ibuf->max_size + 5 (IBUF_CONTRACT_ON_INSERT_SYNC)时,执行一次同步ibuf merge,位置随机;
    • 当前Ibuf->size介于ibuf->max_size 和ibuf->max_size +5 之间时。执行一次异步ibuf merge,位置随机。 参考函数:ibuf_insert_low —> ibuf_contract_after_insert
  5. 后台master线程发起merge master线程有三种工作状态: IDLE:实例处于空闲状态,以100%的io capacity来作merge操作:

     n_pages = PCT_IO(100);
    

    相当于一次merge的page数等于innodb_io_capacity 参考函数:srv_master_do_idle_tasks

    ACTIVE:实例处于活跃状态,这时候会以如下算法计算需要merge的page数:

        /* By default we do a batch of 5% of the io_capacity */
        n_pages = PCT_IO(5);
    
        mutex_enter(&ibuf_mutex);
    
        /* If the ibuf->size is more than half the max_size
        then we make more agreesive contraction.
        +1 is to avoid division by zero. */
        if (ibuf->size > ibuf->max_size / 2) {
                ulint diff = ibuf->size - ibuf->max_size / 2;
                n_pages += PCT_IO((diff * 100)
                                   / (ibuf->max_size + 1));
        }
    
        mutex_exit(&ibuf_mutex);
    

    可见在系统active时,会以比较温和的方式去做merge,如果当前ibuf btree size超过最大值的一半,则尝试多做一些merge操作。 参考函数: srv_master_do_active_tasks

    SHUTDOWN:当执行slow shutdown时,会强制做一次全部的ibuf merge 参考函数:srv_master_do_shutdown_tasks

  6. 对某个表执行flush table 操作时,会触发对该表的强制ibuf merge,例如执行:

     flush table tbname for export;
     flush table tbname with read lock;
    

    实际上强制ibuf merge主要是为flush for export准备的,当执行该命令后,为了保证能安全的将ibd拷贝到其他实例上, 需要对该表应用全部的ibuf 缓存。 参考函数:row_quiesce_table_start

************************

有一些监控命令,官网也有介绍。举个例子:

show variables like '%change_buffer%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| innodb_change_buffer_max_size | 25    |
| innodb_change_buffering       | all   |
+-------------------------------+-------+
2 rows in set (0.00 sec)

关注change buffer在innodb buffer pool中的占比
1、innodb_change_buffer_max_size:表示change buffer在buffer pool中的最大占比,默认25%,最大50%
2、innodb_change_buffering:表示索引列merge对象,all表示对IDU索引列都起作用,都进行merge,如果只想对insert索引列进行merge,就把all改为inserts。

每一篇整理都充满挑战,期待能坚持下来。

参考:

http://mysql.taobao.org/monthly/2015/07/01/

https://www.cnblogs.com/geaozhang/p/7235953.html

http://hedengcheng.com/?p=94

猜你喜欢

转载自blog.csdn.net/bohu83/article/details/81837872