Elasticsearch 最佳性能優化

  • 2019 年 10 月 4 日
  • 筆記

本文轉自: http://wangnan.tech/post/elkstack-es03/,作者: Ghost Stories 侵刪。

批量提交

在 CRUD 章節,我們已經知道 ES 的數據寫入是如何操作的了。喜歡自己動手的讀者可能已經迫不及待的自己寫了程式開始往 ES 里寫數據做測試。這時候大家會發現:程式的運行速度非常一般,即使 ES 服務運行在本機,一秒鐘大概也就能寫入幾百條數據。

這種速度顯然不是 ES 的極限。事實上,每條數據經過一次完整的 HTTP POST 請求和 ES indexing 是一種極大的性能浪費,為此,ES 設計了批量提交方式。在數據讀取方面,叫 mget 介面,在數據變更方面,叫 bulk 介面。mget 一般常用於搜索時 ES 節點之間批量獲取中間結果集,對於 Elastic Stack 用戶,更常見到的是 bulk 介面。

bulk 介面採用一種比較簡樸的數據積累格式,示例如下:

# curl -XPOST http://127.0.0.1:9200/_bulk -d'  { "create" : { "_index" : "test", "_type" : "type1"  } }  { "field1" : "value1" }  { "delete" : { "_index" : "test", "_type" : "type1" } }  { "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }  { "field1" : "value2" }  { "update" : {"_id" : "1", "_type" : "type1", "_index" : "test"} }  { "doc" : {"field2" : "value2"} }  '  

格式是,每條 JSON 數據的上面,加一行描述性的元 JSON,指明下一行數據的操作類型,歸屬索引資訊等。

採用這種格式,而不是一般的 JSON 數組格式,是因為接收到 bulk 請求的 ES 節點,就可以不需要做完整的 JSON 數組解析處理,直接按行處理簡短的元 JSON,就可以確定下一行數據 JSON 轉發給哪個數據節點了。這樣,一個固定記憶體大小的 network buffer 空間,就可以反覆使用,又節省了大量 JVM 的 GC。

事實上,產品級的 logstash、rsyslog、spark 都是默認採用 bulk 介面進行數據寫入的。對於打算自己寫程式的讀者,建議採用 Perl 的 Search::Elasticsearch::Bulk 或者 Python 的 elasticsearch.helpers.* 庫。

bulk size

在配置 bulk 數據的時候,一般需要注意的就是請求體大小(bulk size)。

這裡有一點細節上的矛盾,我們知道,HTTP 請求,是可以通過 HTTP 狀態碼 100 Continue 來持續發送數據的。但對於 ES 節點接收 HTTP 請求體的 Content-Length 來說,是按照整個大小來計算的。所以,首先,要確保 bulk 數據不要超過 http.max_content_length 設置。

那麼,是不是盡量讓 bulk size 接近這個數值呢?當然不是。

依然是請求體的問題,因為請求體需要全部載入到記憶體,而 JVM Heap 一共就那麼多(按 31GB 算),過大的請求體,會擠占其他執行緒池的空間,反而導致寫入性能的下降。

再考慮網卡流量,磁碟轉速的問題,所以一般來說,建議 bulk 請求體的大小,在 15MB 左右,通過實際測試繼續向上探索最合適的設置。

注意:這裡說的 15MB 是請求體的位元組數,而不是程式里里設置的 bulk size。bulk size 一般指數據的條目數。不要忘了,bulk 請求體中,每條數據還會額外帶上一行元 JSON。

以 logstash 默認的 bulk_size => 5000 為例,假設單條數據平均大小 200B ,一次 bulk 請求體的大小就是 1.5MB。那麼我們可以嘗試 bulk_size => 50000;而如果單條數據平均大小是 20KB,一次 bulk 大小就是 100MB,顯然超標了,需要嘗試下調至 bulk_size => 500。

gateway

gateway 是 ES 設計用來長期存儲索引數據的介面。一般來說,大家都是用本地磁碟來存儲索引數據,即 gateway.type 為 local。

數據恢復中,有很多策略調整我們已經在之前分片控制小節講過。除開分片級別的控制以外,gateway 級別也還有一些可優化的地方:

  • gateway.recover_after_nodes 該參數控制集群在達到多少個節點的規模後,才開始數據恢復任務。這樣可以避免集群自動發現的初期,分片不全的問題。
  • gateway.recover_after_time 該參數控制集群在達到上條配置設置的節點規模後,再等待多久才開始數據恢復任務。
  • gateway.expected_nodes 該參數設置集群的預期節點總數。在達到這個總數後,即認為集群節點已經完全載入,即可開始數據恢復,不用再等待上條設置的時間。

注意:gateway 中說的節點,僅包括主節點和數據節點,純粹的 client 節點是不算在內的。如果你有更明確的選擇,也可以按需求寫:

  • gateway.recover_after_data_nodes
  • gateway.recover_after_master_nodes
  • gateway.expected_data_nodes
  • gateway.expected_master_nodes

共享存儲上的影子副本

雖然 ES 對 gateway 使用 NFS,iscsi 等共享存儲的方式極力反對,但是對於較大量級的索引的副本數據,ES 從 1.5 版本開始,還是提供了一種節約成本又不特別影響性能的方式:影子副本(shadow replica)。

首先,需要在集群各節點的 elasticsearch.yml 中開啟選項:

node.enable_custom_paths: true  

同時,確保各節點使用相同的路徑掛載了共享存儲,且目錄許可權為 Elasticsearch 進程用戶可讀可寫。

然後,創建索引:

# curl -XPUT 'http://127.0.0.1:9200/my_index' -d '  {      "index" : {          "number_of_shards" : 1,          "number_of_replicas" : 4,          "data_path": "/var/data/my_index",          "shadow_replicas": true      }  }'    

針對 shadow replicas ,ES 節點不會做實際的索引操作,而是單純的每次 flush 時,把 segment 內容 fsync 到共享存儲磁碟上。然後 refresh 讓其他節點能夠搜索該 segment 內容。

如果你已經決定把數據放到共享存儲上了,採用 shadow replicas 還是有一些好處的:

  1. 可以幫助你節省一部分不必要的多副本分片的數據寫入壓力;
  2. 在節點出現異常,需要在其他節點上恢復副本數據的時候,可以避免不必要的網路數據拷貝。

但是請注意:主分片節點還是要承擔一個副本的寫入過程,並不像 Lucene 的 FileReplicator 那樣通過複製文件完成,所以達不到完全節省 CPU 的效果。

shadow replicas 只是一個在某些特定環境下有用的方式。在資源允許的情況下,還是應該使用 local gateway。而另外採用 snapshot 介面來完成數據長期備份到 HDFS 或其他共享存儲的需要。

集群狀態維護

我們都知道,ES 中的 master 跟一般 MySQL、Hadoop 的 master 是不一樣的。它即不是寫入流量的唯一入口,也不是所有數據的元資訊的存放地點。所以,一般來說,ES 的 master 節點負載很輕,集群性能是可以近似認為隨著 data 節點的擴展線性提升的。

但是,上面這句話並不是完全正確的。

ES 中有一件事情是只有 master 節點能管理的,這就是集群狀態(cluster state)。

集群狀態中包括以下資訊:

  • 集群層面的設置
  • 集群內有哪些節點
  • 各索引的設置,映射,分析器和別名等
  • 索引內各分片所在的節點位置

這些資訊在集群的任意節點上都存放著,你也可以通過 /_cluster/state 介面直接讀取到其內容。注意這最後一項資訊,之前我們已經講過 ES 怎麼通過簡單地取余知道一條數據放在哪個分片里,加上現在集群狀態里又記載了分片在哪個節點上,那麼,整個集群里,任意節點都可以知道一條數據在哪個節點上存儲了。所以,數據讀寫才可以發送給集群里任意節點。

至於修改,則只能由 master 節點完成!顯然,集群狀態里大部分內容是極少變動的,唯獨有一樣除外——索引的映射。因為 ES 的 schema-less 特性,我們可以任意寫入 JSON 數據,所以索引中隨時可能增加新的欄位。這個時候,負責容納這條數據的主分片所在的節點,會暫停寫入操作,將欄位的映射結果傳遞給 master 節點;master 節點合併這段修改到集群狀態里,發送新版本的集群狀態到集群的所有節點上。然後寫入操作才會繼續。一般來說,這個操作是在一二十毫秒內就可以完成,影響也不大。

但是也有一些情況會是例外。

批量新索引創建

在較大規模的 Elastic Stack 應用場景中,這是比較常見的一個情況。因為 Elastic Stack 建議採用日期時間作為索引的劃分方式,所以定時(一般是每天),會統一產生一批新的索引。而前面已經講過,ES 的集群狀態每次更新都是阻塞式的發布到全部節點上以後,節點才能繼續後續處理。

這就意味著,如果在集群負載較高的時候,批量新建新索引,可能會有一個顯著的阻塞時間,無法寫入任何數據。要等到全部節點同步完成集群狀態以後,數據寫入才能恢復。

不巧的是,中國使用的是北京時間,UTC +0800。也就是說,默認的 Elastic Stack 新建索引時間是在早上 8 點。這個時間點一般日誌寫入量已經上漲到一定水平了(當然,晚上 0 點的量其實也不低)。

對此,可以通過定時任務,每天在最低谷的早上三四點,提前通過 POST mapping 的方式,創建好之後幾天的索引。就可以避免這個問題了。

如果你的日誌是比較嚴重的非結構化數據,這個問題在 2.0 版本後會變得更加嚴重。Elasticsearch 從 2.0 版本開始,對 mapping 更新做了重構。為了防止欄位類型衝突和減少 master 定期下發全量 cluster state 導致的大流量壓力,新的實現和舊實現的區別在:

  • 過去:每次 bulk 請求,本地生成索引後,將更新的 mapping,按照 _type 為單位構成 mapping 更新請求發給 master;
  • 現在:每次 bulk 請求,遍歷每條數據,將每條數據要更新的 mapping,都單獨發給 master,等到 master 通知完全集群,本地才能生成這一條數據的索引。

也就是說,一旦你日誌中欄位數量較多,在新創建索引的一段時間內,可能長達幾十分鐘一直被反覆鎖死!

過多欄位持續更新

這是另一種常見的濫用。在使用 Elastic Stack 處理訪問日誌時,為了查詢更方便,可能會採用 logstash-filter-kv 插件,將訪問日誌中的每個 URL 參數,都切分成單獨的欄位。比如一個 "/index.do?uid=1234567890&action=payload" 的 URL 會被轉換成如下 JSON:

  "urlpath" : "/index.do",    "urlargs" : {      "uid" : "1234567890",      "action" : "payload",      ...    }  

但是,因為集群狀態是存在所有節點的記憶體里的,一旦 URL 參數過多,ES 節點的記憶體就被大量用於存儲欄位映射內容。這是一個極大的浪費。如果碰上 URL 參數的鍵內容本身一直在變動,直接撐爆 ES 記憶體都是有可能的!

以上是真實發生的事件,開發人員莫名的選擇將一個 UUID 結果作為 key 放在 URL 參數里。直接導致 ES 集群 master 節點全部 OOM。

如果你在 ES 日誌中一直看到有新的 updating mapping [logstash-2015.06.01] 字樣出現的話,請鄭重考慮一下自己是不是用的上如此細分的欄位列表吧。

好,三秒鐘過去,如果你確定一定以及肯定還要這麼做,下面是一個變通的解決辦法。

nested object

用 nested object 來存放 URL 參數的方法稍微複雜,但還可以接受。單從 JSON 數據層面看,新方式的數據結構如下:

"urlargs": [      { "key": "uid", "value": "1234567890" },      { "key": "action", "value": "payload" },      ...    ]  

沒錯,看起來就是一個數組。但是 JSON 數組在 ES 里是有兩種處理方式的。

如果直接寫入數組,ES 在實際索引過程中,會把所有內容都平鋪開,變成 Arrays of Inner Objects。整條數據實際類似這樣的結構:

{    "urlpath" : ["/index.do"],    "urlargs.key" : ["uid", "action", ...],    "urlargs.value" : ["1234567890", "payload", ...]  

這種方式最大的問題是,當你採用 urlargs.key:"uid" AND urlargs.value:"0987654321"語句意圖搜索一個 uid=0987654321 的請求時,實際是整個 URL 參數中任意一處 value 為 0987654321 的,都會命中。

要想達到正確搜索的目的,需要在寫入數據之前,指定 urlargs 欄位的映射類型為 nested object。命令如下:

curl -XPOST http://127.0.0.1:9200/logstash-2015.06.01/_mapping -d '{    "accesslog" : {      "properties" : {        "urlargs" : {          "type" : "nested",          "properties" : {              "key" : { "type" : "string", "index" : "not_analyzed", "doc_values" : true },              "value" : { "type" : "string", "index" : "not_analyzed", "doc_values" : true }          }        }      }    }  }'  

這樣,數據實際是類似這樣的結構:

{    "urlpath" : ["/index.do"],  },  {    "urlargs.key" : ["uid"],    "urlargs.value" : ["1234567890"],  },  {    "urlargs.key" : ["action"],    "urlargs.value" : ["payload"],  }  

當然,nested object 節省欄位映射的優勢對應的是它在使用的複雜。Query 和 Aggs 都必須使用專門的 nested query 和 nested aggs 才能正確讀取到它。

nested query 語法如下:

curl -XPOST http://127.0.0.1:9200/logstash-2015.06.01/accesslog/_search -d '  {    "query": {      "bool": {        "must": [          { "match": { "urlpath" : "/index.do" }},          {            "nested": {              "path": "urlargs",              "query": {                "bool": {                  "must": [                    { "match": { "urlargs.key": "uid" }},                    { "match": { "urlargs.value": "1234567890" }}                  ]          }}}}        ]  }}}'  

nested aggs 語法如下:

curl -XPOST http://127.0.0.1:9200/logstash-2015.06.01/accesslog/_search -d '  {    "aggs": {      "topnuid": {        "nested": {          "path": "urlargs"        },        "aggs": {          "uid": {            "filter": {              "term": {                "urlargs.key": "uid",              }            },            "aggs": {              "topn": {                "terms": {                  "field": "urlargs.value"                }              }            }          }        }      }    }  }'  

快取

ES 內針對不同階段,設計有不同的快取。以此提升數據檢索時的響應性能。主要包括節點層面的 filter cache 和分片層面的 request cache。下面分別講述。

filter cache

ES 的 query DSL 在 2.0 版本之前分為 query 和 filter 兩種,很多檢索語法,是同時存在 query 和 filter 里的。比如最常用的 term、prefix、range 等。怎麼選擇是使用 query 還是 filter 成為很多用戶頭疼的難題。於是從 2.0 版本開始,ES 乾脆合併了 filter 統一歸為 query。但是具體的檢索語法本身,依然有 query 和 filter 上下文的區別。ES 依靠這個上下文判斷,來自動決定是否啟用 filter cache。

query 跟 filter 上下文的區別,簡單來說:

  • query 是要相關性評分的,filter 不要;
  • query 結果無法快取,filter 可以。

所以,選擇也就出來了:

  • 全文搜索、評分排序,使用 query;
  • 是非過濾,精確匹配,使用 filter。

不過我們要怎麼寫,才能讓 ES 正確判斷呢?看下面這個請求:

# curl -XGET http://127.0.0.1:9200/_search -d '  {      "query": {          "bool": {              "must_not": [                  { "match": { "title": "Search" } }              ],              "must": [                  { "match": { "content": "Elasticsearch" } }              ],              "filter": [                  { "term":  { "status": "published" } },                  { "range": { "publish_date": { "gte": "2015-01-01" } } }              ]          }      }  }'  

在這個請求中,

  1. ES 先看到一個 query,那麼進入 query 上下文。
  2. 然後在 bool 里看到一個 must_not,那麼改進入 filter 上下文,這個有關 title 欄位的查詢不參與評分。
  3. 然後接著是一個 must 的 match,這個又屬於 query 上下文,這個有關 content 欄位的查詢會影響評分。
  4. 最後碰到 filter,還屬於 filter 上下文,這個有關 status 和 publish_date 欄位的查詢不參與評分。

需要注意的是,filter cache 是節點層面的快取設置,每個節點上所有數據在響應請求時,是共用一個快取空間的。當空間用滿,按照 LRU 策略淘汰掉最冷的數據。

可以用 indices.cache.filter.size 配置來設置這個快取空間的大小,默認是 JVM 堆的 10%,也可以設置一個絕對值。注意這是一個靜態值,必須在 elasticsearch.yml 中提前配置。

shard request cache

ES 還有另一個分片層面的快取,叫 shard request cache。5.0 之前的版本中,request cache 的用途並不大,因為 query cache 要起作用,還有幾個先決條件:

  1. 分片數據不再變動,也就是對當天的索引是無效的(如果 refresh_interval 很大,那麼在這個間隔內倒也算有效);
  2. 使用了 "now" 語法的請求無法被快取,因為這個是要即時計算的;
  3. 快取的鍵是請求的整個 JSON 字元串,整個字元串發生任何位元組變動,快取都無效。

以 Elastic Stack 場景來說,Kibana 里幾乎所有的請求,都是有 @timestamp 作為過濾條件的,而且大多數是以最近 N 小時/分鐘這樣的選項,也就是說,頁面每次刷新,發出的請求 JSON 里的時間過濾部分都是在變動的。query cache 在處理 Kibana 發出的請求時,完全無用。

而 5.0 版本的一大特性,叫 instant aggregation。解決了這個先決條件的一大阻礙。

在之前的版本,Elasticsearch 接收到請求之後,直接把請求原樣轉發給各分片,由各分片所在的節點自行完成請求的解析,進行實際的搜索操作。所以快取的鍵是原始 JSON 串。

而 5.0 的重構後,接收到請求的節點先把請求的解析做完,發送到各節點的是統一拆分修改好的請求,這樣就不再擔心 JSON 串多個空格啥的了。

其次,上面說的『拆分修改』是怎麼回事呢?

比如,我們在 Kibana 里搜索一個最近 7 天(@timestamp:["now-7d" TO "now"])的數據,ES 就可以根據按天索引的判斷,知道從 6 天前到昨天這 5 個索引是肯定全覆蓋的。那麼這個橫跨 7 天的 date range query 就變成了 5 個 match_all query 加 2 個短時間的 date_rangequery。

現在你的儀錶盤過 5 分鐘自動刷新一次,再提交上來一次最近 7 天的請求,中間這 5 個 match_all 就完全一樣了,直接從 request cache 返回即可,需要重新請求的,只有兩頭真正在變動的 date_range 了。

注1:match_all 不用遍歷倒排索引,比直接查詢 @timestamp:* 要快很多。 注2:判斷覆蓋修改為 match_all 並不是真的按照索引名稱,而是 ES 從 2.x 開始提供的 field_stats 介面可以直接獲取到 @timestamp 在本索引內的 max/min 值。當然從概念上如此理解也是可以接受的。

field_stats 介面

curl -XGET "http://localhost:9200/logstash-2016.11.25/_field_stats?fields=timestamp"  

響應結果如下:

{      "_shards": {          "total": 1,          "successful": 1,          "failed": 0      },      "indices": {          "logstash-2016.11.25": {              "fields": {                  "timestamp": {                      "max_doc": 1326564,                      "doc_count": 564633,                      "density": 42,                      "sum_doc_freq": 2258532,                      "sum_total_term_freq": -1,                      "min_value": "2008-08-01T16:37:51.513Z",                      "max_value": "2013-06-02T03:23:11.593Z",                      "is_searchable": "true",                      "is_aggregatable": "true"                  }              }          }      }  }  

和 filter cache 一樣,request cache 的大小也是以節點級別控制的,配置項名為 indices.requests.cache.size,其默認值為 1%。

欄位數據

欄位數據(fielddata),在 Lucene 中又叫 uninverted index。我們都知道,搜索引擎會使用倒排索引(inverted index)來映射單詞到文檔的 ID 號。而同時,為了提供對文檔內容的聚合,Lucene 還可以在運行時將每個欄位的單詞以字典序排成另一個 uninverted index,可以大大加速計算性能。

作為一個加速性能的方式,fielddata 當然是被全部載入在記憶體的時候最為有效。這也是 ES 默認的運行設置。但是,記憶體是有限的,所以 ES 同時也需要提供對 fielddata 記憶體的限額方式:

  • indices.fielddata.cache.size 節點用於 fielddata 的最大記憶體,如果 fielddata 達到該閾值,就會把舊數據交換出去。該參數可以設置百分比或者絕對值。默認設置是不限制,所以強烈建議設置該值,比如 10%。
  • indices.fielddata.cache.expire 進入 fielddata 記憶體中的數據多久自動過期。注意,因為 ES 的 fielddata 本身是一種數據結構,而不是簡單的快取,所以過期刪除 fielddata 是一個非常消耗資源的操作。ES 官方在文檔中特意說明,這個參數絕對絕對不要設置!

Circuit Breaker

Elasticsearch 在 total,fielddata,request 三個層面上都設計有 circuit breaker 以保護進程不至於發生 OOM 事件。在 fielddata 層面,其設置為:

  • indices.breaker.fielddata.limit 默認是 JVM 堆記憶體大小的 60%。注意,為了讓設置正常發揮作用,如果之前設置過 indices.fielddata.cache.size 的,一定要確保 indices.breaker.fielddata.limit 的值大於 indices.fielddata.cache.size 的值。否則的話,fielddata 大小一到 limit 閾值就報錯,就永遠道不了 size 閾值,無法觸發對舊數據的交換任務了。

doc values

但是相比較集群龐大的數據量,記憶體本身是遠遠不夠的。為了解決這個問題,ES 引入了另一個特性,可以對精確索引的欄位,指定 fielddata 的存儲方式。這個配置項叫:doc_values。

所謂 doc_values,其實就是在 ES 將數據寫入索引的時候,提前生成好 fielddata 內容,並記錄到磁碟上。因為 fielddata 數據是順序讀寫的,所以即使在磁碟上,通過文件系統層的快取,也可以獲得相當不錯的性能。

注意:因為 doc_values 是在數據寫入時即生成內容,所以,它只能應用在精準索引的欄位上,因為索引進程沒法知道後續會有什麼分詞器生成的結果。

由於在 Elastic Stack 場景中,doc_values 的使用極其頻繁,到 Elasticsearch 5.0 以後,這兩者的區別被徹底強化成兩個不同欄位類型:text 和 keyword。

"myfieldname": {      "type": "text"  }  

等同於過去的:

"myfieldname": {      "type": "string",      "fielddata": false  }  

而

"myfieldname": {      "type": "keyword"  }  

等同於過去的:

"myfieldname": {      "type": "string",      "index": "not_analyzed",      "doc_values": true  }  

也就是說,以後的用戶,已經不太需要在意 fielddata 的問題了。不過依然有少數情況,你會需要對分詞欄位做聚合統計的話,你可以在自己接受範圍內,開啟這個特性:

{      "mappings": {          "my_type": {              "properties": {                  "message": {                      "type": "text",                      "fielddata": true,                      "fielddata_frequency_filter": {                          "min": 0.1,                          "max": 1.0,                          "min_segment_size": 500                      }                  }              }          }      }  }  

你可以看到在上面加了一段 fielddata_frequency_filter 配置,這個配置是 segment 級別的。上面示例的意思是:只有這個 segment 里的文檔數量超過 500 個,而且含有該欄位的文檔數量占該 segment 里的文檔數量比例超過 10% 時,才載入這個 segment 的 fielddata。

下面是一個可能有用的對分詞欄位做聚合的示例:

curl -XPOST 'http://localhost:9200/logstash-2016.07.18/logs/_search?pretty&terminate_after=10000&size=0' -d '  {      "aggs": {          "group": {              "terms": {                  "field": "punct"              },              "aggs": {                  "keyword": {                      "significant_terms": {                          "size": 2,                          "field": "message"                      },                      "aggs": {                          "hit": {                              "top_hits": {                                  "_source": {                                      "include": [ "message" ]                                  },                                  "size":1                              }                          }                      }                  }              }          }      }  }'  

這個示例可以對經過了 logstash-filter-punct 插件處理的數據,獲取每種 punct 類型日誌的關鍵詞和對應的代表性日誌原文。其效果類似 Splunk 的事件模式功能:

curator

如果經過之前章節的一系列優化之後,數據確實超過了集群能承載的能力,除了拆分集群以外,最後就只剩下一個辦法了:清除廢舊索引。

為了更加方便的做清除數據,合併 segment,備份恢復等管理任務,Elasticsearch 在提供相關 API 的同時,另外準備了一個命令行工具,叫 curator 。curator 是 Python 程式,可以直接通過 pypi 庫安裝:

pip install elasticsearch-curator  

注意,是 elasticsearch-curator 不是 curator。PyPi 原先就有另一個項目叫這個名字

參數介紹

和 Elastic Stack 里其他組件一樣,curator 也是被 Elastic.co 收購的原開源社區周邊。收編之後同樣進行了一次重構,命令行參數從單字母風格改成了長單詞風格。新版本的 curator 命令可用參數如下:

Usage: curator [OPTIONS] COMMAND [ARGS]…

Options 包括:

–host TEXT Elasticsearch host.  –url_prefix TEXT Elasticsearch http url prefix.  –port INTEGER Elasticsearch port.  –use_ssl Connect to Elasticsearch through SSL.  –http_auth TEXT Use Basic Authentication ex: user:pass  –timeout INTEGER Connection timeout in seconds.  –master-only Only operate on elected master node.  –dry-run Do not perform any changes.  –debug Debug mode  –loglevel TEXT Log level  –logfile TEXT log file  –logformat TEXT Log output format [default|logstash].  –version Show the version and exit.  –help Show this message and exit.  

Commands 包括:

alias Index Aliasing  allocation Index Allocation  bloom Disable bloom filter cache  close Close indices  delete Delete indices or snapshots  open Open indices  optimize Optimize Indices  replicas Replica Count Per-shard  show Show indices or snapshots  snapshot Take snapshots of indices (Backup)  

針對具體的 Command,還可以繼續使用 --help 查看該子命令的幫助。比如查看 close 子命令的幫助,輸入 curator close --help,結果如下:

Usage: curator close [OPTIONS] COMMAND [ARGS]...      Close indices    Options:    --help  Show this message and exit.    Commands:    indices  Index selection.  

常用示例

在使用 1.4.0 以上版本的 Elasticsearch 前提下,curator 曾經主要的一個子命令 bloom 已經不再需要使用。所以,目前最常用的三個子命令,分別是 close, delete 和 optimize,示例如下:

curator --timeout 36000 --host 10.0.0.100 delete indices --older-than 5 --time-unit days --timestring '%Y.%m.%d' --prefix logstash-mweibo-nginx-  curator --timeout 36000 --host 10.0.0.100 delete indices --older-than 10 --time-unit days --timestring '%Y.%m.%d' --prefix logstash-mweibo-client- --exclude 'logstash-mweibo-client-2015.05.11'  curator --timeout 36000 --host 10.0.0.100 delete indices --older-than 30 --time-unit days --timestring '%Y.%m.%d' --regex '^logstash-mweibo-d+'  curator --timeout 36000 --host 10.0.0.100 close indices --older-than 7 --time-unit days --timestring '%Y.%m.%d' --prefix logstash-  curator --timeout 36000 --host 10.0.0.100 optimize --max_num_segments 1 indices --older-than 1 --newer-than 7 --time-unit days --timestring '%Y.%m.%d' --prefix logstash-  

這一頓任務,結果是:

logstash-mweibo-nginx-yyyy.mm.dd 索引保存最近 5 天,logstash-mweibo-client-yyyy.mm.dd 保存最近 10 天,logstash-mweibo-yyyy.mm.dd 索引保存最近 30 天;且所有七天前的 logstash-* 索引都暫時關閉不用;最後對所有非當日日誌做 segment 合併優化。

profiler

profiler 是 Elasticsearch 5.0 的一個新介面。通過這個功能,可以看到一個搜索聚合請求,是如何拆分成底層的 Lucene 請求,並且顯示每部分的耗時情況。

啟用 profiler 的方式很簡單,直接在請求里加一行即可:

curl -XPOST 'http://localhost:9200/_search' -d '{      "profile": true,      "query": { ... },      "aggs": { ... }  }'  

可以看到其中對 query 和 aggs 部分的返回是不太一樣的。

query

query 部分包括 collectors、rewrite 和 query 部分。對複雜 query,profiler 會拆分 query 成多個基礎的 TermQuery,然後每個 TermQuery 再顯示各自的分階段耗時如下:

"breakdown": {      "score": 51306,      "score_count": 4,      "build_scorer": 2935582,      "build_scorer_count": 1,      "match": 0,      "match_count": 0,      "create_weight": 919297,      "create_weight_count": 1,      "next_doc": 53876,      "next_doc_count": 5,      "advance": 0,      "advance_count": 0  }  

aggs

"time": "1124.864392ms",  "breakdown": {      "reduce": 0,      "reduce_count": 0,      "build_aggregation": 1394,      "build_aggregation_count": 150,      "initialise": 2883,      "initialize_count": 150,      "collect": 1124860115,      "collect_count": 900  }  

我們可以很明顯的看到聚合統計在初始化階段、收集階段、構建階段、匯總階段分別花了多少時間,遍歷了多少數據。

注意其中 reduce 階段還沒實現完畢,所有都是 0。因為目前 profiler 只能在 shard 級別上做統計。

collect 階段的耗時,有助於我們調整對應 aggs 的 collect_mode 參數選擇。目前 Elasticsearch 支援 breadth_first 和 depth_first 兩種方式。

initialise 階段的耗時,有助於我們調整對應 aggs 的 execution_hint 參數選擇。目前 Elasticsearch 支援 map、global_ordinals_low_cardinality、global_ordinals 和 global_ordinals_hash 四種選擇。在計算離散度比較大的欄位統計值時,適當調整該參數,有益於節省記憶體和提高計算速度。

對高離散度欄位值統計性能很關注的讀者,可以關注 https://github.com/elastic/elasticsearch/pull/21626 這條記錄的進展。

文本整理自《ELKstack權威指南》

Tags: