做分布式查询引擎前,我先用 Rust 写了一个最小执行 POC

25 阅读3分钟

最近在看一个 分布式查询引擎(Distributed Query Engine) 的 Rust 项目需求,目标其实很直接:

  • 多节点执行
  • 低延迟 / 高吞吐
  • 数据 Shuffle / Partition
  • 可长期维护

在真正开始堆功能之前,我先做了一件工程上很“慢”、但很值的事:

先写一个最小可运行的查询执行 POC,验证执行模型本身是不是对的。


一、为什么这个阶段我直接选 Rust

在分布式查询引擎这种系统里,语言不是偏好问题,而是工程约束的一部分

我选择 Rust,主要是基于三个现实考虑:

1. 并发安全直接决定系统下限

执行层是天然的并发系统,多 worker 同时跑算子,如果内存和并发模型不清晰,后期问题一定会被放大。
Rust 至少把一部分风险前移到了编译期。

2. async 非常贴合执行层模型

查询执行本质是 IO、计算和中间结果流转的组合,async 能让这些行为在代码结构上更清晰,而不是靠约定。

3. 这是一个长期演进系统

查询引擎不是一次性交付,而是会不断加算子、加规则、加优化策略,维护成本必须可控。


二、POC 阶段,我刻意没做“看起来很重要”的东西

这个 POC 有意不实现很多常见模块

  • SQL Parser
  • 成本优化器(CBO)
  • 网络通信层
  • 完整的容错 / 恢复机制

原因很简单:
在执行模型没跑通之前,这些模块只会放大复杂度。

在这个阶段,我只关心三件事:

  1. 查询是否被明确建模成 Execution Plan
  2. 执行是否能自然拆分到多个 worker
  3. 中间数据是否必须经过 Shuffle 才能继续算

三、最小查询执行模型

在 POC 里,查询不是 SQL 字符串,而是直接建模成执行节点:

enum Query {
    Scan { source: String },
    Filter { predicate: String },
    Aggregate { key: String, agg: String },
}

这样做的目的,是把注意力集中在 执行语义 上,而不是语法本身。

执行流程被明确限制为:

Scan -> Filter -> Shuffle -> Aggregate

四、最小“分布式”执行方式

为了避免一开始就引入网络复杂度,这个 POC 采用了非常直接的模型:

  • 一个 Coordinator
  • 多个 Worker(async task)
  • 使用 Channel 传递中间结果

结构大致是这样:

Coordinator
   |
   |-- Worker 1: Scan + Filter
   |-- Worker 2: Scan + Filter
   |
   +--> Aggregate

每个 Worker 独立执行算子,中间结果必须返回并参与后续阶段。


五、为什么 Shuffle 在 POC 阶段就必须出现

即便是最小 POC,Shuffle 也不能省

实现方式非常简单:

hash(key) % worker_count

但这一行代码已经意味着:

  • 数据需要重新分布
  • 执行流程出现同步点
  • 性能与资源问题开始显性化

从这里开始,查询引擎的复杂度就不再是“写不写得出来”的问题了。


六、这个 POC 想验证的工程判断

这个 POC 并不是为了证明“我能写查询引擎”,而是验证几个基础判断:

  1. 查询引擎的复杂度主要集中在 执行层(Execution Layer)
  2. Shuffle 是系统级问题,而不是后期优化细节
  3. Rust 在这种系统里是工程选择,而不是噱头

七、POC 阶段刻意不解决的问题

为了避免误解,这个 POC 明确不解决

  • 高可用
  • 完整容错
  • 复杂优化策略
  • 云原生部署

这些问题都应该在执行模型稳定之后再引入。


八、为什么我会先做这个最小 POC

在分布式查询引擎这类系统里,一个很现实的经验是:

如果执行模型一开始就不清晰,
后续功能基本都会变成补救。

最小 POC 的价值,只是尽早把这些问题暴露出来。


如果你也在做或评估类似系统,
建议先把 Execution 跑通,再谈功能完整性。