美文网首页
MySQL:参数Open_tables/Open_table_d

MySQL:参数Open_tables/Open_table_d

作者: 重庆八怪 | 来源:发表于2023-07-28 17:10 被阅读0次

    一、基本概念

    在打开一个表准备访问数据的时候,通常要先打开其数据字典,其中包含了字段信息,索引信息,默认值,字符集,统计数据,自增字段,自增锁等等信息,其中某些在MySQL层,有些在Innodb层(比如统计信息),又比如字段类型在MySQL层和Innodb层的表示是不同的,实际上这包含3层信息:

    • table instance:MySQL层相关的字典信息,每个会话独占由table share生成而来。在语句结束的时候释放,这里的释放并不是真正的释放,可能是缓存,其缓存的个数和table_open_cache和table_open_cache_instances有关,其缓存位置约定为table cache,内部为TABLE。
    • table share:MySQL层相关字典的信息,整个数据库只有一份,每个表都包含一个,5.7来自FRM文件,8.0则来自新的SDI缓存相关字典信息。当没有table cache引用的时候会考虑释放,但是同前面一样,释放可能是缓存,缓存的个数和table_definition_cache有关,其缓存位置约定为table def cache,内部为TABLE_SHARED。
    • dict table:Innodb层的信息,这个数据库只有一份,每个表都包含一个,其信息来自Innodb的数据字典,主要是SYS_TABLES/SYS_COLUMN等几个表。当没有table cache引用的时候会考虑释放,同样释放可能是缓存,缓存的个数依旧和table_definition_cache有关,其缓存位置约定为dict table cache,内部为dict_table_t

    我们可以不管那一层都涉及到缓存,如果缓存能够命中,则能够大大的减少获取表字典信息的代价,这也是缓存的意义。

    二、关于相关的数据结构和LRU管理

    当分析他们的数据结构的时候,可以发现无一例外的是table cache/table def cache/dict table cache均为一个HASH结构和一个LRU结构,HASH结构通常用于定位某一个表的字典信息,而LRU结构通常用于判断先淘汰哪个字典信息,并且HASH结构的key 都是DBNAME.TABLENAME通过hash函数计算得到的,也就是查找的时候通过DBNAME.TABLENAME就可以了。
    不同的是table cache更为复杂,它一共分了table_open_cache_instances个实例,我们这里以16为例,每个instance(约定为instance)能够缓存的table instance的数量为table_open_cache/table_open_cache_instances,并且其并不是直接在hash结构存储的table instance的指针而是存储的Table_cache_element元素的指针,也就是每个instance中按照表名为单位进行缓存的这个元素叫做Table_cache_element简称el,而打开的table instance则挂载在其el下面,并且每个el也分为了used_tables链表和free_tables链表,并且含一个table shared的指针。除此之外在table shared下也包含一个el指针的数组,并且大小为table_open_cache_instances的个数,这是因为在分配table instance的时候是按照session来分配的,也就是一个session最多用到一个instance,因此最多的情况下一个表的字典信息可能在每个instance都会存在,因此需要一个el对象。
    并且每个基础对象,及TABLE/TABLE_SHARE/dict_table_t 都要包含一些完成链表的结构,将他们串联起来,但是需要注意的是TABLE中有2个链表信息分别为:

    • TABLE *next, *prev :用于el中的free_tables或者used_tables链表。
    • TABLE *cache_next, **cache_prev:用于Table_cache中的m_unused_tables链表。

    而在这些链表中很多带有LRU功能如下:

    • table cache的m_unused_tables为LRU管理
    • table def cache的unused_share list为LRU管理
    • dict table cache的table_LRU为LRU管理

    同时相关的参数就只有3个:

    • table_open_cache
    • table_open_cache_instances
    • table_definition_cache

    前面两个已经描述过了,主要用于判定每个instance是否有需要淘汰的table instance。而table_definition_cache则更加诡异,居然和innodb的master线程有关,其实这个参数不仅控制了table def cache的大小,还控制dict table cache的大小,并且其清理并不是由当前线程自己做的,而是master线程循环完成清理。

    那么他们三者的数据结构图大概为如下,当然这里只是画出和本处相关的一些结构:


    image.png image.png image.png

    三、table instace打开过程

    打开table instace的过程,实际上就是按照缓存分为了3步。

    • 第1步,是否可以通过table cache直接找到可用的table instance
      首先通过session的thread id获取到底用那个instance,这里也代表某个session只能用一个instance,如下
    thd->thread_id() % table_cache_instances
    

    然后通过DBNAME.TABLENAME去table cache中查找,实际上就是先通过hash结构找到el,然后在el的free_tables链表中获取一个table instance就可以了。当然获取后需要维护相关的结构比如从free_tables链表放入到used_tables链表,并且在m_unused_tables取下。如果这一步命中则统计值Table_open_cache_hits增加1,代表本次打开命中table cache缓存,如果不能命中则Table_open_cache_misses增加1。

    • 第2步,如果第1步没有命中,则需要查找table def cache是否可以直接打开table share,然后构造table instace

    首先依旧是在table def cache中查找(get_table_share_with_discover),如果没有找到则需要打开frm文件或者sdi缓存进行table share的构建,并且加入到table def cache中。如果找到了则是维护unused share list链表,需要将其摘下。同时不管在table def cache中找到了table share还是没找到,只要到了这一步都需要将share->ref_count++,这样代表本table share被前端某个table instance引用了,如果淘汰是不能淘汰这种table share的。那么share->ref_count就代表了前端有多少个table instance基于这个table share打开。

    接着就需要判断是否需要淘汰table def cache中缓存的table share,判定标准为:

    • A:是否超过了table_definition_cache的大小
    • B:unused share list链表是否存在值

    这个清理过程会一直持续到条件不满足为止。

    • 第3步,接下来虽然table share有了,但是还需要构建table instance才可以

    这一步(open_table_from_share)重点的就是建立table instance和innodb 之间的句柄,并且完成table share信息到table instance的拷贝,还要完成dict table的查找或者建立,这里主要描述和学习的是dict table的查找或者建立,我们就描述一下。首先,在dict table cache中查找,如果不能找到则需要通过字典表进行dict_table_t的建立,然后插入到dict table cache和dict LRU链表中,并且本dict table的n_ref_count指标加1,通table share一样,这个指标是dict table cache清理的时候能否清理的重要标准,也代表前端有多少table instance应用了本dict table。
    当table instance建立好于innodb的联系后,本table instance 同样要加入到table cache中(Table_cache::add_used_table),当维护数据结构之后依旧需要判断table cache是否需要淘汰某些table instance(Table_cache::free_unused_tables_if_necessary),淘汰的标准有2个如下:

    • A:大于table_open_cache/table_open_cache_instances
    • B:m_unused_tables中有没有用到的table instance

    这个清理过程也是持续到条件不满足为止。
    这个淘汰过程除了维护数据结构,任何一个table instance淘汰都会导致dict table的计数器n_ref_count 减1,同时也会触发share->ref_count计数器的建1,并且如果table def cache中缓存的table share大于了参数table_definition_cache的设置,也会进行table share的淘汰。当这里淘汰任何一个table instance的时候统计值Table_open_cache_overflows都会+1。

    因此我们发现dict table中的n_ref_count和table share中的ref_count都代表了前端有多少table instance依赖他们,同时也是各自cache清理的标准。打开table instance的过程可能还会涉及到table cache和table def cache的淘汰,但是dict table cache的淘汰,不是session 自己干的。

    因此对于table def cache的淘汰,有2个地方:

    • 新建table share插入到table def cache中的时候
    • 新建table instance或者关闭table instance的时候(Table_cache::free_unused_tables_if_necessary调用)

    四、table instace关闭过程

    当一个session的语句执行完成后会执行close_thread_tables,这个过程就涉及到table instance的关闭,关闭后到底是缓存还是淘汰这依据参数设置而不同。

    首先,先维护table cache的数据结构,将本次关闭的表从el的used_tables list中取下来放入到free_tables中,并且放入到Table cahe的m_unused_tables中。
    接下来直接调用Table_cache::free_unused_tables_if_necessary来判断是否需要进行table cache/table def cache的淘汰,淘汰标准前文已经描述。并且将table share和dict table的相应计数器减1。
    当然这个过程如果不需要淘汰table cache中的内容则table share和dict table各自的计数器不会减1。

    五、关于dict cache的淘汰和flush tables的影响

    前面我们分析了table cache/table def cache/dict cache的加入和table cache/table def cache的淘汰,但是对于dict cache的淘汰则是由master线程完成如下:

    srv_master_thread
     ->srv_master_do_idle_tasks
       ->srv_master_evict_from_table_cache
         ->dict_make_room_in_cache
    n_tables_evicted = dict_make_room_in_cache(innobase_get_table_cache_size(), pct_check);
    

    这里的innobase_get_table_cache_size()就是参数table_definition_cache的大小,其淘汰的方式为:

    • A.如果是active每次不能清理超过缓存数量的50%(线上通常为这个)
    • B.如果是idle每次为100%
    • C.如果缓存数量小于innobase_get_table_cache_size的大小这不做清理

    而对于flush tables来说,它会清空所有的table cache和table def cache,但是对于dict cache来讲并不会清理,debug如下:

    (gdb) p dict_sys->table_LRU
    $4 = {count = 19, start = 0x7fff1002a5b0, end = 0x64957e0, node = &dict_table_t::table_LRU, init = 51966}
    (gdb) p table_def_cache
    $5 = {key_offset = 0, key_length = 0, blength = 1, records = 0, flags = 0, array = {buffer = 0x2fa6910 "\377\377\377\377", elements = 0, max_element = 400, alloc_increment = 511, 
        size_of_element = 16, m_psi_key = 11}, get_key = 0x14da2a2 <table_def_key(uchar const*, size_t*, my_bool)>, free = 0x14da2da <table_def_free_entry(TABLE_SHARE*)>, 
      charset = 0x2d05440 <my_charset_bin>, hash_function = 0x1886943 <cset_hash_sort_adapter>, m_psi_key = 11}
    (gdb) p table_def_cache.records
    $6 = 0
    (gdb) p dict_sys->table_LRU.count
    $7 = 19
    

    这是执行了flush tables后的结果,可以看到dict table的LRU链表中依旧有19个元素,而table def cache中的记录为0。

    六、代价和命中率

    从整个代价来看,如果只是table instance在table cache中比命中,但是table def cache和dict table cache都命中的话,代价会小很多,而对于table cache的命中我们通常观察的指标如下:

    • Table_open_cache_hits:能够从table share 的free list 中找到一个instance(table cache),则看做命中,值+1。
    • Table_open_cache_misses:Table_open_cache_hits相反,如果找不到则需要重新实例化值+1,这通常发生在初始化第一次加载表,或者由于超过参数table_open_cache的设置被淘汰后需要重新实例化。
    • Table_open_cache_overflows:就是上面说的淘汰的instance(table cache)的数量,每次淘汰值+1。
    • Open_tables:总的instance(table cache)的总数。

    但是table def cache和dict table cache的命中率视乎没有指标,但是我们将参数table_definition_cache可以稍微设置大一点,因为这两个缓存都是全局的,每个表就1个,如果我系统又3000个表,我们可以设置为4096之类的,尽量让每个表的基本定义都能够缓存。当然我们也可以观察,

    • Open_table_definitions:这个指标就是原滋原味的table def cache的大小如下
    uint cached_table_definitions(void)
    {
      return table_def_cache.records;
    }
    

    但是需要注意这个指标代表是table def cache,而不能代表dict table cache,但是我们前面知道dict table cache也依赖参数table_definition_cache参数,除了flush tables后不太一致,一般情况下还是比较一致的。

    七、关于 show open tables语句

    这个语句实际上table cache和table_def_cache息息相关,其调用的函数为list_open_tables,其主要有用的字段为In_use字段,这个语句实际上是遍历table_def_cache每个缓存的table share,然后在table cache进行查找,到底有多少个缓存的table instance就输出到In_use字段,其实现方式也就是查询table share中的cache_element数组,然后统计每个el中的used_tables链表长度即可。而对于Name_locked字段貌似没什么用,代码如下:

    while (it++) ++(*start_list)->in_use;
     (*start_list)->locked = 0; /* Obsolete. */
    

    可以看到Name_locked始终为0,且标记为 Obsolete。

    八、相关代码

    
    hash值来源 --> mdl lock key 
               --> el
    计算hash值函数
     my_calc_hash(&table_def_cache, (uchar*) key, key_length)
         
    open table阶段打开 table供线程使用:
    open_table
      ->Table_cache *tc= table_cache_manager.get_cache(thd)
        获取一个 cache 注意这里是根据thd进行获取的,因此一个session只能用一个cache instance
      ->table= tc->get_table(thd, hash_value, key, key_length, &share)
        是否找到空闲的table cache instance,其中key为库名\0+表名\0,key_length为长度,hash_value为通过hash函数计算的key 的hash值
        -> el= (Table_cache_element*)my_hash_search_using_hash_value
          先找到el元素
        ->table= el->free_tables.front()
          获取free tables列表的第一个元素
        ->el->free_tables.remove(table)
          从free_tables链表中剔除,这是table中hash的el元素的数据结构
        ->unlink_unused_table(table)
          从unused链表中剔除,这是table cache的链表结构
        ->el->used_tables.push_front(table); 
          插入到used tables链表中
        ->table->in_use= thd
          线程在使用
      -> 如果找了table cache
        table_open_cache_hits++
        跳到
        goto table_found
      -> 如果没找到
         检查table share是否存在,如果存在跳到
         goto share_found
         这是因为可能el 已经建立只是其中的table cache全部被淘汰掉了
      -> 如果什么都没找到table share,这建立table share
         ->get_table_share_with_discover 
           先在table share cache查找,如果没找到则5.7打开frm文件,8.0为在缓存的数据字典中查找
           ->get_table_share
             ->my_hash_search_using_hash_value((&table_def_cache),...)
               如果在table shared cache中找到跳到goto found
             ->share= alloc_table_share(table_list, key, key_length)
               如果没知道就需要建立table share了
             ->my_hash_insert(&table_def_cache, (uchar*) share)
               插入table_def_cache hash 结构中
    found:
             ->share->ref_count++
             ->open_table_def(thd, share, db_flags)
               这是5.7的调用为根据frm信息建立share信息,8.0 就是根据字典缓存复杂很多
             ->维护unused 链表,从unused 链表中删除
                   *share->prev= share->next;
                    share->next->prev= share->prev;
                    share->next= 0;
                    share->prev= 0;
             ->while((table_def_cache.records > table_def_size && oldest_unused_share->next))
               如果大于参数table def设置的大小进行删除,从没有使用的shared中删除,通过
               oldest_unused_share为LRU管理
               oldest_unused_share和end_of_unused_share 管理着链表头和链表尾
               ->my_hash_delete(&table_def_cache, (uchar*) oldest_unused_share)
    share_found:
      ->跳过一些MDL LOCK相关的内容
      ->table= (TABLE*) my_malloc(key_memory_TABLE,sizeof(*table), MYF(MY_WME))
        新建table结构
      ->open_table_from_share
        通过table share 打开table,并且打开表的定义字典dict_table
        ->handler::ha_open
          ->ha_innobase::open
            ->dict_table_t ha_innobase::open_dict_table
              ->dict_table_open_on_name
                ->dict_table_check_if_in_cache_low
                  dict_sys->table_hash 扫描, 查看是否已经缓存,没有就要进行加载
                ->if (table == NULL) 
                  如果没有找到注意这里是dict_table_t
                  ->dict_load_table
                    ->dict_load_table_one(table_name, cached, ignore_err,fk_list)
                      ->dict_load_table_low
                        ->dict_sys_tables_rec_read
                          读取sys记录
                        ->dict_mem_table_create
                          建立dict_table_t
                      ->dict_table_add_to_cache
                        ->HASH_INSERT
                          加入到dict table hash   key为通过table->name.m_name计算的fold,value为 dict_table_t 指针
                        ->UT_LIST_ADD_FIRST(dict_sys->table_LRU, table)
                          同时也加入到dict sys LRU中
                    ->fk_table_name.m_name
                      将外键表一起加载dict_load_table_one
                ->dict_table_t::acquire
                  这里引入号+1,每次table cache的建立都会+1,当然语句结束后table cache会
                  进入缓存,这个是能够释放dict table的重要根据
                  ++n_ref_count        
            ->dict_stats_init(ib_table)
              初始化统计数据
            ->m_prebuilt = row_create_prebuilt(ib_table, table->s->reclength)
              Create a prebuilt struct for a MySQL table handle
      ->Table_cache *tc= table_cache_manager.get_cache(thd)
      ->tc->add_used_table(thd, table)
        加入到add 列表
      ->thd->status_var.table_open_cache_misses++
        增加miss
    table_found:  
      ->
        
    
    加入:
    
    Table_cache::add_used_table
      ->el= table->s->cache_element[table_cache_manager.cache_index(this)]
        先在table的share的cache元素中查找
      ->if (!el) 
        如果指针不存在,也代表没有缓存过这个表的table cache
        ->el= new Table_cache_element(table->s)
          分配一个Table_cache_element元素的内存
        ->my_hash_insert(&m_cache, (uchar*)el)
          因为cache instance已经定了,这里就直接插入这个instance的hash查找表就可以了,key 看起来为el元素的
          指针
          -> pos->data=(uchar*) record 这里的record就是el的指针
        ->table->s->cache_element[table_cache_manager.cache_index(this)]= el
          并且要放入到响应table shared的cache中。也就是这个指针两个地方都要放
      -> el->used_tables.push_front(table)
         将table cache放入到这个table shared的的cache_element的used链表中,插入到开头
      -> m_table_count++
         增加
      -> free_unused_tables_if_necessary
         同下。
    
    close_thread_tables
     ->close_open_tables 
       ->intern_close_table          
         ->closefrm
           ->release_table_share          
                
    删除:
    close_thread_tables
     ->close_open_tables 
       if (table->s->has_old_version() || table->needs_reopen() || table_def_shutdown_in_progress)
       只有在这些情况下才会考虑调用intern_close_table
       ->intern_close_table           
         ->closefrm
           ->release_table_share   
       否则只是调用如下 
       ->tc->release_table(thd, table)
         ->Table_cache_element *el= table->s->cache_element[table_cache_manager.cache_index(this)];
         ->el->used_tables.remove(table);
         ->el->free_tables.push_front(table);
         ->link_unused_table(table);
         ->Table_cache::free_unused_tables_if_necessary(thd)
           判断是否有必要释放unused链表的表
           ->while (if (m_table_count > table_cache_size_per_instance && m_unused_tables))
             A、大于table_cache_size_per_instance  B、存在m_unused_tables
             循环删除
             ->TABLE *table_to_free= m_unused_tables
             ->Table_cache::remove_table(table_to_free)
               ->Table_cache_element *el= table->s->cache_element[table_cache_manager.cache_index(this)]
               ->el->free_tables.remove(table)
               ->unlink_unused_table(table);
               ->m_table_count--
               -> if (el->used_tables.is_empty() && el->free_tables.is_empty())
                 如果el中的几个链表为空这需要删除这个el
                 ->my_hash_delete(&m_cache, (uchar*) el)
                   从hash结构删除
                 ->table->s->cache_element[table_cache_manager.cache_index(this)]
                   设置为NULL
             ->intern_close_table
               ->closefrm
                 ->handler::ha_close
                   ->ha_innobase::close
                     ->row_prebuilt_free
                       ->dict_table_clos
                         ->dict_table_t::release
                          ->--n_ref_count  
                   ->唤醒master线程清理dict      
                 ->delete table->file
                   删除和innodb的句柄
                 ->release_table_share
                   ->如果--share->ref_count 计数减少到0,这需要将这个table share放到unused_share中,而淘汰table share也是以unused_share
                     为来源的,先维护unused_share list 链表
                   ->if (!--share->ref_count)
                     根据ref_count来判断是否放入unused_share list
                   ->加入到unused_share list的尾部
                     share->prev= end_of_unused_share.prev;
                     *end_of_unused_share.prev= share;
                     end_of_unused_share.prev= &share->next;
                     share->next= &end_of_unused_share;
                   ->table_def_cache.records > table_def_size
                     从my_hash_delete(&table_def_cache, (uchar*) oldest_unused_share)
                     删除一个table shared
             ->thd->status_var.table_open_cache_overflows
               增加
                 
             
    
    master 线程会检测是否有需要清理的dict table,其中重要的标准就是refcount为0,但是有几个判断
    A.如果是active刷新每次不能清理超过缓存数量的50%
    B.如果是空闲刷线每次为100%
    C.如果缓存数量小于innobase_get_table_cache_size的大小这不做清理
    
             
    srv_master_evict_from_table_cache     
    

    相关文章

      网友评论

          本文标题:MySQL:参数Open_tables/Open_table_d

          本文链接:https://www.haomeiwen.com/subject/skarpdtx.html