前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Presto内存调优及原理(基础篇)

Presto内存调优及原理(基础篇)

原创
作者头像
sundyxiong
修改于 2018-12-04 02:06:42
修改于 2018-12-04 02:06:42
9.7K00
代码可运行
举报
运行总次数:0
代码可运行

Presto是一个开源的分布式SQL查询引擎,适用于交互式分析查询,数据量支持GB到PB字节。Presto支持在线数据查询,包括Hive, Cassandra, 关系数据库以及专有数据存储。 一条Presto查询可以将多个数据源的数据进行合并,可以跨越整个组织进行分析。Presto以分析师的需求作为目标,他们期望响应时间小于1秒到几分钟。 Presto终结了数据分析的两难选择,要么使用速度快的昂贵的商业方案,要么使用消耗大量硬件的慢速的“免费”方案。

presto0.131开始对内存模型进行优化,直至当前EMRv2中上线版本0.188(包括EMRV1的0.161)都是使用的这个内存模型。使用的是一种称为内存池(memory-pool)的机制来管理presto中任务及presto本身的内存使用。

目标和设计思路可查看:https://github.com/prestodb/presto/issues/2624

当前presto已经实现了其设定的目标:

  1. 更容易的让用户控制每个查询内存限制。
  2. 预防内存溢出及由此引发的崩溃。
  3. 充分利用集群内存,不被一个“大查询”引起整个集群内存资源的限制。

Presto内存调优参数

presto把每个worker节点可分配内存(jvm Xmx)分成三份,分别是系统内存池(SystemMemoryPool),保留内存池(ReservedMemoryPool)和普通内存池(GeneralMemoryPool)。在Presto启动时,它们会随着worker节点初始化时被分配,然后通过服务发现各个worker节点上报给coordinator节点。

下图是presto worker节点的内存示意图:

worker   节点内存分布示意图
worker 节点内存分布示意图

从示意图中可以看到,一个worker节点的内存堆大小可以最大分成两份:系统预留内存+查询内存。而查询内存又分为最大查询内存+其他查询内存。

系统预留内存:worker节点初始化和执行任务必要的内存,包括preto发现服务的定时上报、每个query中task管理数据结构等。使用resources.reserved-system-memory配置项配置,默认是worker节点堆大小的0.4。

除了系统预留内存,其余给woker 内存都会给查询使用:

最大查询内存:coordinator节点会定时调度查看每个query使用的时长和内存,在此过程中会找到耗用内存最大的一个query,并会为此query调度最大的内存使用。这个query可获得各个worker节点最大配置的专用最大内存量。使用query.max-memory-per-node配置项可以配置,默认是worker节点堆大小的0.1。这个值可根据query监控的peak Mem作为参考设定。

其他查询内存:worker节点堆中除了系统预留的内存和最大查询的内存就是其他查询内存。

worker节点的堆内存的配置跟用户使用两个场景关系最大:

1.用户查询数据量/复杂性

2.用户查询并发度

1.决定了改用多大的最大查询内存 2.决定了该用多大jvm堆。

举个简单的例子,如果有一批(n=5)查询同时提交有10个worker节点的presto集群,其中数据量/复杂度最高的一个query语句一共要使用20GB的内存,那么我们应该给每个worker节点最大查询内存即是20GB/10 = 2GB,而需要并发执行,那么留给查询内存的应该是2GB*5大约为10GB,此时可以推出应该给每个worker节点配置堆大小为10/0.6 =17。(预留暂用0.4,留给查询的为0.6)。

除了上述的三个配置,还有一个配置需要关注,即查询最大可支持内存:表示每个query最大可支持内存。配置项为:query.max-memory这个值可配置为 query.max-memory-per-node*worker数目,以上个例子来说就是2GB*10=20GB。

用这个几个参数就能基本解决在使用presto集群时碰到的大部分查询慢和OOM问题。当然,需要对一个presto集群做更多精细化内存管理:比如针对到用户的内存调度,比如使用排队限制进入集群而限制整个集群query使用内存的限额,比如coordinator的内存精细管理。可以查看下一篇文章presto调优中级篇。

Presto内存调优原理

看完上一部分可以直观的在emr配置下发控制台操作实践起来了,对于想了解其中原理和排查更深层原因可以继续往下看(开始从源码角度讲原理,因为源码才能了解一切细节):

presto把每个worker节点可分配内存(jvm Xmx)分成三份,分别是系统内存池(SystemMemoryPool),保留内存池(ReservedMemoryPool)和普通内存池(GeneralMemoryPool)。在Presto启动时,它们会随着worker节点初始化时被分配,然后通过服务发现各个worker节点上报给coordinator节点。具体初始化是通过构造LocalMemoryManager类时完成的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final class LocalMemoryManager
{
    public static final MemoryPoolId GENERAL_POOL = new MemoryPoolId("general");
    public static final MemoryPoolId RESERVED_POOL = new MemoryPoolId("reserved");
    public static final MemoryPoolId SYSTEM_POOL = new MemoryPoolId("system");

    private final DataSize maxMemory;
    private final Map<MemoryPoolId, MemoryPool> pools;

    @Inject
    public LocalMemoryManager(NodeMemoryConfig config, ReservedSystemMemoryConfig systemMemoryConfig)
    {
        requireNonNull(config, "config is null");
        requireNonNull(systemMemoryConfig, "systemMemoryConfig is null");
        long maxHeap = Runtime.getRuntime().maxMemory();
        checkArgument(systemMemoryConfig.getReservedSystemMemory().toBytes() < maxHeap, "Reserved memory %s is greater than available heap %s", systemMemoryConfig.getReservedSystemMemory(), new DataSize(maxHeap, BYTE));
        maxMemory = new DataSize(maxHeap - systemMemoryConfig.getReservedSystemMemory().toBytes(), BYTE);

        ImmutableMap.Builder<MemoryPoolId, MemoryPool> builder = ImmutableMap.builder();
        checkArgument(config.getMaxQueryMemoryPerNode().toBytes() <= maxMemory.toBytes(), format("%s set to %s, but only %s of useable heap available", QUERY_MAX_MEMORY_PER_NODE_CONFIG, config.getMaxQueryMemoryPerNode(), maxMemory));
        builder.put(RESERVED_POOL, new MemoryPool(RESERVED_POOL, config.getMaxQueryMemoryPerNode()));
        DataSize generalPoolSize = new DataSize(Math.max(0, maxMemory.toBytes() - config.getMaxQueryMemoryPerNode().toBytes()), BYTE);
        builder.put(GENERAL_POOL, new MemoryPool(GENERAL_POOL, generalPoolSize));
        builder.put(SYSTEM_POOL, new MemoryPool(SYSTEM_POOL, systemMemoryConfig.getReservedSystemMemory()));
        this.pools = builder.build();
    }

reservatedMemoryPool的大小根据config.properties文件配置query.max-memory-per-node决定。这个值的默认值是jvm(xmX)的0.1。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class NodeMemoryConfig
{
    public static final String QUERY_MAX_MEMORY_PER_NODE_CONFIG = "query.max-memory-per-node";

    private DataSize maxQueryMemoryPerNode = new DataSize(Runtime.getRuntime().maxMemory() * 0.1, BYTE);

    @NotNull
    public DataSize getMaxQueryMemoryPerNode()
    {
        return maxQueryMemoryPerNode;
    }

    @Config(QUERY_MAX_MEMORY_PER_NODE_CONFIG)
    public NodeMemoryConfig setMaxQueryMemoryPerNode(DataSize maxQueryMemoryPerNode)
    {
        this.maxQueryMemoryPerNode = maxQueryMemoryPerNode;
        return this;
    }
}

systemMemoryPool大小则根据配置resources.reserved-system-memory决定,这个值的默认值是jvm(xmX)的0.4。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ReservedSystemMemoryConfig
{
    private DataSize reservedSystemMemory = new DataSize(Runtime.getRuntime().maxMemory() * 0.4, BYTE);

    @NotNull
    public DataSize getReservedSystemMemory()
    {
        return reservedSystemMemory;
    }

    @Config("resources.reserved-system-memory")
    public ReservedSystemMemoryConfig setReservedSystemMemory(DataSize reservedSystemMemory)
    {
        this.reservedSystemMemory = reservedSystemMemory;
        return this;
    }
}

最大内存调度策略

Presto一条query语句宏观上来说是把它转换成基于stage的逻辑执行计划。然后再通过逻辑执行计划转成物理执行计划在worker节点间分成并行的task,每个task内又转换成具体的Operator实际执行。在真正执行物理计划前,内存需求都来自于systemMemoryPool,包括临时数据结构,传输buffer等。执行物理计划时,不同的Operator类型都根据需要申请内存,比如aggregationOperator使用getEsctimatedSize()方法预估需要的内存。这里获取的内存来自于reservatedMemoryPool或者generalMemoryPool,究竟使用哪个pool取决于当前查询是否耗用内存最大。

在查询创建时,默认使用generalMemoryPool:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public SqlTaskManager(
            LocalExecutionPlanner planner,
            LocationFactory locationFactory,
            TaskExecutor taskExecutor,
            QueryMonitor queryMonitor,
            NodeInfo nodeInfo,
            LocalMemoryManager localMemoryManager,
            TaskManagementExecutor taskManagementExecutor,
            TaskManagerConfig config,
            NodeMemoryConfig nodeMemoryConfig,
            LocalSpillManager localSpillManager,
            NodeSpillConfig nodeSpillConfig)
    {     

...      

  queryContexts = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(

                queryId -> new QueryContext(
                        queryId,
                        maxQueryMemoryPerNode,
                        localMemoryManager.getPool(LocalMemoryManager.GENERAL_POOL),
                        localMemoryManager.getPool(LocalMemoryManager.SYSTEM_POOL),
                        taskNotificationExecutor,
                        driverYieldExecutor,
                        maxQuerySpillPerNode,
                        localSpillManager.getSpillSpaceTracker())));

presto会定时检查所有查询消耗的内存,这个定时器在presto初始化时被构造,实现如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
queryManagementExecutor.scheduleWithFixedDelay(() -> {
    ...
    enforceMemoryLimits();
    ...
}, 1, 1, TimeUnit.SECONDS);

其中的updateAssignments方法,其作用是找出最耗费内存的查询,并放入RESERVED_POOL: 

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (reservedPool.getAssignedQueries() == 0 && generalPool.getBlockedNodes() > 0) {
    QueryExecution biggestQuery = null;
    long maxMemory = -1;
    for (QueryExecution queryExecution : queries) {

        if (resourceOvercommit(queryExecution.getSession())) {     
            continue;
        }

        long bytesUsed = queryExecution.getTotalMemoryReservation();

        if (bytesUsed > maxMemory) {
            biggestQuery = queryExecution;
            maxMemory = bytesUsed;
        }
    }

    if (biggestQuery != null) {
        biggestQuery.setMemoryPool(new VersionedMemoryPoolId(RESERVED_POOL, version));
    }

}

再看外层的updateNodes方法,可以发现RESERVED POOL的这种分配策略会应用到每个节点。也就是说这个查询在每个节点都会独占RESERVED POOL的空间。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (RemoteNodeMemory node : nodes.values()) {
    node.asyncRefresh(assignments);
}

Presto内存监控

可以通过presto提供的WebUi来即时追踪整个集群或者每个查询执行的内存的使用情况。presto的WebUI后台使用的是DI框架Guice,前台是jquery+react实现。

presto 集群overview
presto 集群overview
presto 某个query监控图
presto 某个query监控图

web页面中reservedMemmory即是ReservedMemoryPool的大小。而在查询资源汇总中,peakMemory表示当前查询使用memory峰值。memory pool表示当前使用的pool(reserved或者genaral)。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
Linux进程间通信--管道(pipe和fifo)
       通过管道来实现进程间的通信的方法很经典,因为多个进程共享3-4G中的内核,所以在内核中存在一个管道(缓冲区),然后进程通过连接管道的两端从而实现通信。假如说我们现在有一根管道,我们从左端放入一个小球,那么它会从右端滚出来,那么如果我们同时向两端都放入一个小球,那么就不可能实现交叉传递了,所以管道是半双工通信(即双方都可以发送信息,但是双方不能同时发送信息),因此管道的两端一端是读端,一端是写端。那么要实现两个进程的同时读写操作,就需要用两个管道。
Ch_Zaqdt
2020/03/03
3.9K0
【Linux进程#1】IPC 进程间通信(一):管道(匿名管道&命名管道)
✨ 无人扶我青云志,我自踏雪至山巅 🌏
IsLand1314
2025/06/02
1350
【Linux进程#1】IPC 进程间通信(一):管道(匿名管道&命名管道)
【Linux】IPC 进程间通信(一):管道(匿名管道&命名管道)
为了实现两个或者多个进程实现数据层面的交互,因为进程独立性的存在,导致进程通信的成本比较高
IsLand1314
2024/11/19
4350
【Linux】IPC 进程间通信(一):管道(匿名管道&命名管道)
Linux:进程间通信(一.初识进程间通信、匿名管道与命名管道、共享内存)
这种双重性使得管道既具有机制的灵活性,又具有文件的可操作性。它可以在不同的进程之间建立连接,实现数据的传递和共享,同时也可以通过标准的文件操作接口进行访问和控制。
是Nero哦
2024/07/09
5750
Linux:进程间通信(一.初识进程间通信、匿名管道与命名管道、共享内存)
Linux 的进程间通信:管道
本文介绍了管道(pipe)在Linux系统中的实现方式,从三个方面进行了详细阐述:管道的原理,命名管道,以及通过匿名管道进行的进程间通信。同时,文章还探讨了管道在Linux系统中的实际应用,包括shell脚本、cron任务以及Linux中的各种守护进程等。
邹立巍
2017/07/31
8.5K3
Linux 的进程间通信:管道
进程间通信(一)/管道
有时候需要多进程协同,让每一个进程专注于自己的事,然后把结果交给另外一个进程去处理。比如使用管道,让多进程协同,简单的有:
二肥是只大懒蓝猫
2023/03/30
5290
进程间通信(一)/管道
Linux 进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)
管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
用户6543014
2020/12/21
2.6K0
Linux 进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)
【linux学习指南】 进程间通信&&匿名管道&&理解管道的本质
在 Unix/Linux 系统中,我们可以使用 fork() 系统调用来创建子进程,并通过共享管道(pipe)进行进程间通信。下面是使用 fork() 来共享管道的原理:
学习起来吧
2024/11/18
1510
【linux学习指南】 进程间通信&&匿名管道&&理解管道的本质
进程间通信
什么是管道? 可以理解为内存中的一个缓冲区,用于将某个进程的数据流导入,由某一个进程导出,实现通信。 再通俗的说,看图:
看、未来
2020/08/26
9030
进程间通信
C++进程间通信 详解2
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问。
Freedom123
2024/03/29
9830
C++进程间通信 详解2
进程通信(一)无名管道和有名管道
《王道考研复习指导》 管道通信是消息传递的一种特殊方式。所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程),以字符流的形式将大量的数据送入(写)管道;而接受管道输出的接受进程(即读进程),则从管道接受(读)数据。为了协调双方的通信,管道机制必须提供一下三个方面的协调能力:互斥、同步和确定对方存在。 下面以linux的管道为例进行说明。在linux中,管道是一种频繁使用的通信机制。从本质上讲,管道也是一种文件,但它又和一般的文件有所不同,管道可以克服使用文件通信的两个问题,具体表现为: 1)限制管道的大小。实际上,管道是一个固定大小的缓冲区。在Linux中,该缓冲区的大小为4KB,使得它不像文件那样不加检验的增长。使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对写管道的write()调用将默认的阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写。 2)读进程也可能工作的比写进程快。当所有当前进程数据已被读走时,管道变空。当这种情况发生时,一个随后的read()调用将默认设置为阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。 注意 :从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。管道只能采用半双工通信,即在某一时刻只能单向传输。要实现父子进程双方互动,需要定义两个管道。
lexingsen
2022/02/24
1.6K0
进程通信(一)无名管道和有名管道
从零开始:实现进程间管道通信的实例
匿名管道(Anonymous Pipe)是进程间通信(IPC)的一种机制,它主要用于本地父子进程之间的通信。
绝活蛋炒饭
2024/12/16
2180
从零开始:实现进程间管道通信的实例
进程间通信Linux
首先自己要用用户层缓冲区,还得把用户层缓冲区拷贝到管道里,(从键盘里输入数据到用户层缓冲区里面),然后用户层缓冲区通过系统调用(write)写到管道里,然后再通过read系统调用,被对方(读端)读取,就要从管道拷贝到读端,然后再显示到显示器上。
ljw695
2024/11/15
2880
进程间通信Linux
【Linux】基于管道进行进程间通信
进程间通信是两个或者多个进程实现数据层面的交换。但是由于进程间存在独立性,所以导致进程间通信的成本比较高。
YoungMLet
2024/03/01
2620
【Linux】基于管道进行进程间通信
深入了解linux系统—— 进程间通信之管道
本篇博客所涉及到的代码一同步到本人gitee:testfifo · 迟来的grown/linux - 码云 - 开源中国
星辰与你
2025/06/08
760
深入了解linux系统—— 进程间通信之管道
C语言第四章(进程间的通信,管道通信,pipe()函数)
本文讲解的是C语言的进程之间的通信,这里讲解的是管道通信,和相关的函数pipe().
GeekLiHua
2025/01/21
2070
Linux进程间通信之管道
1,进程间通信 (IPC ) Inter-Process Communication   比较好理解概念的就是进程间通信就是在不同进程之间传播或交换信息。 2,linux下IPC机制的分类:管道、信号、共享内存、消息队列、信号量、套接字 3,这篇主要说说管道:本质是文件,其他理论什么的网上已经有一大堆了,我就只写一点用法吧。 3.1 特点      1)管道是最古老的IPC,但目前很少使用      2)以文件做交互的媒介,管道分为有名管道和无名管道      3)历史上的管道通常是指半双工管道 3.2 管
xcywt
2018/01/11
2.7K0
Linux进程间通信之管道
【Linux】进程间通信——管道
而我们所说的不同通信种类本质就是:上面所说的资源,是OS中的哪一个模块提供的。如文件系统提供的叫管道通信;OS对应的System V模块提供的…
平凡的人1
2023/10/15
3400
【Linux】进程间通信——管道
进程间通信--管道
有时候我们需要多个进程协同的去完成某种任务,因此需要进程之间能够相互通信。但是进程之间具有独立性,要让进程之间能通信就要打破这种独立性,所以通信的代价一定是不低的。打破这种独立性就是要让两个不同的进程看到同一份资源,这个资源只能由操作系统来提供。因为如果是某个进程来提供因为独立性,这个资源就只能被提供这个资源的进程看到。
始终学不会
2023/10/17
2450
进程间通信--管道
Linux进程通信之管道解析
管道是 UNIX系统 IPC的最古老的形式,所有的UNIX系统都提供此种通信。所谓的管道,也就是内核里面的一串缓存,从管道的一段写入的数据,实际上是缓存在内核中的,令一端读取,也就是从内核中读取这段数据。对于管道传输的数据是无格式的流且大小受限。对于管道来说,也分为匿名管道和命名管道,其中命名管道也被叫做 FIFO,下面则分别阐述这两种管道。
wenzid
2021/07/20
1.4K0
Linux进程通信之管道解析
推荐阅读
相关推荐
Linux进程间通信--管道(pipe和fifo)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档