在大数据计算引擎的讨论中,PrestoDB 与 Apache Spark 常被简单对比为:
但这种结论如果没有落到执行范式(execution paradigm)层面上进行理解,本质上是不完整的。
本文讨论的是两种执行范式在典型分析型工作负载下形成的系统特征,而不是比较所有场景下的绝对性能。这里所说的响应速度和数据规模上限,均指两种架构设计在目标场景中的典型表现, 而非任何配置和任何负载下的绝对结论。另外,本文所说的数据规模上限,并非系统理论上能够读取的数据总量,而是指在给定集群资源约束下,系统能够稳定完成计算的工作负载规模 (workload scale)。
本文的核心观点是:
两者的差异并不在“谁更强”,而在于它们对“计算如何展开”的根本假设不同,而这种假设直接决定了响应速度与可处理数据规模的上限。
Presto 的核心执行哲学可以概括为:
尽量避免中间结果物化,让数据以流式方式一次性流过整个计算链路(one-pass execution)。
其直接表现为:
这种持续的数据流模型不仅影响查询级别的执行路径,也要求 Worker 节点具备更细粒度的执行调度能力,以支持多个计算片段在有限资源上的持续并发推进。
Spark 的核心哲学则是:
通过物化的中间结果将计算拆分为多个阶段(stage),分段执行。
其表现为:
两者的根本区别可以压缩为一句话:
是否允许在计算过程中“切断数据流”。
stage1 → stage2 → stage3 → ... → stageN
(持续 streaming)
stage1 →(物化截断,缓存/溢写)→ stage2 →(物化截断,缓存/溢写)→ stage3
每个 stage 结束后:
下一阶段再启动
👉 这个差异直接导致了:
Presto 是“整条链路同时存在”,Spark 是“链路按时间段拆开存在”。
这种差异不仅体现在查询计划(DAG / TREE)如何展开,也会进一步影响 Worker 节点内部的执行粒度和线程调度方式。
Presto 的低延迟优势来自两个层面:首先是 Pipeline Streaming 减少单个查询内部的数据等待和中间结果物化;其次是 Worker 内部细粒度执行调度降低并发查询之间的资源竞争。
👉 结果是:
即使在并发执行的情况下,查询也可以在较短时间内返回第一批结果,整体延迟更低,整体资源使用率更高。
而对于 Spark 来说,由于采用 Stage-based Execution 模型:
因此:
此外,由于 Spark 在 Worker 节点内部采用 Task + Executor Thread 的执行模型:
1 n
Stage ---------> Task
|
| occupies during execution
|
v
Executor Core (execution slot)
|
Executor Thread
在典型执行路径下,一个 Task 在获得 Executor 线程后,通常会持续执行直到完成,其执行线程生命周期与 Task 生命周期高度绑定。
这意味着:
因此,在批处理场景下,这种模型能够减少频繁调度开销,最大化单个任务吞吐;但在高并发交互式查询场景下,容易因为任务粒度较粗、线程资源被长任务占用而产生更明显的排队等待。
👉 这导致:
Spark 虽然能够通过 Stage 切分和物化机制获得更强的大规模计算和容错能力,但由于执行链路和 Worker 调度模型更加偏向吞吐优化,而非低延迟响应,因此相比专门面向交互式分析设计的查询引擎,Spark SQL 通常不具备同等程度的低延迟响应能力。
接着,关键问题来了:
在资源(CPU/Memory)都能打满的情况下,为什么 Spark 能处理更大规模?
关键不在 CPU,而在“峰值资源压力”。在大规模 OLAP(尤其是 join / aggregation)场景下,真正的瓶颈是:
在某一时刻,系统需要同时承载:
并且:
这些来自多个 stage,且同时存在
在Spark的执行过程中,在某一时刻:
主要计算资源集中在当前 stage 的状态 + 当前数据
这是因为:
此外,Stage 边界天然提供了失败恢复和执行隔离边界。已经完成的 Stage 输出可以作为后续阶段的稳定输入,当某个 Task 失败时,Spark 通常只需要重新计算受影响的依赖链,而无需重新执行整个查询。这种基于阶段边界的容错机制,使 Spark 在超大规模批处理任务中具备更好的稳定性和可恢复能力。
👉 他们之间的差异如下表所示:
| 维度 | Presto | Spark |
|---|---|---|
| 执行方式 | pipeline | 分段 |
| 数据流 | 持续流动 | 分阶段 |
| 状态存在 | 同时叠加 | 分批存在 |
| 峰值压力 | 高 | 低 |
| 可扩展性 | 受限 | 更强 |
一个本质的表达是:Presto 在任一时刻需要承载“整条计算链路的资源压力”,而 Spark 只需要承载“当前阶段的资源压力”。
或者更直白一点:
之前注意到,很多分析文章中只会关注到算子本身状态所占用的资源,例如:
但在 Presto 中,还有一个不可忽视的因素:
数据流本身(streaming data)也是资源占用
包括:
👉 这意味着:
这也是 Presto 在极端规模下资源更早触顶的重要原因之一。
综合全文的对比分析,我们可以清晰地看到:Presto 与 Spark 之间绝对不存在所谓的优劣之分,它们的差异根植于对”计算应当如何展开”这一根本问题的不同回答。
Presto 选择了 pipeline streaming 执行路径,在查询执行层尽量避免中间结果物化,使数据能够持续流经整个计算链路,从而降低单个查询的执行延迟;同时,在 Worker 内部进一步采用 Driver 级细粒度执行模型和协作式调度机制,使多个执行单元能够共享有限计算资源,因此在高并发交互式场景下仍然保持较好的响应能力。
Spark 则选择了 stage-based materialization 执行路径,通过 Shuffle 边界将计算划分为多个执行阶段,并在 Worker 内部以 Task 为基本执行单元,通过 Executor 线程池执行任务。这种模型使计算压力能够通过 Stage 边界进行隔离,因此在大规模批处理场景下具备更强的扩展能力和稳定性。
这两种选择在各自的优势场景下都是正确的,但同时也都带来了必须承受的代价。Presto 以牺牲极端规模下的扩展能力并引入额外调度复杂性为代价,换来了更快的响应速度;Spark 则以更高的执行延迟为代价,换来了对更大数据规模和更复杂工作负载的承载和容错能力。
最终,如果要用一句话将两种范式的本质差异刻入认知,那就是:
Pipeline Execution 倾向于将计算链路中的资源压力在空间上叠加,而 Stage Execution 倾向于通过阶段边界将资源压力在时间上分摊;与此同时,Driver 级协作式调度执行与 Task 级线程绑定执行又进一步决定了系统在并发查询场景下的响应能力差异。
本文基于作者当前的理解与实践经验整理而成,难免存在疏漏或值得进一步探讨之处。
如果您对文中的观点有不同看法,发现任何问题,或有相关实践经验,欢迎通过 GitHub Issue 与作者交流讨论。
期待与更多同行围绕数据基础设施相关技术展开交流,分享实践经验,共同学习、共同进步。