1. 程式人生 > >ElasticSearch學習9_ES記憶體那點事

ElasticSearch學習9_ES記憶體那點事

“該給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


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
Filter cache是用來快取使用過的filter的結果集的,需要注意的是這個快取也是常駐heap,無法GC的。我的經驗是預設的10% heap設定工作得夠好了,如果實際使用中heap沒什麼壓力的情況下,才考慮加大這個設定。


Field Data cache
在有大量排序、資料聚合的應用場景,可以說field data cache是效能和穩定性的殺手。 對搜尋結果做排序或者聚合操作,需要將倒排索引裡的資料進行解析,然後進行一次倒排。 這個過程非常耗費時間,因此ES 2.0以前的版本主要依賴這個cache快取已經計算過的資料,提升效能。但是由於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這樣的方式; 對大結果集做深度分頁;一次性拉取全量資料等等。


小結:
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使用情況做持續的監控。

原文來自:

http://elasticsearch.cn/article/32

作者介紹:

wood

主頁是:http://elasticsearch.cn/people/wood

附錄:

ES中文論壇:http://elasticsearch.cn/