逐層拆分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 Cache、Warmer Cache等。
ElasticSearch的文件索引過程
叢集視角索引文件
一次新增文件(索引文件),在叢集視角的流程:
- 客戶端向Es服務(叢集)傳送新增資料請求,請求首先到達Master節點
- Master節點為每個節點建立一個批次請求,並將這些請求並行轉發到每個包含主分片的節點上。
- 每個節點上的主分片接收到插入請求,主分片進行資料索引並行轉發新文件(或刪除)到相應的副本分片(跨節點)。 一旦所有的副本分片報告所有操作成功,該節點將向Master節點報告成功,協調節點將這些響應收集整理並返回給客戶端。
分片內部索引時具體在做什麼
- 當分片所在的節點接收到資料新增請求後,在分片內部,首先會將資料請求寫入到Memory Buffer,然後定時(預設是每隔1秒,可在索引中設定)寫入到Filesystem Cache(系統快取),從Momery Buffer到Filesystem Cache的過程就是常說的refresh;這也是為什麼對於Es的記憶體配置時不要過大,要預留給作業系統足夠的記憶體空間的原因,因為這裡十分依賴系統記憶體;
- 同時為保證資料的可靠性,防止資料在Momery Buffer和Filesystem Cache中丟失,Es額外追加了TransLog機制,到達分片的新增請求,資料同時會非同步寫入 TransLog 一份(磁碟記錄)。
- 當TransLog增長過大(預設為512M)或到達配置的時間時(預設30分鐘),FilesystemCache中的內容被寫入到磁碟中,然後舊的TransLog將被刪除並開始一個新的TransLog。 這個過程被稱作Flush
refresh過程中segment的活動
文件資料被寫入後,首先進入到Memory Buffer和TransLog中,此時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的數量。
合併程序選擇一小部分大小相似的段,並且在後臺將它們合併成為更大的段(過程中並不會中斷索引和搜尋)。合併後的新Segment被Flush到磁碟中,然後開啟新的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去進行合併、快取等操作