找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4125

积分

0

好友

544

主题
发表于 昨天 07:27 | 查看: 7| 回复: 0

十多年前,这更像是一个团队项目;到了 vibe coding 时代,一个人也终于能认真把它做起来。

我第一次动手写 MySQL Proxy 时,产生了一种很熟悉的错觉:“先把 TCP 监听搞定,再顺手解析一下 SQL,不就差不多了?”

现实很快教育了我。

你以为自己在写一个“会转发流量的小工具”,实际上你面对的是一个状态化协议、一整套连接生命周期、复杂的认证流程、严格的包边界定义、各种结果集格式,以及一堆稍不注意就会让客户端断连的细微之处。

这也解释了为什么在很多年前,开发一个 MySQL Proxy 听起来就不像是一个“周末写着玩”的个人项目。

它更像一个需要专家团队拆解的系统性问题:需要有人懂协议,有人擅长连接管理,有人能处理认证和异常,还要有人盯着性能和线上稳定性。难点从来不只是代码量,而在于你必须清楚自己到底在触碰什么。

但现在,情况确实有些不同了。

不是因为 MySQL 协议变简单了,也不是中间件工程的门槛消失了,而是因为我们进入了一个新的阶段:在 vibe coding 时代,复杂系统的原型搭建第一次对个人开发者真正友好了。

以前,一个人做这种项目,最容易被消耗掉的不是理解力,而是体力。参数解析要自己铺,日志框架要自己搭,测试脚手架要自己写,状态机雏形要自己抠,网络样板代码要自己磨。你明明知道大方向,但在真正触及“协议核心”之前,往往已经先累倒在无穷无尽的边角工作中。

现在不同了。样板代码、参数解析、基础测试、日志埋点、模块骨架,这些事情的实现成本已经大幅降低。大模型不会替你理解协议,也不会替你承担线上事故,但它确实把“从 0 到 1 先让项目跑起来”这件事的门槛压低了许多。于是,我们终于有机会把更多精力留给真正关键的问题:

在 MySQL Proxy 中,哪些部分可以先做透明转发,哪些部分又必须真正理解协议?

所以,在这个系列里,我想做一件具体的事:

使用 Rust,从零开始,一步一步构建一个 MySQL Proxy。

这不是一篇“AI 一键生成代理”的爽文,也不是吹嘘“看完就能做出生产级中间件”的故事。而是踏实地,从第一步开始,把一个看似需要团队协作的课题,拆解成今天个人开发者也能持续推进的工程。

而第一步,可能需要一点反直觉:

先别急着解析 SQL,最重要的是把流量通道打通。

Rust MySQL Proxy 架构示意图

先别装懂,先把路打通

这是我越来越相信的一点:开发代理,初期最重要的能力不是“理解协议”,而是“知道什么时候不该乱理解协议”。

MySQL 不像一个简单的文本协议,随便 read() 几下就能糊弄过去。它的数据传输是按 packet(数据包)组织的,每个包有自己的头部、长度和序号。连接建立时,客户端和服务端要先完成握手、能力协商、可选的 TLS 加密以及认证流程;这些步骤都结束后,才真正进入后续的命令阶段。

也就是说,在你还没摸清这些状态边界之前,最稳妥的做法,不是“看起来很专业地解析几段字节”,而是先老老实实地搭建一个透明的字节流通道。

听起来有点保守?但工程上,这种保守往往是对的。

很多人第一步就栽在“手太快”。还没搞清 packet 边界,就想打印 SQL;还没搞清认证流程,就想修改 capability 标志位;还没搞清连接阶段的切换逻辑,就开始尝试拦截握手包。最终结果往往不是“离成功更近一步”,而是“客户端直接连不上了”。

所以,今天我们就做一件朴素但至关重要的事:

编写一个最小可运行的 MySQL Proxy。

它不懂 SQL,不懂结果集,也不懂 prepared statement。它只是简单地将客户端和真正的 MySQL 后端连接起来,然后安安静静地双向转发字节流。

这听起来像是一个入门级的代理。但别小看它,许多中间件项目最终夭折,原因不是后面的功能太复杂,而是第一步根本没把数据传输链路稳稳地打通。

我们今天到底要交付什么

目标非常具体:

  • 监听一个本地端口,例如 127.0.0.1:3307
  • 接收 MySQL 客户端的连接。
  • 再去连接真正的 MySQL 服务器,例如 127.0.0.1:3306
  • 把客户端和服务端两边的数据原样互相转发。

就这么简单。

如果最后你能通过这个 Rust 程序成功登录 MySQL,然后执行一句最基础的 SELECT 1;,那第一篇的任务就算圆满完成了。

别嫌它基础。这一步一旦打通,后面所有“开始理解 MySQL 协议”的工作,才有了真正的落脚点和试验田。

用 Rust 来写这件事,刚刚好

我喜欢用 Rust 写这类网络中间件,不仅仅是因为“性能高”这句空话,而是因为它特别适合将这类程序写得收敛、严谨。

这类程序最怕的不是一开始不会写,而是越写越散:连接处理逻辑散,日志分散,错误处理路径散,状态管理也散,最后整个工程像一个用异步调用和 if-else 临时缝合起来的帐篷。

Rust 的好处在于,它天然鼓励你把连接生命周期、字节流边界、错误路径和并发模型提前想清楚。编写时可能会觉得它有些较真,但当你开始深入处理协议解析、状态流转和连接上下文管理时,你会发现这种严谨特别有价值。

今天这一篇还比较简单,感受可能没那么深。但等到后面我们开始拆解数据包、处理握手、识别 COM_QUERY 和 prepared statement 时,这种“先把类型和边界立住”的设计优势会越来越明显。

先把项目建起来

Cargo.toml 先放上最小依赖:

[package]
name = "mini-mysql-proxy"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }

这里没有炫技,就是一个朴素的组合:

  • tokio 负责异步网络 IO。
  • clap 负责将监听地址、上游地址封装为命令行参数。
  • tracing 负责为每条连接附加日志上下文。
  • anyhow 先把错误处理简化,别在第一篇就深陷复杂的错误类型转换中。

你会发现,这也是 vibe coding 时代一个显著的变化:以前搭建项目骨架很消耗意志力,现在它已不再是最大的阻力。真正决定项目能否持续推进的,是你后续如何拆解和实现协议逻辑,而不是这里多写或少写几十行样板代码。

写一个完全不耍小聪明的代理

src/main.rs

use anyhow::Result;
use clap::Parser;
use tokio::io::copy_bidirectional;
use tokio::net::{TcpListener, TcpStream};
use tracing::{error, info, info_span, Instrument};

#[derive(Debug, Parser, Clone)]
struct Args {
    /// 代理监听地址,例如 127.0.0.1:3307
    #[arg(long, default_value = "127.0.0.1:3307")]
    listen: String,

    /// 后端 MySQL 地址,例如 127.0.0.1:3306
    #[arg(long, default_value = "127.0.0.1:3306")]
    upstream: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter("info")
        .with_target(false)
        .init();

    let args = Args::parse();
    let listener = TcpListener::bind(&args.listen).await?;

    info!(listen = %args.listen, upstream = %args.upstream, "proxy started");

    loop {
        let (mut client, client_addr) = listener.accept().await?;
        let upstream = args.upstream.clone();

        let span = info_span!("mysql_conn", %client_addr, %upstream);

        tokio::spawn(
            async move {
                let mut server = match TcpStream::connect(&upstream).await {
                    Ok(stream) => stream,
                    Err(err) => {
                        error!(error = ?err, "connect upstream failed");
                        return;
                    }
                };

                match copy_bidirectional(&mut client, &mut server).await {
                    Ok((client_to_server, server_to_client)) => {
                        info!(
                            client_to_server,
                            server_to_client,
                            "connection closed"
                        );
                    }
                    Err(err) => {
                        error!(error = ?err, "proxy io failed");
                    }
                }
            }
            .instrument(span),
        );
    }
}

整段代码看下来,最核心的其实只有一句:copy_bidirectional(&mut client, &mut server).await

它的意思非常直白:左边(客户端)来的字节原样送到右边(服务端),右边来的字节原样送回左边。

今天,我们不试图理解中间流动的字节是什么含义,只确保这条传输通路是畅通无阻的。这就是我所说的“完全不耍小聪明”。

这里真正有价值的,不是转发,而是克制

如果你有网络编程经验,可能会下意识冒出一个念头:“既然流量都从我这过了,要不顺便把前几个字节读出来打印看看?”

千万别。

这是第一篇里最值得克制手痒的一刻。因为一旦你开始“先读一点,再转发”,你就不再是一个透明代理了。你开始需要对上层协议语义和底层缓冲区管理负责。你得考虑半包、粘包、顺序、重放、缓冲区残留、两端读写节奏不一致等一系列复杂问题。你本来只是想修一条路,结果下一秒就把自己变成了繁忙的交通调度中心。

很多“第一版就写崩”的代理项目,都是这么开始的。今天最聪明的做法,恰恰是别显得“太聪明”。

还有一个隐蔽的坑:异步日志上下文

在这段代码里,特意使用了 .instrument(span) 的写法:

tokio::spawn(
    async move {
        // ...
    }
    .instrument(span),
);

而不是写成:

let _guard = span.enter();
// 然后执行一堆 await 操作

原因很简单:在异步代码中,很多人会把同步场景下好用的日志上下文写法直接照搬,结果把 enter() 返回的 guard 一路带过多个 .await 点。表面上不会报错,但实际上日志上下文已经开始串线。最终你看到的追踪日志可能像灵异事件:明明是 A 连接的日志,却混入了 B 连接的信息。

这种 Bug 特别烦人,因为它不是“程序一运行就崩溃”,而是“程序好像还能跑,但日志越来越不可信”。而对于中间件来说,最可怕的是什么?就是出了问题时,连最基本的日志都不可信。

所以,从第一篇开始,我们就希望把这些基础设定摆正。代码可以不复杂,但每条连接的上下文必须保持干净、独立。

把它跑起来

  1. 启动你的代理:

    cargo run -- --listen 127.0.0.1:3307 --upstream 127.0.0.1:3306
  2. 用 MySQL 客户端连接代理(注意端口是代理监听的 3307,而非 MySQL 本身的 3306):

    mysql -h 127.0.0.1 -P 3307 -u root -p
  3. 如果能正常输入密码并进入 MySQL shell,先别激动,执行一句最基础但最重要的测试:

    SELECT 1;

    只要它能正常返回结果,第一篇的核心目标就达成了。

代理的运行日志大概会长这样:

INFO proxy started listen=127.0.0.1:3307 upstream=127.0.0.1:3306
INFO mysql_conn{client_addr=127.0.0.1:53421 upstream=127.0.0.1:3306}: connection closed client_to_server=187 server_to_client=524

看到这样的日志时,通常会很欣慰。不是因为它炫酷,而是它说明了一件非常实际的事:你的 Rust 程序已经稳稳坐在了 MySQL 客户端和服务端之间,并且没有把事情搞砸。

这一步听起来像开胃菜,但它其实是整个系列后续所有工作的地基。

这一版代理,故意没做什么

先把边界讲清楚,以免误会我们已经“写出了 MySQL Proxy”。

并没有。 目前写出来的,更准确地说,是一个 MySQL 流量隧道

它现在还不会:

  • 拆解 MySQL packet。
  • 识别包头中的长度和序号。
  • 分辨当前连接是处于握手阶段还是命令阶段。
  • 识别 COM_QUERY 命令。
  • 理解服务端返回的是 OK、ERR 还是结果集。
  • 处理 prepared statement。
  • 进行 SQL 日志、审计、改写或路由。

换句话说,它现在完全不懂 MySQL 协议,只是恰好能传输 MySQL 协议流量。

但这完全没有问题,因为这正是第一篇该有的样子。第一篇最怕的不是“功能少”,而是“做了半懂不懂、边界模糊的功能”。我们要的是一个边界清晰、稳固可靠的起点,而不是一个充满隐藏假设和误解的雏形。

为什么第一篇不碰协议,反而是正确路线

我越来越不喜欢那种一上来就教人“解析 SQL”的 MySQL Proxy 教程。

不是说 SQL 不重要,而是它根本不属于第一层要解决的问题。你连连接阶段、字节流边界、基础转发和连接生命周期都还没理顺,就去盯着 SQL 解析,这就像还没学会平稳驾驶就开始改装发动机。

真正靠谱的推进顺序应该是:

  1. 先做透明通道,打通链路。
  2. 学会识别和解析 packet。
  3. 搞明白握手和认证流程。
  4. 才开始看 COM_QUERY 和结果集格式。
  5. 后面再慢慢涉及 prepared statement、路由、审计、可观测性、工程化封装。

简单来说,先会搬(转发),再会看(解析),最后才会改(干预)。 这个顺序听起来不够“性感”,但非常符合真实的工程演进路径。

vibe coding 时代,真正值钱的还是判断力

写到这里,这个系列的主线已经清晰了。你可能会想,既然现在大模型能辅助写代码,这种教程的意义是不是变小了?

我反而觉得恰恰相反。因为在 vibe coding 时代,最容易被低估的不是生成代码的能力,而是 判断下一步该做什么、以及如何正确拆解问题的能力

你可以让 AI 很快生成一个代理骨架,也可以让它帮你补全命令行参数、日志或测试。但如果你自己没有想清楚“第一篇应该只做透明转发,不要过早触碰协议解析”,那么 AI 只会高效地陪你一起走上弯路。

所以我想写这个系列,不只是为了教“怎么写代码”,更是为了把这种工程判断和拆解过程也呈现出来:为什么第一步不碰 SQL?为什么现在要先做通道?为什么后续要先拆包,再讲握手和认证?为什么有些“看起来聪明”的过早优化,其实是在为未来制造故障?

这部分思考,在当今时代,有时比代码本身更为重要。

这一篇到这里,算是迈出了很像样的第一步

今天我们没有做任何华丽的事情。没有分析协议,没有改写查询,没有实现读写分离、连接池或高可用。

我们只是编写了一个最小可运行的 Rust 程序,让 MySQL 协议流量真的从我们这里穿行而过。

但请不要低估这一步。因为只有当你真正“坐在”客户端和 MySQL 服务器之间,后面的世界才会真正向你打开。你终于有了一个可以持续观察、实验和演进的坚实起点。

下一篇开始,我们将把这个“只会搬运字节的简单代理”,升级为一个“开始能看得懂 MySQL packet 的智能代理”。到那时,我们才会第一次真正触碰到 MySQL Proxy 的核心骨架。

而今天,先把这条数据传输的道路稳稳修通,就已经是非常不错的开始了。如果你对这类从零开始的 开源实战 感兴趣,欢迎在 云栈社区 交流讨论,共同成长。




上一篇:Rust安全访问的性能开销与Concryptor高速加密工具实战
下一篇:Rust 实现命令行 ASCII 艺术生成器:原理、代码与终端美化实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 09:16 , Processed in 0.515742 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表