第 9
篇 在算子层讲过 hash join 的 build 表与 partial/final
aggregation;第 11
篇 讲过 Exchange 缓冲
Page。生产里更常遇到的是:查询还没算完,Coordinator
报 Exceeded memory limit、个别 Worker spill
到盘却更慢、多租户抢内存导致全员排队——这些属于
集群级内存治理,不是单算子代码能单独解释的。
本文是系列 第 16 篇,建立 Trino 主线上的 内存账户 → spill → resource group 心智模型,并和 stream-processing 第 18 篇背压 对照:流式背压 限的是管道 buffer 与 checkpoint;交互式 OLAP 限的是 查询峰值内存与 revocable 回收。
环境说明:本机 未部署 Trino 集群,正文机制来自 Trino 476+ 官方文档 Memory management、Spill、Resource groups(A 级)。不粘贴未实测的 spill 字节数或
EXPLAIN ANALYZE内存字段。DuckDB 单进程内存行为见 第 14 篇 边界一句。
一、为什么 OLAP 内存是「账户制」
OLTP(postgresql-kernel)单查询内存通常由
work_mem 等参数封顶,超出可能 sort/hash
落盘或直接失败。分布式 OLAP 额外有:
- 多 Stage 并行:每个 Worker 上多个 Driver 同时跑 hash build、agg 状态、Exchange 缓冲。
- 跨节点不可见堆:Coordinator 需要 汇总各节点 memory context 才能 kill 或 spill。
- revocable 内存:join/agg 的 hash 表在压力时可 部分写入 spill 文件,与「已承诺给网络/shuffle 的 non-revocable」区分(Trino docs, Memory management)。
Trino 用 分层 memory pool + query
context 跟踪:GENERAL
池、RESERVED、以及 per-query 的
user memory / revocable memory
计数(文档术语,具体类名见源码
ClusterMemoryManager)。
flowchart TB
CLUSTER["Cluster memory limit"] --> POOL["General pool"]
POOL --> Q1["Query A context"]
POOL --> Q2["Query B context"]
Q1 --> OP["Operators<br/>hash / agg / exchange"]
OP -->|pressure| SPILL["Local spill files"]
POOL -->|exceeded| KILL["Query failure / OOM"]
二、Query / User / Cluster 限额
Trino 配置层常见限额(config.properties /
session,文档 Properties):
| 层级 | 典型属性 | 作用 |
|---|---|---|
| Cluster | query.max-memory |
单查询在所有节点 user memory 之和上限 |
query.max-total-memory |
含 revocable 在内的总量上限 | |
| Node | memory.heap-headroom-per-node |
留给 OS/GC 的头寸,防节点 OOM |
| Session | query_max_memory |
覆盖单 session 查询上限 |
user memory 近似「算子已分配且短期不会还给池子」的部分;revocable 是 可被 spill 回收 的部分(hash table、部分 agg 状态)。Exchange 缓冲、某些 pipeline 缓冲属于 non-revocable 或单独记账——这就是为什么「query 看起来没跑 join 仍 OOM」:可能是 shuffle 缓冲 + broadcast 表 占满。
与 第 5 篇 联动:broadcast join 把小表复制到每个 Worker,user memory 按 Worker 数放大;估错行数时 broadcast 误判是生产 OOM 主因之一。
三、Spill:哪些算子、什么路径
Trino 文档 Spill(A 级)说明下列算子可在内存压力下 写本地磁盘(格式为引擎内部二进制,非 Parquet):
| 算子 | Spill 内容 | 触发直觉 |
|---|---|---|
| Hash Join | Build 侧 partition 落盘,probe 多次回放 | 大 build 表 |
| Hash Aggregation | 分组状态 partition | 高基数 GROUP BY |
| Order By | 排序 run 外排 | 大结果集排序 |
| Window | 部分场景 partition 缓冲 | 大窗口帧 |
流程概要:
- Memory pool 达到 revocable 回收阈值。
- Operator 收到 spill 信号,将 部分 hash
partition 序列化到 spill
目录(
spiller-spill-path等配置)。 - 继续处理剩余内存内 partition;probe 阶段 读回 spill 完成 join。
代价:磁盘 IO 与多次 probe 使 wall time
陡增;EXPLAIN ANALYZE 中可见
Spilled 相关指标(文档 Explain
analyze;本机未实测数值)。Spill
不保证 一定避免失败——若 spill
速度跟不上分配速度,或 disk 满,仍
Exceeded memory limit。
第 9
篇 实验设计:调低 query.max-memory 构造大
join,观察 plan 是否出现 spill——需在 Docker Trino
栈执行(reproduce/docker-compose.yml),本篇不伪造输出。
四、Resource Group:并发与隔离
Resource group(Trino docs, Resource groups,A 级)在 Coordinator 做 多租户队列:
| 机制 | 说明 |
|---|---|
| Hard concurrency limit | 组内同时运行查询数上限 |
| Soft limit + queue | 超出进入队列,按优先级出队 |
| Per-group memory limit | 组内查询共享内存预算 |
| Selector | 按 user、source、query type 路由到组 |
典型划分:交互式 BI(低并发、短查询、紧 memory limit) vs ETL 批查询(高并发、长查询、独立组)。与 Spark Fair Scheduler pool 类似,但 Trino 在 Coordinator 单点 决策,Worker 只执行已批准 query。
Hard concurrency limit = 0 可维护模式拒绝新查询;soft limit 过高会导致 内存求和仍超 cluster,触发全局 kill。
五、与流式背压的对照
stream-processing/18 的 Flink credit-based 背压 解决 有界 buffer 下生产者别压垮消费者:下游慢 → credit 不释放 → 上游停读。
| 维度 | 流式(Flink) | 交互式 OLAP(Trino) |
|---|---|---|
| 控制对象 | Subtask 间 record 流 | Query 内存账户 + 线程池 |
| 反压信号 | Input gate credit | Memory pool 压力、队列 |
| 缓冲突出 | 状态后端 + checkpoint | Spill 文件 |
| 隔离单元 | 作业 / 算子链 | Resource group / query |
| 用户感知 | Lag、checkpoint 超时 | 排队、query killed |
OLAP 通常无全局 backpressure 协议:慢查询占满 Worker 线程与内存,靠 resource group + kill policy 隔离,而不是逐 Page 反压上游 scan(scan 仍可能全速读直到 memory 触顶)。这是 第 1 篇 交互式 vs 流式 边界在生产资源层的体现。
六、Spark 与 DuckDB 边界(对照一句)
- Spark SQL:Executor memory +
disk
spill(shuffle、agg);AQE 在 OOM
前可 coalesce partition(第 13
篇)。配置
spark.memory.fraction、spark.sql.shuffle.partitions与 Trino 的query.max-memory不同口径,不直接换算。 - DuckDB:单进程 temp directory spill(文档 Memory Management),无 resource group;并发靠应用层串行化。
七、运维检查清单
- Kill 前看
query.max-memoryvs 实际 broadcast 表大小 × worker 数。 - Spill 目录 磁盘空间与本地 SSD;HDD spill 易成 straggler(第 17 篇)。
- Resource group 是否把重 ETL 与 BI 混在同一 general pool。
- Revocable 比例:大 join + 大 agg 同 query 时 spill 顺序与重复读盘。
- Node OOM:区分 JVM heap OOM(调 heap、headroom)与 query limit(调 plan 或 SQL)。
八、小结
- Trino 内存是 cluster / query / operator 分层账户;revocable 可 spill,non-revocable(部分 exchange/broadcast)更易顶满。
- Spill 覆盖 hash join、hash agg、order by 等;是 降级路径 不是免费午餐。
- Resource group 管并发与组级预算,是 multi-tenant 交互式 OLAP 的隔离主手段。
- 流式 背压 与 OLAP 内存 kill/spill 解决不同层问题;读湖裁剪减 IO,不能替代内存治理(第 15 篇)。
上一篇:Iceberg 下推全链路
下一篇:经典故障与排查
返回 系列目录
参考资料
- Trino Documentation 476+, Memory management / Spill / Resource groups / Properties(A 级)。
- Trino Documentation, Explain analyze(A 级;字段说明,无本机数值)。
- stream-processing/18 背压(B 级对照)。
- DuckDB Documentation, Memory Management(A 级;嵌入式边界)。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【分布式 OLAP 查询引擎】Hash Join 与 Hash Aggregation:Build/Probe 与 Spill
拆解 hash join build/probe 内存布局、outer join 标记,以及 partial/final 两阶段聚合在 MPP 上的语义;对照 Trino 476+ spill/revocable memory 与 DuckDB 本机 HASH_JOIN 实测。
【分布式 OLAP 查询引擎】OLAP 查询引擎全景:从单进程到 MPP
从 OLTP/OLAP/HTAP 边界、嵌入式 DuckDB 与分布式 Trino/Spark 分工、批式扫描与交互式查询延迟口径出发,闭合 lakehouse 与 stream-processing 之间的查询层缺口,并给出本系列 18 篇地图。
【分布式 OLAP 查询引擎】Join 重排与物理算子选择
Join order enumeration、Hash/Merge/Nested Loop 适用条件;Trino broadcast vs partitioned join 与 shuffle 网络代价;Dynamic partition pruning 与 runtime filter;DuckDB HASH_JOIN 实测与 Spark AQE 对照边界。
【分布式 OLAP 查询引擎】引擎选型与数据平台阅读地图
用决策树收束 Trino/Spark/ClickHouse/DuckDB/DataFusion/PostgreSQL 的适用边界:交互式联邦、批 ETL、嵌入式分析、流批一体各走哪条路径;给出能力对照表(无吞吐排名)与 postgresql→columnar→lakehouse→stream→query-engine 全栈阅读顺序,闭合数据平台栈。