在数据分析与数仓建设中,大规模数据关联(Join)、聚合(Aggregation)和排序(Sort)往往需要消耗海量的内存资源。随着业务数据量的激增、数据基数的膨胀或偶发的数据倾斜,查询的实际内存需求常常会远超系统预估。
一旦可用内存耗尽,系统就会触发 OOM(Out of Memory,内存溢出)错误。这意味着,一个可能已经运行了数小时的重度 ETL 任务会瞬间终止。以季度财务结算为例:一项任务需要关联过去 3 年的交易流水、客户维表和产品层级,再按大区进行聚合运算。如果由于内存耗尽导致任务失败,数据团队往往需要连夜排查、缩小查询范围、拆分作业并重跑,原定次日早晨交付管理层的报表将面临严重的延期风险。
为了从根本上解决这一痛点,Spill to Disk(数据落盘) 技术应运而生。当查询面临内存瓶颈时,该技术能够将部分暂时不用的中间数据“溢出”并写入磁盘,腾出宝贵的内存空间让查询继续执行;待需要时,再从磁盘中按需读取恢复。这不仅极大降低了 OOM 导致的任务中断风险,更为企业节省了拆分任务和重复计算带来的高昂运维成本。
在 Apache Doris 4.1 版本中,我们对 Spill to Disk 能力进行了全面重构与升级,在核心算子覆盖率、数据倾斜处理机制以及主动内存控制上实现了质的飞跃,使其能够真正稳定支撑超大规模的分析型查询。
1、Apache Doris 4.1 的三大核心跃升
在最新版本中,Apache Doris 针对 Spill to Disk 进行了三项关键增强:
- 核心内存消耗算子全覆盖:Hash Join(哈希关联)、Aggregation(聚合)和 Sort(排序)均已支持 Spill。这三类操作是分析型数据库中最主要的内存压力来源,实现全覆盖意味着绝大多数内存密集型查询都能获得监管把控。
- 支持递归重分区(应对数据倾斜):在真实业务中,常会出现某些特定维度数据过度集中的现象(数据倾斜)。如果某批落盘的数据在恢复时依然庞大到无法载入内存,Doris 的执行引擎会对其进行“递归重分区”。这一机制确保了即使在极端倾斜场景下,任务依然能平稳运行。
- 支持主动内存压力感知与触发:区别于“不到最后一刻不落盘”的被动策略,Doris 4.1 引入了预判机制。系统会实时监控内存压力,在触及系统内存硬性红线之前,主动将数据落盘,彻底避免了因内存分配彻底失败而导致的进程崩溃。
2、为什么内存密集型算子必须支持落盘?
在 Apache Doris 的执行引擎中,以下三类核心算子在输出最终结果前,必须在内存中持续维护庞大的中间状态:

由于这些中间状态在逻辑上可以被序列化(转为连续的字节流),因此它们无需时刻驻留内存。Spill to Disk 的核心逻辑,就是用相对廉价的磁盘 I/O 时间,换取宝贵的内存空间,从而显著压低查询的峰值内存。
3、全局视角的 Spill to Disk 架构设计
在架构实现上,Apache Doris 并没有让各个算子独自去盲目落盘,而是设计了一套统一的控制机制。它由上至下分为四个层次,协调处理内存预留、任务调度和文件读写:
控制层:负责全局调控。
PipelineTask负责执行前的内存预留与压力探测;WorkloadGroupMgr统筹处理因内存不足而暂停的查询,决策是否需要落盘;QueryTaskController则精准挑选出最适合落盘的算子。算子层:各类物理算子(如 Partitioned Hash Join、Aggregation 等)接收到指令后,调用统一接口执行自身状态的磁盘写入和后续的读取恢复。
基础设施层:提供标准化的磁盘文件读写能力(
SpillFileManager等),自动处理文件分片、垃圾回收以及多级重分区逻辑,减轻算子层的负担。内存管理层:构建了“查询级 -> 负载分组(WG)级 -> 进程级”三道内存水位防线,实时校验查询能否继续进行。

4、核心触发流程:从预留、暂停到恢复
Spill 并不是在查询已经发生 OOM 后才启动。Apache Doris 会在执行过程中持续检查内存状态,发现风险时及时暂停查询,将部分可回收的中间状态写入磁盘。释放出足够的内存后,查询再从暂停位置继续执行。
完整流程可以概括为 4 个阶段:
预留(Reserve):算子在进行下一步计算前,先检查可用内存。
暂停(Pause):一旦发现内存不足或压力过高,立即暂停当前查询。
落盘(Spill):选择具有较多可回收内存的算子,将其中间状态写入磁盘。
恢复(Resume): 内存腾出后,重新调度暂停的任务,完成计算。

4.1 智能的主动触发机制
除了在 Reserve 失败后被动触发 Spill,Doris 的预判机制是保障稳定性的关键。当以下三个条件同时满足时,系统将主动出击触发落盘:
- 大额内存请求预判:预计当前算子下一步需要的内存量极大:(
reserve_size × parallelism > query_limit / 5) - 系统处于高压状态:当前查询的内存使用率已突破 90%,或其所属的资源组(Workload Group)触及了预设的内存高水位线。
- 具备落盘价值:算子当前持有的可回收内存足够多,落盘行为能释放出有效空间:(
revocable_mem × parallelism ≥ query_limit × 20%)
这样智能、主动的触发机制,无需等到内存分配失败后再进行处理,能更大程度的保障查询的性能和系统的稳定性。
4.2 精准挑选落盘目标
当确定要执行 Spill(落盘)时,系统并不会让所有算子一拥而上。
QueryTaskController会按照可回收内存的规模进行降序排列,精准挑选出一批算子,其目标是释放当前实际内存消耗的 20%。
例如,当前查询消耗了 1GB 内存,系统只会让约占用 200MB 的算子状态落盘。这种克制的策略最大程度减少了不必要的磁盘 I/O,保障了查询性能。
5、各核心算子是如何落盘的?
底层算子的 Spill 逻辑极为精巧,不同算子的处理方式因其计算特征而异:
- Hash Join(化大为小,逐个击破) :当用于匹配的内存哈希表过大时,Doris 会将数据按 Join Key 进行哈希分区并写入磁盘。为了确保匹配逻辑的正确性,左右两表(Build 端与 Probe 端)会采用相同的规则进行分区落盘。在恢复阶段,系统只需将落在同一分区的两表小文件读回内存进行局部 Join。一个极其庞大、可能撑爆内存的 Join,就这样被拆解成了若干个安全、可控的微型 Join。

- Aggregation(状态暂存,延后合并): 聚合算子落盘时,写入磁盘的不是最终结果,是中间状态(例如 SUM 操作当前的局部累计值)。系统根据分组键(Group Key)将这些中间状态分区落盘,待内存宽裕时重新读出,将相同分组的状态进行二次合并,最终输出准确结果。

- Sort(分批排序,多路归并): 排序算子采用经典的外部归并排序(External Merge Sort)。数据在内存中完成局部排序后,化作一个个有序的数据块(Run)写入磁盘;所有数据处理完毕后,系统再对磁盘上的多份有序数据进行多路归并,输出全局有序的结果。

6、基准测试:单 BE 16G 内存挑战 10TB 数据量
为了验证 Apache Doris 4.1 Spill to Disk 的极限能力,我们使用 1 台 16GB 内存 的 BE(计算节点)上,进行了 TPC-DS (10 TB 规模) 基准测试。
测试结果表明,开启 Spill to Disk 后,查询和系统稳定性均有较强的表现:
- 所有涉及复杂 Join、聚合的重量级查询(如
Query14,Query23,Query78等)均顺利执行完毕,无一因 OOM 中断。 - 在部分极端用例中(如
Query78),单节点内存消耗被严格控制在 8GB 红线以内,而其读写本地存储(Spill 临时文件)的数据量高达 1000GB+,是典型的以空间换内存运行机制。 - CPU 利用率在数据落盘与计算恢复之间平滑切换,避免了因内存变化导致的系统卡死。
(注:当前版本中 Intersect/Except 集合算子暂不支持 Spill,测试中通过语义等价的 Join 改写顺利完成。)
完整测试结果可见附件:https://cdn.selectdb.com/static/Spill_to_Disk_Doris_BE_16_G_10_TB_c3e62ccdda.xlsx
7、结语
Apache Doris 4.1 的 Spill to Disk 是一套深度融合了内存预留、智能调度、压力感知的现代化查询生命周期管理机制。对于重度数据分析用户而言,提供了一种在有限硬件资源下,安全、可靠进行超大规模分析任务的绝对保障。随着算子覆盖范围和执行机制的持续完善,Spill to Disk 将进一步提升 Apache Doris 在复杂分析场景下的稳定性和可扩展性。
本文着重解析了核心架构与设计思想。如果您希望在生产环境中开启 Spill to Disk 功能,或了解更为详尽的系统级参数配置,欢迎查阅 Apache Doris 官方文档。如果希望进一步体验稳定的、云原生托管版本,也可以了解 SelectDB 提供的相关服务。