TiDB new feature max_execution_time
作者: mantuliu 原文来源:https://tidb.net/blog/e95bc56a
max_execution_time 的作用
据说 max_execution_time 是 mysql5.7.8(没有实际的验证过) 提供的一个 feature,能够有效地控制慢查询,尤其对于数据库性能要求比较高的业务场景非常有用。我在 mysql 和 TiDB 的生产环境下,之前都遇到过因为慢查询消耗过多的机器资源,从而影响生产环境可用性的情况:
mysql 遇到问题的场景是:我们丰巢的一个基础服务采用的 mycat 分库分表,有个字段是字符串类型,但是开发小哥哥在查询数据的时候传的值是数值,造成索引失效,从而全表扫描。当时对于 io 等资源消耗非常严重,直接影响了生产环境的使用;
TiDB 的问题也是一样的,如果 column 是字符串,但传的值是数值类型,它的执行计划会不断的读取 TiKV 的返回值并在 TiDB 做运算 cast,当时我们用的是千兆网卡,网络直接打满;
虽然我们丰巢针对生产环境的数据库做了很多的限制,但总是难免会出现类似于上面提到的问题。在我们之前使用的 TiDB 版本 (2.1.4) 是没有 max_execution_time 这个特性的,当时我们为了防止上面的问题再次出现,还专门的写了一个 TiDB 的监控模块,当出现高消耗的慢 sql 时,监控程序可以直接 kill tidb session 来完成。现在有了 max_execution_time,一切都变得简单了,我们可以在系统变量的级别来设置 max_execution_time 的值,从而限定了 query sql 的最大执行毫秒数,避免灾难的发生。
TiDB max_execution_time 初见
我记得 TiDB 在 2.1 的某一个小版本里面便引入了 max_execution_time 的 hint 语法,但那时没有实际的作用,看 release 的介绍,max_execution_time 可以在实际环境中使用,是在 2.1.14 版本才开始的。如图:
global 变量方式测试
我只进行了 global 级别的系统变量的测试,没有测试 hint 的方式,因为 hint 的方式在我们丰巢使用的概率比较低,两方面的原因:一是 hint 的对于 sql 的改动较大;二是很难完成所有的 sql 添加此 hint,一旦出现漏网之鱼并且出了问题,那前面做的工作都没有啥意义。测试的准备环境如下:
TiDB 版本 2.1.15
TiDB 节点数量:3
TiDB 开启 binlog 服务 pump
TiKV 节点数量:12(3 台物理机,每台 4 个实例)
PD 情况:与 TiDB、pump 服务部署在相同的 3 台物理机上,3 个节点
硬件条件:全部是 SSD 的磁盘
负载均衡:nginx1.17.1+stream
测试用例
我的测试用例都比较简单,首先使用数据同步工具和丰巢自研的流量录制和实时回放工具,将生产环境的实际数据实时同步到测试环境,单表最大行数在几十亿这个级别。主要是想测试 3 种实际的情况:
变量 max_execution_time 的测试:多次设置 global max_execution_time 的值,测试它在不同 session 的情况下是否实时生效;
测试在 TiDB 端和 TiKV 端都有大量计算的查询语句情况,这个测试用例比较容易就可以实现,就用上面说的那个把网络打满的生产问题用例即可,使用传递的值为数字,但是列为字符串的 sql 语句,像这样:select user_id,user_name from test where user_name = 18612345678;
测试计算主要是在 TiKV 端执行的语句,其中列 user_content 没有索引:select user_id,user_name,user_content from test where user_content = ‘123456’;
测试结果
变量实时生效结果测试
我在一开始测试的时候,变量在 1 秒和 10 秒之间来回设置,如下:
发现我在把最大执行时间从 1 秒切到 10 秒时,query 被中断的时间却还是 1 秒。经过深入的分析,发现是 global 和 session 级别的原因,当我们设置了 global 的变量后,已经启动的 session 实际用到的变量值,还是之前的 session 变量值,也就是旧值。这个时候,如果我们重新打开一个 session,MAX_EXECUTION_TIME 是生效的,也就是说新的 session 会读取最新的 global 变量的值。那么,这里就需要我们在实际的生产环境使用的时候要注意,因为大部分的生产环境都是使用的长连接,session 很长时间都不会被关闭的,因为和我们的预期值不一致,很有可能会带来生产问题。关于 TiDB 和 mysql 的 session 和 global 的详细细节,我也没有深入了解过,后面有时间会对这块做个分析,我觉得类似于 MAX_EXECUTION_TIME 这种变量,最好是只有 global 和 hint 两种级别,否则很容易带来理解上的混淆以及潜在的生产环境问题。我猜测 TiDB 这样做,是为了要兼容 mysql 的原因。
TiDB 端和 TiKV 端都有大量计算测试结果
首先设置 MAX_EXECUTION_TIME 为 10 秒
再启动一个新的连接,执行类似于下面的语句,user_name 为字符串类型变量,test 表行数有一亿以上的数据
测试结果为
从结果上看,此种慢查询的语句,在超过最大执行时间后,是可以被 TiDB 正常的结束掉的。
计算主要是在 TiKV 端执行的语句测试结果
这个测试用例的目的是想看看,计算已经下推导 TiKV 上的 query 语句,能不能在超过最大执行时间后正常的结束掉,还是在超时时间为 10 秒的情况下,执行下面的语句,如前面说的,user_content 是没有索引的
测试结果如下:
测试结果说明,TiDB 是无法正常结束这种计算都是在 TiKV 上做的语句的,在 TiDB 判断了超时时间过后,是无法通知到 TiKV 去结束掉这次计算的,只能等待 TiKV 返回结果后,再做决定。
源码分析
大家有兴趣,可以跟随这个 PR,Add support for MAX_EXECUTION_TIME,去详细的分析源码,在这里我们来简单的看一下相关的源码。TiDB 里面有一个 processinfo 的存储空间,主要是存储所有 session 的当前执行 sql 的情况,我之前还写过一篇源码分析show processlist 的源码里面有讲到过 processinfo 的情况。
首先我们来看看 max_execution_time 是如何存储到 processinfo 中的:
代码在adapter.go的 Exec 方法中,主要就是在 sql 执行前,先获取 max_execution_time 的实际值,然后存到当前 session 的 processinfo 存储空间里面。
getMaxExecutionTime 那么 maxExecutionTime 的具体值的到底是怎么来的呢?当 hint 和 session 同时存在时,优先级是如何计算的呢?
由上面的代码可知,hint 的优先级会高于 session 的优先级,这也符合我们正常的思维方式。
如何 kill 掉超时的 query 最后我们来看看,TiDB 是如何判断 query 超时了,并 kill 掉它的,在expensivequery.go中有一个 goroutine 会不断的 check,主要逻辑如下:
这个 goroutine 会通过 ShowProcessList 不断的读取当前正在执行的 sql 语句,并判断 costTime 是否已经超过了之前设置到 processinfo 中的 MaxExecutionTime,如果超过了,则 kill 掉这条 query。其中的 time.Millisecond 也表明了 MaxExecutionTime 的单位是毫秒。
最后
我个人觉得这个 feature 对于高并发的交易型业务是非常有必要的,它是可以作为一个最后的兜底策略。希望 PingCap 公司后面能在 TiKV 层面也能支持这个 feature,真正的将风险降到最低,我本人对于 TiDB 是充满了无限期待的,希望它能越来越 NB。
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/76b86dadaa8a249e8238c48c3】。文章转载请联系作者。
评论