天天看點

核心調優 | 如何提升Elasticsearch master排程性能40倍

作者:興豐__阿裡雲Elasticsearch團隊 進階開發工程師

本文字數:1299

閱讀時間:2~5分鐘

以下是正文

背景

我們在協助某

Elasticsearch

使用者準備将自建叢集遷往阿裡雲Elasticsearch的過程中發現,自建叢集從ES6.3.2版本更新到7.4.0版本後,master變得特别卡,建立索引和删除耗時超過1分鐘。該叢集當時有3個專有主節點、10個熱節點、2個冷節點,超過5萬個shard,絕大部分索引/shard都是關閉的,當索引過期移動到冷節點就close掉,需要查詢時再調用open指令打開。同時,在試過6.x到7.x的多個版本後,發現自7.2.0後的版本都有問題,而即使把專有主節點更新到32c64g的規格,還是不行。

思考

由于是自建的線上生産叢集,登入機器和檢視叢集狀态極為不便,也有一定的風險。是以計劃先從Elasticsearch代碼的變更入手,查找版本7.2的哪一個pull request最有可能改變master的排程行為,很快發現pr#39499引入的“支援對已關閉索引進行shard資料複制“有最大的嫌疑。

在引入pr#39499之前,一個索引關閉之後,資料節點對應的engine會關閉,不再提供查詢和寫入服務,當然也不能進行relocate和資料的拷貝,當下掉一個資料節點後,上面已關閉索引的shard資料就會丢失。

引入pr#39499之後,master會在cluster state中繼續保留關閉的索引,對索引的shard進行排程,資料節點使用NoOpEngine打開索引,不支援查詢和寫入,和正常的engine相比開銷很小。是以就轉化為,叢集狀态存在很多shard時,master排程很慢的問題。

複現

搭建一個最小化測試環境

  • Elasticsearch版本: 7.4.0
  • 專有主節點: 3 * 16c64g
  • 資料節點: 2 * 16c64g

先建立5000個索引,每個索引有5個primary,0個replica,總共25000個shard。然後測試發現每次建立新索引需要58s。master所在機器有一個cpu使用率一直處于100%,通過top -Hp $ES_PID, 得到忙碌的線程ID。通過和jstack擷取master節點的調用棧資訊對比,發現是masterServices線程一直調用shardsWithState導緻的。

"elasticsearch[iZ2ze1ymtwjqspsn3jco0tZ][masterService#updateTask][T#1]" #39 daemon prio=5 os_prio=0 cpu=150732651.74ms elapsed=258053.43s tid=0x00007f7c98012000 nid=0x3006 runnable  [0x00007f7ca28f8000]

  java.lang.Thread.State: RUNNABLE
       at java.util.Collections$UnmodifiableCollection$1.hasNext(java.base@13/Collections.java:1046)
       at org.elasticsearch.cluster.routing.RoutingNode.shardsWithState(RoutingNode.java:148)
       at org.elasticsearch.cluster.routing.allocation.decider.DiskThresholdDecider.sizeOfRelocatingShards(DiskThresholdDecider.java:111)
       at org.elasticsearch.cluster.routing.allocation.decider.DiskThresholdDecider.getDiskUsage(DiskThresholdDecider.java:345)
       at org.elasticsearch.cluster.routing.allocation.decider.DiskThresholdDecider.canRemain(DiskThresholdDecider.java:290)
       at org.elasticsearch.cluster.routing.allocation.decider.AllocationDeciders.canRemain(AllocationDeciders.java:108)
       at org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator$Balancer.decideMove(BalancedShardsAllocator.java:668)
       at org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator$Balancer.moveShards(BalancedShardsAllocator.java:628)
       at org.elasticsearch.cluster.routing.allocation.allocator.BalancedShardsAllocator.allocate(BalancedShardsAllocator.java:123)
       at org.elasticsearch.cluster.routing.allocation.AllocationService.reroute(AllocationService.java:405)
       at org.elasticsearch.cluster.routing.allocation.AllocationService.reroute(AllocationService.java:370)
       at org.elasticsearch.cluster.metadata.MetaDataIndexStateService$1$1.execute(MetaDataIndexStateService.java:168)
       at org.elasticsearch.cluster.ClusterStateUpdateTask.execute(ClusterStateUpdateTask.java:47)
       at org.elasticsearch.cluster.service.MasterService.executeTasks(MasterService.java:702)
       at org.elasticsearch.cluster.service.MasterService.calculateTaskOutputs(MasterService.java:324)
       at org.elasticsearch.cluster.service.MasterService.runTasks(MasterService.java:219)
       at org.elasticsearch.cluster.service.MasterService.access$000(MasterService.java:73)
       at org.elasticsearch.cluster.service.MasterService$Batcher.run(MasterService.java:151)
       at org.elasticsearch.cluster.service.TaskBatcher.runIfNotProcessed(TaskBatcher.java:150)
       at org.elasticsearch.cluster.service.TaskBatcher$BatchedTask.run(TaskBatcher.java:188)
       at org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingRunnable.run(ThreadContext.java:703)
       at org.elasticsearch.common.util.concurrent.PrioritizedEsThreadPoolExecutor$TieBreakingPrioritizedRunnable.runAndClean(PrioritizedEsThreadPoolExecutor.java:252)
       at org.elasticsearch.common.util.concurrent.PrioritizedEsThreadPoolExecutor$TieBreakingPrioritizedRunnable.run(PrioritizedEsThreadPoolExecutor.java:215)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@13/ThreadPoolExecutor.java:1128)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@13/ThreadPoolExecutor.java:628)
       at java.lang.Thread.run(java.base@13/Thread.java:830)           

分析&解決

通過閱讀對應的代碼,發現所有觸發reroute操作的請求,比如建立、删除、更新叢集狀态等,都會調用BalancedShardsAllocator來周遊叢集的所有started shard,再計算shard所在節點被relocated shard的磁盤占用大小。這需要找到節點所有處于INITIALIZING且正在relocated的shard,目前的實作是周遊節點的所有shard。

最外層對shard的周遊是O(n), 内層對每個節點的shard的循環是O(n/m), 其中n是叢集shard總數,m是節點個數,整體複雜度是O(n^2/m),對一個固定節點個數的叢集來說,m可以認為是常數,則每輪reroute的排程複雜度是O(n^2)。

考慮到每次都要重新周遊節點所有shard,尋找處于INITIALIZING和RELOCATING的shard,這部分可以在初始化時算一次,然後在每次shard有狀态變化時,簡單更新下即可,那麼整體複雜度降為O(n)。對ES7.4.0的代碼做了簡單的修改、打包和測試,建立索引等觸發reroute操作的請求時間從之前的58s降為1.2s,ES的hot_threads api和jstack都顯示shardsWithState不再是熱點,效果顯著。

核心調優 | 如何提升Elasticsearch master排程性能40倍

臨時解決方案

master内部使用MasterService類來管理叢集任務管理工作,為了保證狀态的一緻性,任務是單線程串行處理的,是以不能通過提升master節點的機器規格來解決。

目前的ES叢集碰到這個問題,可以通過設定cluster.routing.allocation.disk.include_relocations為false來繞過,讓master排程時不考慮正在relocating的shard磁盤占用。但是這會導緻磁盤使用被錯誤估計,有反複觸發relocate的風險。

相關活動

核心調優 | 如何提升Elasticsearch master排程性能40倍

掃碼關注公衆号‘Elasticsearch技術’,收獲大咖最佳行業應用經驗

核心調優 | 如何提升Elasticsearch master排程性能40倍

往期好文

【産品解讀】阿裡雲 Elasticsearch 在日志場景中實作“低成本高性能” 年度盤點 | “三年磨一劍” 阿裡雲Elasticsearch幹貨手冊 Elasticsearch大咖說|攜程旅行:從日志分析平台到綜合性 Elasticsearch 管理平台

繼續閱讀