跳到主要内容

查询阻断

查询熔断是一种保护机制,用于防止长时间运行或消耗过多资源的查询对系统产生负面影响。当查询超过预设的资源或时间限制时,熔断机制会自动终止该查询,以避免对系统性能、资源使用以及其他查询造成不利影响。这种机制确保了集群在多用户环境下的稳定性,防止单个查询导致系统资源耗尽、响应变慢或发生死锁,从而提高整体的可用性和效率。 在 Doris 内,有两种熔断策略:

  • 规划时熔断,即SQL Block Rule,用于阻止符合特定模式的语句执行。阻止规则对任意的语句生效,包括DDL和DML。通常,阻止规则由数据库管理员(DBA)进行配置,用以提升集群的稳定性。比如,
    • 阻止一个查询扫描过多行的数据
    • 阻止一个查询扫描过多的分区
    • 阻止一个修改全局变量的语句,以防止集群配置被意外的修改。
    • 阻止一个通常会占用非常多资源的查询模式
  • 运行时熔断,即Workload Policy,它是在运行时,实时监测查询的执行时间,扫描的数据量,消耗的内存,实现基于规则的查询熔断。

SQL Block Rule

按照阻止模式,可以分为

  • 扫描行数阻止规则
  • 扫描分区数阻止规则
  • 扫描分桶数阻止规则
  • 查询语句正则匹配阻止规则
  • 查询语句哈希值匹配阻止规则

阻止规则按照阻止范围,可以分为

  • 全局级别阻止规则
  • 用户级别阻止规则

使用方法

全局级别阻止规则

CREATE SQL_BLOCK_RULE rule_001
PROPERTIES (
"sql"="select \\* from t",
"global" = "true",
"enable" = "true"
)

这样,我们就创建了一个全局级别的阻止规则。规则名为rule_001。配置了查询语句正则匹配规则,用于阻止所有可以被正则select \* from t 所匹配的查询语句。 由于是全局级别的阻止规则,所以任意用户执行可以被上述正则匹配的语句都会被阻止。例如:

MySQL root@127.0.0.1:test> select * from t;
(1105, 'errCode = 2, detailMessage = errCode = 2, detailMessage = sql match regex sql block rule: rule_001')

用户级别阻止规则

CREATE SQL_BLOCK_RULE rule_001
PROPERTIES (
"sql"="select * from t",
"global" = "false",
"enable" = "true"
)

不同于全局级别的阻止规则。用户级别的阻止规则只对指定用户生效。当我们创建阻止规则时,设置属性"global"为"false"。那么这个阻止规则,将被视为用户级别的阻止规则。 为了使得用户级别的阻止规则生效。还需要为需要使用此规则的用户设置相应的属性。例如:

set property for 'root' 'sql_block_rules' = 'rule_001';

这样,经过上面的配置,root用户在执行查询时,将被应用名字为rule_001的阻止规则。

MySQL root@127.0.0.1:test> set property for 'root' 'sql_block_rules' = '';
Query OK, 0 rows affected
Time: 0.018s
MySQL root@127.0.0.1:test> select * from t;
+----+----+
| id | c1 |
+----+----+
| 1 | 1 |
+----+----+

1 row in set
Time: 0.027s
MySQL root@127.0.0.1:test> set property for 'root' 'sql_block_rules' = 'rule_001';
Query OK, 0 rows affected
Time: 0.008s
MySQL root@127.0.0.1:test> select * from t;
(1105, 'errCode = 2, detailMessage = errCode = 2, detailMessage = sql match regex sql block rule: rule_001')

如果想对一个用户添加多个用户级别的阻止规则。在规则列表中列举所有的规则名字,以英文逗号隔开。 如果想移除一个用户的所有用户级别阻止规则。将规则列表置为空字符串即可。

其他操作

如果需要修改或者删除阻止规则,可以参考阻止规则的SQL手册。

使用场景

可以在以下几种场景使用:

  • 阻止扫描超过指定行数的数据
  • 阻止扫描超过指定分区数量的数据
  • 阻止扫描超过指定分桶数量的数据
  • 阻止特定模式的查询

阻止扫描超过指定行数的数据

由于扫描数据会显著的消耗BE的IO资源和CPU资源。所以,不必要的数据扫描会对集群的稳定性带来比较大的挑战。日常使用中,经常会出现盲目的全表扫描操作。例如SELECT * FROM t。为了防止这种查询对集群产生破坏。可以设置单个查询扫描单表行数的上限。

CREATE SQL_BLOCK_RULE rule_card 
PROPERTIES
(
"cardinality" = "1000",
"global" = "true",
"enable" = "true"
);

当设置了如上的规则,当单表扫描超过1000行时,则禁止此查询的执行。 需要注意的是,由于扫描行数的计算是在规划阶段,而非执行阶段完成的。所以计算行数时,只会考虑分区和分桶裁剪,而不会考虑其他过滤条件对于扫描行数的影响。也就是考虑最坏情况。所以,实际扫描的行数小于设置值的查询也有可能被阻止。

阻止扫描超过指定分区数量的数据

对过多分区的扫描会显著的增加BE的CPU消耗。同时,如果查询的外表,那更有可能带来显著的网络开销和元数据拉取的开销。在日常使用中,这多是由于忘记写分区列上的过滤条件或者写错导致的。为了防止这种查询对集群产生破坏。可以设置单个查询扫描单表的分区数的上限。

CREATE SQL_BLOCK_RULE rule_part_num 
PROPERTIES
(
"partition_num" = "30",
"global" = "true",
"enable" = "true"
);

当设置了如上的规则,当单表扫描分区数超过30个时,则禁止此查询的执行。 需要注意的是,由于扫描分区数的计算是在规划阶段,而非执行阶段完成的。所以有可能出现因为分区裁剪不完全,而保留了更多分区的情况。所以,实际扫描的分区数小于设置值的查询也有可能被阻止。

阻止扫描超过指定分桶数量的数据

对过多分桶的扫描会显著的增加BE的CPU消耗。为了防止这种查询对集群产生破坏。可以设置单个查询扫描单表的分区数的上限。

CREATE SQL_BLOCK_RULE rule_teblet_num 
PROPERTIES
(
"tablet_num" = "200",
"global" = "true",
"enable" = "true"
);

当设置了如上的规则,当单表扫描分桶数量超过200个时,则禁止此查询的执行。 需要注意的是,由于扫描分桶数的计算是在规划阶段,而非执行阶段完成的。所以有可能出现因为分桶裁剪不完全,而保留了更多分区的情况。所以,实际扫描的分桶数小于设置值的查询也有可能被阻止。

阻止特定模式的查询

由于各种原因,比如计算复杂度高,规划时间长等,可能会希望阻止使用模式的查询。 以阻止函数abs举例。可以使用如下的正则表达式阻止规则,完成此目的。

CREATE SQL_BLOCK_RULE rule_abs
PROPERTIES(
"sql"="(?i)abs\\s*\\(.+\\)",
"global"="true",
"enable"="true"
);

上述正则表达式中

  • (?i)表示大小写不敏感
  • abs为想要阻止的目标函数
  • \s*表示在abs和左括号之间可以有任意个空白
  • \(.+\)匹配函数参数 同理,也可以使用类似的方法阻止set global,以防止非预期的变量改变。或者阻止truncate table,以防止非预期的删除数据。

常见问题

Q:正则匹配阻止规则会对集群产生副作用吗?

A:是的。由于正则匹配是计算密集型的。当使用复杂的正则表达式,或者正则匹配规则过多时。会给FE的CPU带来显著的压力提升。所以,要谨慎添加正则匹配的阻止规则。除非必要,尽量不要使用复杂的正则表达式。

Q:可以临时关闭一个阻止规则吗?

A:可以。修改阻止规则,将其属性中的"enable"改为"false"即可。

Q:阻止规则中的正则表达式使用哪种规范?

A:阻止规则的正则表达式使用java的正则表达式规范。常用表达式可以参考SQL语法手册。完整的手册可以参考https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html

Workload Group Policy

注意事项

  • 同一个policy的condition和action要么都是FE的,要么都是BE的,比如set_session_variable和cancel_query无法配置到同一个policy中。condition be_scan_rows和condition username无法配置到同一个policy中。
  • 由于目前的policy是异步线程以固定时间间隔执行的,因此策略的生效存在一定的滞后性。比如用户配置了scan行数大于100万就取消查询的策略,如果此时集群资源比较空闲,那么有可能在取消策略生效之前查询就已经结束了。目前这个时间间隔为500ms,这意味着运行时间过短的查询可能会绕过策略的检查。
  • 当前支持的负载类型包括select/insert select/stream load/broker load/routine load。
  • 一个查询可能匹配到多个policy,但是只有优先级最高的policy会生效。
  • 目前不支持action和condition的修改,只能通过删除新建的方式修改。

熔断策略范围

自 Doris 2.1 版本起,可以通过 Workload Policy 可以实现大查询的熔断。

版本2.1
select
insert into select
insert into valuesX
stream load
routine load
backupX
compactionX

创建熔断策略

使用 CREATE WORKLOAD POLICY 命令可以创建资源管理策略。 在下面的例子中创建一个名为test_cancel_policy的 policy,它会取消掉集群中运行时间超过 1000ms 的查询,当前状态为启用。 创建 Workload Policy 需要 admin_priv 权限。

create workload policy test_cancel_policy
conditions(query_time > 1000)
actions(cancel_query)
properties('enabled'='true');

在创建 Workload Policy 时需要指定以下内容:

  • conditions 表示策略触发条件,可以多个 condition 串联,使用逗号“,”隔开,表示“与”的关系。在上例中 query_time > 1000 表示在查询时间大于 1s 时触发 policy;
  • action 表示条件触发时采取的动作,目前一个 policy 只能定义一个 action(除 set_session_variable)。在上例中,cancel_query 表示取消查询;
  • properties,定义了当前 policy 的属性,包括是否启用和优先级。

一个 policy 只能指定作用在 FE 或 BE 其中一个组件,无法同时作用在 FE 与 BE 上。这是因为 FE 与 BE 有独立的 conditions 与 actions 选项,policy 不区分 FE 与 BE 组件。下表中列出 policy 的子句选项:

组件子句选项说明
FEconditionsusername当一个查询的username为某个值时,就会触发相应的action。
actionsset_session_variable触发 set session variable 语句。同一个 policy 可以有多个 set_session_variable 选项。
BEconditionsbe_scan_rows一个sql在单个BE进程内scan的行数,如果这个sql在BE上是多并发执行,那么就是多个并发的累加值。
be_scan_bytes一个sql在单个BE进程内scan的字节数,如果这个sql在BE上是多并发执行,那么就是多个并发的累加值,单位是字节。
query_time一个sql在单个BE进程上的运行时间,时间单位是毫秒。
query_be_memory_bytes一个sql在单个BE进程内使用的内存用量,如果这个sql在BE上是多并发执行,那么就是多个并发的累加值,单位是字节。
actionscancel_query取消查询。
FE&BEpropertiesenabled取值为true或false,默认值为true,表示当前policy处于启用状态,false表示当前policy处于禁用状态。
priority取值范围为0到100的正整数,默认值为0,代表policy的优先级,该值越大,优先级越高。这个属性的主要作用是,当匹配到多个policy时,选择优先级最高的policy。
workload_group目前一个policy可以绑定一个workload group,代表这个policy只对某个workload group生效。默认为空,代表对所有查询生效。

将熔断策略绑定 Workload Group

默认情况下,Workload Policy 会对所有支持的查询生效。如果想指定 policy 只针对与某一个 Workload Group,需要通过 workload_group 选项绑定 Workload Group。语句如下:

create workload policy test_cancel_big_query
conditions(query_time > 1000)
actions(cancel_query)
properties('workload_group'='normal')

Workload Policy 效果演示

1 session 变量修改测试

尝试修改 admin 账户的 session 变量中的并发相关的参数

// 登录 admin账户查看并发参数
mysql [(none)]>show variables like '%parallel_fragment_exec_instance_num%';
+-------------------------------------+-------+---------------+---------+
| Variable_name | Value | Default_Value | Changed |
+-------------------------------------+-------+---------------+---------+
| parallel_fragment_exec_instance_num | 8 | 8 | 0 |
+-------------------------------------+-------+---------------+---------+
1 row in set (0.00 sec)

// 创建修改admin账户并发参数的policy
create workload policy test_set_var_policy
conditions(username='admin')
actions(set_session_variable 'parallel_fragment_exec_instance_num=1')

// 过段时间后再次查看admin账户的参数
mysql [(none)]>show variables like '%parallel_fragment_exec_instance_num%';
+-------------------------------------+-------+---------------+---------+
| Variable_name | Value | Default_Value | Changed |
+-------------------------------------+-------+---------------+---------+
| parallel_fragment_exec_instance_num | 1 | 8 | 1 |
+-------------------------------------+-------+---------------+---------+
1 row in set (0.01 sec)

2 大查询熔断测试

测试对运行时间超过 3s 的查询进行熔断,以下是一个 ckbench 的 q29 运行成功时的审计日志,可以看到这个 sql 跑完需要 4.5s 的时间

mysql [hits]>SELECT REGEXP_REPLACE(Referer, '^https?://(?:www\.)?([^/]+)/.*$', '\1') AS k, AVG(length(Referer)) AS l, COUNT(*) AS c, MIN(Referer) FROM hits WHERE Referer <> '' GROUP BY k HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25;
+-----------------------------------------------------------------------+------------------+----------+---------------------------------------------------------------------------------------------------------------------+
| k | l | c | min(Referer) |
+-----------------------------------------------------------------------+------------------+----------+---------------------------------------------------------------------------------------------------------------------+
| 1 | 85.4611926713085 | 67259319 | http://%26ad%3D1%25EA%25D0%26utm_source=web&cd=19590&input_onlist/би-2 место будущей кондицин |
| http:%2F%2Fwwww.regnancies/search&evL8gE&where=all&filmId=bEmYZc_WTDE | 69 | 207347 | http:%2F%2Fwwww.regnancies/search&evL8gE&where=all&filmId=bEmYZc_WTDE |
| http://новострашная | 31 | 740277 | http://новострашная |
| http://loveche.html?ctid | 24 | 144901 | http://loveche.html?ctid |
| http://rukodeliveresult | 23 | 226135 | http://rukodeliveresult |
| http://holodilnik.ru | 20 | 133893 | http://holodilnik.ru |
| http://smeshariki.ru | 20 | 210736 | http://smeshariki.ru |
| http:%2F%2Fviewtopic | 20 | 391115 | http:%2F%2Fviewtopic |
| http:%2F%2Fwwww.ukr | 19 | 655178 | http:%2F%2Fwwww.ukr |
| http:%2F%2FviewType | 19 | 148907 | http:%2F%2FviewType |
| http://state=2008 | 17 | 139630 | http://state=2008 |
+-----------------------------------------------------------------------+------------------+----------+---------------------------------------------------------------------------------------------------------------------+
11 rows in set (4.50 sec)

创建一个运行时间超过 3s 就取消查询的 policy

create workload policy test_cancel_3s_query
conditions(query_time > 3000)
actions(cancel_query)

再次执行 sql 可以看到 SQL 执行会直接报错

mysql [hits]>SELECT REGEXP_REPLACE(Referer, '^https?://(?:www\.)?([^/]+)/.*$', '\1') AS k, AVG(length(Referer)) AS l, COUNT(*) AS c, MIN(Referer) FROM hits WHERE Referer <> '' GROUP BY k HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25;
ERROR 1105 (HY000): errCode = 2, detailMessage = (10.16.10.8)[CANCELLED]query cancelled by workload policy,id:12345