Elasticsearch性能優(yōu)化

image

歡迎訪問我的博客查看原文:http://wangnan.tech

注:文本整理自《ELKstack權(quán)威指南》

目錄

  • 批量提交
  • gateway
  • 集群狀態(tài)維護(hù)
  • 緩存
  • 字段數(shù)據(jù)
  • curator
  • profiler

批量提交

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

這種速度顯然不是 ES 的極限。事實(shí)上,每條數(shù)據(jù)經(jīng)過一次完整的 HTTP POST 請求和 ES indexing 是一種極大的性能浪費(fèi),為此,ES 設(shè)計(jì)了批量提交方式。在數(shù)據(jù)讀取方面,叫 mget 接口,在數(shù)據(jù)變更方面,叫 bulk 接口。mget 一般常用于搜索時(shí) ES 節(jié)點(diǎn)之間批量獲取中間結(jié)果集,對于 Elastic Stack 用戶,更常見到的是 bulk 接口。

bulk 接口采用一種比較簡樸的數(shù)據(jù)積累格式,示例如下:

# 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 數(shù)據(jù)的上面,加一行描述性的元 JSON,指明下一行數(shù)據(jù)的操作類型,歸屬索引信息等。

采用這種格式,而不是一般的 JSON 數(shù)組格式,是因?yàn)榻邮盏?bulk 請求的 ES 節(jié)點(diǎn),就可以不需要做完整的 JSON 數(shù)組解析處理,直接按行處理簡短的元 JSON,就可以確定下一行數(shù)據(jù) JSON 轉(zhuǎn)發(fā)給哪個(gè)數(shù)據(jù)節(jié)點(diǎn)了。這樣,一個(gè)固定內(nèi)存大小的 network buffer 空間,就可以反復(fù)使用,又節(jié)省了大量 JVM 的 GC。

事實(shí)上,產(chǎn)品級的 logstash、rsyslog、spark 都是默認(rèn)采用 bulk 接口進(jìn)行數(shù)據(jù)寫入的。對于打算自己寫程序的讀者,建議采用 Perl 的 Search::Elasticsearch::Bulk 或者 Python 的 elasticsearch.helpers.* 庫。

bulk size

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

這里有一點(diǎn)細(xì)節(jié)上的矛盾,我們知道,HTTP 請求,是可以通過 HTTP 狀態(tài)碼 100 Continue 來持續(xù)發(fā)送數(shù)據(jù)的。但對于 ES 節(jié)點(diǎn)接收 HTTP 請求體的 Content-Length 來說,是按照整個(gè)大小來計(jì)算的。所以,首先,要確保 bulk 數(shù)據(jù)不要超過 http.max_content_length 設(shè)置。

那么,是不是盡量讓 bulk size 接近這個(gè)數(shù)值呢?當(dāng)然不是。

依然是請求體的問題,因?yàn)檎埱篌w需要全部加載到內(nèi)存,而 JVM Heap 一共就那么多(按 31GB 算),過大的請求體,會擠占其他線程池的空間,反而導(dǎo)致寫入性能的下降。

再考慮網(wǎng)卡流量,磁盤轉(zhuǎn)速的問題,所以一般來說,建議 bulk 請求體的大小,在 15MB 左右,通過實(shí)際測試?yán)^續(xù)向上探索最合適的設(shè)置。

注意:這里說的 15MB 是請求體的字節(jié)數(shù),而不是程序里里設(shè)置的 bulk size。bulk size 一般指數(shù)據(jù)的條目數(shù)。不要忘了,bulk 請求體中,每條數(shù)據(jù)還會額外帶上一行元 JSON。

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

gateway

gateway 是 ES 設(shè)計(jì)用來長期存儲索引數(shù)據(jù)的接口。一般來說,大家都是用本地磁盤來存儲索引數(shù)據(jù),即 gateway.typelocal

數(shù)據(jù)恢復(fù)中,有很多策略調(diào)整我們已經(jīng)在之前分片控制小節(jié)講過。除開分片級別的控制以外,gateway 級別也還有一些可優(yōu)化的地方:

  • gateway.recover_after_nodes
    該參數(shù)控制集群在達(dá)到多少個(gè)節(jié)點(diǎn)的規(guī)模后,才開始數(shù)據(jù)恢復(fù)任務(wù)。這樣可以避免集群自動發(fā)現(xiàn)的初期,分片不全的問題。

  • gateway.recover_after_time
    該參數(shù)控制集群在達(dá)到上條配置設(shè)置的節(jié)點(diǎn)規(guī)模后,再等待多久才開始數(shù)據(jù)恢復(fù)任務(wù)。

  • gateway.expected_nodes
    該參數(shù)設(shè)置集群的預(yù)期節(jié)點(diǎn)總數(shù)。在達(dá)到這個(gè)總數(shù)后,即認(rèn)為集群節(jié)點(diǎn)已經(jīng)完全加載,即可開始數(shù)據(jù)恢復(fù),不用再等待上條設(shè)置的時(shí)間。

注意:gateway 中說的節(jié)點(diǎn),僅包括主節(jié)點(diǎn)和數(shù)據(jù)節(jié)點(diǎn),純粹的 client 節(jié)點(diǎn)是不算在內(nèi)的。如果你有更明確的選擇,也可以按需求寫:

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

共享存儲上的影子副本

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

首先,需要在集群各節(jié)點(diǎn)的 elasticsearch.yml 中開啟選項(xiàng):

node.enable_custom_paths: true

同時(shí),確保各節(jié)點(diǎn)使用相同的路徑掛載了共享存儲,且目錄權(quán)限為 Elasticsearch 進(jìn)程用戶可讀可寫。

然后,創(chuàng)建索引:

# 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 節(jié)點(diǎn)不會做實(shí)際的索引操作,而是單純的每次 flush 時(shí),把 segment 內(nèi)容 fsync 到共享存儲磁盤上。然后 refresh 讓其他節(jié)點(diǎn)能夠搜索該 segment 內(nèi)容。

如果你已經(jīng)決定把數(shù)據(jù)放到共享存儲上了,采用 shadow replicas 還是有一些好處的:

  1. 可以幫助你節(jié)省一部分不必要的多副本分片的數(shù)據(jù)寫入壓力;
  2. 在節(jié)點(diǎn)出現(xiàn)異常,需要在其他節(jié)點(diǎn)上恢復(fù)副本數(shù)據(jù)的時(shí)候,可以避免不必要的網(wǎng)絡(luò)數(shù)據(jù)拷貝。

但是請注意:主分片節(jié)點(diǎn)還是要承擔(dān)一個(gè)副本的寫入過程,并不像 Lucene 的 FileReplicator 那樣通過復(fù)制文件完成,所以達(dá)不到完全節(jié)省 CPU 的效果。

shadow replicas 只是一個(gè)在某些特定環(huán)境下有用的方式。在資源允許的情況下,還是應(yīng)該使用 local gateway。而另外采用 snapshot 接口來完成數(shù)據(jù)長期備份到 HDFS 或其他共享存儲的需要。

集群狀態(tài)維護(hù)

我們都知道,ES 中的 master 跟一般 MySQL、Hadoop 的 master 是不一樣的。它即不是寫入流量的唯一入口,也不是所有數(shù)據(jù)的元信息的存放地點(diǎn)。所以,一般來說,ES 的 master 節(jié)點(diǎn)負(fù)載很輕,集群性能是可以近似認(rèn)為隨著 data 節(jié)點(diǎn)的擴(kuò)展線性提升的。

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

ES 中有一件事情是只有 master 節(jié)點(diǎn)能管理的,這就是集群狀態(tài)(cluster state)。

集群狀態(tài)中包括以下信息:

  • 集群層面的設(shè)置
  • 集群內(nèi)有哪些節(jié)點(diǎn)
  • 各索引的設(shè)置,映射,分析器和別名等
  • 索引內(nèi)各分片所在的節(jié)點(diǎn)位置

這些信息在集群的任意節(jié)點(diǎn)上都存放著,你也可以通過 /_cluster/state 接口直接讀取到其內(nèi)容。注意這最后一項(xiàng)信息,之前我們已經(jīng)講過 ES 怎么通過簡單地取余知道一條數(shù)據(jù)放在哪個(gè)分片里,加上現(xiàn)在集群狀態(tài)里又記載了分片在哪個(gè)節(jié)點(diǎn)上,那么,整個(gè)集群里,任意節(jié)點(diǎn)都可以知道一條數(shù)據(jù)在哪個(gè)節(jié)點(diǎn)上存儲了。所以,數(shù)據(jù)讀寫才可以發(fā)送給集群里任意節(jié)點(diǎn)。

至于修改,則只能由 master 節(jié)點(diǎn)完成!顯然,集群狀態(tài)里大部分內(nèi)容是極少變動的,唯獨(dú)有一樣除外——索引的映射。因?yàn)?ES 的 schema-less 特性,我們可以任意寫入 JSON 數(shù)據(jù),所以索引中隨時(shí)可能增加新的字段。這個(gè)時(shí)候,負(fù)責(zé)容納這條數(shù)據(jù)的主分片所在的節(jié)點(diǎn),會暫停寫入操作,將字段的映射結(jié)果傳遞給 master 節(jié)點(diǎn);master 節(jié)點(diǎn)合并這段修改到集群狀態(tài)里,發(fā)送新版本的集群狀態(tài)到集群的所有節(jié)點(diǎn)上。然后寫入操作才會繼續(xù)。一般來說,這個(gè)操作是在一二十毫秒內(nèi)就可以完成,影響也不大。

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

批量新索引創(chuàng)建

在較大規(guī)模的 Elastic Stack 應(yīng)用場景中,這是比較常見的一個(gè)情況。因?yàn)?Elastic Stack 建議采用日期時(shí)間作為索引的劃分方式,所以定時(shí)(一般是每天),會統(tǒng)一產(chǎn)生一批新的索引。而前面已經(jīng)講過,ES 的集群狀態(tài)每次更新都是阻塞式的發(fā)布到全部節(jié)點(diǎn)上以后,節(jié)點(diǎn)才能繼續(xù)后續(xù)處理。

這就意味著,如果在集群負(fù)載較高的時(shí)候,批量新建新索引,可能會有一個(gè)顯著的阻塞時(shí)間,無法寫入任何數(shù)據(jù)。要等到全部節(jié)點(diǎn)同步完成集群狀態(tài)以后,數(shù)據(jù)寫入才能恢復(fù)。

不巧的是,中國使用的是北京時(shí)間,UTC +0800。也就是說,默認(rèn)的 Elastic Stack 新建索引時(shí)間是在早上 8 點(diǎn)。這個(gè)時(shí)間點(diǎn)一般日志寫入量已經(jīng)上漲到一定水平了(當(dāng)然,晚上 0 點(diǎn)的量其實(shí)也不低)。

對此,可以通過定時(shí)任務(wù),每天在最低谷的早上三四點(diǎn),提前通過 POST mapping 的方式,創(chuàng)建好之后幾天的索引。就可以避免這個(gè)問題了。

如果你的日志是比較嚴(yán)重的非結(jié)構(gòu)化數(shù)據(jù),這個(gè)問題在 2.0 版本后會變得更加嚴(yán)重。 Elasticsearch 從 2.0 版本開始,對 mapping 更新做了重構(gòu)。為了防止字段類型沖突和減少 master 定期下發(fā)全量 cluster state 導(dǎo)致的大流量壓力,新的實(shí)現(xiàn)和舊實(shí)現(xiàn)的區(qū)別在:

  • 過去:每次 bulk 請求,本地生成索引后,將更新的 mapping,按照 _type 為單位構(gòu)成 mapping 更新請求發(fā)給 master;
  • 現(xiàn)在:每次 bulk 請求,遍歷每條數(shù)據(jù),將每條數(shù)據(jù)要更新的 mapping,都單獨(dú)發(fā)給 master,等到 master 通知完全集群,本地才能生成這一條數(shù)據(jù)的索引。

也就是說,一旦你日志中字段數(shù)量較多,在新創(chuàng)建索引的一段時(shí)間內(nèi),可能長達(dá)幾十分鐘一直被反復(fù)鎖死!

過多字段持續(xù)更新

這是另一種常見的濫用。在使用 Elastic Stack 處理訪問日志時(shí),為了查詢更方便,可能會采用 logstash-filter-kv 插件,將訪問日志中的每個(gè) URL 參數(shù),都切分成單獨(dú)的字段。比如一個(gè) "/index.do?uid=1234567890&action=payload" 的 URL 會被轉(zhuǎn)換成如下 JSON:

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

但是,因?yàn)榧籂顟B(tài)是存在所有節(jié)點(diǎn)的內(nèi)存里的,一旦 URL 參數(shù)過多,ES 節(jié)點(diǎn)的內(nèi)存就被大量用于存儲字段映射內(nèi)容。這是一個(gè)極大的浪費(fèi)。如果碰上 URL 參數(shù)的鍵內(nèi)容本身一直在變動,直接撐爆 ES 內(nèi)存都是有可能的!

以上是真實(shí)發(fā)生的事件,開發(fā)人員莫名的選擇將一個(gè) UUID 結(jié)果作為 key 放在 URL 參數(shù)里。直接導(dǎo)致 ES 集群 master 節(jié)點(diǎn)全部 OOM。

如果你在 ES 日志中一直看到有新的 updating mapping [logstash-2015.06.01] 字樣出現(xiàn)的話,請鄭重考慮一下自己是不是用的上如此細(xì)分的字段列表吧。

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

nested object

用 nested object 來存放 URL 參數(shù)的方法稍微復(fù)雜,但還可以接受。單從 JSON 數(shù)據(jù)層面看,新方式的數(shù)據(jù)結(jié)構(gòu)如下:

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

沒錯(cuò),看起來就是一個(gè)數(shù)組。但是 JSON 數(shù)組在 ES 里是有兩種處理方式的。

如果直接寫入數(shù)組,ES 在實(shí)際索引過程中,會把所有內(nèi)容都平鋪開,變成 Arrays of Inner Objects。整條數(shù)據(jù)實(shí)際類似這樣的結(jié)構(gòu):

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

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

要想達(dá)到正確搜索的目的,需要在寫入數(shù)據(jù)之前,指定 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 }
        }
      }
    }
  } 
}'

這樣,數(shù)據(jù)實(shí)際是類似這樣的結(jié)構(gòu):

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

當(dāng)然,nested object 節(jié)省字段映射的優(yōu)勢對應(yīng)的是它在使用的復(fù)雜。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 內(nèi)針對不同階段,設(shè)計(jì)有不同的緩存。以此提升數(shù)據(jù)檢索時(shí)的響應(yīng)性能。主要包括節(jié)點(diǎn)層面的 filter cache 和分片層面的 request cache。下面分別講述。

filter cache

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

query 跟 filter 上下文的區(qū)別,簡單來說:

  • query 是要相關(guān)性評分的,filter 不要;
  • query 結(jié)果無法緩存,filter 可以。

所以,選擇也就出來了:

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

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

# 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" } } }
            ]
        }
    }
}'

在這個(gè)請求中,

  1. ES 先看到一個(gè) query,那么進(jìn)入 query 上下文。
  2. 然后在 bool 里看到一個(gè) must_not,那么改進(jìn)入 filter 上下文,這個(gè)有關(guān) title 字段的查詢不參與評分。
  3. 然后接著是一個(gè) mustmatch,這個(gè)又屬于 query 上下文,這個(gè)有關(guān) content 字段的查詢會影響評分。
  4. 最后碰到 filter,還屬于 filter 上下文,這個(gè)有關(guān) status 和 publish_date 字段的查詢不參與評分。

需要注意的是,filter cache 是節(jié)點(diǎn)層面的緩存設(shè)置,每個(gè)節(jié)點(diǎn)上所有數(shù)據(jù)在響應(yīng)請求時(shí),是共用一個(gè)緩存空間的。當(dāng)空間用滿,按照 LRU 策略淘汰掉最冷的數(shù)據(jù)。

可以用 indices.cache.filter.size 配置來設(shè)置這個(gè)緩存空間的大小,默認(rèn)是 JVM 堆的 10%,也可以設(shè)置一個(gè)絕對值。注意這是一個(gè)靜態(tài)值,必須在 elasticsearch.yml 中提前配置。

shard request cache

ES 還有另一個(gè)分片層面的緩存,叫 shard request cache。5.0 之前的版本中,request cache 的用途并不大,因?yàn)?query cache 要起作用,還有幾個(gè)先決條件:

  1. 分片數(shù)據(jù)不再變動,也就是對當(dāng)天的索引是無效的(如果 refresh_interval 很大,那么在這個(gè)間隔內(nèi)倒也算有效);
  2. 使用了 "now" 語法的請求無法被緩存,因?yàn)檫@個(gè)是要即時(shí)計(jì)算的;
  3. 緩存的鍵是請求的整個(gè) JSON 字符串,整個(gè)字符串發(fā)生任何字節(jié)變動,緩存都無效。

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

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

在之前的版本,Elasticsearch 接收到請求之后,直接把請求原樣轉(zhuǎn)發(fā)給各分片,由各分片所在的節(jié)點(diǎn)自行完成請求的解析,進(jìn)行實(shí)際的搜索操作。所以緩存的鍵是原始 JSON 串。

而 5.0 的重構(gòu)后,接收到請求的節(jié)點(diǎn)先把請求的解析做完,發(fā)送到各節(jié)點(diǎn)的是統(tǒng)一拆分修改好的請求,這樣就不再擔(dān)心 JSON 串多個(gè)空格啥的了。

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

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

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

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

field_stats 接口

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

響應(yīng)結(jié)果如下:

{
    "_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 的大小也是以節(jié)點(diǎn)級別控制的,配置項(xiàng)名為 indices.requests.cache.size,其默認(rèn)值為 1%

字段數(shù)據(jù)

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

作為一個(gè)加速性能的方式,fielddata 當(dāng)然是被全部加載在內(nèi)存的時(shí)候最為有效。這也是 ES 默認(rèn)的運(yùn)行設(shè)置。但是,內(nèi)存是有限的,所以 ES 同時(shí)也需要提供對 fielddata 內(nèi)存的限額方式:

  • indices.fielddata.cache.size
    節(jié)點(diǎn)用于 fielddata 的最大內(nèi)存,如果 fielddata 達(dá)到該閾值,就會把舊數(shù)據(jù)交換出去。該參數(shù)可以設(shè)置百分比或者絕對值。默認(rèn)設(shè)置是不限制,所以強(qiáng)烈建議設(shè)置該值,比如 10%
  • indices.fielddata.cache.expire
    進(jìn)入 fielddata 內(nèi)存中的數(shù)據(jù)多久自動過期。注意,因?yàn)?ES 的 fielddata 本身是一種數(shù)據(jù)結(jié)構(gòu),而不是簡單的緩存,所以過期刪除 fielddata 是一個(gè)非常消耗資源的操作。ES 官方在文檔中特意說明,這個(gè)參數(shù)絕對絕對不要設(shè)置!

Circuit Breaker

Elasticsearch 在 total,fielddata,request 三個(gè)層面上都設(shè)計(jì)有 circuit breaker 以保護(hù)進(jìn)程不至于發(fā)生 OOM 事件。在 fielddata 層面,其設(shè)置為:

  • indices.breaker.fielddata.limit
    默認(rèn)是 JVM 堆內(nèi)存大小的 60%。注意,為了讓設(shè)置正常發(fā)揮作用,如果之前設(shè)置過 indices.fielddata.cache.size 的,一定要確保 indices.breaker.fielddata.limit 的值大于 indices.fielddata.cache.size 的值。否則的話,fielddata 大小一到 limit 閾值就報(bào)錯(cuò),就永遠(yuǎn)道不了 size 閾值,無法觸發(fā)對舊數(shù)據(jù)的交換任務(wù)了。

doc values

但是相比較集群龐大的數(shù)據(jù)量,內(nèi)存本身是遠(yuǎn)遠(yuǎn)不夠的。為了解決這個(gè)問題,ES 引入了另一個(gè)特性,可以對精確索引的字段,指定 fielddata 的存儲方式。這個(gè)配置項(xiàng)叫:doc_values

所謂 doc_values,其實(shí)就是在 ES 將數(shù)據(jù)寫入索引的時(shí)候,提前生成好 fielddata 內(nèi)容,并記錄到磁盤上。因?yàn)?fielddata 數(shù)據(jù)是順序讀寫的,所以即使在磁盤上,通過文件系統(tǒng)層的緩存,也可以獲得相當(dāng)不錯(cuò)的性能。

注意:因?yàn)?doc_values 是在數(shù)據(jù)寫入時(shí)即生成內(nèi)容,所以,它只能應(yīng)用在精準(zhǔn)索引的字段上,因?yàn)樗饕M(jìn)程沒法知道后續(xù)會有什么分詞器生成的結(jié)果。

由于在 Elastic Stack 場景中,doc_values 的使用極其頻繁,到 Elasticsearch 5.0 以后,這兩者的區(qū)別被徹底強(qiáng)化成兩個(gè)不同字段類型:textkeyword

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

等同于過去的:

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

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

等同于過去的:

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

也就是說,以后的用戶,已經(jīng)不太需要在意 fielddata 的問題了。不過依然有少數(shù)情況,你會需要對分詞字段做聚合統(tǒng)計(jì)的話,你可以在自己接受范圍內(nèi),開啟這個(gè)特性:

{
    "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 配置,這個(gè)配置是 segment 級別的。上面示例的意思是:只有這個(gè) segment 里的文檔數(shù)量超過 500 個(gè),而且含有該字段的文檔數(shù)量占該 segment 里的文檔數(shù)量比例超過 10% 時(shí),才加載這個(gè) segment 的 fielddata。

下面是一個(gè)可能有用的對分詞字段做聚合的示例:

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
                            }
                        }
                    }
                }
            }
        }
    }
}'

這個(gè)示例可以對經(jīng)過了 logstash-filter-punct 插件處理的數(shù)據(jù),獲取每種 punct 類型日志的關(guān)鍵詞和對應(yīng)的代表性日志原文。其效果類似 Splunk 的事件模式功能:

[圖片上傳失敗...(image-b0b69f-1511752650964)]

curator

如果經(jīng)過之前章節(jié)的一系列優(yōu)化之后,數(shù)據(jù)確實(shí)超過了集群能承載的能力,除了拆分集群以外,最后就只剩下一個(gè)辦法了:清除廢舊索引。

為了更加方便的做清除數(shù)據(jù),合并 segment,備份恢復(fù)等管理任務(wù),Elasticsearch 在提供相關(guān) API 的同時(shí),另外準(zhǔn)備了一個(gè)命令行工具,叫 curator 。curator 是 Python 程序,可以直接通過 pypi 庫安裝:

pip install elasticsearch-curator

注意,是 elasticsearch-curator 不是 curator。PyPi 原先就有另一個(gè)項(xiàng)目叫這個(gè)名字

參數(shù)介紹

和 Elastic Stack 里其他組件一樣,curator 也是被 Elastic.co 收購的原開源社區(qū)周邊。收編之后同樣進(jìn)行了一次重構(gòu),命令行參數(shù)從單字母風(fēng)格改成了長單詞風(fēng)格。新版本的 curator 命令可用參數(shù)如下:

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,還可以繼續(xù)使用 --help 查看該子命令的幫助。比如查看 close 子命令的幫助,輸入 curator close --help,結(jié)果如下:

Usage: curator close [OPTIONS] COMMAND [ARGS]...

  Close indices

Options:
  --help  Show this message and exit.

Commands:
  indices  Index selection.

常用示例

在使用 1.4.0 以上版本的 Elasticsearch 前提下,curator 曾經(jīng)主要的一個(gè)子命令 bloom 已經(jīng)不再需要使用。所以,目前最常用的三個(gè)子命令,分別是 close, deleteoptimize,示例如下:

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-

這一頓任務(wù),結(jié)果是:

logstash-mweibo-nginx-yyyy.mm.dd 索引保存最近 5 天,logstash-mweibo-client-yyyy.mm.dd 保存最近 10 天,logstash-mweibo-yyyy.mm.dd 索引保存最近 30 天;且所有七天前的 logstash-* 索引都暫時(shí)關(guān)閉不用;最后對所有非當(dāng)日日志做 segment 合并優(yōu)化。

profiler

profiler 是 Elasticsearch 5.0 的一個(gè)新接口。通過這個(gè)功能,可以看到一個(gè)搜索聚合請求,是如何拆分成底層的 Lucene 請求,并且顯示每部分的耗時(shí)情況。

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

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

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

query

query 部分包括 collectors、rewrite 和 query 部分。對復(fù)雜 query,profiler 會拆分 query 成多個(gè)基礎(chǔ)的 TermQuery,然后每個(gè) TermQuery 再顯示各自的分階段耗時(shí)如下:

"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
        }

我們可以很明顯的看到聚合統(tǒng)計(jì)在初始化階段、收集階段、構(gòu)建階段、匯總階段分別花了多少時(shí)間,遍歷了多少數(shù)據(jù)。

注意其中 reduce 階段還沒實(shí)現(xiàn)完畢,所有都是 0。因?yàn)槟壳?profiler 只能在 shard 級別上做統(tǒng)計(jì)。

collect 階段的耗時(shí),有助于我們調(diào)整對應(yīng) aggs 的 collect_mode 參數(shù)選擇。目前 Elasticsearch 支持 breadth_firstdepth_first 兩種方式。

initialise 階段的耗時(shí),有助于我們調(diào)整對應(yīng) aggs 的 execution_hint 參數(shù)選擇。目前 Elasticsearch 支持 mapglobal_ordinals_low_cardinalityglobal_ordinalsglobal_ordinals_hash 四種選擇。在計(jì)算離散度比較大的字段統(tǒng)計(jì)值時(shí),適當(dāng)調(diào)整該參數(shù),有益于節(jié)省內(nèi)存和提高計(jì)算速度。

對高離散度字段值統(tǒng)計(jì)性能很關(guān)注的讀者,可以關(guān)注 https://github.com/elastic/elasticsearch/pull/21626 這條記錄的進(jìn)展。

(本文完)

文本整理自《ELKstack權(quán)威指南》

歡迎關(guān)注我的微信訂閱號:


歡迎關(guān)注我的開發(fā)者頭條獨(dú)家號搜索:269166

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,750評論 1 375
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容