Dong Wang
Presto vs Spark:执行范式差异如何决定响应速度与数据规模上限
在大数据计算引擎的讨论中,PrestoDB 与 Apache Spark 常被简单对比为:
Presto:快(低延迟)
Spark:稳(大规模)
但这种结论如果没有落到执行范式(execution paradigm)层面上进行理解,本质上是不完整的。
本文讨论的是两种执行范式在典型分析型工作负载下形成的系统特征,而不是比较所有场景下的绝对性能。这里所说的响应速度和数据规模上限,均指两种架构设计在目标场景中的典型表现,
而非任何配置和任何负载下的绝对结论。另外,本文所说的数据规模上限,并非系统理论上能够读取的数据总量,而是指在给定集群资源约束下,系统能够稳定完成计算的工作负载规模
(workload scale)。
本文的核心观点是:
两者的差异并不在“谁更强”,而在于它们对“计算如何展开”的根本假设不同,而这种假设直接决定了响应速度与可处理数据规模的上限。
一、两种完全不同的执行哲学
1. Presto:Pipeline Streaming(一次性流式计算)
Presto 的核心执行哲学可以概括为:
尽量避免中间结果物化,让数据以流式方式一次性流过整个计算链路(one-pass execution)。
其直接表现为:
多个 stage 同时调度执行
上下游算子通过 exchange 流式连接
数据在算子之间边生产边消费
这种持续的数据流模型不仅影响查询级别的执行路径,也要求 Worker 节点具备更细粒度的执行调度能力,以支持多个计算片段在有限资源上的持续并发推进。
2. Spark:Stage-based + Materialization(分段计算)
Spark 的核心哲学则是:
通过物化的中间结果将计算拆分为多个阶段(stage),分段执行。
其表现为:
以 shuffle 为边界划分 stage
stage 之间通过数据落地(物化)解耦
计算以 DAG 分阶段推进
二、执行模型的本质差异
两者的根本区别可以压缩为一句话:
是否允许在计算过程中“切断数据流”。
Presto(不断流)
stage1 → stage2 → stage3 → ... → stageN
(持续 streaming)
数据流贯穿整个执行过程
各 stage 必须同时推进
Spark(可切断)
stage1 →(物化截断,缓存/溢写)→ stage2 →(物化截断,缓存/溢写)→ stage3
每个 stage 结束后:
数据在逻辑上被物化(优先存入内存缓存,必要时溢写磁盘)
资源被释放
下一阶段再启动
👉 这个差异直接导致了:
Presto 是“整条链路同时存在”,Spark 是“链路按时间段拆开存在”。
这种差异不仅体现在查询计划(DAG / TREE)如何展开,也会进一步影响 Worker 节点内部的执行粒度和线程调度方式。
三、响应速度:为什么 Presto 更快
Presto 的低延迟优势来自两个层面:首先是 Pipeline Streaming 减少单个查询内部的数据等待和中间结果物化;其次是 Worker 内部细粒度执行调度降低并发查询之间的资源竞争。
Early Output(提前产出),数据一旦被扫描,就可以:
立即参与 join / aggregation
立即向下游传递
无需等待上游 stage 完成
无物化开销,避免:
磁盘 IO
序列化 / 反序列化
shuffle 落地
提升有限资源下的并发查询响应能力
执行线程不与单个 Driver 长时间绑定,而是在多个执行单元之间动态复用;
长时间运行的查询不会简单独占固定线程资源;
短查询和交互式查询能够更快获得 CPU 执行机会;
多个并发查询之间具有更好的资源公平性。
👉 结果是:
即使在并发执行的情况下,查询也可以在较短时间内返回第一批结果,整体延迟更低,整体资源使用率更高。
而对于 Spark 来说,由于采用 Stage-based Execution 模型:
上游 Stage 必须完成计算;
Shuffle 数据需要写出并物化;
下游 Stage 才能继续拉取并执行。
因此:
数据无法像 Pipeline Streaming 一样持续穿透整个计算链路;
中间结果物化增加了额外的 IO 和序列化开销;
查询无法快速获得 Early Output。
此外,由于 Spark 在 Worker 节点内部采用 Task + Executor Thread 的执行模型:
1 n
Stage ---------> Task
|
| occupies during execution
|
v
Executor Core (execution slot)
|
Executor Thread
在典型执行路径下,一个 Task 在获得 Executor 线程后,通常会持续执行直到完成,其执行线程生命周期与 Task 生命周期高度绑定。
这意味着:
长时间运行的 Task 会持续占用 Executor 计算线程;
短查询需要等待可用线程资源;
多个并发查询之间缺少类似 Presto Driver 级别的细粒度协作调度。
因此,在批处理场景下,这种模型能够减少频繁调度开销,最大化单个任务吞吐;但在高并发交互式查询场景下,容易因为任务粒度较粗、线程资源被长任务占用而产生更明显的排队等待。
👉 这导致:
Spark 虽然能够通过 Stage 切分和物化机制获得更强的大规模计算和容错能力,但由于执行链路和 Worker 调度模型更加偏向吞吐优化,而非低延迟响应,因此相比专门面向交互式分析设计的查询引擎,Spark SQL 通常不具备同等程度的低延迟响应能力。
四、数据规模上限:为什么 Spark 更强
接着,关键问题来了:
在资源(CPU/Memory)都能打满的情况下,为什么 Spark 能处理更大规模?
关键不在 CPU,而在“峰值资源压力”。在大规模 OLAP(尤其是 join / aggregation)场景下,真正的瓶颈是:
memory(hash table / aggregation state)
network(shuffle / exchange)
buffer(数据流缓存)
Presto 的资源模型(叠加)
在某一时刻,系统需要同时承载:
多个 stage 的算子状态
exchange buffer(网络缓冲)
in-flight data(流动中的数据)
并且:
这些来自多个 stage,且同时存在
Spark 的资源模型(分段)
在Spark的执行过程中,在某一时刻:
主要计算资源集中在当前 stage 的状态 + 当前数据
这是因为:
上一阶段:
已物化
已释放资源
下一阶段:
尚未开始
此外,Stage 边界天然提供了失败恢复和执行隔离边界。已经完成的 Stage 输出可以作为后续阶段的稳定输入,当某个 Task 失败时,Spark 通常只需要重新计算受影响的依赖链,而无需重新执行整个查询。这种基于阶段边界的容错机制,使 Spark 在超大规模批处理任务中具备更好的稳定性和可恢复能力。
👉 他们之间的差异如下表所示:
维度
Presto
Spark
执行方式
pipeline
分段
数据流
持续流动
分阶段
状态存在
同时叠加
分批存在
峰值压力
高
低
可扩展性
受限
更强
一个本质的表达是:Presto 在任一时刻需要承载“整条计算链路的资源压力”,而 Spark 只需要承载“当前阶段的资源压力”。
或者更直白一点:
Presto 是“倾向于把所有计算同时压在系统上”,
Spark 是“倾向于把计算拆开分时间段压在系统上”。
五、补充一个常被忽略的关键点:数据流本身就是成本
之前注意到,很多分析文章中只会关注到算子本身状态所占用的资源,例如:
join state
aggregation state
但在 Presto 中,还有一个不可忽视的因素:
数据流本身(streaming data)也是资源占用
包括:
exchange buffer
operator page buffer
network in-flight data
backpressure 堆积等
👉 这意味着:
即使只有一个 stage 有“重状态”,
整个 pipeline 的数据流仍然有可能占用大量系统内存
这也是 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 与作者交流讨论。
期待与更多同行围绕数据基础设施相关技术展开交流,分享实践经验,共同学习、共同进步。
This site is open source. Improve this page.