Pisa-Proxy 之 SQL 解析实践

Pisa-Proxy 之 SQL 解析实践SQL 语句解析是一个重要且复杂的技术,数据库流量相关的 SQL 审计、读写分离、分片等功能都依赖于 SQL 解析,而 Pisa-Proxy 作为 Database Mesh 理念的一个实践,对数据库

Pisa-Proxy 之 SQL 解析实践

SQL 语句解析是一个重要且复杂的技术,数据库流量相关的 SQL 审计、读写分离、分片等功能都依赖于 SQL 解析,而 Pisa-Proxy 作为 Database Mesh 理念的一个实践,对数据库流量的治理是其核心,因此实现 SQL 解析是一项很重要的工作。本文将以 Pisa-Proxy 实践为例,为大家展现 Pisa-Proxy 中的 SQL 解析实现,遇到的问题及优化。

一、背景

关于语法分析

语法分析一般通过词法分析器,如 Flex,生成相应的 token,语法分析器通过分析 token,来判断是否满足定义的语法规则。

语法分析器一般会通过解析生成器生成。

语法分析算法常用的有以下:

  • LL(自上而下)

与上下文无关文法,从左到右扫描,从最左推导语法树,相比 LR 更容易理解,错误处理更友好。

  • LR(自下而上)

与上下文无关文法,从左到右扫描,从最右节点推导语法树,相比 LL 速度快。

  • LALR

与 LR 类似,在解析时比 LR 生成的状态更少,从而减少 Shift/Reduce 或者 Reduce/Reduce 冲突,被业界广泛使用的 bison/yacc 生成的就是基于 LALR 解析器。

关于调研

在开发 SQL 解析之初,我们从性能、维护性、开发效率、完成度四方面分别调研了 antlr_rust,sqlparser-rs,nom-sql 项目,但都存在一些问题。

  • antlr_rust

ShardingSphere 实现了基于 Antlr 的不同的 SQL 方言解析,为了使用它的 Grammar,我们调研了 antlr_rust 项目,此项目不够活跃,成熟度不够高。

  • sqlparser-rs

在 Rust 社区里,sqlparser-rs 项目是一个较为成熟的库,兼容各种 SQL 方言,Pisa-Proxy 在未来也会支持多种数据源,但是由于其词法和语法解析都是纯手工打造的,对我们来说会不易维护。

  • nom-sql

nom-sql 是基于 nom 库实现的 SQL 解析器,但是未实现完整,性能测试不如预期。

  • grmtools

Grmtools 是在寻找 Rust 相关的 Yacc 实现时发现的库,该库实现了兼容绝大部分 Yacc 功能,这样就可以复用 MySQL 官方的语法文件,但是需要手写 Lex 词法解析,经过对开发效率及完成度权衡后,我们决定做难且正确的事,实现自己的 SQL 解析器,快速实现一个 Demo 进行测试。

编码完成后,测试效果还不错。

总结如下:

工具 antlr_rust sqlparser-rs nom-sql grmtools
完成度
性能
维护性
开发效率

最终我们选择了 Grmtools 来开发 Pisa-Proxy 中的 SQL 解析。

二、Grmtools 使用

使用 Grmtools 解析库大致分为两个步骤,下面以实现计算器为例。

  1. 编写 Lex 和 Yacc 文件

Lex:创建 calc.l,内容如下:

/%%
[0-9]+ "INT"
+ "+"
* "*"
( "("
) ")"
[	 ]+ ;

Grammar:创建 calc.y 内容如下:

%start Expr
%avoid_insert "INT"
%%
Expr -> Result<u64, ()>:
      Expr "+" Term { Ok($1? + $3?) }
    | Term { $1 }
    ;

Term -> Result<u64, ()>:
      Term "*" Factor { Ok($1? * $3?) }
    | Factor { $1 }
    ;

Factor -> Result<u64, ()>:
      "(" Expr ")" { $2 }
    | "INT"
      {
          let v = $1.map_err(|_| ())?;
          parse_int($lexer.span_str(v.span()))
      }
    ;
%%
  1. 构造词法和语法解析器

Grmtools 需要在编译时生成词法和语法解析器,因此需要创建 build.rs,其内容如下:

use cfgrammar::yacc::YaccKind;
use lrlex::CTLexerBuilder;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    CTLexerBuilder::new()
        .lrpar_config(|ctp| {
            ctp.yacckind(YaccKind::Grmtools)
                .grammar_in_src_dir("calc.y")
                .unwrap()
        })
        .lexer_in_src_dir("calc.l")?
        .build()?;
    Ok(())
}
  1. 在应用中集成解析
use std::env;

use lrlex::lrlex_mod;
use lrpar::lrpar_mod;

// Using `lrlex_mod!` brings the lexer for `calc.l` into scope. By default the
// module name will be `calc_l` (i.e. the file name, minus any extensions,
// with a suffix of `_l`).
lrlex_mod!("calc.l");
// Using `lrpar_mod!` brings the parser for `calc.y` into scope. By default the
// module name will be `calc_y` (i.e. the file name, minus any extensions,
// with a suffix of `_y`).
lrpar_mod!("calc.y");

fn main() {
    // Get the `LexerDef` for the `calc` language.
    let lexerdef = calc_l::lexerdef();
    let args: Vec<String> = env::args().collect();
    // Now we create a lexer with the `lexer` method with which we can lex an
    // input.
    let lexer = lexerdef.lexer(&args[1]);
    // Pass the lexer to the parser and lex and parse the input.
    let (res, errs) = calc_y::parse(&lexer);
    for e in errs {
        println!("{}", e.pp(&lexer, &calc_y::token_epp));
    }
    match res {
        Some(r) => println!("Result: {:?}", r),
        _ => eprintln!("Unable to evaluate expression.")
    }
}

详见: grmtools – grmtools

上文已经提到,我们需要手写词法解析,是因为在原生的 Grmtools 中,词法解析是用正则匹配的,对于灵活复杂的 SQL 语句来说,不足以满足,因此需要手工打造词法解析,在 Grmtools 中实现自定义词法解析需要我们实现以下 Trait:

lrpar::NonStreamingLexer

另外也提供了一个方便的方法去实例化:

lrlex::LRNonStreamingLexer::new()

三、遇到的问题

基于以上,我们开发了 SQL 词法解析,复用了 MySQL 官方的 sql_yacc 文件,在开发过程中,也遇到了以下问题。

  1. Shift/Reduce 错误
Shift/Reduce conflicts:
     State 619: Shift("TEXT_STRING") / Reduce(literal: "text_literal")

这是使用 LALR 算法经常出现的错误,错误成因一般通过分析相关规则解决,例如常见的 If-Else 语句,规则如下:

%nonassoc LOWER_THEN_ELSE 
%nonassoc ELSE 
stmt: 
    IF expr stmt %prec LOWER_THEN_ELSE
  | IF expr stmt ELSE stmt

当 ELSE 被扫描入栈时,此时会有两种情况。

1)按第二条规则继续 Shift

2)按第一条规则进行 Reduce

这就是经典的 Shift/Reduce 错误。

回到我们的问题,有如以下规则:

literal -> String:
    text_literal 
    { }
  | NUM_literal  
    { }
 ...
 
 text_literal -> String:
    "TEXT_STRING" {}
  | "NCHAR_STRING" {}
  | text_literal "TEXT_STRING" {}
 ...

分析:

stack Input token action
test Shift test
test $ Reduce: text_literal/Shift: TEXT_STRING

方案:

需要设置优先级解决,给 text_literal 设置更低的优先级,如以下:

%nonassoc "LOWER_THEN_TEXT_STRING"
%nonassoc "TEXT_STRING"


literal -> String:
    text_literal  %prec "LOWER_THEN_TEXT_STRING" 
    { }
  | NUM_literal  
    { }
 ...
 
 text_literal -> String:
    "TEXT_STRING" {}
  | "NCHAR_STRING" {}
  | text_literal "TEXT_STRING" {}
 ...
  1. SQL 包含中文问题

在使用词法解析时,.chars() 生成字符串数组会出现数组长度和字符长度不一致的情况,导致解析出错,要更改为 .as_bytes() 方法。

四、优化

  1. 在空跑解析(测试代码见附录),不执行 action 的情况下,性能如下:
[mworks@fedora examples]$ time ./parser

real        0m4.788s
user        0m4.781s
sys         0m0.002s

尝试优化,以下是火焰图:

Pisa-Proxy 之 SQL 解析实践

通过火焰图发现,大部分 CPU 耗时在序列化和反序列化,以下是定位到的代码:

Pisa-Proxy 之 SQL 解析实践

可以看出在每次解析的时候都需要反序列化数据,在编译完之后,__GRM_DATA__STABLE_DATA 是固定不变的, 因此 grmstable 这两个参数可以作为函数参数传递,更改为如下:

Pisa-Proxy 之 SQL 解析实践

  1. 再分析,每次解析的时候,都会初始化一个 actions 的数组,随着 grammar 中语法规则的增多,actions 的数组也会随之增大,且数组元素类型是 dyn trait 的引用,在运行时是有开销的。

再看代码,发现 actions 数组是有规律的,如以下:

::std::vec![&__gt_wrapper_0,
               &__gt_wrapper_1,
               &__gt_wrapper_2,
               ...
            ]

因此我们可以手动构造函数,以下是伪代码:

match idx {
    0 => __gt_wrapper_0(),
    1 => __gt_wrapper_1(),
    2 => __gt_wrapper_2(),
    ....
}

通过 gobolt 查看汇编,发现差异还是很大,省去了数组的相关开销,也能极大地减少内存使用。

Pisa-Proxy 之 SQL 解析实践

详见:https://rust.godbolt.org/z/zTjW479f6

但是随着 actions 数组的不断增大,会有大量的 je,jmp 指令,不清楚是否会影响 CPU 的分支预测,如影响是否可以通过 likely/unlikely 方式优化,目前还没有进行测试。

最终火焰图对比

Pisa-Proxy 之 SQL 解析实践

Pisa-Proxy 之 SQL 解析实践

最终测试结果

[mworks@fedora examples]$ time ./parser

real        0m2.677s
user        0m2.667s
sys         0m0.007s

五、总结

本文为 Pisa-Proxy SQL 解析解读系列第一篇,介绍了在 Pisa-Proxy 中开发 SQL 解析背后的故事,后续我们会陆续为大家详细介绍 Yacc 语法规则的编写,Grmtools 中组件及实用工具等内容,敬请期待。

附录

Pisa-Proxy 的 SQL 解析代码:

pisanix/pisa-proxy/parser/mysql at master · database-mesh/pisan

测试代码

    let input = "select id, name from t where id = ?;"
    let p = parser::Parser::new();
    for _ in 0..1_000_000
    {
        let _ = p.parse(input);
    }

Pisanix

项目地址:https://github.com/database-mesh/pisanix

官网地址:https://www.pisanix.io/

Database Mesh:https://www.database-mesh.io/

SphereEx 官网:https://www.sphere-ex.com

原文地址:https://www.cnblogs.com/sphereex/archive/2022/06/27/16415904.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/5079.html

(0)
上一篇 2023-05-24
下一篇 2023-05-24

相关推荐

  • Python CGI示例:从Web服务器获取表单数据

    Python CGI示例:从Web服务器获取表单数据CGI(Common Gateway Interface)是一种让web服务器能够执行程序和脚本的标准协议,它允许web浏览器或客户端向服务器发送请求,然后服务器返回与请求相关的信息或数据。对于Python程序员来说,CGI是一个非常重要的工具,可以使他们轻松地开发以Python为基础的web应用程序。本文将介绍如何在Python中使用CGI从web服务器获取表单数据。

    2024-03-28
    80
  • python自动化怎么样啊(python如何做自动化)

    python自动化怎么样啊(python如何做自动化)当然可以,Python除了可以做自动化开发、自动化运维、自动化测试之外,还可以从事人工智能、爬虫等工作。

    2023-11-01
    145
  • 【2020Python修炼记】MySQL之 视图、触发器、事务、存储过程、函数

    【2020Python修炼记】MySQL之 视图、触发器、事务、存储过程、函数【目录】(其余均为了解知识) 一 视图 二 触发器 三 事务(掌握) 四 存储过程 五 函数 六 流程控制 七、索引理论 一、视图 1、什么是视图 视图是一个虚拟表(非真实存在),其本质是【根据SQL

    2023-02-26
    149
  • python中验证ip正则(正则验证ip地址)

    python中验证ip正则(正则验证ip地址)首先给出一个c函数的原型:int sscanf(const char *buffer,const char *format,[argument ]…)它的返回值是参数的数据,也就是argument的个数,buffer:存储的数据,format:格式控制字符串,argument:选择性设定字符串。这个程序从标准流读取数据,可以进行无限制的输入。下面贴出代码,然后引出另外一个问题,将字符串ip转换成整形ip地址。[cpp]

    2023-11-19
    139
  • Python中将元组转换为列表的方法

    Python中将元组转换为列表的方法Python中元组(tuple)和列表(list)是两种具有不同性质的序列类型。在某些情况下,需要将元组转换为列表,因为列表在某些操作中具有更好的可变性和处理性能。本文将详细介绍Python中将元组转换为列表的方法,包括使用列表解析、使用typecasting、使用extend方法、使用list构造函数以及使用循环遍历等方法。

    2024-03-26
    89
  • mysql基础题库_2000道基础题

    mysql基础题库_2000道基础题1.一张表,里面有 ID 自增主键,当 insert 了 17 条记录之后, 删除了第 15, 16, 17 条记录,再把 Mysql 重启,再 insert 一条记 录,这条记录的 ID 是 18

    2023-05-02
    160
  • MySQL 多表关联一对多查询取最新的一条数据「终于解决」

    MySQL 多表关联一对多查询取最新的一条数据「终于解决」SQL语句 SELECT SQL_CALC_FOUND_ROWS * FROM tableA a LEFT JOIN ( SELECT BC.* FROM ( SELECT MAX( id ) AS…

    2023-03-08
    160
  • 数据分析利器:pandas

    数据分析利器:pandas在数据分析领域,处理数据是基本工作之一。在Python中,pandas是一个常用的数据处理工具。pandas是基于NumPy数组构建的,常用于处理结构化数据,例如来自SQL数据库或Excel电子表格的数据。

    2024-02-27
    100

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注