January 2013

[MySQL学习]Innodb压缩表之内存分配/回收

最近看到Yoshinori Matsunobu在官方buglist上提交的一个Bug#68077,大意是说,当使用压缩表时,在bp吃紧时,存在过度碎片合并的情况。Innodb压缩表由于存在不同的Page Size,因此使用buddy allocator的方式进行内存分配,他的内存块来自于buffer pool中。 如bug#68077所提到的,如果我们使用的全部是4kb的内存块,那么把他们合并成8k的又有什么意义呢?并且在碎片整理的过程中,函数buf_buddy_free 的效率是很低的。在之前已经碰到过很多类似的案例,例如DROP TABLE,在遍历block,释放adaptive hash index记录时,可能会在这里hang住。 但从Marko的角度来看,由于内存是从buffer pool中分配的,如果不做碎片合并的话,很容易导致大量的4KB的页面存留在内存中,这对非压缩表而言是不可用的;如果你释放了4*n*4k的压缩页内存,那在buffer pool中就有n个16kb的block是可以为非压缩表所用的。 不过合并成8KB的block对只使用4kb和16kb的场景用处并不大,在回收内存时,也存在时间复杂度较高的情况 简单看看buddy allocator如何分配和回收内存。   A.分配内存 接口函数:buf_buddy_alloc 参数描述: buf_pool_t* buf_pool //当前page所在的bp对象 ulint size  //压缩页需要的内存大小 ibool* lru  //表示是否是从bp->LRU上分配的内存 通过size,计算出其在buf_pool->zip_free[]数组中的slot(buf_buddy_get_slot),例如1kb,对应slot 0,4kb对应slot 2 具体内存分配实现函数是buf_buddy_alloc_low,注意调用这个函数需要持有bp->mutex,并且不可持有bp->zip_mutex或者任何block->mutex。内存分配的方式也是典型的binary buddy allocator 算法   1.首先尝试从buf_pool->zip_free[]数组中查找(buf_buddy_alloc_zip),优先从buddy system中分配block >>根据slot,查看bp->zip_free对应数组元素链表,如果有的话,则将其从zip_free中移除,以占用这个block(buf_buddy_remove_from_free) >>如果没有对应slot的空闲块,则查找更大的空闲块,例如,如果没有4kb的空闲块,我们就去看有没有8kb的空闲块,这里采用递归调用buf_buddy_alloc_zip(buf_pool, i + 1)的方法来返回block。 虽然递归算法存在效率问题,不过幸好这里的递归深度不会很大,最多4层调用 例如,我们申请4kb的内存块,这里我们获得一个8kb的内存块,只将低位的拿来用,而高位的4kb则放到bp->zip_free链表上,状态设置为BUF_BLOCK_ZIP_FREE 如果我们申请的是2kb的内存块,当前只有8kb的空闲块,那么会产生一个新的4kb的和2Kb的空闲块 这种情况下,我们可以直接返回获得的block首地址;   2.如果bp->zip_free上没有空闲块,就从bp->free上取一个block(buf_LRU_get_free_only) 如果buffer pool没有的话,就调用buf_LRU_get_free_block,获得的block的状态被设置为BUF_BLOCK_READY_FOR_USE 这里存在的困惑是,buf_LRU_get_free_block同样也会调用buf_LRU_get_free_only,因为它是一个通用的获取空闲块的函数,同样也会先去看看free list上有没有空闲块,这里似乎存在着重复调用,尽管这对性能影响不大。   […]

[MySQL学习] Innodb change buffer(2) 相关函数及流程

简单的代码跟踪,顺便弄清了之前一直困惑的bp->watch的用途。。。。 //////////////////////////////// A.相关结构体 在介绍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) 区分老版本 4字节 page no 类型信息,包括: 5.5特有的counter(2字节) 、操作类型(1字节) 、flags1(1字节, 值为IBUF_REC_COMPACT.   剩下的是实际数据 B.何时决定使用ibuf: 当我们更新一条数据的时候,首先是更新聚集索引记录,然后再更新二级索引,当通过聚集索引记录寻找搜索二级索引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 […]

[MySQL学习] Innodb change buffer(1)之初识篇

从MySQL5.5版本开始,Insert buffer更名为change buffer,除了缓冲对二级索引的insert操作,还包括update/delete/后台purge操作,由参数innodb_change_buffering来控制。因此这里统一称为change buffer。 ////////////////////////////////////////////////////////// 当更新/插入的非聚集索引的数据所对应的页不在内存中时(对非聚集索引的更新操作通常会带来随机IO),会将其放到一个insert buffer中,当随后页面被读到内存中时,会将这些变化的记录merge到页中。当服务器比较空闲时,后台线程也会做merge操作 但change buffer会占用buffer pool,并且在非聚集索引很少时,并不总是必要的,反而会降低buffer pool做data cache的能力。 change buffer只作用于二级索引的叶子节点,并且无法处理可能导致索引分裂或合并的操作。 另外和5.1有所不同的是,由于5.5引入了更多buffer的操作,因此需要在ibuf中保证对一个page的操作是有序的。 当文件页在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大神博客上扣的图: 另外,change buffer的数据实际上是在系统表空间ibdata中,对应的内部系统表名为SYS_IBUF_TABLE。因此change buffer也会存储到磁盘。 其中,ibdata的第4个page(FSP_IBUF_HEADER_PAGE_NO)存储了ibuf数的SEGEMENT信息。 第5个page(FSP_IBUF_TREE_ROOT_PAGE_NO)是ibuf树的根节点。 在Percona版本的5.5中,有几个参数来控制change buffer的行为: innodb_change_buffering 取值包括all/none/inserts/deletes/changes/purges 其中deletes包括了delete和update操作(二级索引的update是 标记删除旧记录,再插入新记录). innodb_ibuf_accel_rate 默认值为100,用于控制做change buffer merge的page数量 一般作为ibuf_contract_for_n_pages函数的第二个参数 是宏PCT_IBUF_IO,定义如下: #define PCT_IBUF_IO(pct) ((ulint) (srv_io_capacity                  * srv_ibuf_accel_rate * […]

bug#66718 Assert from DROP TABLE concurrent with ibuf merges

http://bugs.mysql.com/bug.php?id=66718 Assert from DROP TABLE concurrent with ibuf merges A.原因: master线程发起的ibuf merge和用户线程的drop table操作可能存在竞争。 1.master线程请求文件page IO,读入内存之前,会调用buf_page_init_for_read->fil_tablespace_deleted_or_being_deleted_in_mem 如果这时候该表尚未标记为删除,那么则认为可以继续读文件Page。 2.用户线程调用fil_delete_tablespace >设置space->stop_ibuf_merges = TRUE >确认space->n_pending_ibuf_merges == 0   设置space->is_being_deleted = TRUE; >确认space->chain->n_pending为0  并且 space->n_pending_flushes = 0 继续下面的逻辑 3.master线程继续调用函数fil_io -> fil_node_prepare_for_io会递增node->n_pending++ 4.用户线程继续往下走,调用 fil_delete_tablespace->fil_space_free->fil_node_free 在这里会做断言: ut_a(node->n_pending == 0); 不过在Percona版本中,断言处就不太一样,而是: ut_a(node->n_pending == 0 || space->is_being_deleted); 因为在free node之前,我们已经设置了space->is_being_deleted为TRUE,因此不会触发断言错误。 B.总结: 根据bug描述及分析,这个bug不影响我们的线上MySQL版本。 C.其他: 我们已经确保space->n_pending_ibuf_merges为0了,为什么还会有对该表的ibuf Merge呢? […]

一个线上死锁问题分析

死锁日志如下: TRANSACTION 48AA4BB9, ACTIVE 0 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 6 lock struct(s), heap size 1248, 4 row lock(s), undo log entries 2 MySQL thread id 1409173, OS thread handle 0x5659f940, query id 1084083936 10.246.138.197 bop_libra update insert into deadlock_test (deadlock_config_id, block_id, type, gmt_create, gmt_modified) values (31643, 92354, 1, […]

[MySQL学习] Innodb锁系统(4) Insert/Delete 锁处理及死锁示例分析

A.INSERT 插入操作在函数btr_cur_optimistic_insert->btr_cur_ins_lock_and_undo->lock_rec_insert_check_and_lock这里进行锁的判断,我们简单的看看这个函数的流程: 1.首先先看看欲插入记录之后的数据上有没有锁,    next_rec = page_rec_get_next_const(rec);    next_rec_heap_no = page_rec_get_heap_no(next_rec);    lock = lock_rec_get_first(block, next_rec_heap_no); 如果lock为空的话,对于非聚集索引,还需要更新page上的最大事务ID。 实际上这里是比较松散的检查,大并发插入的时候,可以大大的降低创建锁开销。 那么其他事务如何发现这些新插入的记录呢(重复插入同一条记录显然应该被阻塞),这里会有个判断,其他事务去看看 新插入记录的事务是否还是活跃的,如果还是活跃的,那么就为这个事务主动增加一个锁记录(所谓的隐式锁就是么有锁。。。。),这个判断是在检查是否存在冲突键的时候进行的(row_ins_duplicate_error_in_clust->row_ins_set_shared_rec_lock->lock_clust_rec_read_check_and_lock->lock_rec_convert_impl_to_expl row_ins_set_shared_rec_lock的目的是为了向记录上加一个LOCK_REC_NOT_GAP的LOCK_S锁,也就是非GAP的记录S锁,如果发现记录上有X锁(隐式锁转换为LOCK_REC | LOCK_X | LOCK_REC_NOT_GAP),显然是需要等待的(返回DB_LOCK_WAIT)   这里设置inherit为FALSE,然后返回DB_SUCCESS; 至于inherit的作用,稍后再议! 2.如果lock不为空,这意味着插入记录的下一个记录上存在锁,设置inherit为TRUE. 检查下一个记录上的锁是否和LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION相互冲突     if (lock_rec_other_has_conflicting(             LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,             […]