关于数据库:在-PisaProxy-中如何利用-Rust-实现-MySQL-代理

5次阅读

共计 9455 个字符,预计需要花费 24 分钟才能阅读完成。

一、前言

背景

在 Database Mesh 中,Pisanix 是一套以数据库为核心的治理框架,为用户提供了诸多治理能力,例如:数据库流量治理,SQL 防火墙,负载平衡和审计等。在 Pisanix 中,Pisa-Proxy 是作为整个 Database Mesh 实现中数据立体的外围组件。Pisa-Proxy 服务自身须要具备 MySQL 协定感知,了解 SQL 语句,能对后端代理的数据库做一些特定的策略,SQL 并发管制和断路等性能。在这诸多个性当中,可能了解 MySQL 协定就尤为重要,本篇将次要介绍 MySQL 协定和在 Pisa-Proxy 中 MySQL 协定的 Rust 实现。

Why Rust

为什么要选用 Rust 语言呢?咱们的考量有以下几个必要条件。

  • 安全性:首先作为数据库治理的外围组件,其语言的安全性是居首位的。Rust 中,类型平安实现内存平安,如所有权机制、借用、生命周期等个性防止了程序开发过程中的空指针、悬垂指针等问题,从而保障了服务在语言层面的安全性。
  • 优良的性能体现:Rust 的指标在性能方面对标 C 语言,但在平安和生产力方面则比 C 更胜一筹。其无 GC,不须要开发人员手动分配内存等个性,极大水平地缩小内存碎片,简化内存治理。
  • 低开销:从开发效率和可读可维护性上来说,有足够的形象能力,并且这种形象没有运行时开销(runtime cost)。零开销形象,通过泛型和 Trait 在编译期开展并实现形象解释。
  • 实用性:有优良的包管理器工具 Crate、文档正文反对、具体的编译器提醒、敌对的错误处理等,在开发过程中可能高效帮忙程序员疾速开发出牢靠、高性能的利用。

二、整体架构,模块设计

整体架构
如图 1,代理服务蕴含服务端和客户端的协定解析、SQL 解析、访问控制、连接池等模块。

工作流程
在图 1 中咱们能够看出整个 Proxy 服务能够概括为以下几个阶段。

  1. 首先 Pisa-Proxy 反对 MySQL 协定,将本人假装为数据库服务端,利用连贯配置只需批改拜访地址即可建连 Pisa-Proxy 通过读取利用发来的握手申请和数据包;
  2. 失去利用发来的 SQL 语句后对该 SQL 进行语法解析,并失去该 SQL 的 AST;
  3. 失去对应 AST 后,基于 AST 实现高级访问控制和 SQL 防火墙能力;
  4. 访问控制和防火墙通过后 SQL 提交执行,SQL 执行阶段的指标将采集为 Prometheus Metrics,最初依据负载平衡策略获取执行该语句的后端数据库连贯;
  5. 如果连接池为空将依据配置建设和后端数据库的连贯,SQL 将从该连贯发往后端数据库;
  6. 最初读取 SQL 执行后果,组装后返回给客户端。

三、如何用 Rust 疾速实现 MySQL 代理服务

如图 2,在整个代理服务中总体分为两局部:服务端和客户端,即代理服务作为服务端解决来自客户端的申请。和服务端,须要对服务端发动认证,并将客户端端命令发送给 MySQL 数据库。在这两局部中咱们须要创立两套 Socket 来实现网络数据包的解决。

技术选型

介绍

在网络报解决和运行时解决上,咱们选用了 Rust 实现的 Tokio(https://github.com/tokio-rs/t…)框架。Tokio 框架是 Rust 编写的牢靠、异步和精简应用程序的运行时。并且 Tokio 是一个事件驱动的非阻塞 I/O 平台,用于应用 Rust 编程语言编写异步应用程序。在高层次上,它提供了几个次要组件:

  • 一个多线程、基于工作窃取的任务调度程序(https://docs.rs/tokio/latest/…)。
  • 由操作系统的事件队列(epoll、kqueue、IOCP 等)反对的响应式编程。
  • 异步 TCP 和 UDP(https://docs.rs/tokio/latest/…)套接字。

同时,Tokio 还提供了丰盛的工具链,例如编解码工具包、分帧包等等,能够使咱们更加方便快捷地解决 MySQL 中各种各样的数据报文。

我的项目地址: https://github.com/tokio-rs/t…

劣势

a. 疾速:Tokio 的设计旨在使应用程序尽可能都快。

b. 零老本形象:Tokio 以 Future 为根底。尽管 Future 并非 Rust 独创,但与其余语言的 Future 不同的是,Tokio Future 编译成了状态机,用 Future 实现常见的同步,不会减少额定开销老本。Tokio 的非阻塞 IO 能够充分发挥零碎劣势,例如实现相似 Linux Epoll 这种多路复用技术,在单个线程上的多路复用容许套接字并批量接管操作系统音讯,从而缩小零碎调用,所有这些都能够缩小应用程序的开销。

c. 牢靠:Rust 的所有权模型和类型零碎能够实现零碎级应用程序,而不用放心内存不平安。

d. 轻量级:没有垃圾收集器,因为 Tokio 是基于 Rust 构建的,所以编译后的可执行文件蕴含起码的语言运行时。这意味着,没有垃圾收集器,没有虚拟机,没有 JIT 编译,也没有堆栈操作。这样在编写多线程并发的零碎时,可能无效防止阻塞。

e. 模块化:每个组件都位于一个独自的库中。如果须要,应用程序能够筛选所需的组件,防止依赖不须要的组件。

代码实现

Rust 中数据包解决

#[derive(Debug)]
pub struct Packet {
    pub sequence: u8,
    pub conn: BufStream<LocalStream>,
    pub header: [u8; 4],
}

以上为 Proxy 数据包解决逻辑中外围的构造体,构造体中蕴含了三个字段别离为:

  • sequence:报文中的序列号 ID
  • conn:解决链接的 Socket
  • header:存储音讯头报文的数组,长度为 4 字节

在包解决逻辑中次要定义了以下函数,在整个代理服务中网络数据交换都由以下办法来实现。

Pisa-Proxy 作为服务端

pub struct Connection {
    salt: Vec<u8>,
    status: u16,
    collation: CollationId,
    capability: u32,
    connection_id: u32,
    _charset: String,
    user: String,
    password: String,
    auth_data: BytesMut,
    pub auth_plugin_name: String,
    pub db: String,
    pub affected_rows: i64,
    pub pkt: Packet,
}

下面的构造体形容了 Pisa- Proxy 作为服务端解决来自于客户端申请时所蕴含的字段。例如,其中蕴含了和 MySQL 客户端进行认证信息,和所蕴含数据包解决逻辑的 Packet。

Pisa-Proxy 作为客户端

#[derive(Debug, Default)]
pub struct ClientConn {
    pub framed: Option<Box<ClientCodec>>,
    pub auth_info: Option<ClientAuth>,
    user: String,
    password: String,
    endpoint: String,
}

Tokio 提供的编解码器

在 Pisa-Proxy 中,大量应用了 Tokio 工具包中的编解码器,应用 codec Rust 会主动帮忙开发者将原始字节转化为 Rust 的数据类型,不便开发者解决数据。应用编解码器,只须要在代码中为定义好的类型实现 Decoder 和 Encoder 两个 Trait,就能够通过 stream 和 sink 进行数据的读写。上面通过一个简略的示例来看一下 Tokio 编解码器的应用步骤。

应用 Tokio 编解码器一共分为三步:

  1. 首先要自定义一个谬误类型,这里定义了一个 ProtocolError,并为它实现一个 from 办法,可能让它接管错误处理。
pub enum ProtocolError {Io(io::Error),
}

impl From<io::Error> for ProtocolError {fn from(err: io::Error) -> Self {ProtocolError::Io(err)
    }
}
  1. 定义一个数据类型,这里咱们申明一个 Message 为 String,而后定义一个构造体,也就是咱们要解析的是原始字节流要理论转换成的构造体,也即 Tokio 中 framed(帧的概念),这里定义一个空构造体 PisaProxy;
type Message = String;
struct PisaProxy;

接下来就是为 PisaProxy 别离实现 Encoder 和 Decoder 这两个 Trait。这里的示例,实现的性能为将数据转为 byte 数组,追加到 buf 中。在编码器中,咱们首先要指定 Item 类型为 Message 和谬误类型,编码解决逻辑这里将字符串进行拼接并返回给客户端。

在这里,Encoder 编码是指将用户自定义类型转换成 BytesMut 类型,写到 TcpStream 中,Decoder 解码指将读到的字节数据序列化为 Rust 的构造体。

impl Encoder<Message> for PisaProxy {
    type Error = ProtocolError;

    fn encode(&mut self, item: Message, dst: &mut BytesMut) ->Result<(), Self::Error> {dst.extend(item.as_bytes());
        Ok(())
    }
}

impl Decoder for PisaProxy {
    type Item = Message;
    type Error = ProtocolError;

    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {if src.is_empty() {return Ok(None);
        }
        let data = src.split();
        let mut buf = BytesMut::from(&b"hello:"[..]);
        buf.extend_from_slice(&data[..]);
        let data = String::from_utf8_lossy(&buf[..]).to_string();

        Ok(Some(data))
    }
}
  1. 当实例化 PisaProxy 构造体后,就能够调用 framed 办法,codec 的 framed 办法(codec.framed(socket))将 TcpStream 转换为 Framed<TcpStream,PisaProxy>,这个 Framed 就是实现了 tokio 中的 Stream 和 Sink 这两个 trait,实现的这两个 Trait 的实例就具备了接管(通过 Stream)和发送(通过 Sink)数据的性能,这样咱们就能够调用 send 办法发送数据了。
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "127.0.0.1:9088";
    let listener = TcpListener::bind(addr).await?;
    println!("listen on: {:?}", addr);

    loop {let (socket, addr) = listener.accept().await?;

        println!("accepted connect from: {}", addr);

        tokio::spawn(async move {let codec = PisaProxy {};
            let mut conn = codec.framed(socket);
            loop {match conn.next().await {Some(Ok(None)) => println("waiting for data..."),
                    Some(Ok(data)) => {println!("data {:?}", data);
                        conn.send(data).await;
                    },
                    Some(Err(e)) => { },
                    None => {},}
            }
        });
    }
}

四、MySQL 协定在 Pisa-Proxy 中的实现

MySQL 协定简介
MySQL 数据库自身是一个很典型的 C/S 构造的服务,客户端和服务端通信能够通过 Tcp 和 Unix Socket 的形式进行交互。在本篇中,咱们次要阐明通过网络 Tcp 的形式来实现 MySQL 代理。

MySQL 客户端和服务端的交互次要蕴含了两个重要的过程:1. 握手认证,2. 发送命令。本篇会次要围绕这两个过程来介绍相干的实现。在客户端和服务端交互的过程中次要蕴含了以下几种类型报文:数据包、数据完结包、胜利报告包以及谬误音讯包,在前面的章节中会为大家具体介绍这几种报文。

交互过程
MySQL 客户端和服务端在交互的过程中次要蕴含了两个过程,即握手认证阶段和执行命令阶段,当然在这两个过程之前首先要经验 TCP 三次握手的过程。在三次握手完结后首先进入握手认证阶段,在替换完信息并且客户端正确登录服务端后,进入执行命令阶段,图 3 残缺形容了整个交互过程。

代码链接:https://github.com/database-m…

握手认证

在握手认证阶段是 MySQL 客户端和服务端建联十分重要的阶段,该阶段产生在 TCP 三次握手之后。首先服务端会给客户端发送服务端信息,其中包含协定版本号、服务版本信息、挑战随机数、权能标记位等等。当客户端接管到服务端发来的响应之后,客户端开始发动认证申请。认证申请中,会携带客户端用户名、数据库名以及通过服务端响应中的挑战随机数将客户端明码加密后,一起发送给服务端进行校验。校验过程中,除了会对用户名明码进行校验,还会匹配客户端所应用的认证插件,如果不匹配则会产生插件的主动切换,以及判断客户端是否应用了加密链接。当以上阶段全副失常实现后,客户端则登录胜利,服务端返回客户端 OK 数据报文。

上述过程别离在 runtime 和 protocol 的 server 中实现。在 runtime 中的 start 函数期待申请进入,Tcp 三次握手完结后,从 handshake 函数如图 3,开始握手阶段。在 handshake 中别离蕴含了三个过程,首先由 write_initial_handshake 给客户端发送服务端信息,而后在 read_handshake_response 客户端拿着服务端信息开始认证申请。

pub async fn handshake(&mut self) -> Result<(), ProtocolError> {match self.write_initial_handshake().await {Err(err) => return Err(err::ProtocolError::Io(err)),
            Ok(_) => debug!("it is ok"),
        }

        match self.read_handshake_response().await {Err(err) => {return Err(err);
            }
            Ok(_) => {self.pkt.write_ok().await?;
                debug!("handshake response ok")
            }
        }

        self.pkt.sequence = 0;

        Ok(())
    }

执行命令

当握手和认证阶段实现后,此时客户端才算是真正意义上的与服务端实现建设链接。那么此时则进入执行命令阶段。在 MySQL 中,可能发送命令的指令类型有很多种,咱们会在下文中为大家进行介绍。

代码链接: https://github.com/database-m…

在上面的代码中能够看到,Pisa-Proxy 在这里会对不同类型的指令进行不同的逻辑解决。例如,初始化 db 的解决逻辑为 handle_init_db,解决查问的逻辑为 handle_query

match cmd {COM_INIT_DB => self.handle_init_db(&payload, true).await,
          COM_QUERY => self.handle_query(&payload).await,
          COM_FIELD_LIST => self.handle_field_list(&payload).await,
          COM_QUIT => self.handle_quit().await,
          COM_PING => self.handle_ok().await,
          COM_STMT_PREPARE => self.handle_prepare(&payload).await,
          COM_STMT_EXECUTE => self.handle_execute(&payload).await,
          COM_STMT_CLOSE => self.handle_stmt_close(&payload).await,
          COM_STMT_RESET => self.handle_ok().await,
          _ => self.handle_err(format!("command {} not support", cmd)).await,
}

MySQL 协定根本数据类型
在 MySQL 协定中次要有以下几种数据类型:

  • 整型值:MySQL 报文中整型值别离有 1、2、3、4、8 字节长度,应用小端传输。
  • 字符串(以 NULL 结尾)(Null-Terminated String):字符串长度不固定,当遇到 ’NULL’(0x00)字符时完结。
  • 二进制数据(长度编码)(Length Coded Binary)

参考链接:https://dev.mysql.com/doc/int…

报文构造
在 MySQL 客户端和服务端所交互的数据最大长度为 16MByte,其根本数据报文构造类型如下:

图 4

图 5

在图 4 和图 5 中形容了 MySQL 报文的根本构造。报文包含了两局部,音讯头和音讯体。其中在音讯头中,3 字节示意数据报文长度,1 字节存储序列号,音讯体中存储理论的报文数据。

音讯头

用于标记以后申请音讯的理论数据长度值,以字节为单位,占用 3 个字节,最大值为 0xFFFFFF,即接 2^24-1。

序列号

序列 ID 从 0 开始随每个数据包递增,并且当进入新的命令时重置为 0。序列号 ID 有可能会产生回绕,当产生回绕时,需将序列号 ID 重置为 0,并且从新开始计数递增。

报文数据

音讯体用于寄存申请的内容及响应的数据,长度由音讯头中的长度值决定。

客户端申请命令报文

该指令用于标识客户所要执行命令的类型,以字节为单位占用 1 个字节。申请命令报文罕用到的有 Text 文本协定和 Binary 二进制协定。这里能够参考【执行命令】中的代码,代码中形容了对不同指令的解决逻辑。

在文本协定中罕用到的有以下指令:

指令 性能
0x01 COM_QUIT 敞开连贯
0x02 COM_INIT_DB 切换数据库
0x03 COM_QUERY SQL 查问申请
0x04 COM_FIELD_LIST 获取数据表字段信息
0x05 COM_CREATE_DB 创立数据库
0x06 COM_DROP_DB 删除数据库
0x08 COM_SHUTDOWN 进行服务器
0x0A COM_PROCESS_INFO 获取以后连贯的列表
0x0B COM_CONNECT (外部线程状态)
0x0E COM_PING 测试连通性

更多请参考:https://dev.mysql.com/doc/int…

在二进制协定,即 Prepare Statement 中罕用到的有以下指令:

指令 性能
0x16 COM_STMT_PREPARE 预处理 SQL 语句
0x17 COM_STMT_EXECUTE 执行预处理语句
0x18 CCOM_STMT_SEND_LONG_DATA 发送 BLOB 类型的数据
0x19 COM_STMT_CLOSE 销毁预处理语句
0x1A COM_STMT_RESET 革除预处理语句参数缓存

更多请参考:https://dev.mysql.com/doc/int…

响应报文

  • OK 响应报文
  • Error 响应报文
  • Field 构造
  • EOF 构造
  • Row Data 构造

……

例如,上面代码中展现了如何给客户端写 OK 和 EOF 报文。

#[inline]
pub async fn write_ok(&mut self) -> Result<(), Error> {let mut data = [7, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0];
  self.set_seq_id(&mut data);
  self.write_buf(&data).await
}
  
#[inline]
pub async fn write_eof(&mut self) -> Result<(), Error> {let mut eof = [5, 0, 0, 0, 0xfe, 0, 0, 2, 0];
  self.set_seq_id(&mut eof);
  self.write_buf(&eof).await
}

com_query 申请流程

如图 6 为 com_query 的申请流程,com_query 指令可能会返回以下后果集:

  • ERR 报文
  • OK 报文
  • Protocol::LOCAL_INFILE_Request 报文
  • Resultset 报文

在(https://github.com/database-m…)文件中次要为对 ResultSet 后果集的解决,能够看到在这里定义了 ResultSet 构造,同样对 ResultSetCodec 别离实现了 Encoder 和 Decoder 两个 Trait,这样就能够通过编解码器来解决 ResultSet 的报文。

#[derive(Debug, Default)]
pub struct ResultsetCodec {
  pub next_state: DecodeResultsetState,
  pub col: u64,
  pub is_binary: bool,
  pub seq: u8,
  pub auth_info: Option<ClientAuth>,
}


图 6

五、总结

以上,就是本篇文章的全部内容。在本篇文章中咱们介绍了应用 Rust 实现 MySQL 代理的动机,介绍了 MySQL 协定中一些罕用到的概念和 MySQL 中数据报文在网络中是如何进行替换数据的;并在最初介绍如何应用 Rust 去疾速实现一个 MySQL 代理服务。更多实现细节请关注 Pisanix 代码仓库。

欢送点击链接查看相干教学视频:https://www.bilibili.com/vide…

六、相干链接

Pisanix
我的项目地址:https://github.com/database-m…

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

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

Mini-Proxy:一个最小化的 MySQL Rust 代理实现
我的项目地址:https://github.com/wbtlb/mini…

社区
目前 Pisanix 社区每两周都会组织线上探讨,具体安顿如下,欢送各位小伙伴一起参加进来。

表列 A 表列 B
邮件列表 https://groups.google.com/g/d…
英文社区双周会(2022 年 2 月 27 日起),周三 9:00 AM PST https://meet.google.com/yhv-z…
中文社区双周会(2022 年 4 月 27 日起),周三 9:00 PM GMT+8 https://meeting.tencent.com/d…
微信小助手 pisanix
Slack https://databasemesh.slack.com/
会议记录 https://bit.ly/39Fqt3x

作者介绍

王波,SphereEx MeshLab 研发工程师,目前专一于 Database Mesh,Cloud Native 研发。Linux、llvm、yacc、ebpf user,Gopher & Rustacean and c bug hunter。

GitHub:https://github.com/wbtlb

正文完
 0