ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

解析改造的最小闭环

解析改造的最小闭环 解析改造的最小闭环在企业的数据库安全治理、多租户数据隔离或 SQL 优化场景中经常需要对 MySQL 的解析器Parser进行定制扩展。常见需求包括自动注入tenant_id过滤条件、拦截无 Index 的全表更新 SQL、透明加密/解密敏感列或是动态注入 Hint。直接修改sql/sql_yacc.yy会增加维护和升级成本。若需求允许可先在代理或插件层实现最小闭环明确哪些语句可改写、哪些必须原样透传。本文介绍一个解耦的最小实现以及从观察到灰度启用的验证路径。一、 组件职责拆分与架构设计为了实现最小可运行架构我们将定制解析器解耦为四个功能独立的模块各模块通过严格定义的 AST抽象语法树数据结构传递上下文1. 词法与语法树构建器Lexer Parser负责将字符串格式的 SQL 切分为 Token 序列并构建内存中的 AST 结构。在 MVP 阶段无需从零写 Yacc 语法推荐使用基于成熟 AST 库如pingcap/parser或 Pythonsqlglot的组件。2. Session 上下文管理器Context Manager提取当前数据库连接的 Session 变量如当前登录用户、租户 ID、读写分离标记并将这些状态绑定至当前 SQL 求解上下文。3. AST 注入与改写引擎AST Rewriter采用访问者模式Visitor Pattern遍历 AST 树节点。当匹配到SelectStatement或UpdateStatement时自动在WHERE子句中追加安全过滤节点。4. 原生 Pass-through 降级模块一旦遇到解析器未覆盖的复杂特殊语法如特定的存储过程或 DDL迅速放弃改写原样透传给 MySQL 执行引擎防止阻断正常业务。二、 方案对比三种定制化解析器实现路线在选择架构路线时团队需要根据研发能力与维护成本进行权衡维度内核源码修改 (sql_yacc.yy)MySQL 插件模式 (Audit/Rewrite Plugin)独立 Proxy 代理层 (MVA)侵入性极高需编译自定义二进制低通过 SO 动态加载零侵入独立进程部署研发门槛极高熟练掌握 C / Yacc中需了解 C Plugin API中~低支持 Go/Python 编写解析性能损耗零损耗极低在 MySQL 进程内低增加 0.5~1ms 网络 Hop维护与升级成本极高合并 upstream 代码极难中受限于 MySQL Plugin ABI 变更极低与 MySQL 内核完全解耦推荐适用场景云厂商深度内核定制内部 DB 团队简单规则拦截中大型企业 SQL 治理 MVP 落地三、 生产级 MVP 极简代码实现租户隔离与安全 Hint 自动注入以下 Python 代码基于 AST Visitor 模式展示了一个可直接运行的迷你 MySQL 解析改写引擎。它实现了自动识别SELECT与UPDATE语句并强行注入tenant_id X条件以及MAX_EXECUTION_TIMEHint。import sys import logging from typing import Optional import sqlglot from sqlglot import parse_one, exp logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) class SafeTenantParserEngine: def __init__(self, default_max_execution_time_ms: int 3000): self.max_time_ms default_max_execution_time_ms def transform_sql(self, raw_sql: str, tenant_id: int) - Tuple[str, bool]: 解析并改写 SQL 1. 在 WHERE 条件中注入 tenant_id {tenant_id} 2. 为 SELECT 语句自动注入 MAX_EXECUTION_TIME Hint :return: (改写后的 SQL, 是否成功改写) if not raw_sql or not raw_sql.strip(): return raw_sql, False try: # 1. 词法与 AST 构建 ast parse_one(raw_sql, readmysql) if ast is None: return raw_sql, False is_modified False # 2. 如果是 SELECT 语句注入 Hint 与 租户隔离 if isinstance(ast, exp.Select): # 注入 租户 ID 条件 tenant_condition exp.condition(ftenant_id {tenant_id}) ast ast.where(tenant_condition) # 转换为表达式结构并生成目标 SQL rewritten_sql ast.sql(dialectmysql) # 注入 MySQL 特性 Hint: /* MAX_EXECUTION_TIME(3000) */ hint_str f/* MAX_EXECUTION_TIME({self.max_time_ms}) */ if rewritten_sql.upper().startswith(SELECT): rewritten_sql SELECT hint_str rewritten_sql[6:] is_modified True return rewritten_sql, is_modified # 3. 如果是 UPDATE / DELETE 语句强行要求且注入 WHERE tenant_id elif isinstance(ast, (exp.Update, exp.Delete)): tenant_condition exp.condition(ftenant_id {tenant_id}) ast ast.where(tenant_condition) rewritten_sql ast.sql(dialectmysql) is_modified True return rewritten_sql, is_modified # 4. 其他类型语句如 DDL/SET跳过改写Pass-through return raw_sql, False except Exception as ex: # 解析遇到不兼容的特殊语法触发安全 Fallback logging.error(fFailed to parse SQL via AST Engine: {raw_sql}. Error: {str(ex)}) return raw_sql, False # --- 单元测试与验证 --- if __name__ __main__: engine SafeTenantParserEngine(default_max_execution_time_ms2000) test_cases [ (SELECT id, name FROM users WHERE age 18, 1001), (UPDATE orders SET status COMPLETED WHERE order_id 99, 1001), (CREATE TABLE test_tbl (id INT), 1001), # DDL 应原样透传 (SELECT * FROM products, 2002) # 无 WHERE 的 SELECT 强行注入 WHERE tenant_id ] print(--- Running Custom Parser MVP Transformation ---) for original, tid in test_cases: res_sql, modified engine.transform_sql(original, tenant_idtid) print(f\n[Original SQL]: {original}) print(f[Tenant ID ]: {tid}) print(f[Modified? ]: {modified}) print(f[Result SQL ]: {res_sql})四、 从 MVP 走向生产环境的演进路线搭建好最小可运行方案后不建议立即全量替换线上流量推荐按以下三个阶段演进第一阶段只解析不改写Dry-Run Mode在 Proxy 中部署 Parser 组件解析所有线上 SQL 并打印 AST 校验日志验证各种边缘复杂 SQL如包含 Table Alias、Window Function、CTE 表达式的解析成功率是否达到 99.99%。第二阶段只改写不拦截Audit Mode将原始 SQL 与改写后 SQL 同时打点至日志系统通过对比实验确保改写后的 SQL 在 MySQL 执行计划上符合预期。第三阶段灰度切流与实时监控按 DB Schema 或 App Connection Pool 逐个灰度开启改写逻辑。同时严密监控解析耗时与数据库 CPU 变动确保系统稳定。组件拆分有助于限定改写范围每个阶段都应记录失败 SQL、语义差异和旁路次数为下一阶段的开关策略提供依据。
返回列表