ES系列8、正排索Doc Values和Field Data

1.Doc Values

聚合使用一個叫Doc Values的數據結構。Doc Values使聚合更快、更高效且內存友好。
Doc Values的存在是由於倒排索引只對某些操做是高效的。倒排索引的優點在於查找包含某個項的文檔,而反過來肯定哪些項在單個文檔裏並不高效。
 
結構相似以下:
Doc      Terms
-----------------------------------------------------------------
Doc_1 | brown, dog, fox, jumped, lazy, over, quick, the
Doc_2 | brown, dogs, foxes, in, lazy, leap, over, quick, summer
Doc_3 | dog, dogs, fox, jumped, over, quick, the

 


     Doc values在索引的時候生成,伴隨倒排索引的建立。像倒排索引同樣基於per-segment,且是不可變,被序列化存儲到磁盤。經過序列化持久化數據結構到磁盤,能夠以來操做系統的文件緩存來代替JVM heap。可是當工做空間須要的內存很大時,Doc Values會被置換出內存,這樣會致使訪問速度下降,可是若是放在JVM heap,將直接致使內存溢出錯誤。
     Doc Values默認對除了分詞的全部字段起做用。由於分此字段產生太多tokens且Doc Values對其並非頗有效。
     因爲Doc Values默認開啓,若是你不會執行基於一個肯定的子段 聚合、排序或執行腳本(Script ),你能夠選擇關閉Doc Values,這能夠爲你節省磁盤空間,提升索引數據的速度。
PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "session_id": {
          "type":       "string",
          "index":      "not_analyzed",
          "doc_values": false
        }
      }
    }
  }
}

 

 
設置doc_values: false,這個字段將再也不支持據聚合、排序和腳本執行(Script);
同時也能夠對倒排索引作相似的配置:
PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "customer_token": {
          "type":       "string",
          "index":      "not_analyzed",
          "doc_values": true,
          "index": "no"
        }
      }
    }
  }
}

 

這個能夠支持聚合,但不支持查詢,由於不會對這個字段生成倒排索引。
 

2.聚合與分析

分析對聚合有兩方面的影響,

2.1.分析影響聚合中使用的 tokens

          例如字符串 "New York" 被分析/分析成 ["new", "york"] 。這些單獨的 tokens ,都被用來填充聚合計數,因此咱們最終看到 new 的數量而不是 New York。
     能夠經過加multifield來修正,以下:聚合時指定爲未分詞的raw字段。
PUT /agg_analysis
{
  "mappings": {
    "data": {
      "properties": {
        "state" : {
          "type": "string",
          "fields": {
            "raw" : {
              "type": "string",
              "index": "not_analyzed"
            }
          }
        }
      }
    }
  }
}

2.1.Doc values 不支持 analyzed

Doc values 不支持 analyzed 字符串字段,由於它們不能頗有效的表示多值字符串。 Doc values 最有效的是,當每一個文檔都有一個或幾個 tokens 時, 但不是無               數的,分詞字符串(想象一個 PDF ,可能有幾兆字節並有數以千計的獨特 tokens)。node

          出於這個緣由,doc values 不生成分詞的字符串,然而,這些字段仍然可使用聚合,那怎麼可能呢?
          答案是一種被稱爲 fielddata 的數據結構。與 doc values 不一樣, fielddata 構建和管理 100% 在內存中,常駐於 JVM 內存堆。這意味着它本質上是不可擴展的,有不少邊緣狀況下要提防。 
  本章的其他部分是解決在分詞字符串上下文中 fielddata 的挑戰。
          從歷史上看,fielddata 是 全部字段的默認設置。可是 Elasticsearch 已遷移到 doc values 以減小 OOM 的概率。分詞字符串是仍然使用 fielddata 的最後一塊陣地。 最終目標是創建一個序列化的數據結構相似於 doc values ,能夠處理高維度的分詞字符串,逐步淘汰 fielddata。
          避免分詞字段的另一個緣由就是:高基數字段在加載到 fielddata 時會消耗大量內存。 分詞的過程會常常(儘管不老是這樣)生成大量的 token,這些 token 大多都是惟一的。 這會增長字段的總體基數而且帶來更大的內存壓力。
          有些類型的分詞對於內存來講 極度 不友好,想一想 n-gram 的分析過程, New York 會被 n-gram 分析成如下 token:
              ne、ew、w 、 y、yo、or、rk
          能夠想象 n-gram 的過程是如何生成大量惟一 token 的,特別是在對成段文本分詞的時候。當這些數據加載到內存中,會垂手可得的將咱們堆空間消耗殆盡。
          在聚合字符串字段以前,請評估狀況:
               a.這是一個 not_analyzed 字段嗎?若是是,能夠經過 doc values 節省內存 。
               b.不然,這是一個 analyzed 字段,它將使用 fielddata 並加載到內存中。這個字段由於 n-grams 有一個很是大的基數?若是是,這對於內存來講極度不友好。

 2.2.text類型默認禁用fielddate,排序、聚合須要手動開啓

POST book1/_mapping/english/?pretty
{
    "english":{
        "properties":{
            "addr":{
                "type":"text",
                "fielddata":true
             }
         }
    }
}

 

Fielddata可能會消耗大量的堆空間,尤爲是在加載高基數text字段時。一旦fielddata已加載到堆中,它將在該段的生命週期內保留。此外,加載fielddata是一個昂貴的過程,可能會致使用戶遇到延遲命中。這就是默認狀況下禁用fielddata的緣由。緩存

若是您嘗試對text 字段上的腳本進行排序,聚合或訪問,您將看到如下異常:session

默認狀況下,在文本字段上禁用Fielddata。設置fielddata=true爲[ your_field_name]以經過同相反向索引在內存中加載fielddata。請注意,這可能會佔用大量內存。數據結構

3.Fielddata

     一旦分詞字符串被加載到 fielddata ,他們會一直在那裏,直到被驅逐(或者節點崩潰)。因爲這個緣由,留意內存的使用狀況,瞭解它是如何以及什麼時候加載的,怎樣限制對集羣的影響是很重要的。
      Fielddata 是 延遲 加載。若是你歷來沒有聚合一個分析字符串,就不會加載 fielddata 到內存中。此外,fielddata 是基於字段加載的, 這意味着只有很活躍地使用字段纔會增長 fielddata 的負擔。
     然而,這裏有一個使人驚訝的地方。假設你的查詢是高度選擇性和只返回命中的 100 個結果。大多數人認爲 fielddata 只加載 100 個文檔。
實際狀況是,fielddata 會加載索引中(針對該特定字段的) 全部的文檔,而無論查詢的特異性。邏輯是這樣:若是查詢會訪問文檔 X、Y 和 Z,那頗有可能會在下一個查詢中訪問其餘文檔。
     與 doc values 不一樣,fielddata 結構不會在索引時建立。相反,它是在查詢運行時,動態填充。這多是一個比較複雜的操做,可能須要一些時間。 將全部的信息一次加載,再將其維持在內存中的方式要比反覆只加載一個 fielddata 的部分代價要低。
JVM 堆 是有限資源的,應該被合理利用。 限制 fielddata 對堆使用的影響有多套機制,這些限制方式很是重要,由於堆棧的亂用會致使節點不穩定(感謝緩慢的垃圾回收機制),甚至致使節點宕機(一般伴隨 OutOfMemory 異常)。
 
     在設置 Elasticsearch 堆大小時須要經過 $ES_HEAP_SIZE 環境變量應用兩個規則:

3.1.不要超過可用 RAM 的 50%

          Lucene 能很好利用文件系統的緩存,它是經過系統內核管理的。若是沒有足夠的文件系統緩存空間,性能會收到影響。 此外,專用於堆的內存越多意味着其餘全部使用 doc values 的字段內存越少。

3.2.不要超過 32 GB

若是堆大小小於 32 GB,JVM 能夠利用指針壓縮,這能夠大大下降內存的使用:每一個指針 4 字節而不是 8 字節。
 

4.Fielddata的大小

     indices.fielddata.cache.size 控制爲 fielddata 分配的堆空間大小。 當你發起一個查詢,分析字符串的聚合將會被加載到 fielddata,若是這些字符串以前沒有被加載過。若是結果中 fielddata 大小超過了指定大小,其餘的值將會被回收從而得到空間。 
     默認狀況下,這個設置是禁用的,Elasticsearch 永遠都不會從 fielddata 中回收數據。
     這個默認設置是刻意選擇的:fielddata 不是臨時緩存。它是駐留內存裏的數據結構,必須能夠快速執行訪問,並且構建它的代價十分高昂。若是每一個請求都重載數據,性能會十分糟糕。
     一個有界的大小會強制數據結構回收數據。
     設想咱們正在對日誌進行索引,天天使用一個新的索引。一般咱們只對過去一兩天的數據感興趣,儘管咱們會保留老的索引,但咱們不多須要查詢它們。不過若是採用默認設置,舊索引的 fielddata 永遠不會從緩存中回收! fieldata 會保持增加直到 fielddata 發生斷熔,這樣咱們就沒法載入更多的 fielddata。
     這個時候,咱們被困在了死衚衕。但咱們仍然能夠訪問舊索引中的 fielddata,也沒法加載任何新的值。相反,咱們應該回收舊的數據,併爲新值得到更多空間。
爲了防止發生這樣的事情,能夠經過在 config/elasticsearch.yml 文件中增長配置爲 fielddata 設置一個上限:
      indices.fielddata.cache.size:  20% : 有了這個設置,最久未使用(LRU)的 fielddata 會被回收爲新數據騰出空間。
      

4.1.監控fileddata

     Fielddata 的使用能夠被監控:
          1).按索引使用 indices-stats API :GET /_stats/fielddata?fields=*
          2).按節點使用 nodes-stats API :  GET /_nodes/stats/indices/fielddata?fields=*
          3).按索引節點:GET /_nodes/stats/indices/fielddata?level=indices&fields=*
 

4.2.斷路器(Circuit Breakers)

     fielddata 大小是在數據加載 以後 檢查的。 若是一個查詢試圖加載比可用內存更多的信息到 fielddata 中會發生什麼?答案很醜陋:咱們會碰到 OutOfMemoryException 。
     Elasticsearch 包括一個 fielddata 斷熔器 ,這個設計就是爲了處理上述狀況。 斷熔器經過內部檢查(字段的類型、基數、大小等等)來估算一個查詢須要的內存。它而後檢查要求加載的 fielddata 是否會致使 fielddata 的總量超過堆的配置比例。
     若是估算查詢的大小超出限制,就會 觸發 斷路器,查詢會被停止並返回異常。這都發生在數據加載 以前 ,也就意味着不會引發 OutOfMemoryException 。
    
     Elasticsearch 有一系列的斷路器,它們都能保證內存不會超出限制:
          1).indices.breaker.fielddata.limit
               fielddata 斷路器默認設置堆的 60% 做爲 fielddata 大小的上限。
          2).indices.breaker.request.limit
               request 斷路器估算須要完成其餘請求部分的結構大小,例如建立一個聚合桶,默認限制是堆內存的 40%。
          3).indices.breaker.total.limit
               total 揉合 request 和 fielddata 斷路器保證二者組合起來不會使用超過堆內存的 70%。
 
     斷路器的限制能夠在文件 config/elasticsearch.yml 中指定,能夠動態更新一個正在運行的集羣: 
PUT /_cluster/settings
{
     "persistent" : {
     "indices.breaker.fielddata.limit" : "40%"
     }
 }

 

     關於給 fielddata 的大小加一個限制,從而確保舊的無用 fielddata 被回收的方法。 indices.fielddata.cache.size 和 indices.breaker.fielddata.limit 之間的關係很是重要。 若是斷路器的限制低於緩存大小,沒有數據會被回收。爲了能正常工做,斷路器的限制 必須 要比緩存大小要高。
 

4.3.fielddata過濾

PUT /music/_mapping/song
{
  "properties": {
    "tag": {
      "type": "string",
      "fielddata": {
        "filter": {
          "frequency": {
            "min":              0.01,
            "min_segment_size": 500 
          }
        }
      }
    }
  }
}

 

     1).只加載那些至少在本段文檔中出現 1% 的項。     
     2).忽略任何文檔個數小於 500 的段。
     有了這個映射,只有那些至少在 本段 文檔中出現超過 1% 的項纔會被加載到內存中。咱們也能夠指定一個 最大 詞頻,它能夠被用來排除 經常使用 項,好比 停用詞 。
     這種狀況下,詞頻是按照段來計算的。這是實現的一個限制:fielddata 是按段來加載的,因此可見的詞頻只是該段內的頻率。可是,這個限制也有些有趣的特性:它可讓受歡迎的新項迅速提高到頂部。
     min_segment_size 參數要求 Elasticsearch 忽略某個大小如下的段。 若是一個段內只有少許文檔,它的詞頻會很是粗略沒有任何意義。 小的分段會很快被合併到更大的分段中,某一刻超過這個限制,將會被歸入計算。
 

5.預加載fielddata

     Elasticsearch 加載內存 fielddata 的默認行爲是 延遲 加載 。 當 Elasticsearch 第一次查詢某個字段時,它將會完整加載這個字段全部 Segment 中的倒排索引到內存中,以便於之後的查詢可以獲取更好的性能。
     對於小索引段來講,這個過程的須要的時間能夠忽略。但若是咱們有一些 5 GB 的索引段,並但願加載 10 GB 的 fielddata 到內存中,這個過程可能會要數十秒。 已經習慣亞秒響應的用戶很難會接受停頓數秒卡着沒反應的網站。
     有三種方式能夠解決這個延時高峯:
          1).預加載 fielddata
          2).預加載全局序號
          3).緩存預熱
     全部的變化都基於同一律念:預加載 fielddata ,這樣在用戶進行搜索時就不會碰到延遲高峯。

5.1.預加載

     第一個工具稱爲 預加載 (與默認的 延遲加載相對)。隨着新分段的建立(經過刷新、寫入或合併等方式), 啓動字段預加載可使那些對搜索不可見的分段裏的 fielddata 提早 加載。
     這就意味着首次命中分段的查詢不須要促發 fielddata 的加載,由於 fielddata 已經被載入到內存。避免了用戶遇到搜索卡頓的情形。
預加載是按字段啓用的,因此咱們能夠控制具體哪一個字段能夠預先加載:
PUT /music/_mapping/_song
{
  "tags": {
    "type": "string",
    "fielddata": {
      "loading" : "eager"
    }
  }
}

 

     Fielddata 的載入可使用 update-mapping API 對已有字段設置 lazy 或 eager 兩種模式。
    

5.2全局序號

     有種能夠用來下降字符串 fielddata 內存使用的技術叫作 序號 。
     設想咱們有十億文檔,每一個文檔都有本身的 status 狀態字段,狀態總共有三種: status_pending 、 status_published 、 status_deleted 。若是咱們爲每一個文檔都保留其狀態的完整字符串形式,那麼每一個文檔就須要使用 14 到 16 字節,或總共 15 GB。
     取而代之的是咱們能夠指定三個不一樣的字符串,對其排序、編號:0,1,2。
Ordinal | Term
-------------------
0       | status_deleted
1       | status_pending
2       | status_published
     序號字符串在序號列表中只存儲一次,每一個文檔只要使用數值編號的序號來替代它原始的值。
Doc     | Ordinal
-------------------------
0       | 1  # pending
1       | 1  # pending
2       | 2  # published
3       | 0  # deleted

     這樣能夠將內存使用從 15 GB 降到 1 GB 如下!app

     但這裏有個問題,記得 fielddata 是按分 段 來緩存的。若是一個分段只包含兩個狀態( status_deleted 和 status_published )。那麼結果中的序號(0 和 1)就會與包含全部三個狀態的分段不同。
     若是咱們嘗試對 status 字段運行 terms 聚合,咱們須要對實際字符串的值進行聚合,也就是說咱們須要識別全部分段中相同的值。一個簡單粗暴的方式就是對每一個分段執行聚合操做,返回每一個分段的字符串值,再將它們概括得出完整的結果。 儘管這樣作可行,但會很慢並且大量消耗 CPU。
取而代之的是使用一個被稱爲 全局序號 的結構。 全局序號是一個構建在 fielddata 之上的數據結構,它只佔用少許內存。惟一值是 跨全部分段 識別的,而後將它們存入一個序號列表中,正如咱們描述過的那樣。
     如今, terms 聚合能夠對全局序號進行聚合操做,將序號轉換成真實字符串值的過程只會在聚合結束時發生一次。這會將聚合(和排序)的性能提升三到四倍。
 
構建全局序號(Building global ordinals)
     固然,天下沒有免費的晚餐。 全局序號分佈在索引的全部段中,因此若是新增或刪除一個分段時,須要對全局序號進行重建。 重建須要讀取每一個分段的每一個惟一項,基數越高(即存在更多的惟一項)這個過程會越長。
     全局序號是構建在內存 fielddata 和 doc values 之上的。實際上,它們正是 doc values 性能表現不錯的一個主要緣由。
     和 fielddata 加載同樣,全局序號默認也是延遲構建的。首個須要訪問索引內 fielddata 的請求會促發全局序號的構建。因爲字段的基數不一樣,這會致使給用戶帶來顯著延遲這一糟糕結果。一旦全局序號發生重建,仍會使用舊的全局序號,直到索引中的分段產生變化:在刷新、寫入或合併以後。
 
預構建全局序號(Eager global ordinals)    
     單個字符串字段 能夠經過配置預先構建全局序號:
PUT /music/_mapping/_song
{
  "song_title": {
    "type": "string",
    "fielddata": {
      "loading" : "eager_global_ordinals"
    }
  }
}

正如 fielddata 的預加載同樣,預構建全局序號發生在新分段對於搜索可見以前。electron

 
     序號的構建只被應用於字符串。數值信息(integers(整數)、geopoints(地理經緯度)、dates(日期)等等)不須要使用序號映射,由於這些值本身本質上就是序號映射。 所以,咱們只能爲字符串字段預構建其全局序號
     也能夠對 Doc values 進行全局序號預構建:
PUT /music/_mapping/_song
{
  "song_title": {
    "type":       "string",
    "doc_values": true,
    "fielddata": {
      "loading" : "eager_global_ordinals"
    }
  }
}

這種狀況下,fielddata 沒有載入到內存中,而是 doc values 被載入到文件系統緩存中。elasticsearch

 
     與 fielddata 預加載不同,預建全局序號會對數據的 實時性 產生影響,構建一個高基數的全局序號會使一個刷新延時數秒。 選擇在因而每次刷新時付出代價,仍是在刷新後的第一次查詢時。若是常常索引而查詢較少,那麼在查詢時付出代價要比每次刷新時要好。若是寫大於讀,那麼在選擇在查詢時重建全局序號將會是一個更好的選擇。
 

5.3.索引預熱器(index warmers)

     最後咱們談談 索引預熱器 。預熱器早於 fielddata 預加載和全局序號預加載以前出現,它們仍然尤爲存在的理由。一個索引預熱器容許咱們指定一個查詢和聚合需要在新分片對於搜索可見以前執行。 這個想法是經過預先填充或 預熱緩存 讓用戶永遠沒法遇到延遲的波峯。
     原來,預熱器最重要的用法是確保 fielddata 被預先加載,由於這一般是最耗時的一步。如今能夠經過前面討論的那些技術來更好的控制它,可是預熱器仍是能夠用來預建過濾器緩存,固然咱們也仍是能選擇用它來預加載 fielddata。
     讓咱們註冊一個預熱器而後解釋發生了什麼:
PUT /music/_warmer/warmer_1
{
  "query" : {
    "bool" : {
      "filter" : {
        "bool": {
          "should": [
            { "term": { "tag": "rock"        }},
            { "term": { "tag": "hiphop"      }},
            { "term": { "tag": "electronics" }}
          ]
        }
      }
    }
  },
  "aggs" : {
    "price" : {
      "histogram" : {
        "field" : "price",
        "interval" : 10
      }
    }
  }
}

     1).預熱器被關聯到索引( music )上,使用接入口 _warmer 以及 ID ( warmer_1 )。工具

     2).爲三種最受歡迎的曲風預建過濾器緩存。
     3).字段 price 的 fielddata 和全局序號會被預加載。  
 
     預熱器是根據具體索引註冊的, 每一個預熱器都有惟一的 ID ,由於每一個索引可能有多個預熱器。
而後咱們能夠指定查詢,任何查詢。它能夠包括查詢、過濾器、聚合、排序值、腳本,任何有效的查詢表達式都絕不誇張。 這裏的目的是想註冊那些能夠表明用戶產生流量壓力的查詢,從而將合適的內容載入緩存。
當新建一個分段時,Elasticsearch 將會執行註冊在預熱器中的查詢。執行這些查詢會強制加載緩存,只有在全部預熱器執行完,這個分段纔會對搜索可見。
相關文章
相關標籤/搜索