从 Hive 到 Presto:一个「快」字背后,是三次豪赌式的设计取舍

2012 年,Facebook 的工程师们受够了「跑个查询去喝杯咖啡」的日子。Presto 的诞生不是又一个 SQL 引擎的堆料,而是一连串清醒的放弃。本文用「舍与得」的视角,拆开 Presto 三项核心架构决策——以及它们各自的账单。

引言:大数据时代的「等待焦虑」

先讲一个老故事。

2012 年前后,任何在 Facebook 规模的数据仓库上工作过的人,都熟悉这种体验:你写了一段 SQL,想看看昨天某个功能的用户留存。提交给 Hive,底层翻译成 MapReduce,然后——

等。

第一个 Map 阶段读数据、算完、写磁盘;第二个 Reduce 阶段再读磁盘、算完、再写磁盘;三五个阶段下来,一个「简单的问题」要等十分钟到一小时。磁盘 I/O 像一道钝刀子,把「交互式分析」切成了「批处理作业」。工程师们甚至形成了独特的应对文化:提交查询,然后去开会、喝咖啡、改别的 bug——因为等待是确定会发生的。

问题出在哪?不是 Hadoop 不行,而是 MapReduce 的设计目标本来就不是交互式查询。它为容错而生:每个阶段的中间结果必须落盘,任何一台机器挂了,从磁盘上的中间结果重跑就行。这份「保险」在批处理场景物有所值,但对「我想马上知道答案」的分析师来说,是在为用不到的容错付全价。

Presto 的研发初衷就一句话:为交互式 SQL 查询专门造一台引擎,PB 级数据,秒级响应。

注意「专门」这个词。Presto 没有试图做一个「又能批处理、又能交互、又能点查」的全能选手——它从一开始就决定,为了快,该扔的就扔。下面是三次关键的「舍」。

取舍一:内存流水线——扔掉容错,换速度

舍:Task 级中间落盘与断点重试

MapReduce 的哲学是「每一步都存档」。Presto 反其道而行:查询被切成多个 Stage,Stage 内的算子组成流水线(Pipeline),数据以「页」(Page)为单位在算子间流动,全程驻留内存,中间结果不落盘。

伪代码对比一下两种执行模型:

# MapReduce 式:每个阶段落盘存档
for stage in stages:
    results = stage.run(input)
    write_to_disk(results)      # ← 每一步都付磁盘 I/O 的钱
    checkpoint(results)          # ← 为容错买单
# 下一阶段再 read_from_disk()

# Presto 式:内存流水线
pipeline = build_pipeline(scan, filter, agg, ...)
for page in source_pages:        # 一页一页流过去
    yield pipeline.process(page) # 算子间直接传递,不落地

差别有多直观?MapReduce 的一次「阶段交接」= 一次序列化 + 一次磁盘写 + 一次磁盘读 + 一次反序列化。Presto 的阶段交接 = 一次内存中的数据页传递。磁盘 I/O 从执行路径里被整个删掉了,这正是秒级响应的物理基础。

得与账单

得到的是量级的速度提升:没有了落盘和重读的往返,CPU 和内存成为主战场,配合向量化批处理,交互式查询从「小时级幻觉」变成「秒级现实」。

账单是容错性的让步:中间结果不落盘,意味着一个 Worker 挂掉,没法从存档点恢复——这个查询失败了就是失败了,只能整体重跑。Presto 的判断是:**交互式查询本来就该短平快,重跑一个 20 秒的查询,远比给所有查询加上落盘开销划算。**这是典型的「按主要场景定价」——为 99% 的短查询优化,让 1% 的长查询自己承担风险。

graph LR A[查询请求] --> B[Coordinator<br/>解析·计划·调度] B --> C1[Worker: Scan→Filter] C1 -->|内存传递 Page| C2[Worker: Agg] C2 -->|内存传递 Page| C3[Worker: Join/Output] C3 --> D[结果返回] style B fill:#e8f0fe

取舍二:存算分离 + Connector——不存数据,才能吃下所有数据

舍:专用存储格式

这是 Presto 最反直觉、也最超前的一刀。2012 年的「性能正确」做法是:自建存储引擎、专用列存格式、数据必须导入系统才能查——像一家餐厅坚持「只吃自家种的菜」。

Presto 的选择是:**自己不存任何数据。**它只做计算层,通过 Connector 插件对接外部存储——Hive/HDFS、S3、MySQL、PostgreSQL、Kafka、Cassandra……查询计划里,跨源 JOIN 是天然能力:

-- 一条 SQL,同时摸 Hive 数仓和 MySQL 业务库
SELECT u.region, SUM(o.amount)
FROM hive.dw.user_behavior u
JOIN mysql.prod.orders o ON u.id = o.user_id
GROUP BY u.region;

得与账单

得到的是联邦查询能力与零数据搬迁:数据留在原地,Presto 只做「计算的下推与聚合的搬运」。企业里最昂贵的往往不是查询本身,而是为了查询把数据复制一份进数仓的 ETL 管道——Presto 直接把这条管道砍掉了大半。一份数据同时被 Spark 批处理、被 Presto 交互式查询,也是这个架构的赠品。

账单有两张

  1. 网络成了新瓶颈。数据不在本地,每次扫描都要过网络。Presto 靠两招对冲:Connector 层的谓词/投影下推(让存储端先过滤,少搬数据)+ 后续版本的缓存机制。但物理规律不变——同等数据量下,远程读就是比本地读慢,这是为「不搬数据」付的传输费。
  2. 无主数据的治理难题。数据散在各处,格式质量参差不齐,Presto 管不了存储端的分区和压缩策略——「湖仓治理」的功课并没有消失,只是换了地方做。

回头看,这刀砍在了时代前面:十多年后「湖仓一体」「Lakehouse」火起来,本质就是沿着 Presto 当年选的路继续走。

取舍三:ANSI SQL——不做 DSL,换整个生态

舍:私有查询语言的「表达自由」

每个雄心勃勃的查询引擎,都曾想过发明自己的 DSL:更简洁、更贴合自己引擎特性、还能形成用户锁定。Presto 拒绝了这条捷径,坚持贴 ANSI SQL 标准,连 GROUP BY 的位置、窗口函数的语法、类型系统都尽量按标准来。

得与账单

得到的是零门槛生态:分析师不用学新语言,Tableau/Superset/PowerBI/JDBC/ODBC 拿来就接,BI 团队无需改造就能把 Presto 当报表 backend。一个数据平台的采用速度,90% 取决于「现有用户会不会用」,SQL 就是那个最大公约数。

账单是标准化的镣铐:SQL 的表达边界限制了某些引擎特性的暴露(早期 Presto 连 UPDATE/DELETE 都不支持——它压根没打算做存储);追平标准的过程也意味着持续的实现成本。但 Presto 算得很清楚:语言的护城河从来不是护城河,生态的迁移成本才是;把门槛让给标准,把力气花在引擎上。

实践:什么时候用,什么时候坚决不用

把三项取舍合起来看,Presto 的能力边界就自动浮现了。

✅ 适用场景

场景为什么合适
即席查询(Ad-hoc)内存流水线就是为「想到即查到」设计的
跨源联邦查询Connector 架构的主场,省掉 ETL 搬数
BI 报表 backendANSI SQL + JDBC,可视化工具零改造接入
数据探索/分析师自助不建表、不导入,指着数据源就能查

❌ 不适用场景

场景为什么别用
长时间高容错 ETL 批处理中间结果不落盘,长任务失败只能整体重跑,这是拿短跑选手去跑马拉松
高 QPS 线上点查Coordinator 是查询级调度,不是为每秒上万次点查设计的;这是 Redis/HBase 的活儿
需要事务/更新的业务库它是查询引擎不是数据库,没有 ACID 事务语义

一张决策图,帮你在选型会上省时间:

graph TD A{查询模式?} -->|秒级交互分析| B{数据在哪?} A -->|长时批处理 ETL| X1[Spark/Hive] A -->|高QPS点查| X2[Redis/HBase/专用KV] B -->|多源散落| C[Presto/Trino ✅] B -->|单一数据湖·固定模式| D[ClickHouse 等一体化引擎也值得比一比] B -->|事务型业务数据| X3[PostgreSQL/MySQL]

选型口诀其实就一句:**Presto 是数据的「访问层」,不是数据的「家」。**你的数据该住哪还住哪,Presto 负责让你用最快的姿势访问它们。

结语:系统设计没有完美,只有权衡

回头看 Presto 的三项取舍,会发现它们共享同一种勇气:

  • 扔掉落盘容错,因为它知道自己服务的是短查询;
  • 扔掉专用存储,因为它知道自己该做的是计算;
  • 扔掉私有 DSL,因为它知道生态比语法护城河值钱。

**每一次「舍」,都是对自己定位的一次确认。**架构设计里最难的不是「能不能做」,而是「敢不敢不做」。全能意味着平庸——什么都想兼顾的系统,往往在每个场景都被专精者打败。

Presto 的故事还有个后续值得玩味:2019 年核心作者出走创立 Trino,社区一分为二,但两个分支都沿着同样的架构哲学继续演进——这恰恰说明,真正有生命力的不是某份代码,而是这套「想清楚再放弃」的设计方法。

所以下次有人问你「最好的架构是什么」,你可以引用这句老话:系统设计没有完美,只有权衡。而优秀的架构师,是那些清楚地知道自己放弃了什么、并且付得起账单的人。