今天在跑高压力高并发下只读查询时,发现个比较有意思的小问题
先来看看performance schema
root@performance_schema 03:13:45>SELECT COUNT_STAR, SUM_TIMER_WAIT, AVG_TIMER_WAIT, EVENT_NAME FROM events_waits_summary_global_by_event_name where COUNT_STAR > 0 and EVENT_NAME like ‘wait/synch/%’ order by SUM_TIMER_WAIT desc limit 20;
+————-+——————+—————-+—————————————————+
| COUNT_STAR | SUM_TIMER_WAIT | AVG_TIMER_WAIT | EVENT_NAME |
+————-+——————+—————-+—————————————————+
| 794349716 | 9217250429384944 | 11603268 |wait/synch/mutex/sql/LOCK_table_cache |
| 395819330 | 8052052171747844 | 20342452 | wait/synch/rwlock/sql/LOCK_grant |
| 18792871674 | 3690042369314320 | 196200 | wait/synch/mutex/sql/THD::LOCK_query_plan |
| 11274795644 | 2143435212448448 | 190096 | wait/synch/mutex/sql/THD::LOCK_thd_data |
可以看到,目前排名最高的是LOCK_table_cache。 从backtrace中发现如下堆栈(简化版本):
18 __lll_lock_wait(libpthread.so.0)…,lock(thr_mutex.h:61),
open_table(thr_mutex.h:61),open_and_process_table(sql_base.cc:4903),open_tables(sql_base.cc:4903),
open_normal_and_derived_tables(sql_base.cc:6084),execute_sqlcom_select(sql_parse.cc:5004),……
大量线程被堵塞在open_table这里了,我们来看看具体的代码 (quoted from 5.7.5, sql/sql_base.cc)
3172 retry_share:
3173 {
3174 Table_cache *tc= table_cache_manager.get_cache(thd);
3175
3176 tc->lock();
3177
3178 /*
3179 Try to get unused TABLE object or at least pointer to
3180 TABLE_SHARE from the table cache.
3181 */
3182 table= tc->get_table(thd, hash_value, key, key_length, &share);
3183
我们知道从5.6开始可以对table cache进行分区,参数table_open_cache_instances来控制分区的个数,从而消除了之前版本的热点锁LOCK_open.
之前没看过这部分的代码,一直以为是根据表名之类的来进行分区的,今天才发现是根据线程id进行分区.
155 /** Get instance of table cache to be used by particular connection. */
156 Table_cache* get_cache(THD *thd)
157 {
158 return &m_table_cache[thd->thread_id() % table_cache_instances];
159 }
这种分区方式可以避免例如热点表这样的场景,但也可能带来负面的影响,例如别的分区里可能有大量空闲的TABLE对象,而当前线程的分区中没有,这种情况下依然要创建TABLE对象加入到当前分区中。
另外LOCK_open并不是能完全避免的,当无法从table cache中找到TABLE对象时,依然要持有LOCK_open来检查TABLE_SHARE的版本是否是有效的。不过创建TABLE对象及加入hash的过程无需持有LOCK_open。