ElasticSearch 開發人員最佳實戰指南

題記

幾個月以來,我一直在記錄自己開發Elasticsearch應用程序的最佳實踐。本文梳理的內容試圖傳達Java的某些思想,我相信其同樣適用於其他編程語言。我嘗試儘量避免重複教程和Elasticsearch官方文檔中已經介紹的內容。本文梳理的內容都是從線上實踐問題和個人總結的經驗彙總得來的。

文章從以下幾個維度展開講解:

  • 映射(Mapping)

  • 設置(Setting)

  • 查詢方式(Querying)

  • 實戰技巧(Strategy)

1、映射(Mapping)

1.1 避免使用nested類型

每個Elasticsearch文檔都對應一個Lucene文檔。

nested類型是個例外,對於nested類型,每個字段都作為單獨的文檔存儲與父Lucene的關聯。

其影響是:

  • nested與父文檔中的字段相比,查詢字段的速度較慢

  • 檢索匹配nested字段會降低檢索速度

  • 一旦更新了包含nested字段的文檔的任何字段(與是否更新嵌套字段無關,則所有基礎Lucene文檔(父級及其所有nested子級)都需要標記為已刪除並重寫)。除了降低更新速度外,此類操作還會產生大量垃圾文件,直到通過段合才能進行清理。

在某些情況下,你可以將nested字段展平。

例如,給定以下文檔:

<code>{
"attributes": [
{"key": "color", "val": "green"},
{"key": "color", "val": "blue"},
{"key": "size", "val": "medium"}
]
}
/<code>

展平如下:

<code>{
"attributes": {

"color": ["green", "blue"],
"size": "medium"
}
}
/<code>

1.2 Mapping設置strict

實際業務中,如果不明確設定字段類型,Elasticsearch有動態映射機制,會根據插入數據自動匹配對應的類型。

假定:本來準備插入浮點型數據,但由於第一個插入數據為整形,Elasticsearch 自定會判定為long類型,雖然後續數據也能寫入,但很明顯“浮點類型”只閹割保留了整形部分。

銘毅給個demo一探究竟:

<code>POST my_index03/_doc/1
{
"tvalue":35
}

POST my_index03/_doc/2
{
"tvalue":3.1415
}

GET my_index03/_mapping

GET my_index03/_search
{
"query": {
"term": {
"tvalue": {
"value": 3.1415
}
}
}
}

/<code>

注意:term查詢是不會返回結果的。

所以,實戰環境中,Mapping設定要注意如下節點:

  • 顯示的指定字段類型

  • 儘量避免使用動態模板(dynamic-templates)

  • 禁用日期檢測 (date_detection),默認情況下處於啟用狀態。“strict”實踐舉例:

<code>PUT my_index
{
"mappings": {
"dynamic": "strict",
"properties": {
"user": {
"properties": {
"name": {
"type": "text"
},
"social_networks": {
"dynamic": "strict",
"properties": {
"network_id": {
"type": "keyword"
},
"network_name": {
"type": "keyword"
}
}
}
}

}
}
}
}
/<code>

1.3 合理的設置string類型

Elasticsearch5.X 之後,String 被分成兩種類型,text和keyword。兩者的區別:

  • text:適用分詞全文檢索場景

  • keyword:適用字符串的精準匹配場景

默認,如果不顯示指定字段類型,字符串類型自定映射後的Mapping如下所示:

<code>"cont" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
/<code>

而公司實戰的業務場景,通常會面臨:

  • 需不需要分詞,不需要的話僅保留keyword即可。

  • 需要用什麼分詞?英文分詞還是中文分詞?

  • 分詞後是否還需要排序和聚合,即fielddata是否需要開啟

  • 是否需要精準匹配,即是否需要保留keyword

所以,回答瞭如上幾個問題,再有針對的顯示設定string類型的Mapping方為上策!

2、設置(Setting)

在這裡,我分享了Elasticsearch集群 設置 相關的技巧。

2.1 避免過度分片

分片是Elasticsearch的最大優勢之一,即將數據分散到多個節點以實施並行化。關於這個主題有過很多討論。

但請注意,索引的主分片一旦設置便無法更改(除非重建索引或者reindex)。

對於新來者來說,過度分片是一個非常普遍的陷阱。在做出任何決定之前,請確保先通讀官方的這篇博文:

我在 Elasticsearch 集群內應該設置多少個分片?

https://www.elastic.co/cn/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster

銘毅提示:

主分片數過多:

  • 批量寫入或者查詢請求被分割成過多的子寫入、子查詢,導致索引的寫入、查詢拒絕率上升。

主分片數過少:

  • 尤其對於數據量非常龐大的索引,若分片數過少或者就1個分片,會導致無法利用集群多節點資源(也就是分佈式特性),造成資源利用率不高或者不均衡,影響寫入或者查詢效率。

  • 並且,一旦該大的主分片出現問題,恢復起來耗時會非常長。

2.2 取消學習任何段合併的技巧

從本質上講,Elasticsearch是另一種分佈式 Lucene產品,就像Solr一樣 。在底層,大多數時候,每個Elasticsearch文檔都對應一個Lucene文檔(nested除外,如1.1所述)。在Lucene中,文檔存儲在 segment中。後臺的Elasticsearch通過以下兩種模式連續維護這些Lucene段:

  • 在Lucene中,當你刪除或更新文檔時,舊文檔被標記為已刪除,而新文檔被創建。Elasticsearch會跟蹤這些標記為deleted的文檔,適時對其段合併。

  • 新添加的文檔可能會產生大小不平衡的段。Elasticsearch可能會出於優化目的而決定將它們合併為更大的段。

ElasticSearch 開發人員最佳實戰指南

實戰中一定要注意:段合併是高度受磁盤I / O和CPU約束的操作。

作為用戶,我們不想讓段合併破壞Elasticsearch的查詢性能。

事實上,在某些情況下可以完全避免使用它們:一次構建索引,不再更改它。儘管在許多應用場景中可能很難滿足此條件。一旦開始插入新文檔或更新現有文檔,段合併就成為不可避免的一部分。

正在進行的段合併可能會嚴重破壞集群的總體查詢性能。在Google上進行隨機搜索,你會發現許多人發帖求助求助:“在段合併中減少對性能的影響的配置“,還有許多人共享某些適用於他們的配置。但,很多配置都是早期1.x,2.X版本的設置,新版本已經廢棄。

綜上,我進行段合併的經驗法則如下:

  • 取消學習任何段合併的技巧。早期版本的段合併配置是與Elasticsearch的內部緊密耦合的操作,新版本一般不再兼容。幾乎沒有“神秘”的底層配置修改可以使它運行得更快。

  • 找到translog flush 的最優配置 。嘗試調整index.translog.sync_interval和index.translog.flush_threshold_size設置。

詳見:https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-translog.html

  • 動態調整index.refresh_interval以滿足業務需求。如果實時性要求不高,可以調大刷新頻率(默認是1s,可以調到30s甚至更大)。

<code>PUT /twitter/_settings
{
"index" : {
"refresh_interval" : "30s"
}
}
/<code>

2.3 注意JVM內存設置

Elasticsearch可以根據兩個主要內存設置產生引人注目的性能特徵:

  • JVM堆空間——主要用途:緩存(節點緩存、分片請求緩存、Field data緩存以及索引緩存)

  • 堆外內存空間——lucene段文件緩存

ElasticSearch 開發人員最佳實戰指南

提醒你不要根據過去的非Elasticsearch JVM應用程序經驗來盲目設置Elasticsearch JVM堆大小。

詳見官方文檔:

https://www.elastic.co/guide/en/elasticsearch/reference/current/heap-size.html

3、查詢方式(Querying)

下面我收集了一些技巧,你可以在Elasticsearch查詢時使用它們。

3.1 Elasticseach裡面多線程修改如何保證數據準確性?

  • 1,用如下兩個參數校驗衝突

<code>PUT products/_doc/1?if_seq_no=1&if_primary_term=1
{ "title":"iphone", "count":100 }
/<code>
  • 2,用version避免衝突

<code>PUT products/_doc/1?version=30000&version_type=external
{ "title":"iphone", "count":100 }
/<code>

3.2 嘗試分割複雜的查詢,並行執行提升性能

如果你同時具有過濾器和聚合組件的複雜查詢,則在大多數情況下,可以將它們拆分為多個查詢並<code>並行/<code>執行它們可以提高查詢性能。

也就是說,在第一個查詢中,僅使用過濾器獲取匹配,然後在第二個查詢中,僅獲取聚合結果而無需再獲取檢索結果,即size: 0。

3.3 瞭解你的數字類型,防止被優化導致精度損失

許多JSON解析器可以進行各種優化,以提供有效的讀/寫性能。但可能造成了精度的損失,所以在選型Jackson json解析器時:優先使用BigDecimal和BigInteger。

3.4 不要使用Elasticsearch Transport / Node客戶端

TransportClient可以支持2.x,5.x版本,TransportClient將會在Elasticsearch 7.X版本棄用並在8.X版本中完成刪除.

官方推薦使用Java High Level REST Client,它使用HTTP請求而不是Java序列化請求。為了安全起見,堅持使用HTTP上的JSON格式,而不使用 SMILE (二進制格式)。

3.5 使用官方的Elasticsearch High-level REST客戶端

非官方客戶端一般更新太慢,幾乎無法跟上Elasticsearch新版本的特性,如:Jest客戶端近一年幾乎沒有更新,只支持到6.X版本。

相比之下,官方REST客戶端仍然是你相對最好的選擇。https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html

3.6 不要使用HTTP緩存來緩存Elasticsearch響應結果

由於便利性和低進入門檻,許多人陷入了將HTTP緩存(例如Varnish http://varnish-cache.org/)置於Elasticsearch集群前面的陷阱。使用HTTP緩存缺點如下:

在生產環境中使用Elasticsearch時,由於各種原因如:彈性擴展、測試和線上環境分離、零停機升級等,你很有可能最終會擁有多個集群。

(1)一旦為每個集群提供專用的HTTP緩存,99%的緩存內容是重複的。

(2)如果你決定對所有集群使用單個HTTP緩存,那麼很難以編程方式配置HTTP緩存以適應不斷變化的集群狀態的需求。

  1. 如何傳達集群負載以使緩存平衡流量?

  2. 如何配置計劃內或手動停機時間?

  3. 在維護時段期間,如何使緩存逐漸從一個集群遷移到另一個集群?

這些都是亟待考慮的問題。

如上所述,HTTP緩存很難以編程方式進行實現。當你需要手動刪除一個或多個條目時,它並不總是像DELETE FROM cache WHERE keys IN (...)查詢那樣容易。還得通過手動實現。

銘毅提示:這一條我實際沒有用過,有用過的童鞋可以留言討論。

3.7 使用基於_doc排序的slice scroll 遍歷數據

Scrolls 是Elasticsearch提供的一種遍歷工具,用來掃描整個數據集以獲取大量甚至全量數據。它在功能上及內部實現上與RDBMS遊標非常相似。但是,大多數人在第一次嘗試中都沒有使正確他。以下是一些基本知識:

  • 如果你接觸到scrolls,你可能正在讀取大量數據。slicing 很可能會幫助你顯著提高讀取性能。

  • 使用_doc進行排序,讀取速度就會提高20%+,而無需進行其他任何更改。(_doc是一個偽字段)

  • scrollId調用之後會有變化。因此,請確保你始終使用最新檢索的滾動scrollId。

  • 在Reindex的時候使用slicing 也能提升索引數據遷移效率。

3.8 單文檔檢索 優先使用 GET /index/type/{id}而非POST /index/_search

Elasticsearch使用不同的線程池來處理 GET /index/type/{id}和 POST /index/_search查詢。

使用POST /index/_search與有效載荷{query: {"match": {"_id": "123"}}}(或類似的東西)佔據搜索專用線程池。

在高負載下,這將同時降低搜索和單個文檔的獲取性能。

所以,單文檔堅持使用:GET /index/type/{id}。

3.9 使用size: 0和includes/ excludes限定字段返回

Elasticsearch在添加size: 0子句前後會帶來顯著的性能差異 。

除非業務需要,才返回必要字段,無需返回的字段通過includes和excludes控制。

3.10 提前做好壓力測試,瞭解系統支持的上限

分享我的個人最佳實踐:

  • 使用應用程序的性能基準( performance benchmarks)測試來估計應用程序能提供支持的性能負載上限。

    如基於esrally測試。

  • 避免將線程池與無限制的任務隊列一起使用。

    隊列的過度增長會對內存增加壓力。

  • 如果你的應用程序是藉助第三方引擎中轉或寫入數據(例如,從kafka隊列到Elasticsearch集群寫入數據),請確保你的生產者對消費者的壓力做出反應。

    也就是說,如果消費者延遲開始增加,則最好開始降低生產者的速度。

ElasticSearch 開發人員最佳實戰指南

3.11 在查詢中提供明確的超時

幾乎所有的Elasticsearch API都允許用戶指定超時。

找出並擺脫耗時長的操作,節省相關資源,建立穩定的服務,這將對你的應用程序和Elasticsearch集群都有幫助。

3.12 不要使用注入變量的JSON模板

永遠不要這樣做:

<code>{
"query": {
"bool": {
"filter": [
{
"term": {
"username": {
"value": {{username}}
}
}
},
{
"term": {
"password": {
"password": {{password}}
}
}
},
]
}
}
}
/<code>

防止SQL注入,只要有人通過惡意username 和password輸入,將暴露你的整個數據集,這只是時間問題。

我建議使用兩種安全的方法來生成動態查詢:

  • 使用Elasticsearch官方客戶端提供的查詢模型。(這在Java上效果很好。)

  • 使用JSON庫(例如Jackson)構建JSON樹並將其序列化為JSON。

4、實戰技巧(Strategy)

在最後一節中,我收集了解決上述未解決問題的便捷的實戰技巧。

4.1 始終(嘗試)堅持使用最新的JVM和ES版本

Elasticsearch是一個Java應用程序。像其他所有Java應用程序一樣,它也有hot paths和垃圾回收問題。幾乎每個新的JVM版本都會帶來很多優化,你可以不費吹灰之力利用這些優化。

Elasticsearch有一個官方頁面,列出了支持的JVM版本和垃圾收集器。在嘗試任何JVM升級之前,請務必先翻一翻如下文章清單:

https://www.elastic.co/guide/en/elasticsearch/guide/current/_don_8217_t_touch_these_settings.html

https://www.elastic.co/cn/support/matrix#matrix_jvm

注意:<code>Elasticsearch升級/<code>也是免費獲得性能提升的來源。

4.2 使用Elasticsearch完整和部分快照進行備份

Elasticsearch可以便捷的實現全部索引的全量快照或者部分索引數據的增量快照。

根據你的更新模式和索引大小,找到適合你的用例的快照最佳組合。

也就是說,例如,在00:00時有1個完整快照,在06:00、12:00和18:00時有3個局部增量快照。將它們存儲在第三方存儲也是一種好習慣。

有一些第三方 插件 可以簡化這些情況。

舉例:https://www.elastic.co/guide/en/elasticsearch/plugins/master/repository.html

與每份備份方案一樣,安全起見,請確保快照可以還原並反覆練習幾次。

4.3 有一個持續的性能測試平臺

像任何其他數據庫一樣,Elasticsearch在不同條件下顯示不同的性能:

  • 索引,文檔大小;

  • 更新,查詢/檢索模式;

  • 索引,集群設置;

  • 硬件,操作系統,JVM版本等。

很難跟蹤每個設置的改變以觀察其對整體性能的影響。確保你(至少)進行每日性能測試,以幫助縮小範圍,快速定位最近引入的、導致性能下降的可能的原因。

這種性能測試說起來容易做起來難。你需要確保測試環境:

  • 能有代表性的生產環境數據

  • 配置和生產環境一致

  • 完全覆蓋用例

  • 考慮包括操作系統緩存的測試的影響。

4.4 使用別名

告訴你一些頗有見地的實操經驗:永遠不要查詢索引,而要查詢 別名。

別名是指向實際索引的指針。你可以將一個或多個索引歸為一個別名。

許多Elasticsearch索引在索引名稱上都有內部上下文,例如events-20190515 代表20190515這一天的數據。

現在,在查詢events-*索引時,應用程序代碼中有兩個選擇:

  • 選擇1:通過特定日期格式即時確定索引名稱:events-YYYYMMDD。

這種方法有兩個主要缺點:

(1)需要回退到特定日期的索引,因此需要對整個代碼庫進行相應的設計以支持這種操作。

(2)撇開所有時鐘同步問題,在凌晨,你需要用程序或者腳本控制索引切換,確保數據寫入下一天索引。

  • 選擇2:創建一個events別名,指向events-*相關的索引。負責創建新索引的組件如:curator或者ILM(索引生命週期管理)可以自動將別名切換到新索引。

這種方法將帶來兩個明顯的好處:

(1)它沒有以前方法的缺點。

(2)只需指向events 別名,代碼就會更簡潔。

4.5 避免擁有大量同義詞

Elasticsearch支持索引階段和查詢階段指定 同義詞。

沒有同義詞,搜索引擎是不完整的,但實戰使用環境,注意如下問題:

  • 索引階段同義詞增加了索引大小,並增加了運行時開銷。

  • 查詢階段同義詞不會增加索引的大小,但顧名思義,這會增加運行時開銷。

  • 使用同義詞,很容易在嘗試修復其他問題時無意間破壞某些其他內容。

所以,要持續監視同義詞對性能的影響,並嘗試為添加的每個同義詞編寫測試用例。

同義詞官方文檔:

https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-synonym-tokenfilter.html

4.6 在啟用副本之前強制段合併及增加帶寬

一個非常常見的Elasticsearch用例是:定期(每兩小時一次)創建一個索引。

關於如何實現最佳性能,SoundCloud上有一篇非常不錯的 文章。從該文中引用,我特別發現以下幾項“必須”。

  • 在完成索引創建後,務必啟用副本。

  • 在啟用副本之前,請確保:

(1)通過強制合併來縮小索引大小;

<code>POST /twitter/_forcemerge
/<code>

(2)臨時增加副本傳輸帶寬,直到分配完成為止 indices.recovery.max_bytes_per_sec。默認:40mb,該屬性允許用戶在恢復過程中控制網絡的流量。設置一個比較大的值會導致網絡變得繁忙,當然恢復過程也會加快。可以通過如下方式調整:

<code>PUT /_cluster/settings
{
"transient": {
"indices.recovery.max_bytes_per_sec": "50mb"
}
}
/<code>

https://developers.soundcloud.com/blog/how-to-reindex-1-billion-documents-in-1-hour-at-soundcloud

4.7 記錄應用程序級別指標

Kibana對Elasticsearch性能提供了多維監控指標儀表盤:

ElasticSearch 開發人員最佳實戰指南
  • indexing,

  • search latency and throughput,

  • flush

  • merge operations

  • GC pauses

  • heap size

  • OS (CPU usage, disk I/O

  • kernel caches 等......

但,這還不夠。如果由多個應用程序使用,Elasticsearch將受到各種訪問模式的影響。

想象一下,你的應用程序A試圖刪除1000萬個不太重要的用戶文檔,而另一個組件B試圖更新用戶帳戶詳細信息。

如果你查看Elasticsearch監控指標,一切都是綠色正常。

但是,此時更新賬戶的用戶可能不滿意他們嘗試更新帳戶時的延遲。

因此,始終為你的Elasticsearch查詢提供額外的應用程序級指標。

儘管Elasticsearch結合kibana或者cerbro已經為整體集群性能提供了足夠的指標,但它們缺乏特定於操作的上下文監控,需要結合實際業務特事特辦。

4.8 重視CPU的配置選型和使用率監控

怎麼強調CPU都不過分。

從我過去的經驗來看,無論是寫負載還是讀負載場景,CPU一直是我們的瓶頸。

4.9 謹慎編寫自定義的Elasticsearch插件

  • 許多Elasticsearch版本包含重大的內部更改。你的插件所基於的公共API很可能會向後不兼容。

  • 你需要調整部署過程,不能再使用原始的Elasticsearch工作。

  • 由於你的應用程序依賴於於插件提供的特定功能,因此在集成測試過程中運行的Elasticsearch實例也需要包含插件。你也就不能再使用原始的Docker鏡像。

5、小結

本文是基於荷蘭計算機博士:Volkan Yazıcı 文章翻譯。翻譯工作得到原作者的同意和許可。

原文名稱:Elasticsearch Survival Guide for Developers

原文地址:https://vlkan.com/blog/post/2019/04/25/elasticsearch-survival-guide/#transport-client

文章很多細節值得實踐中進一步消化吸收。文章沒有直譯,而在原文基礎上,部分內容做了增刪,部分內容加了實踐和貼圖,以達到簡潔、通透的目的。

由於語言差異,儘管我翻譯後又修正了2遍,難免部分細節還可能有些拗口,歡迎大家留言討論。


分享到:


相關文章: