环境
ES
version:2.3.0
部署:Master Node+Data Node
配置:3台 master为8C8G,data node为8C16G
备注:此问题与JDK、操作系统无关,因此就没列详细信息
问题描述
ES集群每天动态创建500+索引,索引保留三天,三天后清掉。近期,Master出现两次OOM,且内存一直不会释放。查看dump文件如下:
(图1)
(图2)
定位一阶段
从dump文件发现InternalClusterService中变量updateTaskPerExecutor持有800M内存,
updateTaskPerExecutor的定义
private final Map<ClusterStateTaskExecutor, List<UpdateTask>> updateTasksPerExecutor = new HashMap<>();
添加操作:
innerSubmitStateUpdateTask中以ClusterStateTaskExecutor为key添加List<UpdateTask>到HashMap中
private <T> void innerSubmitStateUpdateTask(...) {
try {
//new 新的updateTask
synchronized (updateTasksPerExecutor) {
if (updateTasksForExecutor == null) {
//创建新的ArrayList
updateTasksPerExecutor.put(executor, new ArrayList<UpdateTask>());
}
//只有此处会添加数据到updateTasksPerExecutor
updateTasksPerExecutor.get(executor).add(updateTask);
}
//timeout不为空,延迟执行updateTask
if (config.timeout() != null) {
updateTasksExecutor.execute(...)
} else {
//在线程池(中有一个线程)中立即执行updateTask
updateTasksExecutor.execute(updateTask);
}
} catch (EsRejectedExecutionException e) {
}
}
删除操作:
Master启动了单线程
//只有一个线程的线程池
private volatile PrioritizedEsThreadPoolExecutor updateTasksExecutor;
在updateTask的runTaskForExecutor中删除List<UpdateTask>,
void runTasksForExecutor(ClusterStateTaskExecutor<T> executor) {
synchronized (updateTasksPerExecutor) {
//按executor删除List<UpdatTask>
List<UpdateTask> pending = updateTasksPerExecutor.remove(executor);
...
}
...
}
思考:
代码看没有问题,有释放内存的地方,难道是线程堵塞了,或者死锁了?
(图3)
很显然线程没有堵塞,也没有死锁。难道是一个线程处理不过来,updateTask任务积压,处理不过来?但为什么内存会持续增加,full gc也没用呢?从dump文件中看Task是积压了,但是晚上没有业务的,积压的任务总会做完,但为什么内存一直不释放呢?
定位二阶段
没把握问题,肯定是细节忽略了。继续看dump文件。图2中显示updateTaskPerExecutor中很多ClusterState的transitive reference引用,占用内存的是 RoutingTable,而且每个ClusterState和其中的RoutingTable的内存地址均不一样。每个ClusterState对象大小差不多。为什么会有这么多ClusterState对象呢?
(图4)
先看InternalClusterService中对ClusterState的定义
//volatile保证线程之间的内存可见性,总是能看到新的clusterState
private volatile ClusterState clusterState;
每个线程都能看到新的clusterState,那更不应该有问题了。
再看新、老clusterState更新
void runTasksForExecutor(ClusterStateTaskExecutor<T> executor) {
...
// update the current cluster state
clusterState = newClusterState;
...
}
还是没有头绪,感觉不应该有问题
定位三阶段
UpdateTask介绍
Master节点的对create索引、删除索引、修改mapping等操作最终都包装成一个UpdateTask添加到updateTaskPerExecutor中由updateTasksExecutor异步执行。
TransportMasterNodeAction类是所有操作类型的父类,主要完成master节点判断请求和处理失败重试功能。实现了doExecute方法,新启动一个异步单线程(异步响应,不占用接收request线程),如果本节点是master节点,则启动执行masterOperation;如果不是,则发送给mastr节点执行,执行抽象方法masterOperation由子类实现;另外通过ClusterStateObserver.waitForNextChange完成了错误重试功能。ClusterStateObserver类,集群状态监控器。每次动作都会创建一个AsyncSingleAction和ClusterStateObserver实例。
分析
分析其中一个clusterState对象的incoming reference
(图5)
注意:每个clusterStateObserver代表一次Master的操作,图5所示,引用关系很复杂。
问题原因
条件:当对Master有频繁的操作时,导致UpdateTask在updateTaskPerExecutor中积压
1. 由于InternalClusterService中定义的clusterState实例对外的引用关系太复杂了,同时clusterState变更保护不够,仅仅是加了一个volatile,很容易产生ABA问题,最终导致就算clusterState变更后,老的clustertState不会被释放
2. ClusterStateObserver中定义的
final AtomicReference<ObservedState> lastObservedState;
还会持有老的引用。最终clusterState是哪个版本只有天知道。
3. 每次创建新的clusterState都会新建一份RoutingTable,
RoutingTable(long version, Map<String, IndexRoutingTable> indicesRouting) {
this.version = version;
//copy一份,这也解释上面看到大小差不多
this.indicesRouting = ImmutableMap.copyOf(indicesRouting);
}
持续分析
ES的高版本是否存在这问题?查看github在5.x解决了这问题
问题:https://github.com/elastic/elasticsearch/issues/21439
问题原因官方解说:https://github.com/elastic/elasticsearch/issues/21568
问题解决:
https://github.com/elastic/elasticsearch/pull/21578
清掉和多线程只是缓解
https://github.com/elastic/elasticsearch/pull/21631,这个改造很有基本上解决了这个问题是,观察者根本不需要观察clusterState本身,而是观察masterid和version即可。