一、基本概念
在打开一个表准备访问数据的时候,通常要先打开其数据字典,其中包含了字段信息,索引信息,默认值,字符集,统计数据,自增字段,自增锁等等信息,其中某些在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
网友评论