ElasticSearch效能原理拆解

糖拌西红柿發表於2024-06-04

逐層拆分ElasticSearch的概念

  • Cluster:叢集,Es是一個可以橫向擴充套件的檢索引擎(部分時候當作儲存資料庫使用),一個Es叢集由一個唯一的名字標識,預設為“elasticsearch”。在配置檔案中指定相同的叢集名,Es會將相同叢集名的節點組成一個叢集。

  • Node:節點,叢集中的任意一個例項物件,是一個節點

  • Index:索引,存放相同型別資料的一個集合,索引有唯一的名字,相比於關係型資料庫,可以理解為一個表(6.0.0廢除type之後)

  • Shard:分片,物理儲存單位,建立一個索引時可以指定分成多少個分片來儲存。每個分片本身也是一個功能完善且獨立的“索引”,可以被放置在叢集的任意節點上。

  • Replication:副本,對資料的備份,主要針對分片進行備份,即分片的備份

  • Document:文件,具體儲存的資料(json結構)

如上圖,每個雲朵代表一個Es叢集Cluster,叢集中由一個個的Es例項Node構成,每個Es例項中存放著一個個的彩色方塊(Shard),同一個叢集中相同顏色的方塊(shard)構成一個索引(index)

索引和分片、副本

實際上,索引是一個抽象的邏輯概念,使用時,我們面向的是索引(Index),只需要指定索引名即可。索引背後真正的實體是分片(Shard)。

同一叢集中,同一索引(Index)可能由多個分片(Shard)組成,這些Shard會分佈在不同的節點(Node)中;而副本(Replication)則可以理解為一種特殊的分片,實際上資料是在一個個的分片上的,而副本的主要職責是對分片進行備份,以滿足Es的穩定性。當副本數設定為1時,則代表著每個分片都會有一個副本,且副本中的內容與分片一樣,肩負資料備份以及維穩責任。

例如我們在2個節點的叢集上建立一個名為user的索引和student索引,user索引設定2個分片1個副本,student索引設定1個分片1個副本;那麼user索引的兩個shard會各自得到一個副本,副本和shard的內容一致,且均勻的分佈在兩個節點上(Es會根據相應的策略來儘可能保證分片和相應的副本不在同一節點中,且保證每個節點的資料都是完整的)。這樣如果當Node1節點崩壞,對於user索引的查詢,會由節點2的 shard1和shard0的副本來承擔,且shard1和shard0的副本的資料之和是一個完整user的資料(等同於shard0和shard1)。

Segment

所以對於es來說,分片(shard)才是資料真正的載體,每一個Shard本質上是一個Lucene的索引(Lucene Index)。

每個Lucene Index(Es的Shard) 是由多個Segment構成 ,Segement才是Lucene和Es查詢效能的核心,Segment主要承載三部分內容:

  • Inverted Index

  • Stored Fields

  • Document Values

Inverted Index(倒排索引)

Segment中最重要的就是倒排索引,也是Es能夠快速檢索的根本,它是Segment基於儲存的資料抽離出來的一個能夠快速檢索的資料結構,一個倒排索引的結構主要由一個有序的資料字典Dictionary(包括單詞Term和它出現的頻率)和 單詞Term對應的Postings(文件的id或位置)組成:

對於倒排索引,Es中是根據欄位不同型別進行不同的策略:

  • Text 欄位

    • 這些欄位用於全文搜尋。
    • 它們會被分析(分詞),並建立倒排索引。
    • 可以包含多個詞項,支援全文搜尋和複雜查詢。
  • Keyword 欄位

    • 這些欄位用於結構化搜尋,如過濾、排序、聚合。
    • 它們不會被分析,而是以整個欄位的值儲存。
    • 每個不同的欄位值都會在倒排索引中擁有一個獨立的條目。
  • Numeric 欄位(如 integer, float, double 等):

    • 用於數值搜尋,如範圍查詢或數值排序。
    • 這些欄位的值通常不會被倒排索引,除非明確設定為可搜尋。
  • Date 欄位

    • 用於日期和時間的搜尋。
    • 類似於數值欄位,它們的值通常不會被倒排索引,除非明確設定為可搜尋。
  • Boolean 欄位Binary 欄位

    • 用於儲存布林值或二進位制資料。
    • 通常不會被倒排索引

構建倒排索引主要根據文件欄位的型別主要為text,構建倒排索引的過程是:

  • 文件分析:提取文件中的文字內容,通常包括標題和上下文。
  • 詞項提取:從文字中提取單詞或片語(分詞器)。
  • 詞項標準化:對提取的詞項進行標準化處理,比如轉小寫、去除標點符號、去除停用詞等(分詞器)。
  • 詞項索引:為每個詞項分配一個唯一的詞項ID。
  • 構建倒排表:為每個詞項建立倒排表,記錄詞項在文件中的位置和頻率。

在Elasticsearch中,每個欄位的倒排索引是獨立的,這意味著對於每個欄位,Elasticsearch都會維護一個單獨的倒排表,該倒排表包含了該欄位中詞項的文件對映資訊。

Stored Fields(儲存欄位)

在索引文件時,Es是會儲存原始內容的,原始文件內容在Es中表現為JSON, 而使用Es時,多是基於一個JSON中的某幾個欄位進行檢索,大部分情況是不需要原始JSON內容的,但是若無特殊指定,Es每次檢索是需要把完整的JSON在查詢結果中透過_source進行攜帶:

{
    "_index": "ariticle",
    "_type": "_doc",
    "_id": "1",
    "_version": 1,
    "_seq_no": 0,
    "_primary_term": 1,
    "found": true,
    "_source": { //不進行查詢指定,預設所有欄位透過_source返回
          "user": "張三",
          "title": "這是一個示例文章",
          "context": "這是文章中的上下文以及具體的文章內容XXX"
       }
}

大批次的欄位返回,除造成了額外的網路傳輸消耗外,在Es內部,也需要對整個文件進行序列化,造成資源浪費。

Stored Fields(儲存欄位)是一種特殊的欄位型別,可以在文件中指定多個欄位並將欄位的原始內容進行額外儲存,形成一個和_source平級的內容,檢索時不需要序列化整個文件,直接讀取額外的儲存空間內容即可。

使用方法是在設計索引mapping時透過store屬性進行欄位指定。

使用 store Fields後,可將指定的欄位額外被Segment儲存一份,檢索時直接讀取

//在索引建立時就固定常用哪些欄位
{
    "ariticle": {
        "aliases": {
        },
        "mappings": {
            "_doc": {
                "properties": {
                    "title": {     //預設沒有store屬性,預設值就是false
                        "type": "text",
                    },
                    "context": {   //預設沒有store屬性,預設值就是false
                        "type": "text"
                    },
                    "user": {   //明確指定store屬性為true
                        "type": "keyword",
                        "store": true  
                    }
                }
            }
        }
}
 //同樣的查詢
 {
  "query": {
    "match_all": {}
  },
  "from":0,
  "size":10
 }  

 //返回結果
{
   "_index": "ariticle",
   "_type": "_doc",
   "_id": "1",
   "_version": 1,
   "found": true,
   "fields": {           //此時多了名稱為fields的欄位,並且沒有了_source
      "user": [          //user的stroe屬性設定為true,因此顯示在結果中
         "張三"
      ]
   }
}

事實上不論設不設定store屬性為true,Elasticsearch都是會把原始文件進行儲存的,當store為false時(預設配置),這些field只儲存在"_source" field中,我們進行檢索時,透過DSL來控制_source中返回的欄位原文內容;但是當使用了store Fields時,會對相應欄位的內容多儲存一份,檢索時針對使用了store Fields的欄位,不需要序列化整個文件,相比透過指定返回欄位查詢效率會快很多,代價就是需要額外的儲存一份內容,且內容在定義時就固定,不如在DSL中使用 _source 指定內容靈活。

Document Values

Document Values主要用於 排序、聚合、指令碼索引中,Document Values對資料內容進行列式儲存,便於快速進行 sort、aggs操作;

這裡Docvalus是相當於倒排索引的正排索引,它作用於除Text型別之外的型別欄位,倒排索引的優勢 在於查詢包含某個項的文件,而對於從另外一個方向的相反操作並不高效,即:確定哪些項是否存在單個文件裡。這種場景下,就需要類似Mysql那種列式儲存,構建一個正排索引

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

DocValues是在索引時與倒排索引同時生成的,並且是不可變的,需要持久化到磁碟中。Doc values 是不支援對需要分詞的欄位進行列儲存的(例如text),然而,這些欄位仍然可以使用聚合,是因為使用了fielddata 的資料結構。與 doc values 不同,fielddata 構建和管理 100% 在記憶體中,常駐於 JVM 記憶體堆

Fielddata預設是不啟用的,因為text欄位比較長,一般只做關鍵字分詞和搜尋,很少拿它來進行全文匹配和聚合還有排序,因為大多數這種情況是無意義的,一旦啟用將會把text都載入到記憶體中,那將帶來很大的記憶體壓力,導致出現記憶體熔斷現象(circuit breaker)。

它透過內部檢查(欄位的型別、基數、大小等等)來估算一個查詢需要的記憶體。它然後檢查要求載入的 fielddata 是否會導致 fielddata 的總量超過堆的配置比例。如果估算查詢大小超出限制,就會觸發熔斷,查詢會被中止並返回異常。

fielddata的記憶體配置在elasticsearch.yml中

indices.breaker.fielddata.limit fielddata級別限制,預設為堆的60% 
indices.breaker.request.limit request級別請求限制,預設為堆的40% 
indices.breaker.total.limit 保證上面兩者組合起來的限制,預設堆的70%

Es的快取

ElasticSearch在查詢時涉及其自身JVM的快取一共分為三類:

  • Node Query Cache

    • 節點級別的快取,被所有分片共享。
    • 主要用於快取過濾器的執行結果,通常是壓縮過的bitset,對應滿足查詢條件的文件ID列表。使用term精確查詢某個值時或者bool配合filter查詢時會觸發
    • Node Query Cache 會在底層的段(segment)發生變更時自動使快取失效,以確保查詢結果的準確性
    • 透過elasticsearch.yml配置來控制:
      • indices.queries.cache.size: 控制查詢快取的記憶體大小,預設為節點堆記憶體的10%。
      • indices.queries.cache.count: 控制快取的總數量,預設值通常是10000。
  • Shard Request Cache

    • 分片級別的快取。
    • 多使用於聚合(aggs)時,只會快取DSL查詢中引數 size=0 的請求,以完整DSL為快取鍵,不會快取 hits,但會快取 hits.total 以及聚合資訊。
    • 快取的生命週期是一個 refresh_interval,即在預設情況下每1秒鐘失效一次。
    • 透過elasticsearch.yml配置來控制:
      • index.requests.cache.enable: 控制是否啟用分片級別的快取,預設為 true
      • indices.requests.cache.size: 控制請求快取在JVM堆中的百分比,預設為1%。
      • indices.requests.cache.expire: 配置快取過期時間,單位為分鐘。
  • Fielddata Cache

    • 分段級別的快取。
    • 用於儲存已分析欄位(analyzed fields)的欄位資料,如果該欄位是 text 型別或者沒有為該欄位設定 doc_values,對於該欄位聚合、排序或者指令碼訪問時會快取。
    • 一旦觸發Fielddata 載入到記憶體中,它會保留在那裡,直到相關段被刪除或更新。
    • 透過es配置檔案指定:
      • indices.fielddata.cache.size: 控制欄位資料快取的大小,預設不限制。
      • indices.breaker.fielddata.limit: 設定 Fielddata 斷路器限制大小,預設為60%的JVM堆記憶體。

以上為查詢時常用的快取,多為Es本身JVM的記憶體進行劃分和使用,另外Es在寫入時還會使用一定的SystemCache,如Recycler CacheWarmer Cache等。

ElasticSearch的文件索引過程

叢集視角索引文件

一次新增文件(索引文件),在叢集視角的流程:

  • 客戶端向Es服務(叢集)傳送新增資料請求,請求首先到達Master節點
  • Master節點為每個節點建立一個批次請求,並將這些請求並行轉發到每個包含主分片的節點上。
  • 每個節點上的主分片接收到插入請求,主分片進行資料索引並行轉發新文件(或刪除)到相應的副本分片(跨節點)。 一旦所有的副本分片報告所有操作成功,該節點將向Master節點報告成功,協調節點將這些響應收集整理並返回給客戶端。

分片內部索引時具體在做什麼

  • 當分片所在的節點接收到資料新增請求後,在分片內部,首先會將資料請求寫入到Memory Buffer,然後定時(預設是每隔1秒,可在索引中設定)寫入到Filesystem Cache(系統快取),從Momery BufferFilesystem Cache的過程就是常說的refresh;這也是為什麼對於Es的記憶體配置時不要過大,要預留給作業系統足夠的記憶體空間的原因,因為這裡十分依賴系統記憶體;
  • 同時為保證資料的可靠性,防止資料在Momery BufferFilesystem Cache中丟失,Es額外追加了TransLog機制,到達分片的新增請求,資料同時會非同步寫入 TransLog 一份(磁碟記錄)。
  • TransLog增長過大(預設為512M)或到達配置的時間時(預設30分鐘),FilesystemCache中的內容被寫入到磁碟中,然後舊的TransLog將被刪除並開始一個新的TransLog。 這個過程被稱作Flush

refresh過程中segment的活動

文件資料被寫入後,首先進入到Memory BufferTransLog中,此時shard中的Segment還是之前已經穩定的資料,新寫入的文件還沒有形成Segment,無法被Es查到。根據 index.refresh_interval 設定 的refresh (沖刷)間隔時間,資料開始進行refresh,Memory Buffer中的文件被內容分析、分詞,形成一個新的Segment,然後Memory Buffer開始清空,refresh後新生成的Segment是暫存在FilesystemCache中的,所以從儲存上看,新的文件從Memory Buffer 轉移到了 Filesystem Cache,到此,新插入的文件資料才可以被Es查詢到

flush過程中segment的活動

隨著TransLog越來越大,會觸發Flush過程,在這個過程中,FilesystemCache中的內容會被寫入到磁碟中,段的fsync將建立一個new commit point,此時清空Filesystem Cache,然後刪除TransLog,再生成一個新的TransLog,記錄後續的內容

segement的 merge

由於refresh流程每次都會建立一個新的段,refresh的頻繁會導致短時間內的段數量暴增。而段數目太多會帶來較大的麻煩。 每一個段都會消耗檔案控制代碼、記憶體和cpu執行週期。而且每個搜尋請求都必須輪流檢查每個段,所以段越多,搜尋也就越慢。於是Es在後他就需要不定期的合併Segment,以減少Segment的數量。

合併程序選擇一小部分大小相似的段,並且在後臺將它們合併成為更大的段(過程中並不會中斷索引和搜尋)。合併後的新SegmentFlush到磁碟中,然後開啟新的Segment的檢索功能,同時刪除磁碟上舊的Segment

ElasticSearch的檢索過程

elasticSearch中的檢索一般分為兩類,一類是Get查詢,即透過_id查詢具體的文件,一類是Search查詢,即向Es發起DSL語句的查詢。這裡主要以Search查詢為例

查詢整體過程

  • 客戶端向Es服務(叢集)傳送指定索引的查詢請求,請求先到達主節點(協調節點)

  • 協調節點根據叢集部署,將請求轉發到其他節點所對應的索引分片上(優先使用主節點)

    • 此過程中Es記憶體機制會判定是否符合Node Cache的標準,進行Node Cache查詢或快取查詢
  • 各個節點上的分片在其內部進行資料檢索,檢索出符合條件的資料

    • 此過程中會根據查詢,判定是否符合Shard Cache標準,進行Cache查詢或快取內容

    • 涉及聚合或分析欄位的聚合操作,內部Segment會判定是否fielddata Cache標準,並啟用該快取

  • 各分片將檢索出的資料發回主節點,主節點進行彙總後返回給客戶端

也就是說,Es是透過分片將同一索引的資料均勻的散佈在叢集中,每個分片依賴所處節點裝置的硬體資源進行獨立查詢,透過網路傳輸,將結果返回。

查詢時segment內部具體在做什麼

Segment的查詢之前補充一下上文Segment的內容部分(注意,在Es或者Lucene中提及的Segment是邏輯概念,不等價於磁碟上的段);Segment的組成部分和文件資料在磁碟上的對應關係:

更多文件型別可以此處檢視

  • 請求來的shard內部,解析出DSL,轉譯為Lucene的語法

  • 透過 commit point記錄分發到segments中,此時的segment分為兩種,一種是經過flush和merge的,我稱之為磁碟版segment(當然不那麼準確);還有一種是處在索引過程中的,上文中存在於FilesystemCache中可被查詢的,我稱之為記憶體版segment。兩者不同之處就在於,前者涉及磁碟IO讀取部分資料來完成查詢,後者不需要IO,直接記憶體進行查詢

  • 每個segment根據詞法分析得出的詞項,進行詞典檢索(詞典的資料.tip檔案一般載入在記憶體中,不需要磁碟IO,非常快),配合倒排表,快速找到相關文件(這個過程需要磁碟IO)

  • 如果涉及數字型別的sum、max、min的聚合或者text的聚合操作,則segment會使用DocValues相關的檔案,藉助列式儲存的優勢快速運算;fielddata快取機制也是在此時發揮作用。

  • segment完成檢索後將內容返回到shard中(其他segment也是同理),由shard去進行合併、快取等操作

相關文章