StarRocks SQL 黑名单管理:拦截危险 SQL 防止集群崩溃或高并发失控
【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks
StarRocks 提供 SQL Blacklist(SQL 黑名单)机制,用于在查询执行前拦截匹配特定正则模式的 SQL,防止某些查询触发集群崩溃或引发意外的高并发负载。本文基于 StarRocks 官方文档 Blacklist Management 展开,完整覆盖黑名单的启用、添加、查看与删除全流程,并结合 FE(Frontend)源码剖析其在解析执行链中的拦截位置、正则匹配语义、元数据持久化方式以及运维中需要注意的边界条件,帮助你在真实生产环境中安全地部署 SQL 拦截规则。
适用场景与版本前提
黑名单的定位很明确:某些 SQL 模式一旦执行,会导致 BE 崩溃、集群资源耗尽或产生不可控的高并发查询。管理员可以通过正则表达式把这些"危险 SQL"模式加入黑名单,之后所有匹配这些模式的语句都会在 FE 侧被直接拒绝,并给用户返回明确的错误提示。
从当前仓库的源码结构看,黑名单的拦截范围与文档描述一致,适用于以下三类语句:
- SELECT 语句:黑名单自推出以来即支持;
- INSERT 语句:自 v3.1 起支持(执行器入口 中对
parsedStmt instanceof ... InsertStmt等类型做了判断); - CTAS(CREATE TABLE AS SELECT)语句:自 v3.4 起支持(源码中对应
CreateTableAsSelectStmt的分支判断)。
管理黑名单的操作要求当前用户具备ADMIN_PRIV权限。文档中的三条核心命令如下:
ADD SQLBLACKLIST "<sql>"; DELETE SQLBLACKLIST <sql_index_number>; SHOW SQLBLACKLIST;启用开关:enable_sql_blacklist
黑名单机制默认是关闭的,需要通过enable_sql_blacklist配置项显式打开:
admin set frontend config ("enable_sql_blacklist" = "true");在 FE 源码中,该配置项定义于 Config.java:
public static boolean enable_sql_blacklist = false;默认值false与文档描述一致。这意味着:
- 即使你向黑名单中添加了规则,只要开关未打开,所有 SQL 都会照常执行,黑名单不产生任何拦截效果;
admin set frontend config是动态配置命令,修改后立即生效,无需重启 FE;- 该配置属于 frontend config,作用域是整个 FE,而非单个 session。
拦截时机与匹配机制(源码级解析)
理解黑名单"在哪里生效"和"如何匹配",有助于编写正确的正则规则。拦截逻辑位于 FE 查询执行入口 StmtExecutor.java,核心条件为:
&& Config.enable_sql_blacklist && !parsedStmt.isExplain() && !isProxy) { ... String originSql = origStmt.originStmt.trim() .toLowerCase().replaceAll(" +", " "); // If this sql is in blacklist, show message. GlobalStateMgr.getCurrentState().getSqlBlackList().verifying(originSql);从这段调用链可以确认几个关键行为:
- 拦截发生在执行阶段:SQL 完成解析(parse)之后、正式执行之前进行校验,因此即使 SQL 尚未运行也会报"在黑名单中"的错误;
- EXPLAIN 不受拦截:
!parsedStmt.isExplain()表明执行EXPLAIN <被禁 SQL>不会被黑名单拒绝,便于排查"某条 SQL 是否会被拦截"; - 统计连接被豁免:源码中对
context.isStatisticsConnection() || context.isStatisticsJob()的分支直接跳过黑名单检查。这与文档中"禁止所有 INSERT INTO ... VALUES 但排除_statistics_.column_statistics"的示例相呼应——FE 内部的统计信息收集走的是受信任通道,默认不受黑名单影响; - 代理请求(Proxy)不做此检查:
!isProxy条件说明经 Proxy 转发的查询不走黑名单分支。
真正的匹配逻辑在 SqlBlackList.java 的verifying方法中:
public void verifying(String sql) throws AnalysisException { String formatSql = sql.replace("\r", " ").replace("\n", " ").replaceAll("\\s+", " "); for (BlackListSql patternAndId : ruleSnapshot) { Matcher m = patternAndId.pattern.matcher(formatSql); if (m.find()) { MetricRepo.COUNTER_SQL_BLOCK_HIT_COUNT.increase(1L); ErrorReport.reportSqlBlackListException(ErrorCode.ERR_SQL_IN_BLACKLIST_ERROR, patternAndId.id); } } }这段代码揭示了四个实现细节:
- 空白归一化:待匹配的 SQL 会先把
\r、\n替换为空格,再把连续空白折叠为单个空格。因此正则规则不需要考虑 SQL 中换行、多空格等排版差异,ADD SQLBLACKLIST添加规则时也会做同样的归一化(见 addBlackSql); - 使用
Matcher.find()而非matches():即黑名单规则是"部分匹配"语义——只要 SQL 文本中任意位置出现与规则匹配的子串,就会被拦截。这就是为什么文档中禁止count(*)的规则写成select count(\\*) from .+这种从语句头部开始锚定的形式,而不需要$结尾; - 小写归一:规则在添加时被统一转小写存储,待匹配 SQL 也转小写。若需要匹配大小写敏感的写法(如文档中的 INSERT 示例),需借助正则自身的
(?i)修饰符来控制,而不是依赖规则字符串的大小写; - 命中计数器:每次命中都会增加
COUNTER_SQL_BLOCK_HIT_COUNT指标,你可以通过 FE 指标观察黑名单的实际拦截量,验证规则是否按预期生效。
命中黑名单后,错误通过 ErrorReport.reportSqlBlackListException 上报,客户端看到的错误形如:
ERROR 1064 (HY000): Access denied; sql 'select count (*) from test_all_type_select_2556' is in blacklist该错误类型由 SqlBlacklistedException(继承自AnalysisException)承载,在 StmtExecutor 中被单独捕获处理,保证用户拿到的是清晰的"SQL 在黑名单中"提示,而不是普通的语法或权限错误。
添加黑名单规则
添加命令:
ADD SQLBLACKLIST "<sql>";其中sql是描述某一类 SQL 的正则表达式。由于 SQL 本身大量使用(、)、*、.这些与正则语义冲突的字符,需要区分处理:
(和)在 SQL 中出现过于频繁,不需要转义;- 其他特殊字符需要加反斜杠
\前缀转义; - 在 SQL 客户端里提交规则字符串时,反斜杠本身通常还要再转义一次(写作
\\),这与下面示例中的写法一致。
完整继承官方文档的七类典型规则:
1. 禁止count(*):
ADD SQLBLACKLIST "select count(\\*) from .+";2. 禁止count(distinct ...):
ADD SQLBLACKLIST "select count(distinct .+) from .+";3. 禁止特定范围的order by ... limit x, y(1 ≤ x ≤ 7,5 ≤ y ≤ 7):
ADD SQLBLACKLIST "select id_int from test_all_type_select1 order by id_int limit [1-7], [5-7]";这里[1-7]、[5-7]是正则字符组,表示单个数字字符,因此能精确匹配limit 1,5、limit 3,7等组合。
4. 禁止一条复杂的嵌套 EXCEPT 查询(复杂 SQL 整体封禁):
ADD SQLBLACKLIST "select id_int \\* 4, id_tinyint, id_varchar from test_all_type_nullable except select id_int, id_tinyint, id_varchar from test_basic except select (id_int \\* 9 \\- 8) \\/ 2, id_tinyint, id_varchar from test_all_type_nullable except select id_int, id_tinyint, id_varchar from test_basic_nullable";5. 禁止所有 INSERT INTO 语句:
ADD SQLBLACKLIST "(?i)^insert\\s+into\\s+.*";6. 禁止所有 INSERT INTO ... VALUES 语句:
ADD SQLBLACKLIST "(?i)^insert\\s+into\\s+.*values\\s*\\(";7. 禁止所有 INSERT INTO ... VALUES,但排除对系统视图_statistics_.column_statistics的写入:
ADD SQLBLACKLIST "(?i)^insert\\s+into\\s+(?!column_statistics\\b).*values\\s*\\(";第 5~7 条示例用了(?i)(忽略大小写)与^锚点,说明规则可以组合使用标准 Java 正则特性(FE 底层即java.util.regex.Pattern)。第 7 条的(?!column_statistics\\b)是负向前瞻,用于"全量禁止 + 例外放行"的场景。
添加规则时的源码行为补充(见 SqlBlackList.addBlackSql):
- 规则字符串会先做
trim、转小写、折叠空白,再调用Pattern.compile编译; - 正则本身不合法时会抛出
SemanticException: Sql syntax error,即规则错误在添加阶段就会被拒绝,而不会污染黑名单; - 重复添加同一条规则不会产生新条目:
put方法以规则的pattern.toString()为 key 存入ConcurrentHashMap,若已存在则直接返回旧 id(见 SqlBlackList.put)。
查看黑名单
SHOW SQLBLACKLIST;结果格式为Index | Forbidden SQL,其中Index即删除操作所需的规则编号。例如:
mysql> show sqlblacklist; +-------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Index | Forbidden SQL | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | 1 | select count\(\*\) from .+ | | 2 | select id_int \* 4, id_tinyint, id_varchar from test_all_type_nullable except select id_int, id_tinyint, id_varchar from test_basic except select \(id_int \* 9 \- 8\) \/ 2, id_tinyint, id_varchar from test_all_type_nullable2 except select id_int, id_tinyint, id_varchar from test_basic_nullable | | 3 | select id_int from test_all_type_select1 order by id_int limit [1-7], [5-7] | | 4 | select count\(distinct .+\) from .+ | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+注意Forbidden SQL列中展示的 SQL 已对所有 SQL 语义字符做了转义显示(如\( \* \)),这只是展示层的可读化处理,内部实际参与匹配的是编译后的Pattern对象。
从源码看,SHOW SQLBLACKLIST返回的数据来自 SqlBlackList.getBlackLists,其内容是一个按 id 排序的不可变快照(ruleSnapshot)。每次增删规则后都会执行refreshSnapshot()重建该列表,读路径(verifying遍历快照)与写路径(增删规则)通过updateLock与 volatile 引用解耦——这意味着黑名单规则更新是原子的,正在执行匹配的检查不会被中间状态影响。
删除黑名单规则
DELETE SQLBLACKLIST <sql_index_number>;<sql_index_number>是逗号分隔的规则编号列表。例如删除上一条SHOW结果中的第 3、4 号规则:
delete sqlblacklist 3, 4;删除后再次查看:
mysql> show sqlblacklist; +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Index | Forbidden SQL | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | 1 | select count\(\*\) from .+ | | 2 | select id_int \* 4, id_tinyint, id_varchar from test_all_type_nullable except select id_int, id_tinyint, id_varchar from test_basic except select \(id_int \* 9 \- 8\) \/ 2, id_tinyint, id_varchar from test_all_type_nullable2 except select id_int, id_tinyint, id_varchar from test_basic_nullable | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+删除操作同样走元数据日志通道:deleteBlackSql 会先写 EditLog(logDeleteSQLBlackList,持久化载体为 DeleteSqlBlackLists),再在 WAL 回调中从内存 map 移除对应 id 的规则并刷新快照。
持久化:规则可跨 FE 重启保留
黑名单不是内存态的临时配置。从 SqlBlackList 的实现可以看到完整的持久化闭环:
- 添加/删除:均通过 EditLog 写入元数据日志(logAddSQLBlackList、
logDeleteSQLBlackList),Follower FE 通过回放日志保持与 Leader 的规则一致; - 落盘镜像:
save方法把全部规则以 SqlBlackListPersistInfo(id + pattern 字符串)的 JSON 形式写入 FE 镜像(SRMetaBlockID.BLACKLIST_MGR块),load方法在 FE 重启时从镜像恢复并重新编译Pattern; - id 单调递增不回绕:
AtomicLong ids在加载镜像时会取持久化 id 的最大值续接,保证SHOW SQLBLACKLIST中的编号在重启后依然稳定、可引用。
因此一次ADD SQLBLACKLIST的效果是持久的:FE 重启后规则仍在,不需要重新配置;要解除拦截只能显式DELETE SQLBLACKLIST或关闭enable_sql_blacklist总开关。
相关机制与使用建议
- SQL Digest 黑名单是独立体系:在 StmtExecutor 中,当
Config.enable_sql_digest或 session 变量enable_sql_digest开启时,还会额外用SqlDigestBlackList(源码)对语句 digest 做匹配。它针对的是语句指纹而非原始文本,适合拦截"参数值不同但结构相同"的一类 SQL,与本文的文本正则黑名单互补; - 规则设计建议:由于匹配是
find()部分匹配 + 小写归一化,规则宜从语句头部(select、^insert)写起并尽量具体,避免误伤正常查询;上线新规则前可先用EXPLAIN验证语法、再观察COUNTER_SQL_BLOCK_HIT_COUNT指标确认命中情况; - 统计作业豁免:FE 的统计信息连接/作业(
_statistics_相关内部写入)在黑名单检查前被显式跳过,若你的规则要覆盖内部行为,需注意这一豁免边界; - 运维流程:
admin set frontend config修改的enable_sql_blacklist属于动态配置,若希望该开关重启后保持,建议同时评估将其固化到 FE 启动配置中(以当前部署环境的配置管理方式为准)。
小结
StarRocks 的 SQL 黑名单机制用"动态总开关 + 正则规则集合 + EditLog 持久化"三层结构,为管理员提供了一道轻量但持久化的 SQL 拦截防线:
- 用
admin set frontend config ("enable_sql_blacklist" = "true")打开开关; - 用
ADD SQLBLACKLIST "<正则>"封禁危险 SELECT / INSERT / CTAS 模式,注意\\双重转义与(?i)、^、负向前瞻等正则特性; - 用
SHOW SQLBLACKLIST核对规则与 Index,必要时DELETE SQLBLACKLIST <id>批量撤销; - 规则命中会返回
ERROR 1064 ... is in blacklist,并可通过 FE 指标COUNTER_SQL_BLOCK_HIT_COUNT观测拦截量。
相关源码入口:黑名单核心实现、执行器拦截点、配置项定义、错误上报,官方文档见 Blacklist Management。
【免费下载链接】starrocksThe world's fastest open query engine for sub-second analytics both on and off the data lakehouse. With the flexibility to support nearly any scenario, StarRocks provides best-in-class performance for multi-dimensional analytics, real-time analytics, and ad-hoc queries. A Linux Foundation project.项目地址: https://gitcode.com/GitHub_Trending/st/starrocks
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考