为什么分布式查询引擎总是在执行层累积复杂性
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
- 更倾向于可预测性而非峰值吞吐量
- 将执行稳定性视为首要关注点
- 接受许多“优化”实际上是损害控制
执行层并非关于快速运行算子,而是关于管理不确定性。
最终思考
如果你的分布式查询引擎在执行层变得越来越复杂——如果你在重构、重新设计,并且质疑之前的决定——这通常并不是失败。它意味着系统已经离开了理想化的世界,进入了现实:
- 真实的数据
- 真实的网络
- 真实的机器
执行层的复杂性是运行在真实世界中的代价。
如果你构建或运营过查询引擎,我很想听听这些问题在你的系统中是如何出现的。