elasticsearch寫入速度優化

        追求極致的寫入速度時,很多是以犧牲可靠性和搜索實時性爲代價的。有時候,業務上對數據可靠性和搜索實時性要求並不高,反而對寫入速度要求很高,此時可以調整一些策略,最大化寫入速度

         如果是集羣首次批量導入數據,則可以將副本數設置爲0,導入完畢再將副本數調整回去,這樣副分片只需要複製,節省了數據同步的過程。

如果是實時數據寫入,綜合來說,提升寫入速度從以下幾方面入手:

  • 加大translog flush間隔,目的是降低iops、writeblock。
  • 加大index refresh間隔,除了降低I/O,更重要的是降低了segment merge頻率。
  • 調整bulk請求。
  • 優化磁盤間的任務均勻情況,將shard儘量均勻分佈到物理主機的各個磁盤。
  • 優化節點間的任務分佈,將任務儘量均勻地發到各節點。
  • 優化Lucene層建立索引的過程,目的是降低CPU佔用率及I/O,例如,禁用_all字段(ES6.0已經默認禁用,不去額外處理即可)。

translog flush間隔優化

從ES 2.x開始,在默認設置下,translog的持久化策略爲:每個請求都“flush”。對應配置項如下:

index.translog.durability: request

這是影響 ES 寫入速度的最大因素。但是隻有這樣,寫操作纔有可能是可靠的。如果系統可以接受一定概率的數據丟失(例如,數據寫入主分片成功,尚未複製到副分片時,主機斷電。由於數據既沒有刷到Lucene,translog也沒有刷盤,恢復時translog中沒有這個數據,數據丟失),則調整translog持久化策略爲週期性和一定大小的時候“flush”,例如:

index.translog.durability: async

設置爲async表示translog的刷盤策略按sync_interval配置指定的時間週期進行。

index.translog.sync_interval: 120s

加大translog刷盤間隔時間。默認爲5s,不可低於100ms。

index.translog.flush_threshold_size: 1024mb

超過這個大小會導致refresh操作,產生新的Lucene分段。默認值爲512MB。

refresh_interval索引刷新間隔優化

默認情況下索引的refresh_interval爲1秒,這意味着數據寫1秒後就可以被搜索到,進而滿足Elasticsearch的近實時查詢,但是每次索引的refresh會產生一個新的Lucene段,這會導致頻繁的segment merge行爲,造成大量的磁盤io和內存佔用,影響效率,如果不需要這麼高的實時性,應該降低索引refresh週期,例如:

index.refresh_interval: 30s

segment merge段合併優化

segment merge操作對系統I/O和內存佔用都比較高,從ES 2.0開始, merge行爲不再由ES控制,而是由Lucene控制,在6.X版本中由以下配置控制:

index.merge.scheduler.max_thread_count

index.merge.policy.*

最大線程數max_thread_count的默認值如下:

Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2))

以上是一個比較理想的值,當節點配置的cpu核數較高時,merge佔用的資源可能會偏高,影響集羣的性能,如果只有一塊硬盤並且非 SSD,則應該把它設置爲1,因爲在旋轉存儲介質上併發寫,由於尋址的原因,只會降低寫入速度;如果Elasticsearch寫路徑配置多個磁盤,可以在磁盤數的範圍內結合集羣的負載取一個合適的值,可以通過下面的命令調整某個index的merge過程的併發度:

index.merge.scheduler.max_thread_count:2

merge策略index.merge.policy有三種:

  • tiered(默認策略);
  • log_byete_size;
  • log_doc;

目前我們使用默認策略,但是對策略的參數進行了一些調整。

索引創建時合併策略就已確定,不能更改,但是可以動態更新策略參數,可以不做此項調整。如果堆棧經常有很多merge,則可以嘗試調整以下策略配置:

index.merge.policy.segments_per_tier

該屬性指定了每層分段的數量,默認爲10,取值越小則最終segment越少,因此需要merge的操作更多,可以考慮適當增加此值。其應該大於等於index.merge.policy.max_merge_at_once(默認爲10)。

index.merge.policy.max_merged_segment

指定了單個segment的最大容量,默認爲5GB,大於這個大小的segment,不用參與歸併。forcemerge 除外,爲了減少參與merge的segment的數量,減少磁盤IO以及內存佔用,可以考慮適當降低此值,此場景適用於按天或者時間段建index的場景,當index變爲只讀後使用forcemerge進行爲強制歸併,在提高檢索和Reindex效率的同時,減少內存的佔用。

indexing buffer索引緩存優化

indexing buffer在爲doc建立索引時使用,當該內存達到上限時時會刷入磁盤,生成一個新的segment,這是除refresh_interval刷新索引外,另一個生成新segment的機會。每個shard有自己的indexing buffer,下面的這個buffer大小的配置需要除以這個節點上所有shard的數量:

indices.memory.index_buffer_size

默認爲整個堆空間的10%。

indices.memory.min_index_buffer_size

默認爲48MB。

indices.memory.max_index_buffer_size

默認爲無限制。

該配置中indices.memory.index_buffer_size如果配置成百分比,則下面兩個參數即min與max生效,用來規約indexing buffer佔用的實際內存的最大值和最小值。

在執行大量的寫入操作時,indices.memory.index_buffer_size的默認設置可能不夠,這和可用堆內存、單節點上的shard數量相關,可以考慮適當增大該值。

例如:

indices.memory.index_buffer_size:15%

(該配置爲集羣配置,配置項寫在conf/Elasticsearch.yml中)

這也說明了爲什麼需要控制一個節點上shard的數量,數量越多,每個shard分配到的indexing buffer的內存就會越少,進而引發頻繁的refresh,生成大量的segment,進而引發頻繁的segment merge,嚴重影響I/O以及內存佔用……

在控制單節點的shard數量的同時,需要對只讀索引進行force_merge,對warm以及code數據進行shrink操作進行shard裁剪,在當前優化後並且還增加了indices.memory.index_buffer_size的值以後還是無法解決寫入性能以及積壓的問題,就需要考慮擴展硬件資源了

大量的寫入考慮使用bulk請求

Bulk寫入索引的過程屬於計算密集型任務,應該使用固定大小的線程池配置,來不及處理的任務放入隊列。線程池最大線程數量應配置爲CPU核心數+1,這也是bulk線程池的默認設置,可以避免過多的上下文切換。隊列大小可以適當增加,但一定要嚴格控制大小,過大的隊列導致較高的GC壓力,並可能導致FGC頻繁發生。

線程池的大小不建議隨意改變,保持默認就好;隊列大小的修改如下:

thread_pool.bulk.queue_size:500

(該配置爲集羣配置,配置項寫在conf/Elasticsearch.yml中)

每個bulk請求的doc數量設定區間推薦爲1k~1w,具體可根據業務場景選取一個適當的數量。

另外需要注意 bulk線程池隊列的reject情況,出現reject代表ES的bulk隊列已滿,客戶端請求被拒絕,此時客戶端會收到429錯誤(TOO_MANY_REQUESTS),客戶端對此的處理策略應該是延遲重試。不可忽略這個異常,否則寫入系統的數據會少於預期。即使客戶端正確處理了429錯誤,我們仍然應該儘量避免產生reject。因此,在評估極限的寫入能力時,客戶端的極限寫入併發量應該控制在不產生reject前提下的最大值爲宜。如果發現在增加了隊列長度仍然無法避免reject的情況下,說明數據的吞吐量已經超過了當前ES集羣的能力,在已經進行了其他優化的前提下就應該考慮擴展硬件資源了

單節點磁盤間的任務均衡

首先在配置文件conf/Elasticsearch.yml中爲path.data配置多個路徑來使用多塊磁盤,多磁盤帶來的並行寫的優勢可以增加吞吐量,這對提升Elasticsearch的寫入是很友好的;但是多磁盤寫入可能會帶來任務不均衡的問題,Elasticsearch在分配shard時,落到各磁盤上的 shard 可能並不均勻,這種不均勻可能會導致某些磁盤繁忙,利用率在較長時間內持續達到100%,而某些磁盤可能使用率很低甚至爲0,這種不均勻達到一定程度會對寫入性能產生負面影響。

對於此種場景,有兩種策略可以考慮:

  • 簡單輪詢:在系統初始階段,簡單輪詢的效果是最均勻的。
  • 基於可用空間的動態加權輪詢:以可用空間作爲權重,在磁盤之間加權輪詢。

節點間的任務均衡

爲了節點間的任務儘量均衡,數據寫入客戶端應該把bulk請求輪詢發送到各個節點。

當使用Java API或REST API的bulk接口發送數據時,客戶端將會輪詢發送到集羣節點,節點列表取決於:

  • 使用Java API時,當設置client.transport.sniff爲true(默認爲false)時,列表爲所有數據節點,否則節點列表爲構建客戶端對象時傳入的節點列表。
  • 使用REST API時,列表爲構建對象時添加進去的節點。

Java API的TransportClient和REST API的RestClient都是線程安全的,如果寫入程序自己創建線程池控制併發,則應該使用單例模式構建同一個Client對象。

在此建議使用REST API,Java API會在未來的版本中廢棄,REST API有良好的版本兼容性好。理論上,Java API在序列化上有性能優勢,但是隻有在吞吐量非常大時才值得考慮序列化的開銷帶來的影響,通常搜索並不是高吞吐量的業務。

如果使用bulk請求來處理數據寫入,需要觀察bulk請求在不同節點間的均衡性,可以通過cat接口觀察bulk線程池和隊列情況:

_cat/thread_pool

Index相關的優化

自動生成doc ID:

通過ES寫入流程可以看出,寫入doc時如果外部指定了id,則ES會先嚐試讀取原來doc的版本號,以判斷是否需要更新。這會涉及一次讀取磁盤的操作,通過自動生成doc ID可以避免這個環節。

調整index的Mappings:

(1)減少字段數量,對於不需要建立索引的字段,不寫入ES。

(2)將不需要建立索引的字段index屬性設置爲not_analyzed或no。對字段不分詞,或者不索引,可以減少很多運算操作,降低CPU佔用。尤其是binary類型,默認情況下佔用CPU非常高,而這種類型進行分詞通常沒有什麼意義。

(3)減少字段內容長度,如果原始數據的大段內容無須全部建立索引,則可以儘量減少不必要的內容。

(4)使用不同的分析器(analyzer),不同的分析器在索引過程中運算複雜度也有較大的差異。

調整_source字段:

_source 字段用於存儲 doc 原始數據,對於部分不需要存儲的字段,可以通過 includes excludes過濾,減少_source字段的存儲量,或者將_source禁用(此時Elasticsearch的update,update by query以及Reindex接口無法使用),一般用於索引和數據分離的實現方案(HBase+es,HBase存儲數據,es存儲索引,HBase與es同步採用HBase的協處理器coprocessor實現)。這樣可以降低 I/O 的壓力,不過實際場景中大多不會禁用_source,而即使過濾掉某些字段,對於寫入速度的提升作用也不大(濾掉某些字段更多的是爲了節省硬盤空間,降低存儲成本),滿負荷寫入情況下,基本是 CPU 先跑滿了,瓶頸在於CPU。

筆者建議以下設計方案:

Elasticsearch以及_source存儲業務需要索引的關鍵字段以及對應HBase原始數據的rowkey,保證在大多數的業務場景下可以通過Elasticsearch的一次查詢即可以返回業務需要的字段,滿足業務需求;在個別場景下需要查詢詳細信息時,通過Elasticsearch和HBase關聯的rowkey去HBase中get詳細數據。_source字段的保留也爲後續的Elasticsearch的擴展性(update以及Reindex)留下了接口。

禁用_all字段:

6.X版本已經默認禁用,不需要做任何配置,之前的版本使用可以在mapping中將enabled設置爲false來禁用_all字段。禁用_all字段可以明顯降低對CPU和I/O的壓力。

對Analyzed的字段禁用Norms:

開啓norms之後,每個document的每個field需要一個字節存儲norms。對於keyword 類型的字段默認關閉 norms對於 text 類型的字段而言是默認開啓norms的,因此對於不需要評分的 text 類型的字段,可以禁用norms:

"title": {"type": "text","norms": {"enabled": false}}

參考配置與分析

下面是筆者的線上環境使用的全局模板和配置文件的部分內容,省略掉了節點名稱、節點列表等基礎配置字段,僅列出與寫入速度優化的相關內容,其他內容會在接下來的章節進行講解。

Elasticsearch索引的建立方式建議使用rollover Api藉助模板進行索引的建立,我們把各個索引通用的配置寫到了模板中,作爲通用的默認配置進行使用

{

       "template": "*",//模板匹配全部的索引

       "order": 0,// 具有最低的優先級,讓用戶定義的模板有更高的優先級,以覆蓋這個模板中的配置

       "settings": {

              "index.merge.policy.max_merged_segment": "2gb",//大於2g的segment不參與merge

              "index.merge.policy.segments_per_tier": "24",//每層分段的數量爲24,增加每個segment的大小,減少segment merge的發生的次數

              "index.number_of_replicas": "1",//數據1備份,容災並提升數據查詢效率

              "index.number_of_shards": "24",//每個index有24個shard,這個數字需要根據數據量進行評估,原則上是儘量的少,畢竟多一個shard對Elasticsearch的壓力也會增加很多,shard數量設計原則可以參考index的shard規劃原則

              "index.optimize_auto_generated_id": "true",//自動生成doc ID

              "index.refresh_interval": "30s",//refresh的自動刷新間隔,刷新後數據可以被檢索到,根據業務的實時性需求來配置該值

              "index.translog.durability": "async",//異步刷新translog

              "index.translog.flush_threshold_size": "1024mb",//translog強制flush的大小閾值

              "index.translog.sync_interval": "120s",//translog定時刷新的間隔,可以根據需求調節該值

              "index.unassigned.node_left.delayed_timeout": "5d"//該配置可以避免某些Rebalancing操作,該操作會帶來很大的開銷,如果節點離開後馬上又回來(如網絡不好,重啓等),則該開銷完全沒有必要,所以在集羣相對穩定以及運維給力的前提下,儘量增大該值以避免不必要的資源開銷

       }

}

elasticsearch.yml中的配置:

indices.memory.index_buffer_size: 15%//當寫入壓力過大時,可以適當增加該值,但是如果增加完該值並優化過後還無法解決寫入積壓的問題,則考慮增加硬件資源

thread_pool.bulk.queue_size:500//修改bulk隊列大小,這個不要太大,否則會引發嚴重的FGC問題,建議不超過1000

終極版優化方案詳見:Elasticsearch配置優化方案最終完整版

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章