前言:
今天早上09:30-09:40时分,presto集群又出现了多个worker节点OOM然后服务挂掉的问题。集群此时非常的不稳定。
看来了下节点的日志,是发生了内存堆溢出
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid171589.hprof ...
那么先观察一下这段时间跑的任务。由于公司的presto集群是配置了内存不足的保护策略的query.low-memory-killer.policy=total-reservation
,所以先看一下任务是否又被主动kill掉的吧。
看一眼kibana:
09:30 - 09: 50 的任务数量其实也没有很多。
那么为什么集群内存不足,但没有触发OOM killer?
任务什么时候会被kill掉?
一个任务什么时候会被kill掉?
1.sql 本身使用的资源超出配置
相关的配置如下:
query.max-memory
query.max-memory-per-node
query.max-total-memory-per-node
2.集群内存不足时,触发内存保护机制
query.low-memory-killer.policy
none
total-reservation-on-blocked-nodes
total-reservation
先来再来看一眼集群的配置吧。
集群配置:
目前集群固定节点有23个节点,然后10点-17点处于集群繁忙状态,会增加5个节点。
config.properties
综上,Presto合适的内存分配配置为如下,-Xmx40G,Worker数大于23台,下面是config.properties
文件一些重要的配置。
# 单个Query在整个集群上允许的最大user memory
query.max-memory=192GB
# 单个Query在单个Worker上允许的最大user memory
query.max-memory-per-node=10GB
# 单个Query在单个Worker上允许的最大user memory + system memory
query.max-total-memory-per-node=16GB
# 禁止掉RESERVED_POOL时
experimental.reserved-pool-enabled=false
# 这个内存主要是第三方库的内存分配,无法被统计跟踪,默认值是XMX*0.3。
memory.heap-headroom-per-node=8GB
# 集群内存不足时保护策略
query.low-memory-killer.policy=total-reservation
jvm.properties
下面是另一个重要的配置文件jvm.properties
内容:
-server
-Xmx40G
-XX:-UseBiasedLocking
-XX:+UseG1GC
-XX:+ExplicitGCInvokesConcurrent
-XX:+HeapDumpOnOutOfMemoryError
-XX:+UseGCOverheadLimit
-XX:OnOutOfMemoryError=kill -9 %p
-DHADOOP_USER_NAME=hive
-Duser.timezone=Asia/Shanghai
-XX:G1ReservePercent=15
-XX:InitiatingHeapOccupancyPercent=40
-XX:ConcGCThreads=8
集群的配置第一眼看着没有什么问题,悬念留到后面再说。
参考文章:http://armsword.com/2020/02/18/presto-memory-kill-policy/
Presto集群内存不足时保护机制
再来看下集群内存不足时,代码逻辑吧!
判断节点是否内存不足:
代码位置:presto-main/src/main/java/com/facebook/presto/memory/ClusterMemoryPool.java
代码如下:
if (poolInfo != null) {
nodes++;
if (poolInfo.getFreeBytes() + poolInfo.getReservedRevocableBytes() <= 0) {
blockedNodes++;
}
内存不足kill任务的代码:
代码位置:
presto-main.src/main/java/com/facebook/presto/memory/ClusterMemoryManager.java
详细如下:
for (QueryExecution query : runningQueries) {
boolean resourceOvercommit = resourceOvercommit(query.getSession());
long userMemoryReservation = query.getUserMemoryReservation().toBytes();
long totalMemoryReservation = query.getTotalMemoryReservation().toBytes();
if (resourceOvercommit && outOfMemory) {
// If a query has requested resource overcommit, only kill it if the cluster has run out of memory
DataSize memory = succinctBytes(getQueryMemoryReservation(query));
query.fail(new PrestoException(CLUSTER_OUT_OF_MEMORY,
format("The cluster is out of memory and %s=true, so this query was killed. It was using %s of memory", RESOURCE_OVERCOMMIT, memory)));
queryKilled = true;
}
if (!resourceOvercommit) {
long userMemoryLimit = min(maxQueryMemory.toBytes(), getQueryMaxMemory(query.getSession()).toBytes());
if (userMemoryReservation > userMemoryLimit) {
query.fail(exceededGlobalUserLimit(succinctBytes(userMemoryLimit)));
queryKilled = true;
}
long totalMemoryLimit = min(maxQueryTotalMemory.toBytes(), getQueryMaxTotalMemory(query.getSession()).toBytes());
if (totalMemoryReservation > totalMemoryLimit) {
query.fail(exceededGlobalTotalLimit(succinctBytes(totalMemoryLimit)));
queryKilled = true;
}
}
// if the cluster is out of memory and we didn't trigger the oom killer we log the state to make debugging easier
if (outOfMemory) {
log.debug("The cluster is out of memory and the OOM killer is not called (query killed: %s, kill on OOM delay passed: %s, last killed query gone: %s).",
queryKilled,
killOnOomDelayPassed,
lastKilledQueryGone);
}
此时一句log.debug
的输出引起了我的注意:
// if the cluster is out of memory and we didn't trigger the oom killer we log the state to make debugging easier
if (outOfMemory) {
log.debug("The cluster is out of memory and the OOM killer is not called (query killed: %s, kill on OOM delay passed: %s, last killed query gone: %s).",
queryKilled,
killOnOomDelayPassed,
lastKilledQueryGone);
}
翻译翻译:
如果集群内存不足并且我们没有触发 oom 杀手,我们会记录状态以使调试更容易
😯哦?那么我只需要看一下OOM节点的日志有没有输出这句话,看看是因为什么在集群内存不足时没有触发OOM killer?
正好有一个节点 开启了DEBUG
级别日志。赶紧来查询一下:
结果出乎意料,日志里并没有出现这段话,那到底是什么原因导致节点OOM,但是没有?
虽然出现了新的疑问,但是却能解决第一个问题,为什么节点OOM,但是并没有触发内存不足保护机制,并不是我们保护机制写错或者配置不正确。
而是当时并没有出现内存不足,或者说还没来得及触发内存保护机制时,导致 JVM已经被kill掉了。
举个例子:之前JVM运行状态是正常的,一个新的计算任务分配到了这个节点上,瞬间导致JVM内存使用超过上限,然后 JVM 被kill 掉。
看来第二个可能性高一些。
也就是说有别的原因导致被kill掉。
- 操作系统触发的OOM
- 结点是否有内存泄露情况。(可以排除,有一个节点10分钟内挂掉2次)
于是将目光投向了第一个原因,是操作系统触发的OOM。
查看操作系统日志
dmesg > dmesg.txt
然后搜索 presto
,发现并不是操作系统层面kill掉的presto。
后面当在看了一眼配置时,发现了问题:
-XX:OnOutOfMemoryError=kill -9 %p
-XX:OnOutOfMemoryError
当发生内存溢出的时候,还可以让JVM调用任一个shell脚本。大多数时候,内存溢出并不会导致整个应用都Crash掉,但是最好还是把应用重启一下,因为一旦发生了内存溢出,可能会让应用处于一种不稳定的状态,一个不稳定的应用可能会提供错误的响应。使用举例:
-XX:OnOutOfMemoryError=kill -9 %p
当给JVM传递上述参数的时候,如果发生了内存溢出,JVM会调用kill -9 将jvm 杀掉。
总结:
XX:OnOutOfMemoryError
是JVM
级别的query.low-memory-killer.policy=total-reservation
是代码级别
两种内存保护策略起了 “冲突”
,当preosto集群发生内存不足时,presto还没来得及触发内存不足保护策略,JVM那边就已经被kill掉了。
XX:OnOutOfMemoryError
这个配置本意是好的,防止内存溢出后,程序不稳定主动kill掉,反而给我们带来了困惑,看来以后排查问题需要更加认真。
还有一点需要注意:即使我们去掉了XX:OnOutOfMemoryError的配置,presto 依旧发生了内存溢出的现象,只不过发生内存溢出的节点并不会被kill掉,这样能保证运行在这个节点上面没有发生OOM的任务能够顺利运行完成,避免雪崩式的任务失败
但是也会引起一些别的问题。所以presto判断内存不足的条件有点苛刻,并且就算检测到内存不足,presto才会在5分钟后,才去将任务杀死,我们应该让他更早的触发内存保护策略。
即让:poolInfo.getFreeBytes() + poolInfo.getReservedRevocableBytes()
比如小于JVM内存的15% ~ 20%。来早的触发内存保护策略。后续的文章有具体的代码改动详情。