一个看起来很矛盾的问题
对象存储(Object Storage)便宜、耐用、可以存 PB 级数据,是数据湖的主流选择。但它有一个天然的短板:它不是数据库,不能直接查询。
传统的做法是在对象存储旁边架一套查询引擎——Apache Spark、Trino 或 Presto。这些系统功能强大,但运维代价也相应高昂:你需要预置集群、管理资源、保障可用性,而这些工作本身与"从数据中获取价值"这个核心目标毫无关系。
Cloudflare 的 R2 SQL 给出了一个不同的答案:把查询引擎本身也做成无服务器的,让 SQL 查询可以直接在数据所在的位置执行,不需要用户操心任何基础设施。
这篇博客是 R2 SQL 的架构深度解析,我们来把它拆清楚。
先搞懂两个前提:Iceberg 和 R2 Data Catalog
在进入引擎架构之前,需要先理解数据是如何组织的。
Apache Iceberg 是一种开放表格式,专门为对象存储上的大规模数据设计。它在对象存储的文件之上建立了一层逻辑结构,提供事务支持、Schema 演进、快照隔离等传统数据库才有的能力。核心思路是:不直接修改数据文件,而是通过操作轻量的元数据文件来管理表的状态,每次变更都形成一个新的不可变快照。
R2 Data Catalog 是 Cloudflare 将 Apache Iceberg Catalog 直接集成进 R2 存储桶的产品,让你的 R2 数据自动具备 Iceberg 的结构化管理能力。
然而,有了有序的数据组织,还需要一个能高效利用这种组织结构的查询引擎。这正是 R2 SQL 的职责所在。
两个核心挑战,一套两阶段架构
对象存储上的 SQL 查询面临两个本质问题:
I/O 问题:一张逻辑表可能由数百万个文件组成。暴力遍历每个文件既慢又贵,必须在读取数据之前就确定哪些文件值得读、读哪些部分。
计算问题:即便经过剪枝,需要处理的数据量依然可能极大。但这个计算需求只存在于查询执行的几秒钟内,之后应该立即归零,不能像传统集群那样持续占用资源。
R2 SQL 的架构设计围绕这两个问题展开,分为两个阶段:
- 查询规划器(Query Planner):利用元数据统计信息,在读取真实数据之前,最大限度地缩小需要处理的范围;
- 查询执行器(Query Execution):将规划好的工作单元分发到 Cloudflare 全球网络的多个节点并行处理。
第一阶段:查询规划器
查询规划器的核心哲学是:最高效的数据处理方式,是根本不去读它。
统计信息是什么?
Iceberg 在元数据文件中存储了关于数据文件内容的摘要统计,这些统计信息构成了数据的粗粒度地图,让规划器无需打开文件就能判断它是否和查询相关。
规划器主要使用两个层次的统计信息:
分区级统计:存储在 Iceberg 清单列表(Manifest List)中,描述某个清单文件(Manifest File)所覆盖的分区值范围。例如,对于按天分区的表,这里会记录该清单包含的最早和最晚日期。
列级统计:存储在清单文件(Manifest File)中,针对每个数据文件的每一列记录:最小值、最大值、空值数量。如果一个查询要找 http_status = 500,而某个文件的统计显示该列的最小值是 200、最大值是 404,那这个文件可以直接跳过,一字节都不需要读。
四层剪枝过程
规划器的工作是一次从上到下的层层排查,分四个步骤:
第一步,获取当前快照:向 Catalog 请求表的最新元数据文件,找到当前快照。
第二步,清单列表 + 分区剪枝:当前快照指向一个清单列表文件。规划器读取这个文件,利用分区级统计,丢弃所有分区范围与查询条件不重叠的清单文件。对于按天分区的表,这一步可以直接排除掉时间范围完全不相关的大量清单。
第三步,清单文件 + 文件级剪枝:对剩余的清单文件,规划器逐一读取,获取实际的 Parquet 数据文件列表,并用列级统计进行第二轮剪枝,排除所有不可能包含目标行的数据文件。
第四步,行组级剪枝:对仍然是候选的数据文件,规划器利用 Parquet 文件尾部存储的行组(Row Group)统计信息,进一步跳过文件内不相关的行组。
经过这四层剪枝,最终输出的是一份精确的工作单元列表——哪些 Parquet 文件的哪些行组需要被处理。这份列表随即被送入执行阶段。
流水线化:边规划边执行
对于包含数百万文件的超大表,等待规划器跑完完整计划再开始执行会引入不可接受的延迟。
R2 SQL 的解决方案是将规划和执行并发化:规划器不等待全部计划完成,而是以流式方式持续产出工作单元,执行节点一旦收到工作单元就立即开始处理。这样,真正昂贵的数据 I/O 工作几乎从规划开始的同时就启动了。
有序流:让最可能有用的数据先处理
在流水线的基础上,规划器还会对工作单元的输出顺序做刻意安排——按照查询的 ORDER BY 子句所要求的方向,优先输出最可能命中最终结果的工作单元。
这个排序发生在两个层次上:
- 在清单列表层面,根据各清单的分区统计(如最新时间戳),决定哪个清单的数据优先处理;
- 在清单文件层面,根据列级统计,决定该清单内各 Parquet 文件的处理顺序。
提前终止:不读完全部数据也能给出正确答案
有序流带来了一个强力的优化:提前终止(Early Stop)。
以 ORDER BY timestamp DESC LIMIT 5 为例,执行过程中规划器同时维护两个状态:
一方面,它维护一个大小为 5 的最大堆,实时存储当前已知的最佳 5 个结果,其中最旧的时间戳作为比较基准;
另一方面,它跟踪当前流中尚未处理的所有工作单元的"最高水位线"——即剩余未处理文件中绝对可能存在的最新时间戳。
规划器不断比较这两个值。一旦堆中最差结果的时间戳,比剩余所有未处理数据的最高水位线还要新,可以数学上证明:剩余的工作单元里不可能存在能进入 Top 5 的数据。查询立即停止,返回完整且正确的结果——即使只扫描了很小一部分候选数据。
这个机制目前仅支持对表的分区键列进行排序,未来会进一步扩展。
第二阶段:查询执行器
协调者与工作节点
接收用户查询请求的服务器承担**协调者(Coordinator)角色,它将规划器产出的工作单元分发给多台工作节点(Worker)**并行处理,最后汇总结果返回给用户。
协调者在分配任务前,会查询 Cloudflare 内部 API,确保只把工作分配给健康运行的节点。节点之间的通信走 Cloudflare Argo Smart Routing,保证低延迟高可靠的内部连接。
工作节点从协调者收到的,除了待处理的行组列表外,还包括 Parquet 文件的序列化元数据——这意味着工作节点已经知道每个行组在文件中的精确字节偏移量,无需再次读取文件头来获取这些信息,直接发起范围读取即可。
Apache DataFusion:向量化并行执行
每个工作节点内部使用 Apache DataFusion 执行 SQL 查询。DataFusion 是用 Rust 编写的开源分析查询引擎,其核心概念是"分区":将数据切分为多个独立的流,并发处理。
在 R2 SQL 的场景中,每个行组就是一个独立的 DataFusion 分区,天然支持并发。与此同时,行组通常包含至少 1000 行,查询在每个分区内以批量向量化的方式执行,摊薄了查询解释的开销,也对 CPU 缓存更友好。
DataFusion 同时针对 Parquet 格式做了深度优化:
列式选择读取:Parquet 是列式存储格式,每列的数据在物理上是连续存放的。DataFusion 结合 R2 的范围读取(Range Read)能力,只下载查询实际用到的列,跳过其余列——如果一个查询只用到 100 列表中的 5 列,可以节省 95% 的 I/O。
谓词下推(Predicate Pushdown):DataFusion 的优化器会将 WHERE 条件下推到读取 Parquet 文件的最底层,在数据刚被读出来的时候就应用过滤,避免将不符合条件的行物化到内存中。
Arrow IPC:节点间高效传输结果
工作节点将计算结果以 Apache Arrow 的内存格式表示,再通过 Arrow IPC 协议序列化后,经由 gRPC 发送给协调者。
Arrow IPC 并非为长期存储设计,而是专门面向进程间通信优化的二进制格式,序列化和反序列化开销极低,非常适合分布式节点之间的结果传递。
整体架构小结
用一句话概括 R2 SQL 的设计哲学:
在读取数据之前,尽可能多地排除不相关数据;在确实需要处理数据时,用全球分布的计算节点并行完成,用完即释放。
| 阶段 | 核心组件 | 核心目标 |
|---|---|---|
| 规划 | Query Planner + Iceberg 元数据 | 多层剪枝,缩小 I/O 范围 |
| 执行 | 协调者 + 工作节点 + DataFusion | 并行处理,向量化计算 |
| 传输 | Apache Arrow IPC + gRPC | 高效序列化,低延迟传输 |
未来规划
R2 SQL 目前已支持过滤查询,Cloudflare 接下来还计划扩展以下能力:
分布式聚合查询:支持 GROUP BY、SUM、COUNT 等聚合操作的分布式执行,目前已有独立博客介绍相关进展;
查询可见性工具:帮助开发者理解查询执行过程,发现性能瓶颈;
更多 Iceberg 配置项支持;
新型索引:探索更快的数据定位机制;
全文检索与地理空间查询;
Dashboard 集成:允许用户直接在 Cloudflare 控制台对 R2 Data Catalog 执行 SQL 查询。
总结
R2 SQL 本质上是一个将分布式计算与智能元数据剪枝结合的查询引擎,它的几个设计决策值得单独拿出来审视:
用元数据统计做多层剪枝,把"我需要读哪些数据"这个问题,从"扫描所有文件"降低到"读取少数精确的字节范围"。这不是数据库领域的新思想,但在对象存储场景下将其用到极致,需要对 Iceberg 的元数据结构有深入的理解。
流水线化规划与执行,以及有序工作单元流 + 提前终止的组合,是一个在分布式查询引擎中并不普遍见到的优化——它同时从"尽早开始"和"尽早结束"两端压缩查询延迟。
选择 DataFusion + Parquet + Arrow 这个开源技术栈,而不是从头造轮子,让 Cloudflare 能够站在社区积累的坚实工程基础上快速迭代,同时也让整个系统的各个层次都有良好的互操作性。
R2 SQL 目前处于公开测试阶段,如果你的数据已经存在 R2 上,可以直接通过官方文档开始试用。
参考来源:Cloudflare Blog — "R2 SQL: a deep dive into our new distributed query engine"