改进动态设置query cache导致额外锁开销的问题分析及解决方法

关键字:dynamic switch for query cache,  lock overhead for query cache

背景

Query Cache是MySQL Server层的一个非常好的特性,对于小数据集或访问量非常集中的应用场景,有非常好的性能提升,内部细节可以参考1,在此处不打算展开Query Cache的一些应用特性。

Query Cache引入了一新的问题, 即如果你不想要Query Cache的功能(彻底地不要执行任何query cache的任何代码),只能在编译时就指定 –without-query-cache,也就是用宏开关把相关代码不编译。
为什么要这么做,原因就是一个社区里的Known Issue: 如果用户在运行时动态关闭query cache, 会导致额外CPU的开销,即对query cache加解锁操作。在负载非常高的MySQL服务器上,这个问题变得尤为突出。

在Oracle MySQL版本中,此问题一直无人解决,在MySQL 5.6中亦如此,在MariaDB中也一样。 Percona对此有提出自己的解决方案,不需要在编译时指定,可以在MySQL启动时指定–query_cache_type=0。这确实已经是一个大的进步,至少用户在想用时不要重新编译。但需要指出的是,Percona版本为些付出的代价是,用户不能再动态地将uery_cache_type从0改为其它值:

SET GLOBAL query_cache_type=ON;
ERROR 1651(HY000): Query cache is disabled; restart the server with query_cache_type=1 to enable it

问题分析

首先看下,Percona的优化为什么必须要在启动时配置–query_cache_type=0才能达到和编译时指定–without-query-cache一样的效果。

对query cache典型的操作,例如在select时要insert cache, 在更新时需要invalidate cache, 判断的依据就是
2307 if (is_disabled())
2308 DBUG_VOID_RETURN;

而实质动作就是返回私有成员变量:m_query_cache_is_disabled
class Query_cache
{
public:
bool is_disabled(void) { return m_query_cache_is_disabled; }
}

这个变量则是在Query_cache构造函数中初始化,Query_cache::init中根据系统变量query_cache_type来设置:
2618 if (global_system_variables.query_cache_type == 0)
2619 query_cache.disable_query_cache();

class Query_cache
{
public:
void disable_query_cache(void) { m_query_cache_is_disabled= TRUE; }
}

这个逻辑就意味着这个值后面不可能被再次更改。
那么,它是如何做到没有调用额外锁开销的呢?

在5.5中,启动时–query_cache_type=0可以完全不用query cache, 以及操作query cache的锁。以PS5.5.18为例来分析其代码逻辑:

2307 if (is_disabled())
2308 DBUG_VOID_RETURN;
….

2326 invalidate_table(thd, tables_used); // 以下为此函数的实现部分代码
3312 lock();
3316 if (query_cache_size > 0)
3317 invalidate_table_internal(thd, key, key_length);
3319 unlock();

这个代码片断已经可以看出其原因了,因为is_disabled()返回false, 此函数直接return。
同时我们也可以看出,如果用户启动时指定query cache,即–query_cache_type=1,那么后面如果不想用query cache时,必须还要将query_cache_size设置为0,否则还是要调用invalidate_table_internal()。但整个过程 还是是调用lock()/unlock()。当然这是针对Oracle MySQL或MariaDB,因为Percona无法动态将0改为1或2。

解决方法

依照上面的分析,需要将is_disabled进行扩展,先看下核心代码片断:

+bool Query_cache::is_disabled_ext(void)
+{
+ /* disabled -> enabled */
+ if (is_trace_disabled() && global_system_variables.query_cache_type !=0)
+ {
+ query_cache.update_query_cache_trace(true);
+ return false;
+ }
+
+ /* enable -> disable */
+ if (!is_trace_disabled() && global_system_variables.query_cache_type ==0)
+ {
+ query_cache.update_query_cache_trace(false);
+ query_cache.flush();
+ return true;
+ }
+
+ return is_trace_disabled();
+}

is_trace_disabled就是判断上次是否设置为disable query cache:
+ bool is_trace_disabled(void) { return !m_query_cache_trace; }
update_query_cache_trace就是更新m_query_cache_trace:
+ void update_query_cache_trace(bool new_flag) { m_query_cache_trace = new_flag; }

其实上面代码应该是比较清楚的,但我还是稍微解释下is_disabled_ext的逻辑,这个含有3个节点的状态机还是比较简易的,用例子说明:

  1. 如果用户启动时为0,此时is_trace_disabled()为true(init中设置为flag为false),并且global_system_variables.query_cache_type为0,那么逻辑是走第三个return返回true,即query cache不可用。
  2. 如果用户启动时为1,此时is_trace_disabled()为false(构造函数中初始化成员列表时被置为true),并且global_system_variables.query_cache_type为1,那么逻辑是走第三个return返回true,即query cache可用。
  3. 如果用户想将query cache从1变为2, 那么is_trace_disabled()返回false(因为曾经是1),并且global_system_variables.query_cache_type为2,那么逻辑是走第三retrun返回false,调用者得知,当前query cache可用(正常的锁开销)
  4. 如果用户想将query cache从0变为1, 那么is_trace_disabled()返回true(因为曾经是0),并且global_system_variables.query_cache_type为1,那么逻辑是走第一个if返回false,调用者得知,当前query cache可用!(正常的锁开销,实现Percona版本存在的缺陷:动态从0到1的效果)下次再判断时,is_trace_disabled()为false,并且global_system_variables.query_cache_type为1, 那么第三个return返回false,即query cache仍可用。
  5. 如果用户想将query cache从1变为0, 那么is_trace_disabled()返回false(因为曾经是1),并且global_system_variables.query_cache_type为0,那么逻辑是走第二个if返回true,调用者得知,当前query cache被disable!(不会走锁开销逻辑,达到启动时设置一样的效果)下次再判断时,is_trace_disabled()为true,并且global_system_variables.query_cache_type为0, 那么第三个return返回true,即query cache仍不可用。

其中1->2和2->1, 0->1和0->2,1->0和2->0逻辑一样。2无非是一种增强约束条件的1而已。

可能的风险

反思为什么各大公司版本中一直没有改动?从上面的过程看,彻底解决此问题是可行的。那不是太难的问题,为什么各大公司的没有去解决呢?我想可能有如下几个因素需要考虑:

  • 并发是否会有读脏数据问题

如果一个用户正在设置query cache开关, 而其它线程可能正在读取query cache的状态,此时,会不会有什么问题?

  1. 如果用户设置0->1, 此时is_disabled_ext()可能返回query cache仍不可用,此处竞争不会导致问题。
  2. 如果用户设置1->0, 此时is_disabled_ext()可能返回query cache仍可用,然后读取其内容。而此时恰有事务修改此记录,这和正常情况一样,读取仍是此事务之前的最新记录值。

另外,query_cache.flush() 会加锁,所以也不用担心这个问题。

  • 并发更新状态值问题

如果多个用户同时更新query cache的开关,即is_trace_disabled()和global_system_variables.query_cache_type的判断不是一个原子操作, 那么这个极低的复杂场景会有风险嘛?答案是不会,因为最多导致用户去读空缓存的瞬间。读取不到仍然会去MySQL层正常执行。

  • net_real_write->query_cache_insert->Query_cache::insert会不会带来数据只写入部分到query cache中的问题。

目前所有的MySQL版本和分支,都不支持动态修改0->1这种模式,可能一个主要的原因是担心部分query cache写入的问题发生。
典型的例子,一个查询结果集大于query_cache_min_res_unit 时,需要多次分配内存给query cache的结果,多次写入中间可能用户有对query_cache_type的改变动作。
为此,我们加一个限制条件,在将query_cache生效之前,将query_cache_type设置为0,然后将query_cache开关打开之后再设置其query_cache_size的值。

测试数据

这个场景的测试数据单纯用TPS或QPS来衡量都意义不太大,我们在下面实验中,主要做三个方面的事情:

  • 原版本不能从OFF->ON
  • 补丁版本解决OFF->ON问题
  • 补丁版本解决额外的LOCK/UNLOCK调用

#########################

#Percona-5.5.18版本

# 启动MySQL,默认情况下不开启Query Cache
/tmp/dbg –defaults-file=/u01/mysqld/my.cnf

# 查询sbtest表中某条记录,连接3次时间相差无几
root@(none) 05:24:29>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (8.96 sec)

root@(none) 05:24:41>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (8.88 sec)

root@(none) 05:24:52>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (9.04 sec)

从上面的查询时间可以看出,query cache确实没有开启。

# 无法再更改query cache type
root@(none) 07:03:40>set global query_cache_type=ON;
ERROR 1651 (HY000): Query cache is disabled; restart the server with query_cache_type=1 to enable it

作为对比,下面是开启query cache的方式来测试:

# 启动MySQL,开启Query Cache
/tmp/dbg –defaults-file=/u01/mysqld/my.cnf –query_cache_type=on –query_cache_size=102400 –query_cache_limit=10240

# 查询sbtest表中某条记录,第一次耗时非常大
root@(none) 05:22:01>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (23.61 sec)

root@(none) 05:22:28>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (0.00 sec)

root@(none) 05:28:48>SHOW STATUS LIKE ‘Qcache_hits’;
+—————+——-+
| Variable_name | Value |
+—————+——-+
| Qcache_hits | 1 |
+—————+——-+
1 row in set (0.01 sec)

#########################

#Percona-5.5.18版本的补丁版本, 查看是否可以动态改变query cache类型

# 启动MySQL,默认关闭Query Cache
/tmp/qc –defaults-file=/u01/mysqld/my.cnf

# 查询sbtest表中某条记录,后三次查询耗时都非常大

root@(none) 05:33:57>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (16.08 sec)

root@(none) 05:34:16>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (2.99 sec)

root@(none) 05:34:44>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (3.01 sec)

root@(none) 05:34:49>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (3.04 sec)

root@(none) 05:35:47>SHOW STATUS LIKE ‘Qcache_hits’;
+—————+——-+
| Variable_name | Value |
+—————+——-+
| Qcache_hits | 0 |
+—————+——-+
1 row in set (0.00 sec)
# 动态改变query cache, OFF -> ON

root@(none) 05:35:54>set global query_cache_type=ON;
Query OK, 0 rows affected (0.00 sec)

root@(none) 05:36:40>set global query_cache_limit=10240;
Query OK, 0 rows affected (0.00 sec)

root@(none) 05:36:46>set global query_cache_size=102400;
Query OK, 0 rows affected (0.00 sec)
root@(none) 05:37:30>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (3.00 sec)

root@(none) 05:37:34>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (0.00 sec)

root@(none) 05:37:35>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (0.01 sec)
# 动态改变query cache, ON -> OFF

root@(none) 05:37:38>set global query_cache_type=OFF;
Query OK, 0 rows affected (0.00 sec)

root@(none) 05:38:19>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (3.02 sec)

root@(none) 05:38:25>select * from sbtest1.sbtest1 where pad=’43131080328-59298′;
Empty set (2.90 sec)

#########################

#Percona-5.5.18版本的补丁版本, 查看是否调用额外的LOCK/UNLOCK

不重启上面开启的MySQLD,继续测试

$ cat qc.test
sql=”select * from sbtest1.sbtest1 where pad=’43131080328-59298′;”
while [ 1 ]
do
mysql -uroot -S run/mysql.sock sbtest -e “$ sql” > /dev/null 2>&1 &
mysql -uroot -S run/mysql.sock sbtest -e “$ sql” > /dev/null 2>&1 &
mysql -uroot -S run/mysql.sock sbtest -e “$ sql” > /dev/null 2>&1 &
mysql -uroot -S run/mysql.sock sbtest -e “$ sql” > /dev/null 2>&1 &
mysql -uroot -S run/mysql.sock sbtest -e “$ sql” > /dev/null 2>&1 &
#sleep 1
done
# 再次开启Query Cache, OFF->ON
root@(none) 05:48:11>set global query_cache_type=ON;
Query OK, 0 rows affected (0.00 sec)

# 启动并发查询,查看processlist是否有LOCK/UNLOCK
sh qc.test
root@(none) 05:49:03>show processlist;
| 2279 | root | localhost | sbtest | Query | 3 | Sending data | select * from sbt|
| 2280 | root | localhost | sbtest | Query | 3 | Sending data | select * from sbt|
| 2281 | root | localhost | sbtest | Query | 3 | Sending data | select * from sbt|
| 2282 | root | localhost | sbtest | Query | 3 | Sending data | select * from sbt|
| 2283 | root | localhost | sbtest | Query | 3 | Sending data | select * from sbt|
| 2284 | root | localhost | sbtest | Query | 3 | Sending data | select * from sbt|
| 2285 | root | localhost | sbtest | Query | 3 | Sending data | select * from sbt|
| 2286 | root | localhost | sbtest | Query | 2 | Sending data | select * from sbt|
| 2287 | root | localhost | sbtest | Query | 2 | Sending data | select * from sbt|

对比未打补丁版本,开启mysqld后再更改query cache, 即ON -> OFF, processlist中有明显的”Waiting for query cache lock“

/tmp/dbg –defaults-file=/u01/mysqld/my.cnf –query_cache_type=on –query_cache_size=102400 –query_cache_limit=10240

root@(none) 06:16:49>set global query_cache_type=OFF;
Query OK, 0 rows affected (0.00 sec)

root@(none) 06:16:51>set global query_cache_size=0;
Query OK, 0 rows affected (0.00 sec)

# 启动并发查询,查看processlist是否有LOCK/UNLOCK
sh qc.test

show processlist的部分结果为:

| 872 | root | localhost | sbtest | Query | 0 | Sending data | select * from sbte|
| 873 | root | localhost | sbtest | Query | 0 | Waiting for query cache lock | select * from sbte|
| 874 | root | localhost | sbtest | Query | 1 | Sending data | select * from sbte|
| 875 | root | localhost | sbtest | Query | 0 | NULL | select * from sbte|
| 876 | root | localhost | sbtest | Query | 0 | Opening tables | select * from sbte|
| 877 | root | localhost | sbtest | Query | 0 | Opening tables | select * from sbte|
| 878 | root | localhost | sbtest | Query | 0 | NULL | select * from sbte|
| 879 | root | localhost | sbtest | Query | 0 | NULL | select * from sbte|
| 880 | root | localhost | sbtest | Query | 0 | Waiting for query cache lock | select * from sbte|
| 881 | root | localhost | sbtest | Query | 0 | NULL | select * from sbte|
| 882 | root | localhost | sbtest | Query | 0 | Opening tables | select * from sbte|

社区的反馈

目前此patch已经被MariaDB的Sergei进行Code Review进一步完善,后续可能会在Percona新版本中(5.5版本)解决(MariaDB更专注于Server层,而Percona更专注于Storage Engine,特别是XtraDB存储引擎层,两者定期会Merge,这点上回在Sergei在ADC:Alibaba Developers Conference 时到阿里巴巴交流时谈到)。

参考
http://www.mysqlperformanceblog.com/2006/07/27/mysql-query-cache/
http://www.percona.com/doc/percona-server/5.5/performance/query_cache_enhance.html?id=percona-server:features:query_cache_enhance#disabling_the_cache_completely
https://bugs.launchpad.net/percona-server/5.5/+bug/1021131/+attachment/3229396/+files/qc.patch

改进动态设置query cache导致额外锁开销的问题分析及解决方法-mysql 5.5 以上版本的更多相关文章

  1. 动态生成的DOM不会触发onclick事件的原因及解决方法

    最近朋友在做一个项目的时候,遇到动态加载微博内容,然后点击“展开评论”后获取该微博的所有评论.这里使用了动态加载的<span mid='123456789′ class='get_comment ...

  2. 出错提示:“Could not flush the DNS Resolver Cache: 执行期间,函数出了问题”的解决方法

    在DNS解析中,出错提示:"Could not flush the DNS Resolver Cache: 执行期间,函数出了问题"的解决方法  . 由于公司网站空间更换了服务商. ...

  3. ORACLE动态sql在存储过程中出现表或视图不存在的解决方法

    Oracle动态sql在存储过程中出现表或视图不存在的解决方法 CREATE OR REPLACE PROCEDURE P_test is strsql varchar2(2000); BEGIN   ...

  4. informix 数据库锁表分析和解决方法

    一.前言 在联机事务处理(OLTP)的数据库应用系统中,多用户.多任务的并发性是系统最重要的技术指标之一.为了提高并发性,目前大部分RDBMS都采用加锁技术.然而由于现实环境的复杂性,使用加锁技术又不 ...

  5. jQuery UI resizble、draggable的div包含iframe导致缩放和拖拽的不平滑解决方法

    前言 不仅仅是jQuery UI resizble的div包含iframe会导致缩放的不平滑,draggable也会出现包含iframe会导致拖放的不平滑,但是因为jQuery UI有为draggab ...

  6. SVN设置忽略文件列表以及丢失了预定增加的文件解决方法

    设置svn忽略列表 Linux下svn命令行配置 1. 修改版本库的相关属性 2. svn 客户端的配置 Windows下 Tortoise SVN 设置 1. Tortoise SVN 上修改版本库 ...

  7. div+css总结—FF下div不设置高度背景颜色或外边框不能正常显示的解决方法(借鉴)

    原地址:http://blog.sina.com.cn/s/blog_60b35e830100qwr2.html 在使用div+css进行网页布局时,如果外部div有背景颜色或者边框,而不设置其高度, ...

  8. 谷歌的ajax.googleapis.com被墙导致访问很多国外网站很慢的解决方法

    比如访问StackOverflow, 更比如flexerasoftware.com(导致Visual Studio的打包程序InstallShield Limited Edition不能注册和下载) ...

  9. 内层元素设置position:relative后父元素overflow:hidden overflow:scroll失效 解决方法

    内层元素设置position:relative后父元素overflow:hidden overflow:scroll 都失效 解决方法:在position:relative的外层父容器加positio ...

随机推荐

  1. MongoDB学习之(一)安装

    第一步:下载MongoDB的安装版进行安装 https://pan.baidu.com/s/1X3hIqORJ61TCG1UJ_yr6ag 由于第二次安装出现一些问题,所有还是记录一下,免得以后踩坑. ...

  2. 【Unity笔记】UGUI中Canvas屏幕适配

    1.通过RectTransform中的Anchors和Pivot来进行控件和窗体的布局适配. Anchors控制当前Panel相对于父窗体的布局位置,可以设置为居中或者左上角,当父窗体拉伸的时候当前P ...

  3. JAVA用POI读取和创建2003和2007版本Excel完美示例

    import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import ja ...

  4. pthread_self()究竟根据什么来得到线程的标识符????

    #include<stdlib.h> #include<pthread.h> #include<stdio.h> #include<sched.h> # ...

  5. Spring使用facotry-method创建单例Bean总结<转>

       阅读目录 1 最原始的实现单例模式的方法(存在线程不安全): 2 通过关键字Synchronized强制线程同步 3 通过静态内部类进行单例 通过spring的factory-method来创建 ...

  6. Fastjson 的简单使用<转>

    简介 Fastjson是一个Java语言编写的高性能功能完善的JSON库. 高性能 fastjson采用独创的算法,将parse的速度提升到极致,超过所有json库,包括曾经号称最快的jackson. ...

  7. C++实现八皇后问题

    C++实现八皇后问题 #include <iostream> using std::cout; using std::endl; #include <iomanip> usin ...

  8. iOS边练边学--UIScrollView和xib文件实现简单分页+定时器初使用

    一.xib文件构成 二.自定义控件类(xib文件与自定义控件类的文件名字相同,并且将xib文件中父类控件的类名改成自定义控件类的名称) ***********自定义控件类需要的属性********** ...

  9. java---final、finally、finalize的区别

    Java finalize方法使用 标签: javaappletobjectwizardjvm工作 2011-08-21 11:37 48403人阅读 评论(5) 收藏 举报  分类: Java(96 ...

  10. linux -- 进程的查看、进程id的获取、进程的杀死

    进程查看 ps ax : 显示当前系统进程的列表 ps aux : 显示当前系统进程详细列表以及进程用户 ps ax|less : 如果输出过长,可能添加管道命令 less查看具体进程, 如:ps a ...