ES 最佳實踐配置
Elasticsearch 效能優化
Elasticsearch 是當前流行的企業級搜尋引擎,設計用於雲端計算中,能夠達到實時搜尋,穩定,可靠,快速,安裝使用方便。作為一個開箱即用的產品,在生產環境上線之後,我們其實不一定能確保其的效能和穩定性。如何根據實際情況提高服務的效能,其實有很多技巧。這章我們分享從實戰經驗中總結出來的 elasticsearch 效能優化,主要從硬體配置優化、索引優化設定、查詢方面優化、資料結構優化、叢集架構優化等方面講解。
硬體配置優化
升級硬體裝置配置一直都是提高服務能力最快速有效的手段,在系統層面能夠影響應用效能的一般包括三個因素:CPU、記憶體和 IO,可以從這三方面進行 ES 的效能優化工作。
CPU 配置
一般說來,CPU 繁忙的原因有以下幾個:
- 執行緒中有無限空迴圈、無阻塞、正則匹配或者單純的計算;
- 發生了頻繁的 GC;
- 多執行緒的上下文切換;
大多數 Elasticsearch 部署往往對 CPU 要求不高。因此,相對其它資源,具體配置多少個(CPU)不是那麼關鍵。你應該選擇具有多個核心的現代處理器,常見的叢集使用 2 到 8 個核的機器。如果你要在更快的 CPUs 和更多的核數之間選擇,選擇更多的核數更好。多個核心提供的額外併發遠勝過稍微快一點點的時鐘頻率。
記憶體配置
如果有一種資源是最先被耗盡的,它可能是記憶體。排序和聚合都很耗記憶體,所以有足夠的堆空間來應付它們是很重要的。即使堆空間是比較小的時候,也能為作業系統檔案快取提供額外的記憶體。因為 Lucene 使用的許多資料結構是基於磁碟的格式,Elasticsearch 利用作業系統快取能產生很大效果。
64 GB 記憶體的機器是非常理想的,但是 32 GB 和 16 GB 機器也是很常見的。少於8 GB 會適得其反(你最終需要很多很多的小機器),大於 64 GB 的機器也會有問題。
由於 ES 構建基於 lucene,而 lucene 設計強大之處在於 lucene 能夠很好的利用作業系統記憶體來快取索引資料,以提供快速的查詢效能。lucene 的索引檔案 segements 是儲存在單檔案中的,並且不可變,對於 OS 來說,能夠很友好地將索引檔案保持在 cache 中,以便快速訪問;因此,我們很有必要將一半的實體記憶體留給 lucene;另一半的實體記憶體留給 ES(JVM heap)。
記憶體分配
當機器記憶體小於 64G 時,遵循通用的原則,50% 給 ES,50% 留給 lucene。
當機器記憶體大於 64G 時,遵循以下原則:
- 如果主要的使用場景是全文檢索,那麼建議給 ES Heap 分配 4~32G 的記憶體即可;其它記憶體留給作業系統,供 lucene 使用(segments cache),以提供更快的查詢效能。
- 如果主要的使用場景是聚合或排序,並且大多數是 numerics,dates,geo_points 以及 not_analyzed 的字元型別,建議分配給 ES Heap 分配 4~32G 的記憶體即可,其它記憶體留給作業系統,供 lucene 使用,提供快速的基於文件的聚類、排序效能。
- 如果使用場景是聚合或排序,並且都是基於 analyzed 字元資料,這時需要更多的 heap size,建議機器上執行多 ES 例項,每個例項保持不超過 50% 的 ES heap 設定(但不超過 32 G,堆記憶體設定 32 G 以下時,JVM 使用物件指標壓縮技巧節省空間),50% 以上留給 lucene。
禁止 swap
禁止 swap,一旦允許記憶體與磁碟的交換,會引起致命的效能問題。可以通過在 elasticsearch.yml 中 bootstrap.memory_lock:true
,以保持 JVM 鎖定記憶體,保證 ES 的效能。
GC 設定
保持 GC 的現有設定,預設設定為:Concurrent-Mark and Sweep(CMS),別換成 G1 GC,因為目前 G1 還有很多 BUG。
保持執行緒池的現有設定,目前 ES 的執行緒池較 1.X 有了較多優化設定,保持現狀即可;預設執行緒池大小等於 CPU 核心數。如果一定要改,按公式 ( ( CPU 核心數 * 3 ) / 2 ) + 1 設定;不能超過 CPU 核心數的 2 倍;但是不建議修改預設配置,否則會對 CPU 造成硬傷。
磁碟
硬碟對所有的叢集都很重要,對大量寫入的叢集更是加倍重要(例如那些儲存日誌資料的)。硬碟是伺服器上最慢的子系統,這意味著那些寫入量很大的叢集很容易讓硬碟飽和,使得它成為叢集的瓶頸。
在經濟壓力能承受的範圍下,儘量使用固態硬碟(SSD)。固態硬碟相比於任何旋轉介質(機械硬碟,磁帶等),無論隨機寫還是順序寫,都會對 IO 有較大的提升。
如果你正在使用 SSDs,確保你的系統 I/O 排程程式是配置正確的。當你向硬碟寫資料,I/O 排程程式決定何時把資料實際傳送到硬碟。大多數預設 *nix 發行版下的排程程式都叫做 cfq(完全公平佇列)。
排程程式分配時間片到每個程序。並且優化這些到硬碟的眾多佇列的傳遞。但它是為旋轉介質優化的:機械硬碟的固有特性意味著它寫入資料到基於物理佈局的硬碟會更高效。
這對 SSD 來說是低效的,儘管這裡沒有涉及到機械硬碟。但是,deadline 或者 noop 應該被使用。deadline 排程程式基於寫入等待時間進行優化,noop 只是一個簡單的 FIFO 佇列。
這個簡單的更改可以帶來顯著的影響。僅僅是使用正確的排程程式,我們看到了 500 倍的寫入能力提升。
如果你使用旋轉介質(如機械硬碟),嘗試獲取儘可能快的硬碟(高效能伺服器硬碟,15k RPM 驅動器)。
使用 RAID0 是提高硬碟速度的有效途徑,對機械硬碟和 SSD 來說都是如此。沒有必要使用映象或其它 RAID 變體,因為 Elasticsearch 在自身層面通過副本,已經提供了備份的功能,所以不需要利用磁碟的備份功能,同時如果使用磁碟備份功能的話,對寫入速度有較大的影響。
最後,避免使用網路附加儲存(NAS)。人們常聲稱他們的 NAS 解決方案比本地驅動器更快更可靠。除卻這些聲稱,我們從沒看到 NAS 能配得上它的大肆宣傳。NAS 常常很慢,顯露出更大的延時和更寬的平均延時方差,而且它是單點故障的。
索引優化設定
索引優化主要是在 Elasticsearch 的插入層面優化,Elasticsearch 本身索引速度其實還是蠻快的,具體資料,我們可以參考官方的 benchmark 資料。我們可以根據不同的需求,針對索引優化。
批量提交
當有大量資料提交的時候,建議採用批量提交(Bulk 操作);此外使用 bulk 請求時,每個請求不超過幾十M,因為太大會導致記憶體使用過大。
比如在做 ELK 過程中,Logstash indexer 提交資料到 Elasticsearch 中,batch size 就可以作為一個優化功能點。但是優化 size 大小需要根據文件大小和伺服器效能而定。
像 Logstash 中提交文件大小超過 20MB,Logstash 會將一個批量請求切分為多個批量請求。
如果在提交過程中,遇到 EsRejectedExecutionException 異常的話,則說明叢集的索引效能已經達到極限了。這種情況,要麼提高伺服器叢集的資源,要麼根據業務規則,減少資料收集速度,比如只收集 Warn、Error 級別以上的日誌。
增加 Refresh 時間間隔
為了提高索引效能,Elasticsearch 在寫入資料的時候,採用延遲寫入的策略,即資料先寫到記憶體中,當超過預設1秒(index.refresh_interval)會進行一次寫入操作,就是將記憶體中 segment 資料重新整理到磁碟中,此時我們才能將資料搜尋出來,所以這就是為什麼 Elasticsearch 提供的是近實時搜尋功能,而不是實時搜尋功能。
如果我們的系統對資料延遲要求不高的話,我們可以通過延長 refresh 時間間隔,可以有效地減少 segment 合併壓力,提高索引速度。比如在做全鏈路跟蹤的過程中,我們就將 index.refresh_interval
設定為30s,減少 refresh 次數。再如,在進行全量索引時,可以將 refresh 次數臨時關閉,即 index.refresh_interval
設定為-1,資料匯入成功後再開啟到正常模式,比如30s。
在載入大量資料時候可以暫時不用 refresh 和 repliccas,index.refresh_interval 設定為-1,index.number_of_replicas 設定為0。
修改 index_buffer_size 的設定
索引緩衝的設定可以控制多少記憶體分配給索引程序。這是一個全域性配置,會應用於一個節點上所有不同的分片上。
indices.memory.index_buffer_size: 10%
indices.memory.min_index_buffer_size: 48mb
indices.memory.index_buffer_size
接受一個百分比或者一個表示位元組大小的值。預設是10%,意味著分配給節點的總記憶體的10%用來做索引緩衝的大小。這個數值被分到不同的分片(shards)上。如果設定的是百分比,還可以設定min_index_buffer_size
(預設 48mb)和max_index_buffer_size
(預設沒有上限)。
修改 translog 相關的設定
一是控制資料從記憶體到硬碟的操作頻率,以減少硬碟 IO。可將 sync_interval 的時間設定大一些。預設為5s。
index.translog.sync_interval: 5s
也可以控制 tranlog 資料塊的大小,達到 threshold 大小時,才會 flush 到 lucene 索引檔案。預設為512m。
index.translog.flush_threshold_size: 512mb
注意_id 欄位的使用
_id 欄位的使用,應儘可能避免自定義 _id,以避免針對 ID 的版本管理;建議使用 ES 的預設 ID 生成策略或使用數字型別 ID 做為主鍵。
注意 _all 欄位及 _source 欄位的使用
_all 欄位及 _source 欄位的使用,應該注意場景和需要,_all 欄位包含了所有的索引欄位,方便做全文檢索,如果無此需求,可以禁用;_source 儲存了原始的 document 內容,如果沒有獲取原始文件資料的需求,可通過設定 includes、excludes 屬性來定義放入 _source 的欄位。
合理的配置使用 index 屬性
合理的配置使用 index 屬性,analyzed 和 not_analyzed,根據業務需求來控制欄位是否分詞或不分詞。只有 groupby 需求的欄位,配置時就設定成 not_analyzed,以提高查詢或聚類的效率。
減少副本數量
Elasticsearch 預設副本數量為3個,雖然這樣會提高叢集的可用性,增加搜尋的併發數,但是同時也會影響寫入索引的效率。
在索引過程中,需要把更新的文件發到副本節點上,等副本節點生效後在進行返回結束。使用 Elasticsearch 做業務搜尋的時候,建議副本數目還是設定為3個,但是像內部 ELK 日誌系統、分散式跟蹤系統中,完全可以將副本數目設定為1個。
查詢方面優化
Elasticsearch 作為業務搜尋的近實時查詢時,查詢效率的優化顯得尤為重要。
路由優化
當我們查詢文件的時候,Elasticsearch 如何知道一個文件應該存放到哪個分片中呢?它其實是通過下面這個公式來計算出來的。
shard = hash(routing) % number_of_primary_shards
routing 預設值是文件的 id,也可以採用自定義值,比如使用者 ID。
不帶 routing 查詢
在查詢的時候因為不知道要查詢的資料具體在哪個分片上,所以整個過程分為2個步驟:
- 分發:請求到達協調節點後,協調節點將查詢請求分發到每個分片上。
- 聚合:協調節點蒐集到每個分片上查詢結果,再將查詢的結果進行排序,之後給使用者返回結果。
帶 routing 查詢
查詢的時候,可以直接根據 routing 資訊定位到某個分配查詢,不需要查詢所有的分配,經過協調節點排序。
向上面自定義的使用者查詢,如果 routing 設定為 userid 的話,就可以直接查詢出資料來,效率提升很多。
Filter VS Query
儘可能使用過濾器上下文(Filter)替代查詢上下文(Query)
- Query:此文件與此查詢子句的匹配程度如何?
- Filter:此文件和查詢子句匹配嗎?
Elasticsearch 針對 Filter 查詢只需要回答「是」或者「否」,不需要像 Query 查詢一樣計算相關性分數,同時Filter結果可以快取。
深度翻頁
在使用 Elasticsearch 過程中,應儘量避免大翻頁的出現。
正常翻頁查詢都是從 from 開始 size 條資料,這樣就需要在每個分片中查詢打分排名在前面的 from+size 條資料。協同節點收集每個分配的前 from+size 條資料。協同節點一共會受到 N*(from+size) 條資料,然後進行排序,再將其中 from 到 from+size 條資料返回出去。如果 from 或者 size 很大的話,導致參加排序的數量會同步擴大很多,最終會導致 CPU 資源消耗增大。
可以通過使用 Elasticsearch scroll 和 scroll-scan 高效滾動的方式來解決這樣的問題。
也可以結合實際業務特點,文件 id 大小如果和文件建立時間是一致有序的,可以以文件 id 作為分頁的偏移量,並將其作為分頁查詢的一個條件。
指令碼(script)合理使用
我們知道指令碼使用主要有 3 種形式,內聯動態編譯方式、_script 索引庫中儲存和檔案指令碼儲存的形式;一般指令碼的使用場景是粗排,儘量用第二種方式先將指令碼儲存在 _script 索引庫中,起到提前編譯,然後通過引用指令碼 id,並結合 params 引數使用,即可以達到模型(邏輯)和資料進行了分離,同時又便於指令碼模組的擴充套件與維護。具體 ES 指令碼的深入內容請參考 Elasticsearch 指令碼模組的詳解。
資料結構優化
基於 Elasticsearch 的使用場景,文件資料結構儘量和使用場景進行結合,去掉沒用及不合理的資料。
儘量減少不需要的欄位
如果 Elasticsearch 用於業務搜尋服務,一些不需要用於搜尋的欄位最好不存到 ES 中,這樣即節省空間,同時在相同的資料量下,也能提高搜尋效能。
避免使用動態值作欄位,動態遞增的 mapping,會導致叢集崩潰;同樣,也需要控制欄位的數量,業務中不使用的欄位,就不要索引。控制索引的欄位數量、mapping 深度、索引欄位的型別,對於 ES 的效能優化是重中之重。
以下是 ES 關於欄位數、mapping 深度的一些預設設定:
index.mapping.nested_objects.limit: 10000
index.mapping.total_fields.limit: 1000
index.mapping.depth.limit: 20
Nested Object vs Parent/Child
儘量避免使用 nested 或 parent/child 的欄位,能不用就不用;nested query 慢,parent/child query 更慢,比 nested query 慢上百倍;因此能在 mapping 設計階段搞定的(大寬表設計或採用比較 smart 的資料結構),就不要用父子關係的 mapping。
如果一定要使用 nested fields,保證 nested fields 欄位不能過多,目前 ES 預設限制是 50。因為針對 1 個 document,每一個 nested field,都會生成一個獨立的 document,這將使 doc 數量劇增,影響查詢效率,尤其是 JOIN 的效率。
index.mapping.nested_fields.limit: 50
對比 | Nested Object | Parent/Child |
---|---|---|
優點 | 文件儲存在一起,因此讀取性高 | 父子文件可以獨立更新,互不影響 |
缺點 | 更新父文件或子文件時需要更新整個文件 | 為了維護 join 關係,需要佔用部分記憶體,讀取效能較差 |
場景 | 子文件偶爾更新,查詢頻繁 | 子文件更新頻繁 |
選擇靜態對映,非必需時,禁止動態對映
儘量避免使用動態對映,這樣有可能會導致叢集崩潰,此外,動態對映有可能會帶來不可控制的資料型別,進而有可能導致在查詢端出現相關異常,影響業務。
此外,Elasticsearch 作為搜尋引擎時,主要承載 query 的匹配和排序的功能,那資料的儲存型別基於這兩種功能的用途分為兩類,一是需要匹配的欄位,用來建立倒排索引對 query 匹配用,另一類欄位是用做粗排用到的特徵欄位,如 ctr、點選數、評論數等等。
叢集架構設計
合理的部署 Elasticsearch 有助於提高服務的整體可用性。
主節點、資料節點和協調節點分離
Elasticsearch 叢集在架構拓樸時,採用主節點、資料節點和負載均衡節點分離的架構,在 5.x 版本以後,又可將資料節點再細分為“Hot-Warm”的架構模式。
Elasticsearch 的配置檔案中有 2 個引數,node.master 和 node.data。這兩個引數搭配使用時,能夠幫助提供伺服器效能。
主(master)節點
配置 node.master:true
和 node.data:false
,該 node 伺服器只作為一個主節點,但不儲存任何索引資料。我們推薦每個叢集執行3 個專用的 master 節點來提供最好的彈性。使用時,你還需要將 discovery.zen.minimum_master_nodes setting
引數設定為 2,以免出現腦裂(split-brain)的情況。用 3 個專用的 master 節點,專門負責處理叢集的管理以及加強狀態的整體穩定性。因為這 3 個 master 節點不包含資料也不會實際參與搜尋以及索引操作,在 JVM 上它們不用做相同的事,例如繁重的索引或者耗時,資源耗費很大的搜尋。因此不太可能會因為垃圾回收而導致停頓。因此,master 節點的 CPU,記憶體以及磁碟配置可以比 data 節點少很多的。
資料(data)節點
配置 node.master:false
和 node.data:true
,該 node 伺服器只作為一個數據節點,只用於儲存索引資料,使該 node 伺服器功能單一,只用於資料儲存和資料查詢,降低其資源消耗率。
在 Elasticsearch 5.x 版本之後,data 節點又可再細分為“Hot-Warm”架構,即分為熱節點(hot node)和暖節點(warm node)。
hot 節點:
hot 節點主要是索引節點(寫節點),同時會儲存近期的一些頻繁被查詢的索引。由於進行索引非常耗費 CPU 和 IO,即屬於 IO 和 CPU 密集型操作,建議使用 SSD 的磁碟型別,保持良好的寫效能;我們推薦部署最小化的 3 個 hot 節點來保證高可用性。根據近期需要收集以及查詢的資料量,可以增加伺服器數量來獲得想要的效能。
將節點設定為 hot 型別需要 elasticsearch.yml 如下配置:
node.attr.box_type: hot
如果是針對指定的 index 操作,可以通過 settings 設定index.routing.allocation.require.box_type: hot
將索引寫入 hot 節點。
warm 節點:
這種型別的節點是為了處理大量的,而且不經常訪問的只讀索引而設計的。由於這些索引是隻讀的,warm 節點傾向於掛載大量磁碟(普通磁碟)來替代 SSD。記憶體、CPU 的配置跟 hot 節點保持一致即可;節點數量一般也是大於等於 3 個。
將節點設定為 warm 型別需要 elasticsearch.yml 如下配置:
node.attr.box_type: warm
同時,也可以在 elasticsearch.yml 中設定 index.codec:best_compression
保證 warm 節點的壓縮配置。
當索引不再被頻繁查詢時,可通過 index.routing.allocation.require.box_type:warm
,將索引標記為 warm,從而保證索引不寫入 hot 節點,以便將 SSD 磁碟資源用在刀刃上。一旦設定這個屬性,ES 會自動將索引合併到 warm 節點。
協調(coordinating)節點
協調節點用於做分散式裡的協調,將各分片或節點返回的資料整合後返回。該節點不會被選作主節點,也不會儲存任何索引資料。該伺服器主要用於查詢負載均衡。在查詢的時候,通常會涉及到從多個 node 伺服器上查詢資料,並將請求分發到多個指定的 node 伺服器,並對各個 node 伺服器返回的結果進行一個彙總處理,最終返回給客戶端。在 ES 叢集中,所有的節點都有可能是協調節點,但是,可以通過設定 node.master
、node.data
、node.ingest
都為 false
來設定專門的協調節點。需要較好的 CPU 和較高的記憶體。
- node.master:false和node.data:true,該node伺服器只作為一個數據節點,只用於儲存索引資料,使該node伺服器功能單一,只用於資料儲存和資料查詢,降低其資源消耗率。
- node.master:true和node.data:false,該node伺服器只作為一個主節點,但不儲存任何索引資料,該node伺服器將使用自身空閒的資源,來協調各種建立索引請求或者查詢請求,並將這些請求合理分發到相關的node伺服器上。
- node.master:false和node.data:false,該node伺服器即不會被選作主節點,也不會儲存任何索引資料。該伺服器主要用於查詢負載均衡。在查詢的時候,通常會涉及到從多個node伺服器上查詢資料,並將請求分發到多個指定的node伺服器,並對各個node伺服器返回的結果進行一個彙總處理,最終返回給客戶端。
關閉 data 節點伺服器中的 http 功能
針對 Elasticsearch 叢集中的所有資料節點,不用開啟 http 服務。將其中的配置引數這樣設定,http.enabled:false
,同時也不要安裝 head, bigdesk, marvel 等監控外掛,這樣保證 data 節點伺服器只需處理建立/更新/刪除/查詢索引資料等操作。
http 功能可以在非資料節點伺服器上開啟,上述相關的監控外掛也安裝到這些伺服器上,用於監控 Elasticsearch 叢集狀態等資料資訊。這樣做一來出於資料安全考慮,二來出於服務效能考慮。
一臺伺服器上最好只部署一個 node
一臺物理伺服器上可以啟動多個 node 伺服器節點(通過設定不同的啟動 port),但一臺伺服器上的 CPU、記憶體、硬碟等資源畢竟有限,從伺服器效能考慮,不建議一臺伺服器上啟動多個 node 節點。
叢集分片設定
ES 一旦建立好索引後,就無法調整分片的設定,而在 ES 中,一個分片實際上對應一個 lucene 索引,而 lucene 索引的讀寫會佔用很多的系統資源,因此,分片數不能設定過大;所以,在建立索引時,合理配置分片數是非常重要的。一般來說,我們遵循一些原則:
- 控制每個分片佔用的硬碟容量不超過 ES 的最大 JVM 的堆空間設定(一般設定不超過 32 G,參考上面的 JVM 記憶體設定原則),因此,如果索引的總容量在 500 G 左右,那分片大小在 16 個左右即可;當然,最好同時考慮原則 2。
- 考慮一下 node 數量,一般一個節點有時候就是一臺物理機,如果分片數過多,大大超過了節點數,很可能會導致一個節點上存在多個分片,一旦該節點故障,即使保持了 1 個以上的副本,同樣有可能會導致資料丟失,叢集無法恢復。所以,一般都設定分片數不超過節點數的 3 倍。