MySQL:AHI 部分代码流程说明

AHI实际上就是多个hash查找表,个数由innodb_adaptive_hash_index_parts进行控制其中的建为记录本生计算的,值为rec指针。

ha_node_set_data(node, block, data);如果开启debug会记录block这个属性。其中data就是rec的内存指针。

注意insert如果进行大量的插入,需要大量的定位操作,而不是一次定位顺序访问。及btr_cur_search_to_nth_level会每行插入都会调入,这为AHI的建立创造了基础。而delete和update如果批量修改则是一次定位顺序修改,不会过多的照成这种问题。

源码版本8.0.21

1、建立,每次数据定位的时候进行判断

btr_cur_search_to_nth_level
  从root节点开始查找,直到需要查找的level,下层page由函数btr_node_ptr_get_child_page_no获取
 ->btr_search_info_update 
    是否为空间索引,是否开启了AHI
    是否访问本索引达到了17次
    ->btr_search_info_update_slow
       ->btr_search_info_update_hash 
         //1、如果是新进入的,初始化btr_search_t结构信息。2、如果是老的信息 则进行判断是否和查询模式匹配 
         //匹配增加一次访问方式info->n_hash_potential++,否则初始化btr_search_t结构信息
       ->btr_search_update_block_hash_info
         //1、如果上一次的查询模式和本次相同,那么可以考虑问访问行数+1,如果不匹配则重置
         //2、如果访问的行数超过了块行数的1/16,并且相同访问模式100次,需要判断当前访问模式是否和本次相同,不同则
              需要新建page AHI结构    
         //3、函数返回值为是否需要新建page AHI结构,build_index(true)     
       ->build_index(true) btr_search_build_page_hash_index
         //查找hash parts所在的位置 受到参数innodb_adaptive_hash_index_parts的影响
         //如果当前的 查询模式和 需要建立的查询模式不匹配则需要删除旧的AHI信息
         ->btr_search_drop_page_hash_index
         //循环本块的全部记录,计算fold
         //index->search_info->ref_count++ 计数器增加
         //循环所有的fold,全部插入到hash parts
         ->每次调用ha_insert_for_fold_func
           ->计算hash值hash_calc_hash
           ->计算cell所在位置hash_get_nth_cell
           ->进行插入ha_node_set_data(node, block, data);

2、查找,每次数据定位的时候使用

数据定位的时候使用,先尝试使用AHI定位,在进行B+结构定位
btr_cur_search_to_nth_level
  ->btr_search_guess_on_hash
    ->ha_search_and_get_data进行hash表查找
    如果没有找到
    查找的page已经没在缓存
    都视为没有查找到

3、删除

DDL可能会调入

btr_drop_ahi_for_table
  是否存在索引
  进入大循环
    循环获取表上的全部索引
      如果这些索引中有block建立了hash结构,说明曾经达到过建立AHI的条件,
      将这个索引加入到扫描队列*end++ = index
    循环每一个instance,获取每一个每一个page,
      获取page的index
      将index在前面的扫描队列进行比对std::search_n(indexes, end, 1, index)
      如果查询成功将page加入到drop vector
    循环drop vector中的每一个数据块(这里已经是索引的全部数据块)
      ->btr_search_drop_page_hash_when_freed 
        根据信息块信息进行获取数据块,buf_page_get_gen,然后进行删除
        ->btr_search_drop_page_hash_index
          计算块所在的hash slot,ahi_slot
          循环块中所有的行
            计算其hash值fold = rec_fold(rec, offsets, n_fields, n_bytes, index_fold, index)
            这个rec_fold函数非常慢,需要通过字段的信息进行判定,见案例
            ->rec_fold
              循环每个字段计算hash值fold
              fold = ut_fold_ulint_pair(fold, ut_fold_binary(data, len));
            加入数组folds[n_cached] = fold
          循环整个数组folds[n_cached],进行hash结构的维护(删除)
            ha_remove_all_nodes_to_page(btr_search_sys->hash_tables[ahi_slot], folds[i],page);

分区表大量insert数据后增加分区慢,栈:

(gdb) bt
#0  0x0000000004fa1e73 in rec_offs_n_fields (offsets=0xb479910) at /opt/mysql/mysql-8.0.21/storage/innobase/rem/rec.h:449
#1  0x0000000004fa3d82 in rec_offs_data_size (offsets=0xb479910) at /opt/mysql/mysql-8.0.21/storage/innobase/include/rem0rec.ic:935
#2  0x0000000004fa7ad1 in rec_validate (rec=0x7fff450e7516 "", offsets=0xb479910) at /opt/mysql/mysql-8.0.21/storage/innobase/rem/rem0rec.cc:1337
#3  0x000000000519eeea in rec_fold (rec=0x7fff450e7516 "", offsets=0xb479910, n_fields=0, n_bytes=1, fold=108872348850564, index=0xb46d900)
    at /opt/mysql/mysql-8.0.21/storage/innobase/include/rem0rec.ic:1122
#4  0x00000000051a32e1 in btr_search_drop_page_hash_index (block=0x7fff4352f158) at /opt/mysql/mysql-8.0.21/storage/innobase/btr/btr0sea.cc:1177
#5  0x00000000051a38c3 in btr_search_drop_page_hash_when_freed (page_id=..., page_size=...) at /opt/mysql/mysql-8.0.21/storage/innobase/btr/btr0sea.cc:1275
#6  0x00000000051a3d23 in btr_drop_ahi_for_table (table=0xb46bc10) at /opt/mysql/mysql-8.0.21/storage/innobase/btr/btr0sea.cc:1354
#7  0x0000000004e61295 in alter_part_normal::try_commit (this=0xbaadc30, table=0x0, altered_table=0xbb75500, old_part=0xb474738, new_part=0xbb293c8)
    at /opt/mysql/mysql-8.0.21/storage/innobase/handler/handler0alter.cc:8550
#8  0x0000000004e4cdd5 in alter_parts::prepare_or_commit_for_new (this=0xb97f240, old_dd_tab=..., new_dd_tab=..., altered_table=0xbb75500, prepare=false)
    at /opt/mysql/mysql-8.0.21/storage/innobase/handler/handler0alter.cc:9657
#9  0x0000000004e4c6fe in alter_parts::try_commit (this=0xb97f240, old_dd_tab=..., new_dd_tab=..., table=0xaaef7b0, altered_table=0xbb75500)
    at /opt/mysql/mysql-8.0.21/storage/innobase/handler/handler0alter.cc:9584
#10 0x0000000004e4f83e in ha_innopart::commit_inplace_alter_partition (this=0xb47a018, altered_table=0xbb75500, ha_alter_info=0x7fff602dc670, commit=true, old_dd_tab=0xaaf06e8, 
    new_dd_tab=0xb924368) at /opt/mysql/mysql-8.0.21/storage/innobase/handler/handler0alter.cc:10515
#11 0x0000000004e4e8ee in ha_innopart::commit_inplace_alter_table (this=0xb47a018, altered_table=0xbb75500, ha_alter_info=0x7fff602dc670, commit=true, old_table_def=0xaaf06e8, 
    new_table_def=0xb924368) at /opt/mysql/mysql-8.0.21/storage/innobase/handler/handler0alter.cc:10216
#12 0x0000000003ad89a3 in handler::ha_commit_inplace_alter_table (this=0xb47a018, altered_table=0xbb75500, ha_alter_info=0x7fff602dc670, commit=true, old_table_def=0xaaf06e8, 
    new_table_def=0xb924368) at /opt/mysql/mysql-8.0.21/sql/handler.cc:4885
#13 0x00000000038242a5 in mysql_inplace_alter_table (thd=0xa9c2e50, schema=..., new_schema=..., table_def=0xaaf06e8, altered_table_def=0xb924368, table_list=0xbfde538, table=0xaaef7b0, 
    altered_table=0xbb75500, ha_alter_info=0x7fff602dc670, inplace_supported=HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE, alter_ctx=0x7fff602dd4f0, columns=..., fk_key_info=0xc0e5578, 
    fk_key_count=0, fk_invalidator=0x7fff602dd420) at /opt/mysql/mysql-8.0.21/sql/sql_table.cc:13007
#14 0x000000000382f85b in mysql_alter_table (thd=0xa9c2e50, new_db=0xbfdeaf0 "test", new_name=0x0, create_info=0x7fff602df2b0, table_list=0xbfde538, alter_info=0x7fff602df140)
    at /opt/mysql/mysql-8.0.21/sql/sql_table.cc:16834
#15 0x0000000003d9b9f8 in Sql_cmd_alter_table::execute (this=0xbfdedb8, thd=0xa9c2e50) at /opt/mysql/mysql-8.0.21/sql/sql_alter.cc:351
#16 0x000000000375bb41 in mysql_execute_command (thd=0xa9c2e50, first_level=true) at /opt/mysql/mysql-8.0.21/sql/sql_parse.cc:4573
#17 0x000000000375e5bf in mysql_parse (thd=0xa9c2e50, parser_state=0x7fff602e0b60) at /opt/mysql/mysql-8.0.21/sql/sql_parse.cc:5393
#18 0x00000000037535b7 in dispatch_command (thd=0xa9c2e50, com_data=0x7fff602e1c10, command=COM_QUERY) at /opt/mysql/mysql-8.0.21/sql/sql_parse.cc:1810
#19 0x0000000003751acc in do_command (thd=0xa9c2e50) at /opt/mysql/mysql-8.0.21/sql/sql_parse.cc:1294
#20 0x0000000003920cc5 in handle_connection (arg=0xa953a90) at /opt/mysql/mysql-8.0.21/sql/conn_handler/connection_handler_per_thread.cc:302
#21 0x00000000054dced5 in pfs_spawn_thread (arg=0xaa0b120) at /opt/mysql/mysql-8.0.21/storage/perfschema/pfs.cc:2880
#22 0x00007ffff7bc6ea5 in start_thread () from /lib64/libpthread.so.0
#23 0x00007ffff5e388dd in clone () from /lib64/libc.so.6    

4、关闭AHI

#0  btr_search_disable (need_mutex=true) at /opt/mysql/mysql-8.0.21/storage/innobase/btr/btr0sea.cc:304
#1  0x0000000004df3f07 in innodb_adaptive_hash_index_update (thd=0x7ffed80067d0, var=0x7ecc140 <mysql_sysvar_adaptive_hash_index>, var_ptr=0x7ed5178 <btr_search_enabled>, 
    save=0x7ffed939daa8) at /opt/mysql/mysql-8.0.21/storage/innobase/handler/ha_innodb.cc:19588
#2  0x0000000003795382 in sys_var_pluginvar::global_update (this=0xa708000, thd=0x7ffed80067d0, var=0x7ffed939da88) at /opt/mysql/mysql-8.0.21/sql/sql_plugin_var.cc:427
#3  0x00000000036493d6 in sys_var::update (this=0xa708000, thd=0x7ffed80067d0, var=0x7ffed939da88) at /opt/mysql/mysql-8.0.21/sql/set_var.cc:296
#4  0x000000000364be15 in set_var::update (this=0x7ffed939da88, thd=0x7ffed80067d0) at /opt/mysql/mysql-8.0.21/sql/set_var.cc:1091
#5  0x000000000364afc2 in sql_set_variables (thd=0x7ffed80067d0, var_list=0x7ffed8009ac8, opened=true) at /opt/mysql/mysql-8.0.21/sql/set_var.cc:797
#6  0x00000000037588da in mysql_execute_command (thd=0x7ffed80067d0, first_level=true) at /opt/mysql/mysql-8.0.21/sql/sql_parse.cc:3606
#7  0x000000000375e5bf in mysql_parse (thd=0x7ffed80067d0, parser_state=0x7fff600d0b60) at /opt/mysql/mysql-8.0.21/sql/sql_parse.cc:5393
#8  0x00000000037535b7 in dispatch_command (thd=0x7ffed80067d0, com_data=0x7fff600d1c10, command=COM_QUERY) at /opt/mysql/mysql-8.0.21/sql/sql_parse.cc:1810
#9  0x0000000003751acc in do_command (thd=0x7ffed80067d0) at /opt/mysql/mysql-8.0.21/sql/sql_parse.cc:1294
#10 0x0000000003920cc5 in handle_connection (arg=0xa7fb290) at /opt/mysql/mysql-8.0.21/sql/conn_handler/connection_handler_per_thread.cc:302
#11 0x00000000054dced5 in pfs_spawn_thread (arg=0xa7de740) at /opt/mysql/mysql-8.0.21/storage/perfschema/pfs.cc:2880
#12 0x00007ffff7bc6ea5 in start_thread () from /lib64/libpthread.so.0
#13 0x00007ffff5e388dd in clone () from /lib64/libc.so.6

五、补充

对于有大量数据定位操作的语句可能会有大量的AHI内存信息(btr_cur_search_to_nth_level),而AHI信息在block中也有很多数据结构进行保存,比如help 代表是否可以使用AHI,还有访问次数的元素。
建立AHI的条件以前理解有误实际上为如下:

同样的数据定位访问方式连续访问了page行数的1/16
同样的数据定位访问方式连续访问了100次
这是两种维度,不仅相同模式访问次数要足够,而且要超过page中行的数量的一定的值才会建立。


key   :查询用到索引列的各列的offset的找到的field值计算出来的fold值
       函数ut_fold_ulint_pair
value :为每行数据所在的内存地址rec

hash 函数为 ut_hash_ulint(fold, table->get_n_cells()) 也是根据fold取余

因为有多个AHI parts,因此需要找到响应的AHI parts,如下是计算hash table 数组的下标的方式,
 const ulint ahi_slot =
     ut_fold_ulint_pair(static_cast<ulint>(index_id),
                        static_cast<ulint>(block->page.id.space())) %
     btr_ahi_parts;



ha_node_t 就是实际的数据包含一个fold和一个rec data,fold为计算后的整数,data为rec 的指针,相关的内存结构如下(8.0.28):

                                                         debug(block frame)
                                                             
                                               ulint fold    ha_node_t *next   rec_t* data
                                                  |                                |
                                                  ----------------------------------           
                                                                |               
hash_table_t[0] -> [hash_cell_t[ha_node_t*]] --> ha_node_t* --> ha_node_t* --> ha_node_t* --> ha_node_t*
                  [hash_cell_t[ha_node_t*]]
                  [hash_cell_t[ha_node_t*]]
                  [hash_cell_t[ha_node_t*]]
                  [hash_cell_t[ha_node_t*]]
                  ...
                  (buf_pool_get_curr_size() / sizeof(void *) / 64 / parts 个 cell)
                  
               -> n_cells
                           -> heap     --> 内存相关,主要存储ha_node_t的实际数据

hash_table_t[1]
hash_table_t[2]
hash_table_t[3]
...              


ha_insert_for_fold_func         
btr_search_sys_create

Hash table size 4425293, node heap has 1 buffer(s)
Hash table size 4425293, node heap has 2 buffer(s)
Hash table size 4425293, node heap has 1225 buffer(s)
Hash table size 4425293, node heap has 3 buffer(s)
Hash table size 4425293, node heap has 14743 buffer(s)
Hash table size 4425293, node heap has 14752 buffer(s)
Hash table size 4425293, node heap has 14750 buffer(s)
Hash table size 4425293, node heap has 4 buffer(s)

Hash table size:
buf_pool_get_curr_size() / sizeof(void *) / 64 / parts = Hash table size 

node heap has 14743 buffer(s) 为heap 里面内存的mem buffer结构的大小(page为16K ),每一行做一个ha_node_t,如果算24字节,每一个buffer 就是14750*16*1024/24 ,debug模式下可能会多8个字节的block指针,因此观察show engine的这个值可以看出当前hash了多少行的数据。


(gdb) p block->len
$5 = 16384
(gdb) 

#0  mem_block_get_len (block=0x7fff857ac000) at /newdata/mysql-8.0.23/storage/innobase/include/mem0mem.ic:86
#1  0x000000000549ace6 in mem_heap_alloc (heap=0x7fffe036ab10, n=32) at /newdata/mysql-8.0.23/storage/innobase/include/mem0mem.ic:167
#2  0x000000000549c788 in ha_insert_for_fold_func (table=0x7fffe036aaa0, fold=11753707794445018339, block=0x7fff425f5100, data=0x7fff43180190 "\200")
   at /newdata/mysql-8.0.23/storage/innobase/ha/ha0ha.cc:246
   
   
一个block,只能被一种访问模式所hash,在函数btr_search_build_page_hash_index
中可以看到明显的替换和删除操作 8.0.28:
 if (block->index &&
     ((block->curr_n_fields != n_fields) || (block->curr_n_bytes != n_bytes) ||
      (block->curr_left_side != left_side))) { //  block 不能出现多次hash  只能被1个访问模式所hash
   btr_search_s_unlock(index);                 //因此 即便是同一个block,只要访问模式不一样
                                               //也可能 hash找不到,比如索引的field不对
   btr_search_drop_page_hash_index(block);     //还需要debug ,然后就进行了drop操作
 } else {
   btr_search_s_unlock(index);
 }
 
在进行了所有rec的fold计算后,最后更新了。
 block->curr_n_fields = n_fields;
 block->curr_n_bytes = n_bytes;
 block->curr_left_side = left_side;
 block->index = index;//这个值就代表了本block是否进行了hash操作,index就是索引的名称,在
select count(*) from information_schema.INNODB_BUFFER_PAGE_LRU where IS_HASHED != 'NO' ;
也这样体现。

drop 分区和add分区都会清空所有分区的AHI信息,最耗时的如下
循环每个分区调用函数
->btr_drop_ahi_for_table
  循环表(或者分区)中的每个索引,如果索引都没有用到AHI,
则退出
  循环innodb buffer中的每个实例,根据LRU链表循环每个page
如果page建立了AHI信息,且是要删除表(或者分区)的相关索引
  则放入drop vector容器中
如果page没有建立AHI信息 
则跳过
如果drop verctor容器中填满1000个page
则清理一次,循环每个page,调用函数
->btr_search_drop_page_hash_index
  计算page所在AHI结构的slot信息,以便找到对应的hash_table_t结构
  循环page中所有的行
    循环行中访问到的索引字段(访问模式),计算出fold信息填入到fold[]数组中
    本循环中会通过函数rec_get_offsets进行字段偏移量的获取,为耗用CPU的函数
      循环fold[]数组,一个fold代表一行数据,调用函数
       ->ha_remove_all_nodes_to_page,为耗用CPU的函数
         ->ha_chain_get_fist
           根据fold信息找到hash结构的cell
         循环本cell中的链表信息
           如果行的地址在本要删除的page上,调用函数
           ->ha_delete_hash_node,为消耗CPU的函数
             进行链表和hash结构的维护
每次处理完1000个page后,yeild线程主动放弃CPU,避免长期占用CPU,醒来后继续处理
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352