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 倍。