为什么分布式查询引擎总是在执行层累积复杂性

发布: (2025年12月27日 GMT+8 10:06)
6 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的具体文本内容,我将为您翻译成简体中文。

执行层并非“仅仅是执行”

架构图通常会把查询引擎拆分成整齐的模块:

  • SQL 解析
  • 优化
  • 计划生成
  • 执行
  • 存储

这种拆分掩盖了一个重要的事实:

执行之前的所有工作基本上是静态的。

执行层才是现实显现的地方。

现实意味着:

  • 数据倾斜
  • 节点慢或故障
  • 网络抖动
  • 只有在规模扩大时才出现的内存压力

所有最终被证明错误的假设都会落到执行层。正因为如此,优化器往往能够保持较长的有效期,而执行层则需要不断地重新改造。

Shuffle:沉默的全系统成本倍增器

Shuffle 通常被描述为“仅仅是重新分区数据”。实际上,Shuffle 同时会对以下资源造成压力:

  • 网络带宽
  • 内存(常伴随尖峰)
  • 磁盘 I/O(作为后备)
  • CPU(哈希、排序、序列化)

更重要的是,Shuffle 会放大不确定性:

  • 一个慢节点会成为全局瓶颈
  • 轻微的数据倾斜会导致 OOM(内存溢出)
  • 小幅网络抖动会演变为查询级别的延迟

许多生产事故都可以追溯到 Shuffle——即使最初并不明显。

并发:易于添加,难以控制

一种常见的早期观点是:

“更多的并行等于更好的性能。”

执行层层反复证明这点是错误的。

常见错误:

  • 对 CPU 密集型算子到处使用 async
  • 将异步运行时与手动线程池混用
  • 在没有背压的情况下激进地生成任务

结果:

  • 调度不可预测
  • 尾部延迟膨胀
  • 行为难以推理

一旦并发泄漏到算子设计中,之后的修复通常意味着重写核心抽象。

Rust 防止内存损坏 — 而非内存惊喜

Rust 在内存安全方面表现出色。但它 能保证:

  • 可预测的内存使用
  • 中间数据的有界生命周期
  • Shuffle 或 Join 过程中的内存峰值保持稳定

大多数执行层面的内存故障并非泄漏,而是:

  • 缓冲区保留时间超出预期
  • 生命周期在阶段之间意外延长
  • 仅在真实工作负载下出现的内存突增

这些问题难以及早发现,且在后期修复成本高昂。

为什么执行层设计经常被重写

在各类系统中,同样的失败模式不断出现。

❌ 模糊的执行模型

早期设计常把查询视为:

  • 一个 SQL 字符串
  • 或者一个松散定义的步骤序列

随后,团队尝试“添加”执行计划、算子图和调度器。在强类型系统(尤其是 Rust)中,这往往难以挽救。执行语义必须从一开始就明确。

❌ 共享可变状态

Arc 让早期进展变得容易。但在大规模时,它会引入:

  • 锁竞争
  • 延迟抖动
  • 异步上下文中的死锁风险

当数据流动而不是状态共享时,执行层的表现更好。

❌ 将 Shuffle 当作优化问题

许多团队认为 Shuffle 可以通过调优消除:

  • 增加分区数
  • 更智能的哈希
  • 更好的缓存

实际上,Shuffle 有物理下界。最有效的优化往往是彻底避免 Shuffle。

❌ 模糊的错误边界

如果没有在以下层面之间保持清晰的分离:

  • 任务级别的失败
  • 阶段级别的失败
  • 查询级别的失败

系统就会变得脆弱。panic 和全局重试在分布式执行中难以扩展。

艰难获得的工程共识

  • 尽可能避免 Shuffle
  • 更倾向于可预测性而非峰值吞吐量
  • 将执行稳定性视为首要关注点
  • 接受许多“优化”实际上是损害控制

执行层并非关于快速运行算子,而是关于管理不确定性。

最终思考

如果你的分布式查询引擎在执行层变得越来越复杂——如果你在重构、重新设计,并且质疑之前的决定——这通常并不是失败。它意味着系统已经离开了理想化的世界,进入了现实:

  • 真实的数据
  • 真实的网络
  • 真实的机器

执行层的复杂性是运行在真实世界中的代价。

如果你构建或运营过查询引擎,我很想听听这些问题在你的系统中是如何出现的。

Back to Blog

相关文章

阅读更多 »

Knotlog:面向 PHP 的广泛日志记录

为什么日志记录糟糕以及如何修复它——正如 loggingsucks.com(https://loggingsucks.com/)精彩阐述的,传统的日志记录在现代环境中根本存在缺陷……