我们知道InnoDB的索引组织结构为Btree。通常情况下,我们需要根据查询条件,从根节点开始寻路到叶子节点,找到满足条件的记录。为了减少寻路开销,InnoDB本身做了几点优化。
首先,对于连续记录扫描,InnoDB在满足比较严格的条件时采用row cache的方式连续读取8条记录(并将记录格式转换成MySQL Format),存储在线程私有的
中;这样一次寻路就可以获取多条记录,在server层处理完一条记录后,可以直接从cache中取数据而无需再次寻路,直到cache中数据取完,再进行下一轮。
- row_prebuilt_t::fetch_cache
另一种方式是,当一次进入InnoDB层获得数据后,在返回server层前,当前在btree上的cursor会被暂时存储到
中,当再次返回InnoDB层捞数据时,如果对应的Block没有发生任何修改,则可以继续沿用之前存储的cursor,无需重新定位。
- row_prebuilt_t::pcur
上面这两种方式都是为了减少了重新寻路的次数,而对于一次寻路的开销,则使用Adaptive hash index来解决。AHI是一个内存结构,严格来说不是传统意义上的索引,可以把它理解为建立在Btree索引上的“索引”。
本文代码分析基于MySQL 5.7.7-rc,描述的逻辑适用于5.7.7之前及5.6版本。但在即将发布的MySQL-5.7.8版本中, InnoDB根据索引id对AHI进行了分区处理,以此来降低btr_search_latch读写锁竞争,由于尚未发布,本文暂不覆盖相关内容。
我们以一个干净启动的实例作为起点,分析下如何进行AHI构建的过程。
AHI在内存中表现就是一个普通的哈希表对象,存储在
中,对AHI的查删改操作都是通过一个全局读写锁
- btr_search_sys_t::hash_index
来保护。
- btr_search_latch
在实例启动,完成buffer pool初始化后,会初始化AHI子系统相关对象,并分配AHI内存,大小为buffer pool的1/64。
参考函数:
- btr_search_sys_create
Tips:MySQL 5.7已经开始支持InnoDB buffer pool的动态调整,其策略是buffer pool的大小改变超过1倍,就重新分配AHI Hash内存(
)。
- btr_search_sys_resize
在系统刚启动时,索引对象上没有足够的信息来启发是否适合进行AHI缓存,因此开始有个信息搜集的阶段,在索引对象上维护了
,类型为
- dict_index_t::search_info
,用于跟踪当前索引使用AHI的关键信息。
- btr_search_t
在第一次执行SQL时,需要从btree的root节点开始,当寻址到匹配的叶子节点时,会走如下逻辑:
btr_cur_search_to_nth_level:
- if (btr_search_enabled && !index->disable_ahi) {
- btr_search_info_update(index, cursor);
- }
这里会判断脏读AHI开关(btr_search_enabled)是否打开,以及
是否为false。第二个条件是MySQL5.7对临时表的优化,避免临时表操作对全局对象的影响,针对临时表不做AHI构建。
- index->diable_ahi
我们看看函数btr_search_info_update的逻辑:
,当
- info->hash_analysis++
值超过
- info->hash_analysis
(17)时,也就是说对该索引寻路到叶子节点17次后,才会去做AHI分析(进入步骤2)
- BTR_SEARCH_HASH_ANALYSIS
- btr_search_info_update_slow
在连续执行17次对相同索引的操作后,满足
大于等于
- info->hash_analysis
的条件,就会调用函数
- BTR_SEARCH_HASH_ANALYSIS
来更新search_info,这主要是为了避免频繁的索引查询分析产生的过多CPU开销。
- btr_search_info_update_slow
InnoDB通过索引条件构建一个可用于查询的tuple,而AHI需要根据tuple定位到叶子节点上记录的位置,既然AHI是构建在Btree索引上的索引,它的键值就是通过索引的前N列的值计算的来,所有的信息搜集统计都是为了确定一个合适的”N” ,这个值也是个动态的值,会跟随应用的负载自适应调整并触发block上的AHI重构建。
包含三个部分:更新索引查询信息、block上的查询信息以及为当前block构建AHI,下面几小节分别介绍。
- btr_search_info_update_slow
参考函数:
- btr_search_info_update_hash
这里涉及到的几个search_info变量包括:
表示如果使用AHI构建索引,潜在的可能成功的次数;
- btr_search_t::n_hash_potential
若设置了新的建议前缀索引模式,则重置为0,随后的17次查询分析可以忽略更新search_info。
- btr_search_t::hash_analysis
下面两个字段表示推荐的前缀索引模式:
推荐构建AHI的索引列数;
- btr_search_t::n_fields
表示是否在相同索引前缀的最左索引记录构建AHI;值为true时,则对于相同前缀索引的记录,只存储最右的那个记录。 通过n_fields和left_side可以指导选择哪些列作为索引前缀来构建(fold, rec)哈希记录。如果用户的SQL的索引前缀列的个数大于等于构建AHI时的前缀索引,就可以用上AHI。
- btr_search_t::left_side
Tip1:在5.7之前的版本中,还支持索引中的字符串前缀作为构建AHI的键值的一部分,但上游认为带来的好处并不明显,因此将
移除了(参见commit 6f5f19b338543277a108a97710de8dd59b9dbb60, 42499d9394bf103a27d63cd38b0c3c6bd738a7c7)。 Tip2:然而上游在测试中发现,如果把n_bytes移除,可能在诸如顺序插入这样的场景存在性能退化(参阅commit 00ec81a9efc1108376813f15935b52c451a268cf),因此在新发布的MySQL5.7.8版本中又重新引入,本文分析代码时统一基于MySQL5.7.7版本。
- btr_search_t::n_bytes
两种情况需要构建建议的前缀索引列:
值为0,需要构建建议的前缀索引列;
- btr_search_t::n_hash_potential
,需要重新设置前缀索引列。
- (info->left_side == (info->n_fields <=cursor->low_match))
相关代码段:
- if (cursor->up_match == cursor->low_match) {
- info->n_hash_potential = 0;
- /* For extra safety, we set some sensible values here */
- info->n_fields = 1;
- info->left_side = TRUE;
- } else if (cursor->up_match > cursor->low_match) {
- info->n_hash_potential = 1;
- if (cursor->up_match >= n_unique) {
- info->n_fields = n_unique;
- } else if (cursor->low_match < cursor->up_match) {
- info->n_fields = cursor->low_match + 1;
- } else {
- info->n_fields = cursor->low_match;
- }
- info->left_side = TRUE;
- } else {
- info->n_hash_potential = 1;
- if (cursor->low_match >= n_unique) {
- info->n_fields = n_unique;
- } else if (cursor->low_match > cursor->up_match) {
- info->n_fields = cursor->up_match + 1;
- } else {
- info->n_fields = cursor->up_match;
- }
- info->left_side = FALSE;
- }
从上述代码可以看到,在low_match和up_match之间,选择小一点match的索引列数的来进行设置,但不超过唯一确定索引记录值的列的个数:
如果不是第一次进入seach_info分析,有两种情况会递增
:
- btr_search_t::n_hash_potential
;
- info->n_hash_potential
- if (info->n_fields >= n_unique && cursor->up_match >= n_unique) {
- increment_potential:
- info->n_hash_potential++;
- return;
- }
- if (info->left_side == (info->n_fields <= cursor->up_match)) {
- goto increment_potential;
- }
很显然,如果对同一个索引的查询交替使用不同的查询模式,可能上次更新的search_info很快就会被重新设置,具有固定模式的索引查询将会受益于AHI索引。
参考函数:
- btr_search_update_block_hash_info
更新数据页block上的查询信息,涉及到修改的变量包括:
最近一次成功(或可能成功)使用AHI;
- btr_search_info::last_hash_succ
计数值,如果使用当前推荐的前缀索引列构建AHI可能命中的次数,用于启发构建/重新构建数据页上的AHI记录项;
- buf_block_t::n_hash_helps
推荐在block上构建AHI的前缀索引列数;
- buf_block_t::n_fields
和search_info上对应字段含义相同。
- buf_block_t::left_side
函数主要流程包括:
为FALSE 这会导致在分析过程中无法使用AHI进行检索,感觉这里的设置不是很合理。这意味着每次分析一个新的block,都会导致AHI短暂不可用。
- btr_search_info::last_hash_succ
当block第一次被touch到并进入该函数时,设置block上的建议索引列值;以后再进入时,如果和索引上的全局search_info相匹配,则递增
- if ((block->n_hash_helps > 0)
- && (info->n_hash_potential > 0)
- && (block->n_fields == info->n_fields)
- && (block->left_side == info->left_side)) {
- if ((block->index)
- && (block->curr_n_fields == info->n_fields)
- && (block->curr_left_side == info->left_side)) {
- /* The search would presumably have succeeded using
- the hash index */
- info->last_hash_succ = TRUE;
- }
- block->n_hash_helps++;
- } else {
- block->n_hash_helps = 1;
- block->n_fields = info->n_fields;
- block->left_side = info->left_side;
- }
,启发后续的创建或重构建AHI。 如果当前数据页block上已经构建了AHI记录项,且
- block->n_hash_helps
等字段和
- buf_block_t::curr_n_fields
上对应字段值相同时,则认为当前SQL如果使用AHI索引能够命中,因此将
- btr_search_info
设置为true,下次再使用相同索引检索btree时就会尝试使用AHI。
- btr_search_info::last_hash_succ
简单来说,当满足下面三个条件时,就会去为整个block上构建AHI记录项:
- if ((block->n_hash_helps > page_get_n_recs(block->frame)
- / BTR_SEARCH_PAGE_BUILD_LIMIT)
- && (info->n_hash_potential >= BTR_SEARCH_BUILD_LIMIT)) {
- if ((!block->index)
- || (block->n_hash_helps
- > 2 * page_get_n_recs(block->frame))
- || (block->n_fields != block->curr_n_fields)
- || (block->left_side != block->curr_left_side)) {
- /* Build a new hash index on the page */
- return(TRUE);
- }
- }
)超过block上记录数的16(
- buf_block_t::n_hash_helps
)分之一;
- BTR_SEARCH_PAGE_BUILD_LIMIT
大于等于
- btr_search_info::n_hash_potential
(100),表示连续100次潜在的成功使用AHI可能性;
- BTR_SEARCH_BUILD_LIMIT
大于page上记录数的两倍、或者当前block上推荐的前缀索引列发生了变化 。
- block->n_hash_helps
如果在上一阶段判断认为可以为当前page构建AHI索引(函数
返回值为TRUE),则根据当前推荐的索引前缀进行AHI构建。
- btr_search_update_block_hash_info
参考函数:
- btr_search_build_page_hash_index
分为三个阶段:
);检查n_fields和page上的记录数;然后释放btr_search_latch的S锁;
- btr_search_drop_page_hash_index
- if (!block->index) {
- index->search_info->ref_count++;
- }
- block->n_hash_helps = 0;
- block->curr_n_fields = n_fields;
- block->curr_left_side = left_side;
- block->index = index;
PS:由于第二阶段释放了btr_search_latch锁,这里还得判断block上的AHI信息是否发生了变化,如果block上已经构建了AHI且block->curr_*几个变量和当前尝试构建的检索模式不同,则放弃本次构建。
AHI的目的是根据用户提供的查询条件加速定位到叶子节点,一般如果有固定的查询pattern,都可以通过AHI受益,尤其是Btree高度比较大的时候。
入口函数:
- btr_cur_search_to_nth_level
相关代码:
- /* Use of AHI is disabled for intrinsic table as these tables re-use
- the index-id and AHI validation is based on index-id. */
- if (rw_lock_get_writer(&btr_search_latch) == RW_LOCK_NOT_LOCKED
- && latch_mode <= BTR_MODIFY_LEAF
- && info->last_hash_succ
- && !index->disable_ahi
- && !estimate
- # ifdef PAGE_CUR_LE_OR_EXTENDS
- && mode != PAGE_CUR_LE_OR_EXTENDS
- # endif /* PAGE_CUR_LE_OR_EXTENDS */
- && !dict_index_is_spatial(index)
- /* If !has_search_latch, we do a dirty read of
- btr_search_enabled below, and btr_search_guess_on_hash()
- will have to check it again. */
- && UNIV_LIKELY(btr_search_enabled)
- && !modify_external
- && btr_search_guess_on_hash(index, info, tuple, mode,
- latch_mode, cursor,
- has_search_latch, mtr)) {
从代码段可以看出,需要满足如下条件才能够使用AHI:
为true表示最近一次使用AHI成功(或可能成功)了;
- btr_search_info::last_hash_succ
–>
- handler::multi_range_read_info_const
–>
- ha_innobase::records_in_range
–>
- btr_estimate_n_rows_in_range
;
- btr_cur_search_to_nth_level
当满足上述条件时,进入函数
,根据当前的查询tuple对象计算fold,并查询AHI;只有当前检索使用的tuple列的个数大于等于构建AHI的列的个数时,才能够使用AHI索引。
- btr_search_guess_on_hash
:
- btr_search_guess_on_hash
设置为false,这意味着随后的查询都不会去使用AHI了,只能等待下一路查询信息分析后才可能再次启动(
- btr_search_info::last_hash_succ
);
- btr_search_failure
)。
- btr_search_check_guess
如果本次查询使用了AHI,但查询失败了(
),并且当前block构建AHI索引的curr_n_fields等字段和btr_search_info上的相符合,则根据当前cursor定位到的记录插入AHI。参考函数:
- cursor->flag == BTR_CUR_HASH_FAIL
。
- btr_search_update_hash_ref
从上述分析可见,AHI如其名,完全是自适应的,如果检索模式不固定,很容易就出现无法用上AHI或者AHI失效的情况。
和
- dict_sys->mutex
的X锁;
- btr_search_latch
和
- dict_sys->table_LRU
链表,将每个表上的所有索引的
- dict_sys->table_non_LRU
设置为0;
- index->search_info->ref_count
;
- dict_sys->mutex
)清空为NULL;
- buf_block_t::index
- btr_search_disable
的ref_count不为0时,无法从数据集词典cache中将对应的表驱逐,workaround的方式是临时关闭AHI开关; 参考函数:
- index->search_info
、
- dict_table_can_be_evicted
- dict_index_remove_from_cache_low
- btr_search_update_hash_on_delete
、
- btr_search_update_hash_node_on_insert
- btr_search_update_hash_on_insert
- btr_search_drop_page_hash_index
在
函数中,首先会去判断在满足一定条件时,使用shortcut模式,利用AHI索引来进行检索。
- row_search_mvcc
只有满足严苛的条件时(例如需要唯一键查询、使用聚集索引、长度不超过八分之一的page size、隔离级别在RC及RC之上、活跃的Read view等等条件,具体的参阅代码),才能使用shortcut:
的S锁;
- btr_search_latch
检索记录;如果找到满足条件的记录,本次查询可以不释放 btr_search_latch,这意味着InnoDB/server层交互期间可能持有AHI锁,但最多在10000次(BTR_SEA_TIMEOUT)交互后释放AHI latch。一旦发现有别的线程在等待AHI X 锁,也会主动释放其拥有的S锁。
- row_sel_try_search_shortcut_for_mysql
然而, Percona的开发Alexey Kopytov认为这种长时间拥有的
的方式是没有必要的,这种设计方式出现在很久之前加锁、解锁非常昂贵的时代,然而现在的CPU已经很先进了,完全没有必要,在Percona的版本中,一次shortcut的查询操作后都直接释放掉
- btr_search_latch
(参阅bug#1218347)。
- btr_search_latch
我们可以通过
来监控AHI模块的运行状态
- information_schema.innodb_metrics
首先打开监控:
- mysql> set global innodb_monitor_enable = module_adaptive_hash;
- Query OK, 0 rows affected (0.00 sec)
- mysql> select status, name, subsystem from INNODB_METRICS where subsystem like '%adaptive_hash%';
- +---------+------------------------------------------+---------------------+
- | status | name | subsystem |
- +---------+------------------------------------------+---------------------+
- | enabled | adaptive_hash_searches | adaptive_hash_index |
- | enabled | adaptive_hash_searches_btree | adaptive_hash_index |
- | enabled | adaptive_hash_pages_added | adaptive_hash_index |
- | enabled | adaptive_hash_pages_removed | adaptive_hash_index |
- | enabled | adaptive_hash_rows_added | adaptive_hash_index |
- | enabled | adaptive_hash_rows_removed | adaptive_hash_index |
- | enabled | adaptive_hash_rows_deleted_no_hash_entry | adaptive_hash_index |
- | enabled | adaptive_hash_rows_updated | adaptive_hash_index |
- +---------+------------------------------------------+---------------------+
- 8 rows in set (0.00 sec)
重置所有的计数
- mysql> set global innodb_monitor_reset_all = 'adaptive_hash%';
- Query OK, 0 rows affected (0.00 sec)
该表搜集了AHI子系统诸如AHI查询次数,更新次数等信息,可以很好的监控其运行状态,在某些负载下,AHI并不适合打开,关闭AHI可以避免额外的维护开销。当然这取决于你针对具体负载的性能测试。
来源: http://mysql.taobao.org/monthly/2015/09/01/