Dong Wang

Presto 查询引擎内核详解:集群资源管理机制解析

本文档详细阐述Presto集群中Coordinator和Worker两个核心角色如何协同完成资源管理(重点是内存),以确保查询高效、稳定地执行。

其核心思想可以概括为:

Coordinator 负责全局约束与策略决策,Worker 负责本地执行与精细化控制。

需要特别强调的是,Presto 的资源管理并非“静态分配”,而是:

一个基于运行时反馈的动态控制系统(feedback-driven system)

第一部分:Coordinator端 —— 集群资源的“大脑”

Presto 的集群资源管理整体机制和架构如下图所示:

Presto Resource Manager Architecture

其中,Coordinator作为集群的协调管控节点,维护着所有节点和查询的全局视图,并据此执行全局性的资源管理策略。概括来说:

Coordinator 维护:

并执行:

1.1 核心查询管理器:SqlQueryManager

SqlQueryManager是Coordinator端管理查询的单例服务。它不仅是所有查询执行的统一入口(createQuery方法),更是一个集监控、动态干预于一体的核心组件。

关键内部组件

ClusterMemoryManager:集群统一内存管理器,也可以被称为全局内存资源仲裁器(Global Arbiter)。它从整个集群的视角,监控所有节点的内存使用 情况,并决定全局策略。例如,是否将某个查询提升至预留内存池(Reserved Pool)或终止内存超限的查询。负责全局内存压力检测、OOM 处理、内存池调整等。

QueryTracker:查询追踪器。维护所有运行中的查询实例(QueryExecution),并负责查询级别的超时、遗弃、任务数限制等管理。

QueryManagerStats:全局查询状态统计器,记录所有查询的执行状况。用以支撑 UI / metrics / event listener 等。

周期性管理任务:

SqlQueryManager启动后,会以1秒为周期,在一个专用的queryManagementExecutor线程池中执行以下任务:

这些后台任务本质上构成了一个以 1S 为周期运行的软实时控制循环(soft real-time control loop),也即:

周期性检测、反馈式控制;资源限制不会阻止查询启动,而是在执行过程中根据运行状态进行裁决。

这种运行时反馈控制机制与 Spark 主要依赖 pre-scheduling 的资源规划方式形成鲜明对比。

1.2 集群内存管理核心:ClusterMemoryManager

这是 Presto 最关键、也是最容易被低估的组件。其本质定位为:

集群级内存压力调度器(pressure-driven scheduler)

ClusterMemoryManager是实现集群内存宏观调控的引擎,其process()方法由上述enforceMemoryLimits()方法周期调用,执行集群内存调控的核心逻辑。

核心职责与流程:

1.3 查询生命周期管理:QueryTracker

QueryTracker专注于查询实例的追踪与微观管理。其核心职责为:

总结:QueryTracker 决定“查询是否应该继续存在”。

第二部分:Worker端——本地资源的“管家”

Worker 是计算真正发生的地方。每个Worker节点负责管理分配给自己的本地资源,并执行实际的计算任务,包括:

2.1 本地资源统一入口:SqlTaskManager

SqlTaskManager是Worker节点上管理所有接收到的任务(SqlTask)的单例服务。它维护着节点上所有查询和任务的本地上下文。

其持有的核心数据结构为 LoadingCache<QueryId, QueryContext> queryContexts:

这是一个以QueryId为键的缓存,为每个在本地执行的查询创建一个QueryContext。这实现了查询级别的内存隔离 —— 同一Worker上属于相同查询的所有任务,会共享同一个QueryContext及其内部的内存管理体系。

2.2 worker节点本地内存池:LocalMemoryManager

LocalMemoryManager代表了Worker节点对自身内存的划分。

内存池划分:根据配置(NodeMemoryConfig),Worker节点将可用堆内存(Runtime.getRuntime().maxMemory())划分为:

📌 关键理解:

MemoryPool 本质上是一个”内存资源许可(Resource Permit)管理器”,而不是内存分配器。

2.3 接收并处理Coordinator的指令:MemoryResource

Worker通过MemoryResource(HTTP端点/v1/memory)接收来自Coordinator的MemoryPoolAssignmentsRequest。

2.4 查询级内存上下文:QueryContext

QueryContext是Worker上为单个查询创建的“资源容器”。它通过MemoryTrackingContext构建了一个层次化的内存追踪体系。

层次化内存追踪体系 (MemoryTrackingContext):

该体系将查询在Worker上执行时产生的所有内存分配,按照Query -> Task -> Pipeline -> Driver -> Operator五个层级,以及User、System、Revocable三种类型进行精细化管理。

核心原理:

上述每个层级的聚合内存上下文和本地内存上下文都各有三个,代表了三种不同的内存类型:

内存申请与预留流程 (MemoryPool & MemoryReservationHandler):

“先用后记”模式:Presto的算子通常先实际使用JVM内存(如创建Java对象),再通过MemoryReservationHandler向MemoryPool“预留记账”。

内存预留接口 (MemoryReservationHandler):

内存预留核心 (MemoryPool):MemoryPool并非实际分配内存,而是一个预留与控制器。它维护着每个Query、每个标签(Tag)的内存使用明细。当reservedBytes超过maxBytes时,会创建一个阻塞future,从而通过阻塞新的 reservation 请求,实现内存压力反馈,达到控制后续内存申请的目的。

📌 关键点:

Presto 使用 Future 的语义实现“协作式阻塞(cooperative blocking)”,即:

2.5 内存回收机制:MemoryRevokingScheduler

当内存紧张时,Presto并非被动等待OOM,而是主动触发内存回收(Spill to Disk,将内存数据溢出到磁盘)。典型场景包括:

触发器:MemoryRevokingScheduler 为 MemoryPool(如GENERAL_POOL)注册了一个监听器。每次成功预留内存(reserve/reserveRevocable)后,都会触发该监听器以执行相应的检查及回收操作。

涉及到的回收策略为:

回收请求最终会设置到算子对应的OperatorContext的memoryRevokingRequested标志上。当Driver线程下次调度到该Operator时,会调用其startMemoryRevoke()方法执行真正的磁盘溢出操作,并释放内存。

2.6 查询终止时的资源清理

当Coordinator决定终止一个查询(如超时、OOM)时,会调用SqlQueryExecution.fail(),最终通过HTTP请求通知所有Worker终止相关任务。

Worker端清理流程(以DELETE /v1/task请求为例):

📌 核心特征:

资源释放是一个自底向上的级联过程

2.7 输出缓冲区内存管理:OutputBufferMemoryManager

OutputBufferMemoryManager是Worker上管理输出缓冲区的专用组件,它封装了LocalMemoryContext来管理系统内存。

总结

Presto的资源管理体系通过Coordinator的全局视角和Worker的本地精细控制,实现了高效、可靠的查询执行:

Presto 资源管理的本质模型可以抽象为三层:

👉 三者形成:

闭环控制系统(Closed-loop Control System)

最后用用一段话概括,Presto 的资源管理体系通过层次化内存预留、Future-based 阻塞机制、Revocable spill 内存回收机制、全局 OOM 仲裁等机制,共同实现在不可预知负载下的稳定执行能力。和resource manager体系相配合,共同保障了Presto在多租户、高并发场景下的性能和稳定性。


欢迎交流

本文基于作者当前的理解与实践经验整理而成,难免存在疏漏或值得进一步探讨之处。

如果您对文中的观点有不同看法,发现任何问题,或有相关实践经验,欢迎通过 GitHub Issue 与作者交流讨论。

期待与更多同行围绕数据基础设施相关技术展开交流,分享实践经验,共同学习、共同进步。