Elasticsearch 技術分析(七): Elasticsearch 的性能優化

硬件選擇

Elasticsearch(後文簡稱 ES)的基礎是 Lucene,全部的索引和文檔數據是存儲在本地的磁盤中,具體的路徑可在 ES 的配置文件../config/elasticsearch.yml中配置,以下:html

# ----------------------------------- Paths ------------------------------------
#
# Path to directory where to store the data (separate multiple locations by comma):
#
path.data: /path/to/data
#
# Path to log files:
#
path.logs: /path/to/logs

磁盤在現代服務器上一般都是瓶頸。Elasticsearch 重度使用磁盤,你的磁盤能處理的吞吐量越大,你的節點就越穩定。這裏有一些優化磁盤 I/O 的技巧:node

  • 使用 SSD。就像其餘地方提過的, 他們比機械磁盤優秀多了。
  • 使用 RAID 0。條帶化 RAID 會提升磁盤 I/O,代價顯然就是當一塊硬盤故障時整個就故障了。不要使用鏡像或者奇偶校驗 RAID 由於副本已經提供了這個功能。
  • 另外,使用多塊硬盤,並容許 Elasticsearch 經過多個 path.data 目錄配置把數據條帶化分配到它們上面。
  • 不要使用遠程掛載的存儲,好比 NFS 或者 SMB/CIFS。這個引入的延遲對性能來講徹底是背道而馳的。
  • 若是你用的是 EC2,小心 EBS。即使是基於 SSD 的 EBS,一般也比本地實例的存儲要慢。

內部壓縮

硬件資源比較昂貴,通常不會花大成本去購置這些,可控的解決方案仍是須要從軟件方面來實現性能優化提高。算法

其實,對於一個分佈式、可擴展、支持PB級別數據、實時的搜索與數據分析引擎,ES 自己對於索引數據和文檔數據的存儲方面內部作了不少優化,具體體如今對數據的壓縮,那麼是如何壓縮的呢?介紹前先要說明下 Postings lists 的概念。json

倒排列表 - postings list

搜索引擎一項很重要的工做就是高效的壓縮和快速的解壓縮一系列有序的整數列表。咱們都知道,Elasticsearch 基於 Lucene,一個 Lucene 索引 咱們在 Elasticsearch 稱做 分片 , 而且引入了 按段搜索 的概念。bootstrap

新的文檔首先被添加到內存索引緩存中,而後寫入到一個基於磁盤的段。在每一個 segment 內文檔都會有一個 0 到文檔個數之間的標識符(最高值 2^31 -1),稱之爲 doc ID。這在概念上相似於數組中的索引:它自己不作存儲,但足以識別每一個item 數據。數組

Segments 按順序存儲有關文檔的數據,在一個Segments 中 doc ID 是 文檔的索引。所以,segment 中的第一個文檔的 doc ID 爲0,第二個爲1,等等。直到最後一個文檔,其 doc ID 等於 segment 中文檔的總數減1。緩存

那麼這些 doc ID 有什麼用呢?倒排索引須要將 terms 映射到包含該單詞 (term) 的文檔列表,這樣的映射列表咱們稱之爲:倒排列表(postings list)。具體某一條映射數據稱之爲:倒排索引項(Posting)性能優化

舉個例子,文檔和詞條之間的關係以下圖所示,右邊的關係表即爲倒排列表:服務器

倒排列表 用來記錄有哪些文檔包含了某個單詞(Term)。通常在文檔集合裏會有不少文檔包含某個單詞,每一個文檔會記錄文檔編號(doc ID),單詞在這個文檔中出現的次數(TF)及單詞在文檔中哪些位置出現過等信息,這樣與一個文檔相關的信息被稱作 倒排索引項(Posting),包含這個單詞的一系列倒排索引項造成了列表結構,這就是某個單詞對應的 倒排列表網絡

Frame Of Reference

瞭解了分詞(Term)和文檔(Document)之間的映射關係後,爲了高效的計算交集和並集,咱們須要倒排列表(postings lists)是有序的,這樣方便咱們壓縮和解壓縮。

針對倒排列表,Lucene 採用一種增量編碼的方式將一系列 ID 進行壓縮存儲,即稱爲Frame Of Reference的壓縮方式(FOR),自Lucene 4.1以來一直在使用。

在實際的搜索引擎系統中,並不存儲倒排索引項中的實際文檔編號(Doc ID),而是代之以文檔編號差值(D-Gap)。文檔編號差值是倒排列表中相鄰的兩個倒排索引項文檔編號的差值,通常在索引構建過程當中,能夠保證倒排列表中後面出現的文檔編號大於以前出現的文檔編號,因此文檔編號差值老是大於0的整數。

以下圖所示的例子中,原始的 3個文檔編號分別是18七、196和199,經過編號差值計算,在實際存儲的時候就轉化成了:18七、九、3。

之因此要對文檔編號進行差值計算,主要緣由是爲了更好地對數據進行壓縮,原始文檔編號通常都是大數值,經過差值計算,就有效地將大數值轉換爲了小數值,而這有助於增長數據的壓縮率。

好比一個詞對應的文檔ID 列表 [73, 300, 302, 332,343, 372] ,ID列表首先要從小到大排好序;

  • 第一步: 增量編碼就是從第二個數開始每一個數存儲與前一個id的差值,即300-73=227302-300=2,...,一直到最後一個數。
  • 第二步: 就是將這些差值放到不一樣的區塊,Lucene使用256個區塊,下面示例爲了方便展現使用了3個區塊,即每3個數一組。
  • 第三步: 位壓縮,計算每組3個數中最大的那個數須要佔用bit位數,好比30、十一、29中最大數30最小須要5個bit位存儲,這樣十一、29也用5個bit位存儲,這樣才佔用15個bit,不到2個字節,壓縮效果很好。

以下面原理圖所示,這是一個區塊大小爲3的示例(其實是256):

考慮到頻繁出現的term(所謂low cardinality的值),好比gender裏的男或者女。若是有1百萬個文檔,那麼性別爲男的 posting list 裏就會有50萬個int值。用 Frame of Reference 編碼進行壓縮能夠極大減小磁盤佔用。這個優化對於減小索引尺寸有很是重要的意義。

由於這個 FOR 的編碼是有解壓縮成本的。利用skip list(跳錶),除了跳過了遍歷的成本,也跳過了解壓縮這些壓縮過的block的過程,從而節省了cpu。

Roaring bitmaps (RBM)

在 elasticsearch 中使用filters 優化查詢,filter查詢只處理文檔是否匹配與否,不涉及文檔評分操做,查詢的結果能夠被緩存。具體的 Filter 和Query 的異同讀者能夠自行網上查閱資料。

對於filter 查詢,elasticsearch 提供了Filter cache 這種特殊的緩存,filter cache 用來存儲 filters 獲得的結果集。緩存 filters 不須要太多的內存,它只保留一種信息,即哪些文檔與filter相匹配。同時它能夠由其它的查詢複用,極大地提高了查詢的性能。

Frame Of Reference 壓縮算法對於倒排表來講效果很好,但對於須要存儲在內存中的 Filter cache 等不太合適。

倒排表和Filter cache二者之間有不少不一樣之處:

  • 倒排表存儲在磁盤,針對每一個詞都須要進行編碼,而Filter等內存緩存只會存儲那些常用的數據。
  • 針對Filter數據的緩存就是爲了加速處理效率,對壓縮算法要求更高。

這就產生了下面針對內存緩存數據能夠進行高效壓縮解壓和邏輯運算的roaring bitmaps算法。

說到Roaring bitmaps,就必須先從bitmap提及。Bitmap是一種數據結構,假設有某個posting list:

[3,1,4,7,8]

對應的Bitmap就是:

[0,1,0,1,1,0,0,1,1]

很是直觀,用0/1表示某個值是否存在,好比8這個值就對應第8位,對應的bit值是1,這樣用一個字節就能夠表明8個文檔id(1B = 8bit),舊版本(5.0以前)的Lucene就是用這樣的方式來壓縮的。但這樣的壓縮方式仍然不夠高效,Bitmap自身就有壓縮的特色,其用一個byte就能夠表明8個文檔,因此100萬個文檔只須要12.5萬個byte。可是考慮到文檔可能有數十億之多,在內存裏保存Bitmap仍然是很奢侈的事情。並且對於個每個filter都要消耗一個Bitmap,好比age=18緩存起來的話是一個Bitmap,18<=age<25是另一個filter緩存起來也要一個Bitmap。

Bitmap的缺點是存儲空間隨着文檔個數線性增加,因此祕訣就在於須要有一個數據結構打破這個魔咒,那麼就必定要用到某些指數特性:

  • 能夠很壓縮地保存上億個bit表明對應的文檔是否匹配filter;
  • 這個壓縮的Bitmap仍然能夠很快地進行AND和 OR的邏輯操做。

Lucene使用的這個數據結構叫作 Roaring Bitmap,即位圖壓縮算法,簡稱BMP

其壓縮的思路其實很簡單。與其保存100個0,佔用100個bit。還不如保存0一次,而後聲明這個0重複了100遍。

這兩種合併使用索引的方式都有其用途。Elasticsearch 對其性能有詳細的對比,可閱讀 Frame of Reference and Roaring Bitmaps

分片策略

合理設置分片數

建立索引的時候,咱們須要預分配 ES 集羣的分片數和副本數,即便是單機狀況下。若是沒有在 mapping 文件中指定,那麼索引在默認狀況下會被分配5個主分片和每一個主分片的1個副本。

分片和副本的設計爲 ES 提供了支持分佈式和故障轉移的特性,但並不意味着分片和副本是能夠無限分配的。並且索引的分片完成分配後因爲索引的路由機制,咱們是不能從新修改分片數的。

例如某個創業公司初始用戶的索引 t_user 分片數爲2,可是隨着業務的發展用戶的數據量迅速增加,這時咱們是不能從新將索引 t_user 的分片數增長爲3或者更大的數。

可能有人會說,我不知道這個索引未來會變得多大,而且事後我也不能更改索引的大小,因此爲了保險起見,仍是給它設爲 1000 個分片吧…

一個分片並非沒有代價的。須要瞭解:

  • 一個分片的底層即爲一個 Lucene 索引,會消耗必定文件句柄、內存、以及 CPU 運轉。
  • 每個搜索請求都須要命中索引中的每個分片,若是每個分片都處於不一樣的節點還好, 但若是多個分片都須要在同一個節點上競爭使用相同的資源就有些糟糕了。
  • 用於計算相關度的詞項統計信息是基於分片的。若是有許多分片,每個都只有不多的數據會致使很低的相關度。

適當的預分配是好的。但上千個分片就有些糟糕。咱們很難去定義分片是否過多了,這取決於它們的大小以及如何去使用它們。 一百個分片但不多使用還好,兩個分片但很是頻繁地使用有可能就有點多了。 監控你的節點保證它們留有足夠的空閒資源來處理一些特殊狀況。

一個業務索引具體須要分配多少分片可能須要架構師和技術人員對業務的增加有個預先的判斷,橫向擴展應當分階段進行。爲下一階段準備好足夠的資源。 只有當你進入到下一個階段,你纔有時間思考須要做出哪些改變來達到這個階段。

通常來講,咱們遵循一些原則:

  1. 控制每一個分片佔用的硬盤容量不超過ES的最大JVM的堆空間設置(通常設置不超過32G,參考下文的JVM設置原則),所以,若是索引的總容量在500G左右,那分片大小在16個左右便可;固然,最好同時考慮原則2。

  2. 考慮一下node數量,通常一個節點有時候就是一臺物理機,若是分片數過多,大大超過了節點數,極可能會致使一個節點上存在多個分片,一旦該節點故障,即便保持了1個以上的副本,一樣有可能會致使數據丟失,集羣沒法恢復。因此, 通常都設置分片數不超過節點數的3倍。

  3. 主分片,副本和節點最大數之間數量,咱們分配的時候能夠參考如下關係:

    節點數<=主分片數*(副本數+1)

建立索引的時候須要控制分片分配行爲,合理分配分片,若是後期索引所對應的數據愈來愈多,咱們還能夠經過索引別名等其餘方式解決。

調整分片分配器的類型

以上是在建立每一個索引的時候須要考慮的優化方法,然而在索引已建立好的前提下,是否就是沒有辦法從分片的角度提升了性能了呢?固然不是,首先能作的是調整分片分配器的類型,具體是在 elasticsearch.yml 中設置cluster.routing.allocation.type 屬性,共有兩種分片器even_shardbalanced(默認)

even_shard 是儘可能保證每一個節點都具備相同數量的分片,balanced 是基於可控制的權重進行分配,相對於前一個分配器,它更暴漏了一些參數而引入調整分配過程的能力。

每次ES的分片調整都是在ES上的數據分佈發生了變化的時候進行的,最有表明性的就是有新的數據節點加入了集羣的時候。固然調整分片的時機並非由某個閾值觸發的,ES內置十一個裁決者來決定是否觸發分片調整,這裏暫不贅述。另外,這些分配部署策略都是能夠在運行時更新的,更多配置分片的屬性也請你們自行查閱網上資料。

推遲分片分配

對於節點瞬時中斷的問題,默認狀況,集羣會等待一分鐘來查看節點是否會從新加入,若是這個節點在此期間從新加入,從新加入的節點會保持其現有的分片數據,不會觸發新的分片分配。這樣就能夠減小 ES 在自動再平衡可用分片時所帶來的極大開銷。

經過修改參數 delayed_timeout ,能夠延長再均衡的時間,能夠全局設置也能夠在索引級別進行修改:

PUT /_all/_settings 
{
  "settings": {
    "index.unassigned.node_left.delayed_timeout": "5m" 
  }
}

經過使用 _all 索引名,咱們能夠爲集羣裏面的全部的索引使用這個參數,默認時間被延長成了 5 分鐘。

這個配置是動態的,能夠在運行時進行修改。若是你但願分片當即分配而不想等待,你能夠設置參數: delayed_timeout: 0

延遲分配不會阻止副本被提拔爲主分片。集羣仍是會進行必要的提拔來讓集羣回到 yellow 狀態。缺失副本的重建是惟一被延遲的過程。

索引優化

Mapping建模

  1. 儘可能避免使用nested或 parent/child,能不用就不用;

    nested query慢, parent/child query 更慢,比nested query慢上百倍;所以能在mapping設計階段搞定的(大寬表設計或採用比較smart的數據結構),就不要用父子關係的mapping。

  2. 若是必定要使用nested fields,保證nested fields字段不能過多,目前ES默認限制是50。參考:

    index.mapping.nested_fields.limit :50

    由於針對1個document, 每個nested field, 都會生成一個獨立的document, 這將使Doc數量劇增,影響查詢效率,尤爲是Join的效率。

  3. 避免使用動態值做字段(key),動態遞增的mapping,會致使集羣崩潰;一樣,也須要控制字段的數量,業務中不使用的字段,就不要索引。

    控制索引的字段數量、mapping深度、索引字段的類型,對於ES的性能優化是重中之重。如下是ES關於字段數、mapping深度的一些默認設置:

    index.mapping.nested_objects.limit :10000
    	index.mapping.total_fields.limit:1000
    	index.mapping.depth.limit: 20
  4. 不須要作模糊檢索的字段使用 keyword類型代替 text 類型,這樣能夠避免在創建索引前對這些文本進行分詞。

  5. 對於那些不須要聚合和排序的索引字段禁用Doc values。

    Doc Values 默認對全部字段啓用,除了 analyzed strings。也就是說全部的數字、地理座標、日期、IP 和不分析( not_analyzed )字符類型都會默認開啓。

    由於 Doc Values 默認啓用,也就是說ES對你數據集裏面的大多數字段均可以進行聚合和排序操做。可是若是你知道你永遠也不會對某些字段進行聚合、排序或是使用腳本操做, 儘管這並不常見,這時你能夠經過禁用特定字段的 Doc Values 。這樣不只節省磁盤空間,也會提高索引的速度。

    要禁用 Doc Values ,在字段的映射(mapping)設置 doc_values: false 便可。

索引設置

  1. 若是你的搜索結果不須要近實時的準確度,考慮把每一個索引的 index.refresh_interval 改到 30s或者更大。 若是你是在作大批量導入,設置 refresh_interval 爲-1,同時設置 number_of_replicas 爲0,經過關閉 refresh 間隔週期,同時不設置副原本提升寫性能。

    文檔在複製的時候,整個文檔內容都被髮往副本節點,而後逐字的把索引過程重複一遍。這意味着每一個副本也會執行分析、索引以及可能的合併過程。

    相反,若是你的索引是零副本,而後在寫入完成後再開啓副本,恢復過程本質上只是一個字節到字節的網絡傳輸。相比重複索引過程,這個算是至關高效的了。

  2. 修改 index_buffer_size 的設置,能夠設置成百分數,也可設置成具體的大小,最多給512M,大於這個值會觸發refresh。默認值是JVM的內存10%,可是是全部切片共享大小。可根據集羣的規模作不一樣的設置測試。

    indices.memory.index_buffer_size:10%(默認)
    	indices.memory.min_index_buffer_size: 48mb(默認)
    	indices.memory.max_index_buffer_size
  3. 修改 translog 相關的設置:

  • a. 控制數據從內存到硬盤的操做頻率,以減小硬盤IO。可將 sync_interval 的時間設置大一些。
    index.translog.sync_interval:5s(默認)。
  • b. 控制 tranlog 數據塊的大小,達到 threshold 大小時,纔會 flush 到 lucene 索引文件。
    index.translog.flush_threshold_size:512mb(默認)
  1. _id字段的使用,應儘量避免自定義_id, 以免針對ID的版本管理;建議使用ES的默認ID生成策略或使用數字類型ID作爲主鍵,包括零填充序列 ID、UUID-1 和納秒;這些 ID 都是有一致的,壓縮良好的序列模式。相反的,像 UUID-4 這樣的 ID,本質上是隨機的,壓縮比很低,會明顯拖慢 Lucene。

  2. _all 字段及 _source 字段的使用,應該注意場景和須要,_all字段包含了全部的索引字段,方便作全文檢索,若是無此需求,能夠禁用;_source存儲了原始的document內容,若是沒有獲取原始文檔數據的需求,可經過設置includes、excludes 屬性來定義放入_source的字段。

  3. 合理的配置使用index屬性,analyzed 和not_analyzed,根據業務需求來控制字段是否分詞或不分詞。只有 groupby需求的字段,配置時就設置成not_analyzed, 以提升查詢或聚類的效率。

查詢效率

  1. 使用批量請求,批量索引的效率確定比單條索引的效率要高。

  2. query_stringmulti_match 的查詢字段越多, 查詢越慢。能夠在 mapping 階段,利用 copy_to 屬性將多字段的值索引到一個新字段,multi_match時,用新的字段查詢。

  3. 日期字段的查詢, 尤爲是用now 的查詢其實是不存在緩存的,所以, 能夠從業務的角度來考慮是否必定要用now, 畢竟利用 query cache 是可以大大提升查詢效率的。

  4. 查詢結果集的大小不能隨意設置成大得離譜的值, 如query.setSize不能設置成 Integer.MAX_VALUE, 由於ES內部須要創建一個數據結構來放指定大小的結果集數據。

  5. 儘可能避免使用 script,萬不得已須要使用的話,選擇painless & experssions 引擎。一旦使用 script 查詢,必定要注意控制返回,千萬不要有死循環(以下錯誤的例子),由於ES沒有腳本運行的超時控制,只要當前的腳本沒執行完,該查詢會一直阻塞。如:

    {
    	    「script_fields」:{
    	        「test1」:{
    	            「lang」:「groovy」,
    	            「script」:「while(true){print 'don’t use script'}」
    	        }
    	    }
    	}
  6. 避免層級過深的聚合查詢, 層級過深的group by , 會致使內存、CPU消耗,建議在服務層經過程序來組裝業務,也能夠經過pipeline 的方式來優化。

  7. 複用預索引數據方式來提升 AGG 性能:

    如經過 terms aggregations 替代 range aggregations, 如要根據年齡來分組,分組目標是: 少年(14歲如下) 青年(14-28) 中年(29-50) 老年(51以上), 能夠在索引的時候設置一個age_group字段,預先將數據進行分類。從而不用按age來作range aggregations, 經過age_group字段就能夠了。

  8. Cache的設置及使用:

    **a) QueryCache: **ES查詢的時候,使用filter查詢會使用query cache, 若是業務場景中的過濾查詢比較多,建議將querycache設置大一些,以提升查詢速度。

    indices.queries.cache.size: 10%(默認),//可設置成百分比,也可設置成具體值,如256mb。

    固然也能夠禁用查詢緩存(默認是開啓), 經過index.queries.cache.enabled:false設置。

    **b) FieldDataCache: **在聚類或排序時,field data cache會使用頻繁,所以,設置字段數據緩存的大小,在聚類或排序場景較多的情形下頗有必要,可經過indices.fielddata.cache.size:30% 或具體值10GB來設置。可是若是場景或數據變動比較頻繁,設置cache並非好的作法,由於緩存加載的開銷也是特別大的。

    **c) ShardRequestCache: **查詢請求發起後,每一個分片會將結果返回給協調節點(Coordinating Node), 由協調節點將結果整合。

    若是有需求,能夠設置開啓; 經過設置index.requests.cache.enable: true來開啓。

    不過,shard request cache 只緩存 hits.total, aggregations, suggestions 類型的數據,並不會緩存hits的內容。也能夠經過設置indices.requests.cache.size: 1%(默認)來控制緩存空間大小。

ES的內存設置

因爲ES構建基於lucene, 而lucene設計強大之處在於lucene可以很好的利用操做系統內存來緩存索引數據,以提供快速的查詢性能。lucene的索引文件segements是存儲在單文件中的,而且不可變,對於OS來講,可以很友好地將索引文件保持在cache中,以便快速訪問;所以,咱們頗有必要將一半的物理內存留給lucene ; 另外一半的物理內存留給ES(JVM heap )。因此, 在ES內存設置方面,能夠遵循如下原則:

  1. 當機器內存小於64G時,遵循通用的原則,50%給ES,50%留給lucene。

  2. 當機器內存大於64G時,遵循如下原則:

    • a. 若是主要的使用場景是全文檢索, 那麼建議給ES Heap分配 4~32G的內存便可;其它內存留給操做系統, 供lucene使用(segments cache), 以提供更快的查詢性能。
    • b. 若是主要的使用場景是聚合或排序, 而且大多數是numerics, dates, geo_points 以及not_analyzed的字符類型, 建議分配給ES Heap分配 4~32G的內存便可,其它內存留給操做系統,供lucene使用(doc values cache),提供快速的基於文檔的聚類、排序性能。
    • c. 若是使用場景是聚合或排序,而且都是基於analyzed 字符數據,這時須要更多的 heap size, 建議機器上運行多ES實例,每一個實例保持不超過50%的ES heap設置(但不超過32G,堆內存設置32G如下時,JVM使用對象指標壓縮技巧節省空間),50%以上留給lucene。
  3. 禁止swap,一旦容許內存與磁盤的交換,會引發致命的性能問題。 經過: 在elasticsearch.yml 中 bootstrap.memory_lock: true, 以保持JVM鎖定內存,保證ES的性能。

  4. GC設置原則:

    • a. 保持GC的現有設置,默認設置爲:Concurrent-Mark and Sweep (CMS),別換成G1GC,由於目前G1還有不少BUG。
    • b. 保持線程池的現有設置,目前ES的線程池較1.X有了較多優化設置,保持現狀便可;默認線程池大小等於CPU核心數。若是必定要改,按公式((CPU核心數* 3)/ 2)+ 1 設置;不能超過CPU核心數的2倍;可是不建議修改默認配置,不然會對CPU形成硬傷。

調整JVM設置

ES 是在 lucene 的基礎上進行研發的,隱藏了 lucene 的複雜性,提供簡單易用的 RESTful Api接口。ES 的分片至關於 lucene 的索引。因爲 lucene 是 Java 語言開發的,是 Java 語言就涉及到 JVM,因此 ES 存在 JVM的調優問題。

  • 調整內存大小。當頻繁出現full gc後考慮增長內存大小,可是堆內存和堆外內存不要超過32G。
  • 調整寫入的線程數和隊列大小。不過線程數最大不能超過33個(es控制死)。
  • ES很是依賴文件系統緩存,以便快速搜索。通常來講,應該至少確保物理上有一半的可用內存分配到文件系統緩存。

參考文檔:

  1. elasticsearch倒排表壓縮及緩存合併策略
  2. Frame of Reference and Roaring Bitmaps
  3. elasticsearch 倒排索引原理
  4. Elasticsearch性能優化總結
  5. 億級 Elasticsearch 性能優化

原文出處:https://www.cnblogs.com/jajian/p/10465519.html

相關文章
相關標籤/搜索