Trino Memory Pool:
general memory pool: user memory(join 时 build 侧的hash table / sort)+ system pool(算子之间的buffer)。每个 worker 上有自己独立的 memory pool,计算方式参考 io.prestosql.memory.LocalMemoryManager#configureMemoryPools,公式为:Runtime.getRuntime().maxMemory() - config.getHeapHeadroom().toBytes(),即 -Xmx - memory.heap-headroom-per-node。query.max-memory-per-node和query.max-total-memory-per-node分别表示单个查询 user memory 和 total memory(user memory + system memory) 在单个 worker 上的最大可用内存,query.max-memory为单个 query 在整个集群中的最大可用内存。
reserved memory pool: 和 general memory pool 大小相等,只有 worker 内存使用量超过配置的最大内存之后才会把查询放在这个 pool 中执行,正常情况下用不到,而且启动时就分配,所以是一种内存浪费,低版本 Presto 生产环境中一般关掉,高版本的 Trino 已经移除了这个 pool。
coordinator: master 节点,虽然查询执行在各个 worker 上,但是 coordinator 也可能会出现 OOM。比如使用 hive connector 时,查询的分片太多,分片元信息存储在 coordinator;或者使用到 Dynamic Filter 时,满足条件的 filter 是通过 coordinator 分发到其它 worker 上去的。
Trino 如何跟踪内存:
每个算子(ScanFilterAndProjectOperator/HashAggregationOperator)都有一个OperatorContext,包含算子的各种信息,同样的还有TaskContext/QueryContext,其中QueryContext中有MemoryTrackingContext,各个 Operator 通过 setBytes来统计当前算子已申请的内存,同时update父节点已申请的内存以及 worker memory pool 内存。Context 通过树结构来组织 cluster → query → stage → task → driver之间的关系,这样就可以统计出根节点RootAggregatedMemoryContext即可知道当前这个查询使用了多少内存,每个 worker 和 coordinator 之间通信,从而知道集群目前整体的内存使用量。
OOM Killer 维持集群的稳定性:
相关的配置有:query.low-memory-killer.delay 以及 query.low-memory-killer.policy。其中 delay 指的是 当前 OOM 时间和上次 未 OOM 的 interval。policy 有两种:total-reservation表示杀掉集群中占有内存最大的SQL,total-reservation-on-blocked-nodes表示杀死在内存不足(阻塞)的节点上使用最多内存的查询。
真的稳定了吗?
答案是否定的。即使有oom-killer,trino worker 仍会重启(oom killer 有 delay:query.low-memory-killer.delay),而且堆外内存使用会持续升高,直到超过机器可用内存,最终被 linux 系统的 oom killer 杀掉 trino 进程(系统oom killer杀掉占用内存最大的进程)。
打开 -XX:NativeMemoryTracking=detail 之后,使用 jcmd 跟踪 JVM 内存使用。发现 reserved 为108GB 左右,但是top中占用为118(刚重启不久的时候top RES是比jcmd reserved/commited小一点的)。因为 jcmd 不包含C语言类库申请的内存,因此多出的 10GB 应该是这部分内存,对比 jcmd detail 和 pmap 详情,确实有一些地址在 pmap 中存在,在 jcmd detail 中不存在。这部分内存是随着时间逐渐增加的,不是在启动的时候就有。从 jcmd 中也可以看到G1垃圾回收器占用内存也有 5GB,不过是空间换时间了。jvm.config中有配置-Djdk.nio.maxCachedBufferSize=2000000,单位是字节,针对具体线程,jcmd中Other部分为这部分Direct Memory,分配给NIO使用,也没有很多。
Native Memory Tracking:
Total: reserved=110538MB, committed=109475MB
- Java Heap (reserved=102400MB, committed=102400MB)
(mmap: reserved=102400MB, committed=102400MB)
- Class (reserved=928MB, committed=928MB)
(classes #82696)
( instance classes #79763, array classes #2933)
(malloc=60MB #742359)
(mmap: reserved=868MB, committed=867MB)
( Metadata: )
( reserved=868MB, committed=867MB)
( used=553MB)
( free=314MB)
( waste=0MB =0.00%)
- Thread (reserved=1084MB, committed=127MB)
(thread #1071)
(stack: reserved=1075MB, committed=118MB)
(malloc=2MB #5458)
(arena=7MB #2140)
- Code (reserved=539MB, committed=433MB)
(malloc=23MB #80172)
(mmap: reserved=516MB, committed=409MB)
- GC (reserved=5068MB, committed=5068MB)
(malloc=1232MB #512363)
(mmap: reserved=3836MB, committed=3836MB)
- Compiler (reserved=4MB, committed=4MB)
(malloc=10MB #9509)
(arena=17592186044410MB #5)
- Internal (reserved=38MB, committed=38MB)
(malloc=38MB #59237)
- Other (reserved=372MB, committed=372MB)
(malloc=372MB #238563)
- Symbol (reserved=42MB, committed=42MB)
(malloc=39MB #555804)
(arena=4MB #1)
- Native Memory Tracking (reserved=36MB, committed=36MB)
(tracking overhead=35MB)
- Module (reserved=27MB, committed=27MB)
(malloc=27MB #107047)
Linux系统默认使用glibc分配内存,分配的大小和MALLOC_ARENA_MAX有关,arena 的数量在 32 位系统上最多是2 * CPU核心数, 64 位系统上最多是 8 * CPU 核心数,每个大小64MB,这一部分最多可以占到86464MB=32GB。尝试通过减小MALLOC_ARENA_MAX,调整MALLOC_ARENA_MAX = 4之后并没有生效,还是有很多64MB的内存块。最后把 glibc 替换为 jemalloc 发现有效。
参考: