0
0
0
0
博客/.../

TiDB SQL 黑名单实践

 克里克里克  发表于  2026-07-30

背景

传统的关系型数据库如Oracle、MySQL在遇到SQL异常高并发的场景导致系统负载剧增的情况普遍没有什么很好的方法:数据库端不停的杀会话,前端应用暂时修改代码屏蔽问题模块。但是在信创类的数据库中大多数都加入了SQL黑名单的功能:将SQL加入到黑名单列表,阻断SQL的执行,避免读对数据库的异常冲击。TiDB同样也有SQL黑名单拦截的功能。

TiDB SQL 黑名单

TiDB在不同的版本中对SQL的限制有不同的方法。

主要分为两类,一是 在v7.5+中,使用query watch 的方式来限制SQL的执行。2是 在低版本中使用hint,临时绑定SQL的执行计划,最长执行1ms,来间接阻断SQL的执行。

使用 query watch 限制SQL的执行

query limit action支持的操作如下:

  • DRYRUN:对执行 Query 不做任何操作,仅记录识别的 Runaway Query。主要用于观测设置条件是否合理。
  • COOLDOWN:将查询的执行优先级降到最低,查询仍旧会以低优先级继续执行,不占用其他操作的资源。
  • KILL:识别到的查询将被自动终止,报错 Quarantined and interrupted because of being in runaway watch list。
  • SWITCH_GROUP:从 v8.4.0 开始引入,将识别到的查询切换到指定的资源组继续执行。该查询执行结束后,后续 SQL 仍保持在原资源组中执行。如果指定的资源组不存在,则不做任何动作。当前仅介绍kill 的操作。

直接中断SQL执行

匹配SQL后会直接中断,不会下发到TiKV。

QUERY WATCH ADD RESOURCE GROUP rg1 ACTION SWITCH_GROUP(rg2) SQL TEXT SIMILAR TO 'select * from test.t2';

如上,如果不指定resource group rg1资源组,默认监控default资源组,即其他资源组下的相同SQL并不会拦截。

因为使用EXACT严格匹配,即使多个空格都失效,可以将exact修改为similar进行模糊匹配。

QUERY WATCH ADD ACTION KILL SQL TEXT EXACT TO 'select count(11) from test.test_info';

# 使用information_schema.RUNAWAY_WATCHES表查看系统中的runway query 策略信息。
MySQL [(none)]> select * from information_schema.RUNAWAY_WATCHES;
+----+---------------------+---------------------+-----------+-------+------------------------------------------+--------+--------+------+
| ID | RESOURCE_GROUP_NAME | START_TIME          | END_TIME  | WATCH | WATCH_TEXT                               | SOURCE | ACTION | RULE |
+----+---------------------+---------------------+-----------+-------+------------------------------------------+--------+--------+------+
|  1 | default             | 2026-07-30 02:33:06 | UNLIMITED | Exact | select count(11) from test.test_info | manual | Kill   | None |
+----+---------------------+---------------------+-----------+-------+------------------------------------------+--------+--------+------+

# 文本严格匹配,执行SQL会被拦截
MySQL [(none)]> select count(11) from test.test_info;
ERROR 8254 (HY000): Quarantined and interrupted because of being in runaway watch list

# 改变count后,文本不匹配,SQL正常执行
MySQL [(none)]> select count(1) from test.test_info;
+----------+
| count(1) |
+----------+
| 35985939 |
+----------+

# 使用mysql.tidb_runaway_queries表,查看被拦截的SQL的信息
MySQL [test]> SELECT * FROM mysql.tidb_runaway_queries LIMIT 1\G;
*************************** 1. row ***************************
resource_group_name: default
         start_time: 2026-07-30 10:33:33
            repeats: 1
         match_type: watch
             action: kill
         sample_sql: select count(11) from test.test_info
         sql_digest: 693cff9b1e10322ccb4f8ff91bbb08a4e39f4d3c216e3bd98cb9fb98f3e7e22b
        plan_digest: 0039a839c2666bddccf6b64ec7b67731897e906bc6d39bd7bbd3901446ebae93
        tidb_server: 10.0.40.121:4000
               rule: None
1 row in set (0.001 sec)

使用similar,将SQL解析成SQL Digest来模糊匹配

MySQL [(none)]> query watch add action kill sql text similar to 'select count(11) from test.test_info';
MySQL [(none)]> select * from information_schema.RUNAWAY_WATCHES;
+----+---------------------+---------------------+-----------+---------+------------------------------------------------------------------+--------+--------+------+
| ID | RESOURCE_GROUP_NAME | START_TIME          | END_TIME  | WATCH   | WATCH_TEXT                                                       | SOURCE | ACTION | RULE |
+----+---------------------+---------------------+-----------+---------+------------------------------------------------------------------+--------+--------+------+
|  2 | default             | 2026-07-30 02:56:05 | UNLIMITED | Exact   | select count(11) from test.test_info                         | manual | Kill   | None |
|  3 | default             | 2026-07-30 03:00:00 | UNLIMITED | Similar | 693cff9b1e10322ccb4f8ff91bbb08a4e39f4d3c216e3bd98cb9fb98f3e7e22b | manual | Kill   | None |
+----+---------------------+---------------------+-----------+---------+------------------------------------------------------------------+--------+--------+------+
# 给表增加别名后,sql_digest变化,因此又可以正常执行。
MySQL [(none)]> select count(112) from test.test_info t;
+------------+
| count(112) |
+------------+
|   35985939 |
+------------+

直接指定SQL Digest

如果SQL文本过长被截断,可直接使用慢SQL中的SQL Digest进行拦截。

# 使用sql digest拦截
query watch add action kill sql digest '693cff9b1e10322ccb4f8ff91bbb08a4e39f4d3c216e3bd98cb9fb98f3e7e22b';

MySQL [test]> SELECT * FROM information_schema.runaway_watches;
+----+---------------------+---------------------+-----------+---------+------------------------------------------------------------------+--------+--------+------+
| ID | RESOURCE_GROUP_NAME | START_TIME          | END_TIME  | WATCH   | WATCH_TEXT                                                       | SOURCE | ACTION | RULE |
+----+---------------------+---------------------+-----------+---------+------------------------------------------------------------------+--------+--------+------+
|  4 | default             | 2026-07-30 07:17:14 | UNLIMITED | Similar | 693cff9b1e10322ccb4f8ff91bbb08a4e39f4d3c216e3bd98cb9fb98f3e7e22b | manual | Kill   | None |
+----+---------------------+---------------------+-----------+---------+------------------------------------------------------------------+--------+--------+------+

# 将SQL切换到指定的resource group中降级执行,策略有重复,会删除原有的数据,插入该新规则
MySQL [test]> query watch add action switch_group (rg_cancel) sql digest '693cff9b1e10322ccb4f8ff91bbb08a4e39f4d3c216e3bd98cb9fb98f3e7e22b';
MySQL [test]> SELECT * FROM information_schema.runaway_watches;
+----+---------------------+---------------------+-----------+---------+------------------------------------------------------------------+--------+------------------------+------+
| ID | RESOURCE_GROUP_NAME | START_TIME          | END_TIME  | WATCH   | WATCH_TEXT                                                       | SOURCE | ACTION                 | RULE |
+----+---------------------+---------------------+-----------+---------+------------------------------------------------------------------+--------+------------------------+------+
|  5 | default             | 2026-07-30 07:21:37 | UNLIMITED | Similar | 693cff9b1e10322ccb4f8ff91bbb08a4e39f4d3c216e3bd98cb9fb98f3e7e22b | manual | SwitchGroup(rg_cancel) | None |
+----+---------------------+---------------------+-----------+---------+------------------------------------------------------------------+--------+------------------------+------+

# 查看SQL Digest对应的SQL文本信息
MySQL [test]> select tidb_decode_sql_digests('["693cff9b1e10322ccb4f8ff91bbb08a4e39f4d3c216e3bd98cb9fb98f3e7e22b"]');
+-------------------------------------------------------------------------------------------------+
| tidb_decode_sql_digests('["693cff9b1e10322ccb4f8ff91bbb08a4e39f4d3c216e3bd98cb9fb98f3e7e22b"]') |
+-------------------------------------------------------------------------------------------------+
| ["select count ( ? ) from `test` . `test_info`"]                                            |
+-------------------------------------------------------------------------------------------------+
1 row in set (0.001 sec)

不中断SQL的执行,对应SQL切换到指定的资源组,限制资源的使用

通过SQL解析成SQL Digest,将rg1资源组的runaway queries监控列表匹配的SQL切换到rg2资源组中

QUERY WATCH ADD RESOURCE GROUP rg1 ACTION SWITCH_GROUP(rg2) SQL TEXT SIMILAR TO 'select * from test.t2';

在资源组设置runaway

具体参考官方步骤。

通过资源组配置runaway的方式适合控制SQL执行时间长或者消耗资源多,自动降级或中断SQL,但业务系统共用资源组,本身就有部分执行时间和消耗资源都比较高的SQL,不好控制,因此不详细介绍。

删除query watch监控的SQL

根据ID删除指定的 query watch

QUERY WATCH REMOVE ${WATCH_ID};

MySQL [(none)]> query watch remove 1;

query watch的方式不仅支持select的拦截,DML操作也可以被拦截(insert into values单行数据例外,虽然可以配置策略,但是不会被拦截)。

使用hint MAX_EXECUTION_TIME 限制 SQL的执行(不推荐)

找到需要加入黑名单的SQL,进行如下的执行计划的绑定,最长执行1毫秒,即可变相的产生黑名单的作用。--实际测试为中断SQL。软限制

CREATE GLOBAL BINDING for SELECT * FROM t1, t2 WHERE t1.id = t2.id USING SELECT /*+ MAX_EXECUTION_TIME(1) */ * FROM t1, t2 WHERE t1.id = t2.id;

移除通过增加hint绑定SQL的策略

DROP GLOBAL BINDING for SELECT * FROM t1, t2 WHERE t1.id = t2.id;

对于hint的方式的缺陷:对于已经下推到TiKV、在TiKV侧阻塞的慢请求,hint的方式不会进行硬中断,只会中断在tidb server上耗时长的场景。如下场景,执行了9秒,hint设置为1ms,SQL并没有中断,看起来像是hint失效(该场景实际未验证成功)。

MySQL [test]> select /*+ MAX_EXECUTION_TIME(1) */ * from test.test_info where source='1231a' and settle_days=2;
Empty set (9.101 sec)

分析执行计划,时间均消耗在tikv层。
MySQL [test]> explain select /*+ MAX_EXECUTION_TIME(1) */ * from test.test_info where source='1231a' and settle_days=2;
+-------------------------+-------------+-----------+------------------+----------------------------------------------------------------------------------+
| id                      | estRows     | task      | access object    | operator info                                                                    |
+-------------------------+-------------+-----------+------------------+----------------------------------------------------------------------------------+
| TableReader_7           | 35.99       | root      |                  | data:Selection_6                                                                 |
| └─Selection_6           | 35.99       | cop[tikv] |                  | eq(test.test_info.settle_days, 2), eq(test.test_info.source, "1231a")    |
|   └─TableFullScan_5     | 35985939.00 | cop[tikv] | table:test_info | keep order:false, stats:partial[source:unInitialized, settle_days:unInitialized] |
+-------------------------+-------------+-----------+------------------+----------------------------------------------------------------------------------+
3 rows in set (0.001 sec)

# 将会话加入到rg_cancel资源组,也可以终止,但是实际也消耗了不少资源(在SQL中使用hint的方式未生效拦截,原因待查)。
MySQL [(none)]> show create resource group rg_cancel;
+----------------+-----------------------------------------------------------------+
| Resource_Group | Create Resource Group                                           |
+----------------+-----------------------------------------------------------------+
| rg_cancel      | CREATE RESOURCE GROUP `rg_cancel` RU_PER_SEC=1, PRIORITY=MEDIUM |
+----------------+-----------------------------------------------------------------+

set resource group rg_cancel;

MySQL [test]> select * from test.test_info where source='1231a' and settle_days=2;
ERROR 8252 (HY000): Exceeded resource group quota limitation

注意

  • 在 v6.4.0 之前,max_execution_time 对所有类型的语句生效。从 v6.4.0 开始,该变量仅用于控制 SELECT 语句的最长执行时间。实际精度在 100ms 级别,而非更准确的毫秒级别。

引用

https://pingkai.cn/docs/tidb/stable/tidb-resource-control-runaway-querieshttps://pingkai.cn/docs/tidb/stable/sql-statement-query-watch#query-watch

0
0
0
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论