es 內存占用優化


對6.3:

修改Elasticsearch中JVM配置文件jvm.options:

Dlog4j2.enable.threadlocals=false



 
        

注: 本文主要針對ES 2.x。

 “該給ES分配多少內存?” 
“JVM參數如何優化?“
“為何我的Heap占用這么高?”
“為何經常有某個field的數據量超出內存限制的異常?“
“為何感覺上沒多少數據,也會經常Out Of Memory?”

以上問題,顯然沒有一個統一的數學公式能夠給出答案。 和數據庫類似,ES對於內存的消耗,和很多因素相關,諸如數據總量、mapping設置、查詢方式、查詢頻度等等。默認的設置雖開箱即用,但不能適用每一種使用場景。作為ES的開發、運維人員,如果不了解ES對內存使用的一些基本原理,就很難針對特有的應用場景,有效的測試、規划和管理集群,從而踩到各種坑,被各種問題挫敗。

要理解ES如何使用內存,先要理解下面兩個基本事實:
1.  ES是JAVA應用
2.  底層存儲引擎是基於Lucene的

看似很普通是嗎?但其實沒多少人真正理解這意味着什么。 

首先,作為一個JAVA應用,就脫離不開JVM和GC。很多人上手ES的時候,對GC一點概念都沒有就去網上抄各種JVM“優化”參數,卻仍然被heap不夠用,內存溢出這樣的問題搞得焦頭爛額。了解JVM GC的概念和基本工作機制是很有必要的,本文不在此做過多探討,讀者可以自行Google相關資料進行學習。如何知道ES heap是否真的有壓力了? 推薦閱讀這篇博客:Understanding Memory Pressure Indicator。 即使對於JVM GC機制不夠熟悉,頭腦里還是需要有這么一個基本概念: 應用層面生成大量長生命周期的對象,是給heap造成壓力的主要原因,例如讀取一大片數據在內存中進行排序,或者在heap內部建cache緩存大量數據。如果GC釋放的空間有限,而應用層面持續大量申請新對象,GC頻度就開始上升,同時會消耗掉很多CPU時間。嚴重時可能惡性循環,導致整個集群停工。因此在使用ES的過程中,要知道哪些設置和操作容易造成以上問題,有針對性的予以規避。

其次,Lucene的倒排索引(Inverted Index)是先在內存里生成,然后定期以段文件(segment file)的形式刷到磁盤的。每個段實際就是一個完整的倒排索引,並且一旦寫到磁盤上就不會做修改。 API層面的文檔更新和刪除實際上是增量寫入的一種特殊文檔,會保存在新的段里。不變的段文件易於被操作系統cache,熱數據幾乎等效於內存訪問。 

基於以上2個基本事實,我們不難理解,為何官方建議的heap size不要超過系統可用內存的一半。heap以外的內存並不會被浪費,操作系統會很開心的利用他們來cache被用讀取過的段文件。

Heap分配多少合適?遵從官方建議就沒錯。 不要超過系統可用內存的一半,並且不要超過32GB。JVM參數呢?對於初級用戶來說,並不需要做特別調整,仍然遵從官方的建議,將xms和xmx設置成和heap一樣大小,避免動態分配heap size就好了。雖然有針對性的調整JVM參數可以帶來些許GC效率的提升,當有一些“壞”用例的時候,這些調整並不會有什么魔法效果幫你減輕heap壓力,甚至可能讓問題更糟糕。

那么,ES的heap是如何被瓜分掉的? 說幾個我知道的內存消耗大戶並分別做解讀:
1.  segment memory
2.  filter cache
3.  field data cache
4.  bulk queue
5.  indexing buffer
6.  state buffer
7.  超大搜索聚合結果集的fetch
8. 對高cardinality字段做terms aggregation


Segment Memory
Segment不是file嗎?segment memory又是什么?前面提到過,一個segment是一個完備的lucene倒排索引,而倒排索引是通過詞典 (Term Dictionary)到文檔列表(Postings List)的映射關系,快速做查詢的。 由於詞典的size會很大,全部裝載到heap里不現實,因此Lucene為詞典做了一層前綴索引(Term Index),這個索引在Lucene4.0以后采用的數據結構是FST (Finite State Transducer)。 這種數據結構占用空間很小,Lucene打開索引的時候將其全量裝載到內存中,加快磁盤上詞典查詢速度的同時減少隨機磁盤訪問次數。

下面是詞典索引和詞典主存儲之間的一個對應關系圖:

lucene_index.png



Lucene  file的完整數據結構參見Apache Lucene - Index File Formats

說了這么多,要傳達的一個意思就是,ES的data node存儲數據並非只是耗費磁盤空間的,為了加速數據的訪問,每個segment都有會一些索引數據駐留在heap里。因此segment越多,瓜分掉的heap也越多,並且這部分heap是無法被GC掉的! 理解這點對於監控和管理集群容量很重要,當一個node的segment memory占用過多的時候,就需要考慮刪除、歸檔數據,或者擴容了。

怎么知道segment memory占用情況呢?  CAT API可以給出答案。
1.  查看一個索引所有segment的memory占用情況:

seg_mem.png



2.  查看一個node上所有segment占用的memory總和:

seg_mem_node.png




那么有哪些途徑減少data node上的segment memory占用呢? 總結起來有三種方法:
1.  刪除不用的索引
2.  關閉索引 (文件仍然存在於磁盤,只是釋放掉內存)。需要的時候可以重新打開。
3.  定期對不再更新的索引做optimize (ES2.0以后更改為force merge api)。這Optimze的實質是對segment file強制做合並,可以節省大量的segment memory。

Filter Cache (5.x里叫做Request cache)
Filter cache是用來緩存使用過的filter的結果集的,需要注意的是這個緩存也是常駐heap,在被evict掉之前,是無法被GC的。我的經驗是默認的10% heap設置工作得夠好了,如果實際使用中heap沒什么壓力的情況下,才考慮加大這個設置。


Field Data cache
在有大量排序、數據聚合的應用場景,可以說field data cache是性能和穩定性的殺手。 對搜索結果做排序或者聚合操作,需要將倒排索引里的數據進行解析,按列構造成docid->value的形式才能夠做后續快速計算。 對於數據量很大的索引,這個構造過程會非常耗費時間,因此ES 2.0以前的版本會將構造好的數據緩存起來,提升性能。但是由於heap空間有限,當遇到用戶對海量數據做計算的時候,就很容易導致heap吃緊,集群頻繁GC,根本無法完成計算過程。 ES2.0以后,正式默認啟用Doc Values特性(1.x需要手動更改mapping開啟),將field data在indexing time構建在磁盤上,經過一系列優化,可以達到比之前采用field data cache機制更好的性能。因此需要限制對field data cache的使用,最好是完全不用,可以極大釋放heap壓力。 需要注意的是,很多同學已經升級到ES2.0,或者1.0里已經設置mapping啟用了doc values,在kibana里仍然會遇到問題。 這里一個陷阱就在於kibana的table panel可以對所有字段排序。 設想如果有一個字段是analyzed過的,而用戶去點擊對應字段的排序表頭是什么后果? 一來排序的結果並不是用戶想要的,排序的對象實際是詞典; 二來analyzed過的字段無法利用doc values,需要裝載到field data cache,數據量很大的情況下可能集群就在忙着GC或者根本出不來結果。


Bulk Queue
一般來說,Bulk queue不會消耗很多的heap,但是見過一些用戶為了提高bulk的速度,客戶端設置了很大的並發量,並且將bulk Queue設置到不可思議的大,比如好幾千。 Bulk Queue是做什么用的?當所有的bulk thread都在忙,無法響應新的bulk request的時候,將request在內存里排列起來,然后慢慢清掉。 這在應對短暫的請求爆發的時候有用,但是如果集群本身索引速度一直跟不上,設置的好幾千的queue都滿了會是什么狀況呢? 取決於一個bulk的數據量大小,乘上queue的大小,heap很有可能就不夠用,內存溢出了。一般來說官方默認的thread pool設置已經能很好的工作了,建議不要隨意去“調優”相關的設置,很多時候都是適得其反的效果。


Indexing Buffer
Indexing Buffer是用來緩存新數據,當其滿了或者refresh/flush interval到了,就會以segment file的形式寫入到磁盤。 這個參數的默認值是10% heap size。根據經驗,這個默認值也能夠很好的工作,應對很大的索引吞吐量。 但有些用戶認為這個buffer越大吞吐量越高,因此見過有用戶將其設置為40%的。到了極端的情況,寫入速度很高的時候,40%都被占用,導致OOM。


Cluster State Buffer
ES被設計成每個node都可以響應用戶的api請求,因此每個node的內存里都包含有一份集群狀態的拷貝。這個cluster state包含諸如集群有多少個node,多少個index,每個index的mapping是什么?有少shard,每個shard的分配情況等等 (ES有各類stats api獲取這類數據)。 在一個規模很大的集群,這個狀態信息可能會非常大的,耗用的內存空間就不可忽視了。並且在ES2.0之前的版本,state的更新是由master node做完以后全量散播到其他結點的。 頻繁的狀態更新就可以給heap帶來很大的壓力。 在超大規模集群的情況下,可以考慮分集群並通過tribe node連接做到對用戶api的透明,這樣可以保證每個集群里的state信息不會膨脹得過大。


超大搜索聚合結果集的fetch
ES是分布式搜索引擎,搜索和聚合計算除了在各個data node並行計算以外,還需要將結果返回給匯總節點進行匯總和排序后再返回。無論是搜索,還是聚合,如果返回結果的size設置過大,都會給heap造成很大的壓力,特別是數據匯聚節點。超大的size多數情況下都是用戶用例不對,比如本來是想計算cardinality,卻用了terms aggregation + size:0這樣的方式; 對大結果集做深度分頁;一次性拉取全量數據等等。
 
對高cardinality字段做terms aggregation
所謂高cardinality,就是該字段的唯一值比較多。 比如client ip,可能存在上千萬甚至上億的不同值。 對這種類型的字段做terms aggregation時,需要在內存里生成海量的分桶,內存需求會非常高。如果內部再嵌套有其他聚合,情況會更糟糕。  在做日志聚合分析時,一個典型的可以引起性能問題的場景,就是對帶有參數的url字段做terms aggregation。 對於訪問量大的網站,帶有參數的url字段cardinality可能會到數億,做一次terms aggregation內存開銷巨大,然而對帶有參數的url字段做聚合通常沒有什么意義。 對於這類問題,可以額外索引一個url_stem字段,這個字段索引剝離掉參數部分的url。可以極大降低內存消耗,提高聚合速度。


小結:

    1. 倒排詞典的索引需要常駐內存,無法GC,需要監控data node上segment memory增長趨勢。
    2. 各類緩存,field cache, filter cache, indexing cache, bulk queue等等,要設置合理的大小,並且要應該根據最壞的情況來看heap是否夠用,也就是各類緩存全部占滿的時候,還有heap空間可以分配給其他任務嗎?避免采用clear cache等“自欺欺人”的方式來釋放內存。
    3. 避免返回大量結果集的搜索與聚合。確實需要大量拉取數據的場景,可以采用scan & scroll api來實現。
    4. cluster stats駐留內存並無法水平擴展,超大規模集群可以考慮分拆成多個集群通過tribe node連接。
    5. 想知道heap夠不夠,必須結合實際應用場景,並對集群的heap使用情況做持續的監控。
    6. 根據監控數據理解內存需求,合理配置各類circuit breaker,將內存溢出風險降低到最低。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM