億級 Elasticsearch 性能優化
前言
最近一年使用 Elasticsearch 完成億級別日誌搜索平臺「ELK」,億級別的分布式跟蹤系統。在設計這些系統的過程中,底層都是采用 Elasticsearch 來做數據的存儲,並且數據量都超過億級別,甚至達到百億級別。
所以趁著有空,就花點時間整理一下具體怎麽做 Elasticsearch 性能優化,希望能對 Elasticsearch 感興趣的同學有所幫助。
背景
Elasticsearch 是一個基於 Lucene 的搜索服務器。它提供了一個分布式多用戶能力的全文搜索引擎,基於 RESTful web 接口。Elasticsearch 是用 Java 開發的,並作為 Apache 許可條款下的開放源碼發布,是當前流行的企業級搜索引擎。設計用於雲計算中,能夠達到實時搜索,穩定,可靠,快速,安裝使用方便。
作為一個開箱即用的產品,在生產環境上線之後,我們其實不一定能確保其的性能和穩定性。如何根據實際情況提高服務的性能,其實有很多技巧。
下面我就從三個方面分別來講解下優化服務的性能:
- 索引效率優化
- 查詢效率優化
- JVM 配置優化
索引效率優化
索引優化主要是在 Elasticsearch 插入層面優化,如果瓶頸不在這塊,而是在產生數據部分,比如 DB 或者 Hadoop 上,那麽優化方向就需要改變下。同時,Elasticsearch 本身索引速度其實還是蠻快的,具體數據,我們可以參考官方的 benchmark 數據。
批量提交
當有大量數據提交的時候,建議采用批量提交。
比如在做 ELK 過程中 ,Logstash indexer 提交數據到 Elasticsearch 中 ,batch size 就可以作為一個優化功能點。但是優化 size 大小需要根據文檔大小和服務器性能而定。
像 Logstash 中提交文檔大小超過 20MB ,Logstash 會請一個批量請求切分為多個批量請求。
如果在提交過程中,遇到 EsRejectedExecutionException 異常的話,則說明集群的索引性能已經達到極限了。這種情況,要麽提高服務器集群的資源,要麽根據業務規則,減少數據收集速度,比如只收集 Warn、Error 級別以上的日誌。
優化硬件
優化硬件設備一直是最快速有效的手段。
- 在經濟壓力能承受的範圍下, 盡量使用固態硬盤 SSD。SSD 相對於機器硬盤,無論隨機寫還是順序寫,都較大的提升。
- 磁盤備份采用 RAID0。因為 Elasticsearch 在自身層面通過副本,已經提供了備份的功能,所以不需要利用磁盤的備份功能,同時如果使用磁盤備份功能的話,對寫入速度有較大的影響。
增加 Refresh 時間間隔
為了提高索引性能,Elasticsearch 在寫入數據時候,采用延遲寫入的策略,即數據先寫到內存中,當超過默認 1 秒 (index.refresh_interval)會進行一次寫入操作,就是將內存中 segment 數據刷新到操作系統中,此時我們才能將數據搜索出來,所以這就是為什麽 Elasticsearch 提供的是近實時搜索功能,而不是實時搜索功能。
當然像我們的內部系統對數據延遲要求不高的話,我們可以通過延長 refresh 時間間隔,可以有效的減少 segment 合並壓力,提供索引速度。在做全鏈路跟蹤的過程中,我們就將 index.refresh_interval 設置為 30s,減少 refresh 次數。
同時,在進行全量索引時,可以將 refresh 次數臨時關閉,即 index.refresh_interval 設置為 -1,數據導入成功後再打開到正常模式,比如 30s。
減少副本數量
Elasticsearch 默認副本數量為 3 個,雖然這樣會提高集群的可用性,增加搜索的並發數,但是同時也會影響寫入索引的效率。
在索引過程中,需要把更新的文檔發到副本節點上,等副本節點生效後在進行返回結束。使用 Elasticsearch 做業務搜索的時候,建議副本數目還是設置為 3 個,但是像內部 ELK 日誌系統、分布式跟蹤系統中,完全可以將副本數目設置為 1 個。
查詢效率優化
路由
當我們查詢文檔的時候,Elasticsearch 如何知道一個文檔應該存放到哪個分片中呢?它其實是通過下面這個公式來計算出來
shard = hash(routing) % number_of_primary_shards
routing 默認值是文檔的 id,也可以采用自定義值,比如用戶 id。
不帶 routing 查詢
在查詢的時候因為不知道要查詢的數據具體在哪個分片上,所以整個過程分為 2 個步驟
- 分發:請求到達協調節點後,協調節點將查詢請求分發到每個分片上。
- 聚合: 協調節點搜集到每個分片上查詢結果,在將查詢的結果進行排序,之後給用戶返回結果。
帶 routing 查詢
查詢的時候,可以直接根據 routing 信息定位到某個分配查詢,不需要查詢所有的分配,經過協調節點排序。
向上面自定義的用戶查詢,如果 routing 設置為 userid 的話,就可以直接查詢出數據來,效率提升很多。
Filter VS Query
Ebay 曾經分享過他們使用 Elasticsearch 的經驗中說到:
Use filter context instead of query context if possible.
盡可能使用過濾器上下文(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 高效滾動的方式來解決這樣的問題。具體寫法,可以參考 Elasticsearch: 權威指南 - scroll 查詢
JVM 設置
32G 現象
Elasticsearch 默認安裝後設置的堆內存是 1 GB。 對於任何一個業務部署來說, 這個設置都太小了。
比如機器有 64G 內存,那麽我們是不是設置的越大越好呢?
其實不是的。
主要 Elasticsearch 底層使用 Lucene。Lucene 被設計為可以利用操作系統底層機制來緩存內存數據結構。 Lucene 的段是分別存儲到單個文件中的。因為段是不可變的,這些文件也都不會變化,這是對緩存友好的,同時操作系統也會把這些段文件緩存起來,以便更快的訪問。
如果你把所有的內存都分配給 Elasticsearch 的堆內存,那將不會有剩余的內存交給 Lucene。 這將嚴重地影響全文檢索的性能。
標準的建議是把 50% 的可用內存作為 Elasticsearch 的堆內存,保留剩下的 50%。當然它也不會被浪費,Lucene 會很樂意利用起余下的內存。
同時了解過 ES 的同學都聽過過「不要超過 32G」的說法吧。
其實主要原因是 :JVM 在內存小於 32 GB 的時候會采用一個內存對象指針壓縮技術。
在 Java 中,所有的對象都分配在堆上,並通過一個指針進行引用。 普通對象指針(OOP)指向這些對象,通常為 CPU 字長 的大小:32 位或 64 位,取決於你的處理器。指針引用的就是這個 OOP 值的字節位置。
對於 32 位的系統,意味著堆內存大小最大為 4 GB。對於 64 位的系統, 可以使用更大的內存,但是 64 位的指針意味著更大的浪費,因為你的指針本身大了。更糟糕的是, 更大的指針在主內存和各級緩存(例如 LLC,L1 等)之間移動數據的時候,會占用更多的帶寬.
所以最終我們都會采用 31 G 設置
-Xms 31g
-Xmx 31g
假設你有個機器有 128 GB 的內存,你可以創建兩個節點,每個節點內存分配不超過 32 GB。 也就是說不超過 64 GB 內存給 ES 的堆內存,剩下的超過 64 GB 的內存給 Lucene
參考
- Elasticsearch: 權威指南 -堆內存:大小和交換
- 關於Elasticsearch性能優化的幾點問題
億級 Elasticsearch 性能優化