環境
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即可。