内存构成
首先看一下Presto的堆内存构成图:
可以看到Presto堆内存主要有heap headroom和user memory, system memory构成
- 根据Trino代码中的MR记录,已经删除System memory,Trino堆内存只剩下heap headroom和user memory
Trino里面几个内存相关的参数:
| 参数 | 默认值 | 描述 |
|---|---|---|
query.max-memory-per-node | JVM max memory * 0.3 | 一个查询在一个 worker 节点上最大使用的内存 |
query.max-memory | 20GB | 一个查询在整个集群上最大使用的内存 |
query.max-total-memory | query.max-memory*2 | 一个查询在集群上最大使用的内存,包括可撤销的内存 |
memory.heap-headroom-per-node | JVM max memory * 0.3 | Trino无法跟踪的内存分配,用于第三方依赖项、执行期间的本地/栈内存分配等 |
虽然配置了heap headroom的大小,但在代码里面却并没有对他的使用加以控制,因为Trino自己都无法跟踪他的内存分配,headroom的配置值只是在生成memory pool的时候起作用,保证memory pool的大小加上headroom小于 jvm的最大值。
内存池解析
Trino内存管理中最重要的一个类:ClusterMemoryManager
该类中包含上面提到的maxQueryMemory, maxQueryTotalMemory等参数,以及ClusterMemoryPool
ClusterMemoryPool中比较重要的几个属性:
| 参数 | 描述 |
|---|---|
| blockedNodes | 当前阻塞的节点个数,判断条件为该节点的可使用内存大小 + 可撤销的已预留内存大小 <= 0 |
| totalDistributedBytes | 集群内存池最大值 |
| reservedDistributedBytes | 集群内存池已预留值 |
| reservedRevocableDistributedBytes | 集群内存池可撤销的已预留值,该参数为溢出到磁盘的大小 |
| freeDistributedBytes | 集群内存池可用大小 = totalDistributedBytes - reservedDistributedBytes - reservedRevocableDistributedBytes; |
ClusterMemoryPool中的属性是收集每个节点的NodeMemoryPool中的属性来进行汇总。
NodeMemoryPool的构造方法如下:
可以看出来节点的MemoryPool的最大值就是JVM的XMX减去heap headroom
所以可以简单的理解为JVM分为heap headroom 和 memory pool两个部分
内存池使用量统计
节点内存池中的reservedBytes是如何统计的呢,哪些内存使用会被统计到内存池里面?
Trino用MemoryTrackingContext 这个类来跟踪内存的使用情况
MemoryTrackingContext以树形层次结构组织,反映了opetator->driver->pipeline->task->query的层次结构. 对于一个Query, 所有的Operator占用的内存以树结构从下到上一路求和最终汇总计入到内存池中。通过这种机制,内存池可以统计每一个Operator使用的内存以及正在运行的Query所用的内存。(具体解释可阅读上图绿色文字部分)
在创建Operator时,每个Operator都有一个OperatorContext,OperatorContext中的MemoryTrackingContext来追踪内存。 以TableScanOperator为例,从下图中可以看到,在读取Page文件后,将source使用的内存大小更新到MemoryTrackingContext中.
memoryContext调用setBytes方法,最终调用父节点的updateBytes方法, 一步步以树形结构向上更新,
到根节点QueryContext时,则是将上报来的内存使用量更新到memory pool中,完成了一次内存分配的更新上报
内存不足Kill策略
kill策略相关的几个配置参数:
query-low-memory-killer-policy:
- none :不会杀死任何查询
- total-reservation :终止当前使用最多总内存的查询。
- total-reservation-on-blocked-nodes :终止当前在内存不足的节点上使用最多内存的查询(默认)
task.low-memory-killer.policy
- none :不会杀死任何task.
- total-reservation-on-blocked-nodes : 终止当前在内存不足的节点上使用最多内存的task(默认)
- least-waste : 终止当前在内存不足的节点上使用大量内存的task. 这个策略会避免终止已经执行很久的task, 所以大量的工作不会被浪费掉.
query.low-memory-killer.delay
- 在发生OOM和query被终止掉中间的时间, 只有上面两个设为none以外的值才有作用
在sqlQueryManager类中,会启用一个定时线程每隔一秒去检查内存使用情况
判断是否发生OOM的情况是看blockedNodes是否大于0,而判断节点是否blocked是看该节点的可用内存大小加上可撤销内存大小是否小于等于0
resource_overcommit 这个session property如果设置为True, 可以允许该查询使用整个memory pool的内存,不受query.max-memory的限制。但该查询即使没有到query.max-memory, 也有可能因为集群内存不足被kill掉。
从下图可以看到,Trino会遍历所有正在执行的Query, 以下三种情况会终止这个Query
- resourceOvercommit 设置为True, 且当前集群OOM
- resourceOvercommit 设置为false,该查询的userMemory使用量大于userMemory的限制值
- resourceOvercommit 设置为false,该查询的totalMemory使用量大于totalMemory的限制值
再根据你配置的不同kill策略,对应不同的lowMemoryKiller对象,会有不同的chooseTargetToKill实现方法,来选择应该kill的Query或者Task。
在节点级别,每次更新预留内存大小时,都会去检查是否超过每个节点允许的最大使用内存,如果超过,这个查询则会抛出异常终止掉