目錄
背景介紹
JVM知識回顧
ES配置說明回顧
現狀分析
調優實戰
總結與展望
一. 背景介紹
專案中的服務整合了springboot-admin做服務監控,最近一直收到郵件告警,提示es出錯。錯誤資訊如下:
org.elasticsearch.ElasticsearchTimeoutException: java.util.concurrent.TimeoutException: Timeout waiting for task.
複製程式碼
頻繁收到這個告警,所以決定花時間研究一下。從報錯資訊看,併發超時異常。ES作為java開發的中介軟體,我們沒有對任何程式碼做過修改,所以就從JVM開始著手嘗試解決,同時還涉及到部分ES知識和springboot的知識。
二. JVM知識回顧
可參考另一篇學習筆記: 深入理解java虛擬機器
1. JVM記憶體模型
- JVM gc的物件:堆
2. 堆記憶體
2.1 堆記憶體劃分
- 堆區分為新生代和老年代
- 新生代又分為Eden區,from survivor區,to survivor區
- Eden區和兩塊較小的survivor空間。大小比例為8:1:1
- java8已經沒有持久代了,改為後設資料區,主要存放後設資料,例如Class、Method的元資訊,與垃圾回收要回收的Java物件關係不大
2.2 堆記憶體檢視
使用 jstat -gc(-gccapacity, -gcutil)命令檢視堆分配情況
- S0C:survivor0區總記憶體大小(Capacity)
- S1C: survivor1區總記憶體大小
- S0U: survivor0區當前記憶體大小(Used)
- S1U: survivor1區當前記憶體大小
- EC:Eden區總記憶體大小
- EU:Eden區當前記憶體大小
- OC:老年代總記憶體大小
- OU:老年代當前記憶體大小
- MC:meta data區總記憶體大小
- MU:meta data區當前記憶體大小
2.3 記憶體分配和回收策略
2.3.1 分配策略
- 大部分物件建立時,在eden區分配
- 大的物件直接進入老年代,比如很長的字串或陣列。這些物件對垃圾回收不友好。
- 長期存活的物件,將從新生代晉升到老年代
2.3.2 回收策略
- eden區滿:觸發一次minor gc,存活的物件複製到其中一個survivor。物件的年齡+1
- 一個survivor區滿:滿足晉升條件的,進入老年代。不滿足的,複製到另一個survivor區
2.3.3 晉升條件的判斷
- Serial和ParNew GC中通過MaxTenuringThreshold引數設定,預設為15
- Parallel收集器自動調整年齡:survivor空間中相同年齡所有物件大小大於空間的一半,大於等於該年齡的物件就直接進入老年代
2.3 關於堆劃分的思考
2.3.1 大堆和小堆堆程式的影響
- 堆太大:垃圾回收時STW的時間過長,影響程式響應時間。據說ZGC(java11釋出)回收器能解決這個問題。java11中ZGC的介紹
- 堆太小:垃圾回收太頻繁
2.3.2 為什麼要劃分為不同的年代
- 每個物件的生命週期是不一樣的,將不同存活時間的物件劃分到不同的區,然後採用不同的垃圾回收演算法
- java很多物件都是朝生夕死的,這些物件不會進入老年代。
2.3.3 為什麼要有survivor區
- 沒有survivor區,只有eden區的話,每進行一次minor gc,物件就被送入老年代。很容易觸發full gc,影響效能
- survivor存在的目的就是減少送入老年代的物件數量,減少full gc的發生
2.3.4 為什麼要設定兩個survivor區
每次minor gc,通過將eden和一個survivor的內容複製到另一個survivor, 避免碎片化問題
3. 垃圾回收演算法
3.1 標記-清除演算法
- 最基礎的收集演算法
- 分為標記和清除兩個階段
- 不足之處:
- 效率問題
- 產生大量不連續的記憶體碎片
3.2 複製演算法
- 將記憶體分為大小相等的兩塊,每次使用其中的一塊
- 一塊用完時,將存活的物件複製到另一塊
- 現代虛擬機器新生代都用該演算法
- 不足:
- 記憶體利用率不高
3.3 標記-整理演算法
- 物件存活率高時大量的複製會影響效率,老年代使用該演算法
- 標記過程與標記-清除演算法一樣
- 後續步驟並不是清理物件,而是讓所有存活的物件都向一段移動,清理邊界以外的記憶體
3.4 分代收集演算法
- 根據物件存活週期不同,採用不同的收集演算法
- 新生代大量物件死亡,少量存活,採用複製演算法
- 老年代物件存活率高,採用標記-清理或者標記-收集演算法
4. 垃圾回收器
4.1 年代劃分
- 新生代收集器有:Serial,ParNew,Paraller Scavenge
- 老年代收集器有:CMS Serial old,Parallel Old
- G1收集器可作用與新生代和老年代
- 沒有連線的兩個收集器不能共存,比如CMS和Paraller Scavenge
4.2 工作機制劃分
- 序列收集器:Serial,Serial Old,單執行緒的一個回收器,簡單、易實現、效率高
- 並行收集器:ParNew,Serial的多執行緒版,可以充分的利用CPU資源,減少回收的時間
- 吞吐量優先收集器:Parallel Scavenge
- 併發收集器:CMS(Concurrent Mark Sweep),停頓時間少優先,基於“標記-清除”演算法實現。
4.3 其他說明
- java11 新出了一款ZGC收集器,效能比G1更高效(還在實驗階段)
- java5預設採用CMS收集器,java9預設收集器被G1代替
- 使用者可自己指定使用哪種垃圾收集器
- 各個垃圾收集器詳細介紹參考深入理解java虛擬機器
4.4 CMS工作原理
- 不會等到老年代空間快滿了才回收(和使用者執行緒併發,留記憶體給使用者執行緒)。配置引數為-XX:CMSInitiazingOccupanyFraction。預設為75%
- 使用標記-清除演算法。整個過程分為四步:
- 初始標記:STW,標記GC Roots能關聯到的物件,速度很快
- 併發標記:GC Roots Tracing過程。耗時。和使用者執行緒一起執行(並行)
- 重新標記:STW,標記併發標記過程中程式執行導致標記變化的物件,時間比初始標記長,遠比並發標記短
- 併發清除:耗時。和使用者執行緒一起執行(並行)
三. ES配置說明回顧
可參考另外一篇筆記:Elasticsearch學習筆記
主要介紹es官網手冊特別說明的一些注意點
1. 關於配置的說明
1.1 ES使用的垃圾回收器
- 預設為CMS,2.x版本官方推薦不要修改為G1,某些版本JAVA G1存在的Bug,會造成Lucene的段檔案損壞。
- 不過5.x以及之後版本,沒有明確說推薦或不推薦G1,預設還是用的CMS
1.2 ES記憶體分配要求
- 不超過32G。因為每個物件的指標都變長了,就會使用更多的 CPU 記憶體頻寬,也就是說你實際上失去了更多的記憶體。
- 不要超過記憶體的一半,因為Lucene也需要記憶體,且這些記憶體不被JVM管理
- 如果不需要對分詞做聚合運算,可降低堆記憶體。堆記憶體越小,Elasticsearch(更快的 GC)和 Lucene(更多的記憶體用於快取)的效能越好。
2. 關於滾動重啟的說明
- 保證不停叢集功能的情況下逐一對每個節點進行升級或維護
- 先停止索引新的資料
- 禁止分片分配。cluster.routing.allocation.enable" : "none"
curl -XPUT http://{ip}:9200/_cluster/settings -d' { "transient" : { "cluster.routing.allocation.enable" : "none" } }' 複製程式碼
- 關閉單個節點,並執行升級維護
- 啟動節點,並等待加入叢集
- 重啟分片分配。cluster.routing.allocation.enable" : "all"
curl -XPUT http://{ip}:9200/_cluster/settings -d' { "transient" : { "cluster.routing.allocation.enable" : "all" } }' 複製程式碼
- 對其他節點重複以上步驟
- 恢復索引更新資料
四. 現狀分析
1. 版本及硬體情況介紹
- java:1.8.0_131
- elasticsearch:5.5.1
- es叢集:4個資料節點
- os: centos7 24核 128G
- 垃圾回收器:老年代(CMS)+ 新生代(ParNew)
2. 目前堆分配情況
要針對jvm調優,必不可少的是先檢視堆記憶體狀況,有以下幾種檢視方法
2.1 jstat -gc命令檢視堆分配情況
2.2 統計ES各個節點堆分配資訊
節點 | 堆總大小 | 新生代 | survivor | eden | 老年代 | 後設資料區 |
---|---|---|---|---|---|---|
節點A | 32G | 1.46G | 0.146G | 1.16G | 30.5G | 81M |
節點B | 32G | 1.46G | 0.146G | 1.16G | 30.5G | 85M |
節點C | 32G | 1.46G | 0.146G | 1.16G | 30.5G | 81M |
節點D | 20G | 1.46G | 0.146G | 1.16G | 18.5G | 76M |
3. 監控工具對比
工具名稱 | 各分割槽情況 | 資料是否直觀 | 是否可檢視歷史資料 | 是否免費 | 備註 |
---|---|---|---|---|---|
jstat | 是 | 否 | 否 | 是 | 主要用於檢視各分割槽大小 |
ElasticHQ | 否 | 是 | 否 | 是 | 主要用於瀏覽es整體資訊 |
cerebro | 否 | 是 | 否 | 是 | 主要用於瀏覽es整體資訊 |
x-pack | 否 | 是 | 是 | 試用期一年 | 試用期到相關功能不可用,不影響現有功能。6.3版本x-pack已經開源,後續版本可能會免費 |
- 由於線上報異常郵件的時間是不確定的,不可能隨時盯著監控皮膚看,所有必須有檢視歷史資料的功能,因此x-pack是我們監控的首選工具
- x-pack監控功能只是其中之一,但是真的非常強大,強烈推薦!!同時期待ES官方儘快使之免費
- 網上有破解x-pack的方法,將jar包反編譯之後修改程式碼,再打包回去,還沒做嘗試。
x-pack安裝過程的一些小問題總結
第一步:證照申請
- register.elastic.co/marvel_regi…
- 填寫資訊後,得到一個json檔案,匯入到es中, 有效期一年。
curl -XPUT 'http://{ip}:9200/_xpack/license?acknowledge=true' -H "Content-Type: application/json" -d @sivabalan-nagarajan-2327c0fa-f56b-443a-a3d6-abef7ecf2220-v5.json
複製程式碼
第二步:安裝x-pack外掛, 包括es和kibana
./bin/kibana-plugin install x-pack 安裝很慢,先把檔案下載下來,用下一個命令安裝
./bin/elasticsearch-plugin install file:///home/breakpad/softs/x-pack-5.5.1.zip
複製程式碼
第三步:修改配置檔案
es配置檔案裡,只啟用監控功能
xpack.security.enabled: false
xpack.monitoring.enabled: true
xpack.graph.enabled: false
xpack.watcher.enabled: false
複製程式碼
kibana配置檔案裡,只啟用監控功能
xpack.security.enabled: false
xpack.monitoring.enabled: true
xpack.graph.enabled: false
xpack.reporting.enabled: false
複製程式碼
報錯問題解決
rpm安裝後,systemctl方式啟動kibana報許可權不足的問題?
- 解除安裝x-pack,以kibana使用者去安裝 sudo -u kibana bin/kibana-plugin install file:///usr/share/kibana/x-pack-5.5.1.zip
- 還是報許可權錯誤,修改報錯的檔案許可權都為kibana
- 安裝包許可權報錯,修改安裝包許可權為kibana
kibana啟動後網頁打不開怎麼解決?
- 在config/kibana.yml裡配置日誌路徑:logging.dest: /var/log/kibana.log
- 修改日誌許可權 touch /var/log/kibana.log chown kibana:kibana /var/log/kibana.log
- 日誌也沒有錯,但是瀏覽器就是打不開。最後無意間換了個瀏覽器竟然正常了,再重新把之前打不開的chrome瀏覽器升級之後,也能正常開啟了!!
4. x-pack監控情況分析(以節點B,週期為7天為例)
曲線中每一天大概24個點,即計算的是每個小時的資料
4.1 gc次數:平均值為250次/h 左右
4.2 minor gc耗時:平均值為10000ms/h
4.3 full gc後:剩餘堆大小:4.2G,兩次full gc的時間分別為2.224s,2.438s
5. 觀察到的現象
- 新生代和老年代分配比例不合理,新生代太小,老年代太大
- 網上很多文章指出新生代和老年代的預設比例為1:2,但是通過觀察發現並不是這樣(我們的機器上約是1:20)。
- 具體原因在網上目前只找到這樣一篇文章有過說明。CMS預設新生代是多大?
- 大致就是:取預設NewRatio計算出來的值和另外一個公式計算出來的值對比,取小的那一個
- 計算公式為:計算機核數*某個引數(64M)*13/10。我們的機器算出來的值為2G,勉強符合這個說法。
- 新生代垃圾回收頻繁
- 老年代收集後:有效記憶體只達4G左右(活躍資料)
6. 針對觀察到現象的初步分析和解決
- 新生代太小:導致minior gc回收頻繁,可適當加大新生代大小
- 老年代太大:導致major gc或full gc回收時間過長,可適當減少老年代大小
- 如何確定新生代老年代大小:根據美團gc優化實戰文章所述:
- 總大小:3-4倍活躍資料大小:
- 節點B為4.2G*4,我們設定為20G。
- 其他機器大概為3.6G*4,我們設定為16G
- 新生代:1-1.5倍活躍資料大小
- 老年代:總大小-新生代。綜上,我們設定新生代:老年代=1:4
- 總大小:3-4倍活躍資料大小:
五. 調優實戰
通過以上分析發現的問題,然後嘗試調整引數,並且觀察調整後的監控結果,驗證我們的推測是否正確。
1. 修改配置引數
- 檔案為{es_home}/config/jvm.options
2. 修改後的堆配置引數
節點 | 堆總大小 | 新生代 | survivor | eden | 老年代 | 後設資料區 |
---|---|---|---|---|---|---|
節點A | 16G | 3.2G | 320M | 2.56G | 12.8G | 77M |
節點B | 20G | 4G | 400M | 3.2G | 16G | 66M |
節點C | 16G | 3.2G | 320M | 2.56G | 12.8G | 77M |
節點D | 16G | 3.2G | 320M | 2.56G | 12.8G | 77M |
3. 修改後的監控資訊
3.1 gc次數整體呈下降趨勢
3.2 gc耗時整體呈下降趨勢,full gc時間大致在900ms左右
4. springboot引數的調整
- 通過對JVM引數調整後,發現還是有不定時報警的情況。於是研究了一下springboot-admin的原理,它是基於springboot-actuator做了封裝,然後看了一下springboot-actuator的原理,找到了其中一個很重要的引數:
management.health.elasticsearch.response-timeout 複製程式碼
- 該參數列示監控程式向es叢集傳送心跳,允許最大的響應時間的多少? 聰明的你應該明白了,如果傳送心跳的時候,es的JVM正在執行垃圾回收,STW導致響應遲遲得不到回覆,就會收到郵件告警。它的預設值是100ms,所以必須將該值設定為至少超過minior gc的時間。
- 但是,如果傳送心跳的時候,恰好JVM正在執行full gc,因為STW的時間一般比較長,所以你必然會收到告警郵件,除非你把response-timeout的值設定為比full gc的時間還長
- 綜上分析,需要根據垃圾回收的時間,給該值設定合理的值。
六. 總結與展望
1. 最終優化總結
1.1 關於JVM的調優
- 減少ES節點分配給JVM的堆大小
- 調整新生代和老年代的比例
1.2 關於springboot的調優
- 加大springboot-actuator針對Elasticsearch健康檢查時的響應時間(預設為100ms)
2. 展望
- 業務優化:此次優化僅僅從JVM的角度做了引數調整,es官方文件其實給出很多高效使用es的方法。後續優化可以從業務的角度去分析,包括:
- 很多不需要分詞的欄位,都沒有做配置,預設都分詞了。特別影響效能。
- 很多查詢沒有用filter,用了query。無法快取。
- 同時,期待將來ZGC投入使用,同時ES很好的相容ZGC,或許那時就不需要任何調優了(不管多大的堆,gc時間在10ms內)。
七. 參考
- 《深入理解java虛擬機器》
- 《elasticsearch權威指南》
- 美團gc優化實戰