Presto集群内存不足时保护机制

为了防止集群里节点OOM,Presto有个循环线程来获取当前集群节点和集群整体内存占用情况。通过这篇文章:Presto内存管理相关参数设置 我们知道Presto里分为RESERVED_POOL和GENERAL_POOL。

判断节点是否阻塞(内存不足):

如果使用RESERVED_POOL(意思是说最大SQL使用这个POOL),那判断集群内存超出内存的方法就是:

1、RESERVED_POOL内存被SQL占用了

2、GENERAL_POOL里有被阻塞的Node

因为RESERVED_POOL会导致内存浪费,我们集群配置参数没有使用这个POOL,只使用了GENERAL_POOL,所以只需要查看下GENERAL_POOL是怎么判断节点是否Block住的。

1
2
3
if (poolInfo.getFreeBytes() + poolInfo.getReservedRevocableBytes() <= 0) {
blockedNodes++;
}

getReservedRevocableBytes 这个是用于获取spill到磁盘的内存,目前我们集群是不允许内存Spill到磁盘的,因为Presto面向的是ad-hoc场景,要求是快,如果说需要spill到磁盘,那spark是一个更好的选择,且早期版本Presto spill到磁盘之前测试过稳定性比较差,场景也比较少。

所以就判断GENERAL_POOL里是否还有剩余内存,如果小于等于0,那就表示该节点是个Block状态。

Kill策略:

把所有query遍历一遍,如果查询有RESOURCE_OVERCOMMIT标志,且内存溢出了,那就把带有此标志的SQL Kill掉。

1
2
3
4
5
6
7
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;
}

如果没有RESOURCE_OVERCOMMIT标志,那就看看内存是否超过集群允许的内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (!resourceOvercommit) {
long userMemoryLimit = min(maxQueryMemory.toBytes(), getQueryMaxMemory(query.getSession()).toBytes());
if (userMemoryReservation > userMemoryLimit) {
query.fail(exceededGlobalUserLimit(succinctBytes(userMemoryLimit)));
queryKilled = true;
}

// enforce global total memory limit if system pool is disabled
long totalMemoryLimit = min(maxQueryTotalMemory.toBytes(), getQueryMaxTotalMemory(query.getSession()).toBytes());
if (!isLegacySystemPoolEnabled && totalMemoryReservation > totalMemoryLimit) {
query.fail(exceededGlobalTotalLimit(succinctBytes(totalMemoryLimit)));
queryKilled = true;
}
}

由上面我们可以知道,想让上述配置生效,query.max-memory需要配置合理的值,比如我们有5台Worker,每台Worker最大允许的内存为10G(超过10GB,每台Worker会自动Kill掉SQL),那此集群允许的最大内存为5*10G = 50GB,如果你配置query.max-memory(maxQueryMemory)100GB,那上面的逻辑将不会走,导致此逻辑失效。

同时,如果发现集群有节点已经OOM了,但是过了5S,依然没有SQL被Kill掉,那就会使用以下触发Presto SQL Kill策略,一共2种策略,query.low-memory-killer.policy指定:

1
2
3
4
5
6
public static class LowMemoryKillerPolicy
{
public static final String NONE = "none";
public static final String TOTAL_RESERVATION = "total-reservation";
public static final String TOTAL_RESERVATION_ON_BLOCKED_NODES = "total-reservation-on-blocked-nodes";
}

total-reservation表示杀掉集群中占有内存最大的SQL,total-reservation-on-blocked-nodes表示杀死在内存不足(阻塞)的节点上使用最多内存的查询。

假如我们通过RESOURCE_GROUP限制集群的并发大小为10,单个节点最大内存为10,集群允许的最大内存少于100GB,那此配置集群肯定不会出现OOM现象(不考虑内存泄露问题),但是这样的话,会导致资源利用率不够,所以线上一般会配置并发数 * 单机最大内存远大于集群允许的最大内存,这就指望LowMemoryKillerPolicy能帮助我们Kill掉内存占用最大的SQL。但是实际上依然会出现Worker OOM情况,此时一般思路:

1、dmesg -T查看系统日志,确定是操作系统触发的OOM还是JVM OOM,如果是操作系统触发的OOM,可能堆外内存或Presto内存泄露会导致这个问题,但是更多情况下需要确定下Presto节点是否有其他服务,一般情况下是其他服务内存突然增大,导致系统内存不够。

2、配置是否正确,是否限制了单机最大内存,是否限制了集群最大内存且小于单机最大内存 * 集群节点数,是否配置了LowMemoryKillerPolicy。

3、如果上面配置都正确,需要考虑2个问题,1)什么原因没有触发LowMemoryKillerPolicy,可以将判断节点阻塞(内存不足)的逻辑变的更严格点,因为不是个实时过程。2)结点是否有内存泄露情况。

从上面可以看出Presto内存管理比较粗暴,内存不够就杀掉SQL,而其他一些引擎做法就比较细腻些,SQL会一直Block住,直到有可用内存为止,不过Presto是面向ad-hoc场景,这种内存管理方法更简单有效吧。