zoukankan      html  css  js  c++  java
  • 改进动态设置query cache导致额外锁开销的问题分析及解决方法-mysql 5.5 以上版本

    改进动态设置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

  • 相关阅读:
    Heritrix 3.1.0 源码解析(二十五)
    Heritrix 3.1.0 源码解析(二十八)
    获取某年某月的第一天和最后一天的Sql Server函数
    C# ToString()用法汇总
    数据库隐式类型转换
    sql server 中 SET identity_insert on
    Linq To DataTable
    ASP.NET Session详解[转载]
    CSS overflow 属性
    HTML相对路径(Relative Path)和绝对路径(Absolute Path)
  • 原文地址:https://www.cnblogs.com/lixiuran/p/3588654.html
Copyright © 2011-2022 走看看