Elasticsearch实现原理分析
介紹
????本文是分析Elasticsearch系列文章中的一篇,是一個譯文。共有三個部分,每部分講解部分Elasticsearch的實現原理。
????在翻譯的過程中,也需要查看對應部分的源碼,來加深對實現原理的理解。但這里并沒有對源碼進行分析,源碼的分析放到后面的系列文章進行介紹。
本文介紹了Elasticsearch的以下原理:
-
是Master/Slave架構,還是Master-less架構?
-
存儲模型是什么?
-
寫入操作是如何工作的?
-
讀操作是如何工作的?
-
搜索結果如何相關?
Elasticsearch介紹
-
Elasticsearch的index
Elasticsearch的索引(index)是用于組織數據的邏輯命名空間(如數據庫)。Elasticsearch的索引有一個或多個分片(shard)(默認為5)。分片是實際存儲數據的Lucene索引,它本身就是一個搜索引擎。每個分片可以有零個或多個副本(replicas)(默認為1)。Elasticsearch索引還具有“類型”(如數據庫中的表),允許您在索引中對數據進行邏輯分區(qū)。Elasticsearch索引中給定“類型”中的所有文檔(documents)具有相同的屬性(如表的模式)。
?圖a顯示了一個由三個主分片組成的Elasticsearch集群,每個主分片分別有一個副本。所有這些分片一起形成一個Elasticsearch索引,每個分片是Lucene索引本身。
? ? ? ? 圖b演示了Elasticsearch索引,分片,Lucene索引和文檔(document)之間的邏輯關系。
-
類比關系數據庫術語
-
Elasticsearch Index ~ Database
-
Types ~ Tables
-
Mapping ~ Schema
?
?
Elasticsearch集群的節(jié)點類型
??? Elasticsearch的一個實例是一個節(jié)點,一組節(jié)點形成一個集群。Elasticsearch集群中的節(jié)點可以通過三種不同的方式進行配置:
-
Master節(jié)點
-
Master節(jié)點控制Elasticsearch集群,并負責在集群范圍內創(chuàng)建/刪除索引,跟蹤哪些節(jié)點是集群的一部分,并將分片分配給這些節(jié)點。主節(jié)點一次處理一個集群狀態(tài),并將狀態(tài)廣播到所有其他節(jié)點,這些節(jié)點需要響應并確認主節(jié)點的信息。
-
在elasticsearch.yml中,將nodes.master屬性設置為true(默認),可以將節(jié)點配置為有資格成為主節(jié)點的節(jié)點。
-
對于大型生產集群,建議擁有一個專用主節(jié)點來控制集群,并且不服務任何用戶請求。
-
Data節(jié)點
-
數據節(jié)點用來保存數據和倒排索引。默認情況下,每個節(jié)點都配置為一個data節(jié)點,并且在elasticsearch.yml中將屬性node.data設置為true。如果您想要一個專用的master節(jié)點,那么將node.data屬性更改為false。
-
Client節(jié)點
????如果將node.master和node.data設置為false,則將節(jié)點配置為客戶端節(jié)點,并充當負載平衡器,將傳入的請求路由到集群中的不同節(jié)點。
????若你連接的是作為客戶端的節(jié)點,該節(jié)點稱為協(xié)調節(jié)點(coordinating node)。協(xié)調節(jié)點將客戶機請求路由到集群中對應分片所在的節(jié)點。對于讀取請求,協(xié)調節(jié)點每次選擇不同的分片來提供請求以平衡負載。
????在我們開始審查發(fā)送到協(xié)調節(jié)點的CRUD請求如何通過集群傳播并由引擎執(zhí)行之前,讓我們看看Elasticsearch如何在內部存儲數據,以低延遲為全文搜索提供結果。
?
存儲模型
??? Elasticsearch使用Apache Lucene,它是由Java編寫的全文搜索庫,由Doug Cutting(Apache Hadoop的創(chuàng)建者)內部開發(fā),它使用稱為倒排索引的數據結構,用于提供低延遲搜索結果。
????文檔(document)是Elasticsearch中的數據單位,并通過對文檔中的術語進行標記來創(chuàng)建倒排索引,創(chuàng)建所有唯一術語的排序列表,并將文檔列表與可以找到該詞的位置相關聯(lián)。
????它非常類似于一本書背面的索引,其中包含書中的所有獨特的單詞和可以找到該單詞的頁面列表。當我們說一個文檔被索引時,我們引用倒排索引。我們來看看下面兩個文檔的倒排索引如何看待:
????如果我們想要找到包含術語“insight”的文檔,我們可以掃描倒排的索引(在哪里排序),找到“insight”這個詞,并返回包含這個單詞的文檔ID,這在這種情況下將是文檔1和Doc 2號文件。
????為了提高可搜索性(例如,為小寫字母和小寫字提供相同的結果),首先分析文檔并對其進行索引。
????分析由兩部分組成:
-
將句子標記成單詞
-
將單詞規(guī)范化為標準表單
默認情況下,Elasticsearch使用標準分析器 -
標準標記器(Standard tokenizer),用于在單詞邊界上分割單詞
-
小寫令牌過濾器(Lowercase token filter)將單詞轉換為小寫
還有許多其他分析儀可用,您可以在文檔中閱讀它們。
注意:標準分析儀也使用停止令牌過濾器,但默認情況下禁用。
?
實現原理分析
-
write(寫)/create(創(chuàng)建)操作實現原理
當您向協(xié)調節(jié)點發(fā)送請求以索引新文檔時,將執(zhí)行以下操作:
-
所有在Elasticsearch集群中的節(jié)點都包含:有關哪個分片存在于哪個節(jié)點上的元數據。協(xié)調節(jié)點(coordinating node)使用文檔ID(默認)將文檔路由到對應的分片。Elasticsearch將文檔ID以murmur3作為散列函數進行散列,并通過索引中的主分片數量進行取模運算,以確定文檔應被索引到哪個分片。
shard = hash(document_id) % (num_of_primary_shards) -
當節(jié)點接收到來自協(xié)調節(jié)點的請求時,請求被寫入到translog(我們將在后續(xù)的post中間講解translog),并將該文檔添加到內存緩沖區(qū)。如果請求在主分片上成功,則請求將并行發(fā)送到副本分片。只有在所有主分片和副本分片上的translog被fsync’ed后,客戶端才會收到該請求成功的確認。
-
內存緩沖區(qū)以固定的間隔刷新(默認為1秒),并將內容寫入文件系統(tǒng)緩存中的新段。此分段的內容更尚未被fsync’ed(未被寫入文件系統(tǒng)),分段是打開的,內容可用于搜索。
-
translog被清空,并且文件系統(tǒng)緩存每隔30分鐘進行一次fsync,或者當translog變得太大時進行一次fsync。這個過程在Elasticsearch中稱為flush。在刷新過程中,內存緩沖區(qū)被清除,內容被寫入新的文件分段(segment)。當文件分段被fsync’ed并刷新到磁盤,會創(chuàng)建一個新的提交點(其實就是會更新文件偏移量,文件系統(tǒng)會自動做這個操作)。舊的translog被刪除,一個新的開始。
-
下圖顯示了寫入請求和數據流程:
-
Update和Delete實現原理
????刪除和更新操作也是寫操作。但是,Elasticsearch中的文檔是不可變的(immutable),因此不能刪除或修改。那么,如何刪除/更新文檔呢?
????磁盤上的每個分段(segment)都有一個.del文件與它相關聯(lián)。當發(fā)送刪除請求時,該文檔未被真正刪除,而是在.del文件中標記為已刪除。此文檔可能仍然能被搜索到,但會從結果中過濾掉。當分段合并時(我們將在后續(xù)的帖子中包括段合并),在.del文件中標記為已刪除的文檔不會被包括在新的合并段中。
????現在,我們來看看更新是如何工作的。創(chuàng)建新文檔時,Elasticsearch將為該文檔分配一個版本號。對文檔的每次更改都會產生一個新的版本號。當執(zhí)行更新時,舊版本在.del文件中被標記為已刪除,并且新版本在新的分段中編入索引。舊版本可能仍然與搜索查詢匹配,但是從結果中將其過濾掉。
??? indexed/updated文檔后,我們希望執(zhí)行搜索請求。我們來看看如何在Elasticsearch中執(zhí)行搜索請求。
-
Read的實現原理
讀操作由兩個階段組成:
-
查詢階段(Query Phase)
-
獲取階段(Fetch Phase)
-
查詢階段(Query Phase)
????在此階段,協(xié)調節(jié)點將搜索請求路由到索引(index)中的所有分片(shards)(包括:主要或副本)。分片獨立執(zhí)行搜索,并根據相關性分數創(chuàng)建一個優(yōu)先級排序結果(稍后我們將介紹相關性分數)。所有分片將匹配的文檔和相關分數的文檔ID返回給協(xié)調節(jié)點。協(xié)調節(jié)點創(chuàng)建一個新的優(yōu)先級隊列,并對全局結果進行排序。可以有很多文檔匹配結果,但默認情況下,每個分片將前10個結果發(fā)送到協(xié)調節(jié)點,協(xié)調創(chuàng)建優(yōu)先級隊列,從所有分片中分選結果并返回前10個匹配。
-
獲取階段(Fetch Phase)
????在協(xié)調節(jié)點對所有結果進行排序,已生成全局排序的文檔列表后,它將從所有分片請求原始文檔。
????所有的分片都會豐富文檔并將其返回到協(xié)調節(jié)點。
如上所述,搜索結果按相關性排序。我們來回顧一下相關性的定義。
-
搜索相關性(Search Relevance)
????相關性由Elasticsearch給予搜索結果中返回的每個文檔的分數確定。用于評分的默認算法為tf / idf(術語頻率/逆文檔頻率)。該術語頻率測量術語出現在文檔中的次數(更高頻率=更高的相關性),逆文檔頻率測量術語在整個索引中出現的頻率占索引中文檔總數的百分比(更高的頻率
==較少的相關性)。最終得分是tf-idf分數與其他因素(如詞語鄰近度(短語查詢)),術語相似度(用于模糊查詢)等的組合。
?
?
?
第二部分
?
?
介紹
????第1部分分析了Elasticsearch基本的讀、寫、更新、存儲等方面的實現原理,本文檔主要介紹Elasticsearch如何實現分布式系統(tǒng)的三個特性(consensus, concurrency和consistency),以及分片的內部概念,例如:translog(Write Ahead Log - WAL)和Lucene segments。
本章主要包括以下內容:
-
Consensus:?腦裂(split-brain)和quorum機制
-
Concurrency(并發(fā))
-
Consistency(一致性):確保一致的寫入和讀取
-
Translog (Write Ahead Log?—?WAL)
-
Lucene segments
Consensus(腦裂和quorum機制)
??? Consensus是一個分布式系統(tǒng)的基本挑戰(zhàn)。它要求分布式系統(tǒng)中的所有的進程/節(jié)點,對給定的數據的值/狀態(tài)達成一致。有很多Consensus的算法,如Raft,Paxos等。這些算法在數學上被證明是有效的,但Elasticsearch實現了它自己的consensus系統(tǒng)(zen discovery),是因為Shay Banon(Elasticsearch的創(chuàng)建者)所描述的原因。
??? zen discovery模塊有兩個部分:
-
Ping:執(zhí)行該過程的節(jié)點,用來發(fā)現對方。
-
Unicast:該模塊包含一個主機名列表,用于控制要ping哪個節(jié)點。
? ? ? Elasticsearch是一種點對點(peer-to-peer)系統(tǒng),其中所有節(jié)點彼此通信,并且只有一個活動的主節(jié)點,該節(jié)點可以更新和控制群集狀態(tài)和操作。新的Elasticsearch集群將會進行一次選舉,該選舉作為ping進程的一部分在節(jié)點中運行。在所有有資格選舉的節(jié)點中,其中有一個節(jié)點被選舉為主節(jié)點(master node),其他節(jié)點加入該主節(jié)點。
? ? ? 默認的ping_interval為1秒,ping_timeout為3秒。當節(jié)點加入時,他們會發(fā)生一個join的請求給master主機,該請求有一個join_timeout時間,默認是參數ping_timeout的20倍。
? ? ? 若主節(jié)點失敗,集群中的幾點再次開始ping,開始另一次新的選舉。如果節(jié)點意外地認為主節(jié)點發(fā)生故障,并且通過其他節(jié)點發(fā)現主節(jié)點,則該ping過程也將有所幫助。
? ? ? 注意:默認情況下,客戶端(client)和數據節(jié)點不會對選舉過程做出貢獻。可以通過改變以下參數的設置,這些參數在elasticsearch.yml 文件中:
?
- ?
- ?
?
? ? 故障檢測的過程是這樣的:主節(jié)點ping所有的節(jié)點來檢查這些節(jié)點是否存活,而所有的節(jié)點回ping主節(jié)點來匯報他們是存活的。
????若使用默認配置,Elasticsearch會遇到腦裂的問題,如果是網絡分區(qū),則節(jié)點可以認為主節(jié)點已經死機,并將其自身選為主節(jié)點,從而導致集群具有多個主節(jié)點。這樣可能導致數據丟失,可能無法正確合并數據。這種情況可以通過將以下屬性設置為主合格節(jié)點的法定數量來避免:
- ?
-
?
?
????該參數要求法定數量的可參加選舉的節(jié)點加入新的選舉過程來完成新的選舉過程,并接受新節(jié)點作為新的master節(jié)點。這是確保集群穩(wěn)定性的極其重要的參數,如果集群大小發(fā)生變化,可以進行動態(tài)更新。圖a和b分別顯示了在minimum_master_nodes屬性被設置,和未被設置的兩種情況。
????注意:對于生產環(huán)境的集群,建議有3個專用主節(jié)點,在任何給定的時間點只有一個處于活動狀態(tài),其他的節(jié)點不服務任何客戶請求。
Concurrency(并發(fā)控制)
??? Elasticsearch是一個分布式系統(tǒng),支持并發(fā)請求。當創(chuàng)建/更新/刪除請求命中主分片時,它同時發(fā)送到副本分片,但是這些請求到達的順序是不確定的。在這種情況下,Elasticsearch使用樂觀并發(fā)控制(optimistic concurrency control)來確保文檔的較新版本不會被舊版本覆蓋。
????每個被索引的文檔都會有一個版本號,每次文檔改變時,該版本號就會增加。使用版本號可以確保對文檔的改變是順序進行的。為確保我們的應用程序中的更新不會導致數據丟失,Elasticsearch的API允許您指定,更改應該應用到目前哪一個版本號。如果請求中指定的版本號,早于分片中存在的版本號,則請求失敗,這意味著該文檔已被其他進程更新。
????如何處理失敗的請求?可以在應用程序級別進行控制。還有其他鎖定選項可用,您可以在這里閱讀。
????當我們向Elasticsearch發(fā)送并發(fā)請求時,下一個問題是 - 我們如何使這些請求保持一致?現在要回答 CAP原則,還不是太清晰,這是接下來要討論的問題。
但是,我們將回顧如何使用Elasticsearch實現一致的寫入和讀取。
Consistency?(確保一致的寫和讀)
? ? ? 對于寫操作,Elasticsearch支持與大多數其他數據庫不同的一致性級別,它允許初步檢查以查看有多少個分片可用于允許寫入。可用的選項是:quorum的可設置的值為:one和all。默認情況下,它被設置為:quorum,這意味著只有當大多數分片可用時,才允許寫入操作。在大部分分片可用的情況下,由于某種原因,寫入復制副本分片失敗仍然可能發(fā)生,在這種情況下,副本被認為是錯誤的,該分片將在不同的節(jié)點上進行重建。
? ? ?? 對于讀操作,新文檔在刷新間隔時間后才能用于搜索。為了確保搜索結果來自最新版本的文檔,可以將復制(replication)設置為sync(默認值),當在主分片和副本分片的寫操作都完成后,寫操作才返回。在這種情況下,來自任何分片的搜索請求將從文檔的最新版本返回結果。
? ? ??即使你的應用程序為了更快的indexing而設置:replication=async,也可以使用_preference參數,可以為了搜素請求把它設置為primary。這樣,查詢主要分片就是搜索請求,并確保結果將來自最新版本的文檔。
? ? ??當我們了解Elasticsearch如何處理consensus,并發(fā)性和一致性時,我們來看看分片內部的一些重要概念,這些概念導致了Elasticsearch作為分布式搜索引擎的某些特征。
Translog
? ? ??自從開發(fā)關系數據庫以來,數據庫世界中一直存在著write ahead log(WAL)或事務日志(translog)的概念。Translog在發(fā)生故障的情況下確保數據的完整性,其基本原則是,在數據的實際更改提交到磁盤之前必須先記錄并提交預期的更改。
? ????當新文檔被索引或舊文檔被更新時,Lucene索引會更改,這些更改將被提交到磁盤以進行持久化。每次寫請求之后進行持久化操作是一個非常消耗性能的操作,它通過一次持久化多個修改到磁盤的方式運行(譯注:也就是批量寫入)。正如我們在之前的一篇博客中描述的那樣,默認情況下每30分鐘執(zhí)行一次flush操作(Lucene commit),或者當translog變得太大(默認為512MB)時)。在這種情況下,有可能失去兩次Lucene提交之間的所有變化。為了避免這個問題,Elasticsearch使用translog。所有的索引/刪除/更新操作都先被寫入translog,并且在每個索引/刪除/更新操作之后(或者每默認默認為5秒),translog都被fsync’s,以確保更改被持久化。在主文件和副本分片上的translog被fsync’ed后,客戶端都會收到寫入確認。
? ? ??在兩次Lucene提交或重新啟動之間出現硬件故障的情況下,會在最后一次Lucene提交之前重播Translog以從任何丟失的更改中恢復,并將所有更改應用于索引。
? ? ? 建議在重新啟動Elasticsearch實例之前顯式刷新translog,因為啟動將更快,因為要重播的translog將為空。POST / _all / _flush命令可用于刷新集群中的所有索引。
使用translog刷新操作,文件系統(tǒng)緩存中的段將提交到磁盤,以使索引持續(xù)更改。
現在我們來看看Lucene segment:
Lucene Segments
??? Lucene索引由多個片段(segment)組成,片段本身是完全功能的倒排索引。片段是不可變的,這允許Lucene增量地向索引添加新文檔,而無需從頭開始重建索引。對于每個搜索請求,搜所有段都會被搜素,每個段消耗CPU周期,文件句柄和內存。這意味著分段數越多,搜索性能就越低。
????為了解決這個問題,Elasticsearch將小段合并成一個更大的段(如下圖所示),將新的合并段提交到磁盤,并刪除舊的較小的段。
合并操作將會自動發(fā)生在后臺,而不會中斷索引或搜索。由于分段合并可能會浪費資源并影響搜索性能,因此Elasticsearch會限制合并過程的資源使用,以獲得足夠的資源進行搜索。
?
?
?
第三部分
?
?
介紹
????在上面的文章中,我們討論了Elasticsearch如何處理分布式系統(tǒng)的一些基本問題。在這個部分中,我們將會審查Elasticsearch的各個方面,例如接近實時的搜索和權衡,它考慮計算Insight Data Engineering Fellows在構建數據平臺時利用的搜索相關性。
-
近實時搜索
-
為什么深度分頁在分布式搜索可能是危險的?
-
計算搜索相關性的權衡
近實時的搜索
????雖然在Elasticsearch中的更改不是立即可見,但它確實提供了一個接近實時的搜索引擎。如前一篇文章所述,將Lucene更改持久化到磁盤是一項非常耗性能的操作。為了避免在文檔搜索可用時提交Lucene修改到磁盤,這里有一個位于內存緩沖區(qū)和磁盤之間的文件系統(tǒng)緩存。每秒刷新內存緩沖區(qū)(默認情況下),并在文件系統(tǒng)緩存中創(chuàng)建一個具有倒排索引的新段。此片段已打開并可用于搜索。
????文件系統(tǒng)的緩存中,可能有文件句柄、被打開的文件,該文件可以被打開、讀取和關閉,但它保存在內存中。然而,默認情況下刷新間隔為1秒,所以更改不能立即可見,因此它只是近實時的。這也有助于CRUD操作的近實時操作。
????文件系統(tǒng)緩存可以具有文件句柄和文件可以被打開,讀取和關閉,但是它存在于內存中。
????由于默認情況下,刷新間隔為1秒,所以更改不可見,因此接近實時。由于translog會把沒有寫入磁盤的更改進行持久化。對于每個請求,在檢查相關分段(relevant segments)之前,會對translog進行最近的更改進行搜索,因此客戶端可以接近實時訪問所有更改。
????您可以在每次創(chuàng)建/更新/刪除操作之后顯式刷新索引(refresh the index),以使更改立即生效,但不推薦這樣做,因為該過程會創(chuàng)建很多小的分段(small segments),從而影響搜索性能。
????對于一個搜索請求,會搜索一個索引中給定分片的所有Lucene分段(Lucene segments) 。然而,在結果頁中獲取所有匹配的文檔或文檔深度,對于你的Elasticsearch集群是危險的。
????我們來看看為什么會這樣:
為什么深度分頁(deep pagination)在分布式搜索可能是危險的?
????當你向Elasticsearch發(fā)送一個搜索請求,若有大量文檔被匹配,默認情況下,只是返回top 10的結果。搜索API有from和size參數,用來確定搜索匹配到的所有結果文檔的深度。
????例如,如果要查看與搜索匹配的50到60行的文檔,則設置from= 50和size = 10。
????當每個分片接收到搜索請求時,它將創(chuàng)建一個from+size大小的優(yōu)先隊列,以滿足搜索結果本身,然后將結果返回給協(xié)調節(jié)點。
????若你想查看從50,000到50,010的搜索結果,那么每個分片將創(chuàng)建一個具有50,010個結果的優(yōu)先隊列,并且協(xié)調節(jié)點(coordinating node)必須對shards* 50,010個數目的結果在內存中進行排序。
????分片的數目進行排序 shards* 50,010個結果存儲在內存中。這種級別的分頁是否能夠完成,取決于你擁有的硬件資源。但是您需要非常小心深層分頁,因為它可以輕易讓你的集群崩潰。
????可以通過 scroll API來獲取所有的文檔,該API的行為很像關系數據庫中的游標。通過禁用排序的 scroll API,并且每個分片只要具有與搜索匹配的文檔,就會繼續(xù)發(fā)送結果。
????若要獲取大量的搜索到的文檔,排序得分結果是非常耗性能的。Elasticsearch是分布式系統(tǒng),計算搜索相關性分數代價是很高的。下面我們看一下計算搜索相關性的一些權衡:
計算搜索相關性的權衡
??? Elasticsearch使用tf-idf來計算搜索相關性(search relevance),由于它的分布式特性,計算搜索全局的idf(inverse document frequency)的代價是很高的。替代的方式是,讓每個分片計算本地分片的idf來分配一個相關性分數給結果文檔,僅返回該分片上的文檔。同樣,所有的分片返回結果文檔,這些文檔帶有由本地idf計算的相關性分數,協(xié)調節(jié)點把所有的結果排序,并返回top n的結果。
????這在大多數情況下是正常的,除非您的索引在關鍵字方面有偏差,或者單個分片上沒有足夠的數據來表示全局分布。
????例如,如果您正在搜索“insight”一詞,并且包含術語“insight”的大多數文檔都位于一個分片上,則與查詢匹配的文檔將不會在每個分片上公平排列為本地idf 的值會有很大的不同,搜索結果可能不太相關。
????類似地,如果沒有足夠的數據,那么局部idf值可能會對某些搜索有很大的不同,并且結果可能不如預期那么相關。在具有足夠數據的實際場景中,本地idf值趨向于均勻,并且搜索結果是相關的,因為文檔得分相當。
有幾種方法來獲取本地的idf分數,但是并不推薦用于生產系統(tǒng)。
-
一種方法是你可以只有一個分片用于索引,然后本地idf是全局idf,但這并不排除并行/縮放的空間,并且對于大的索引是不實用的。
-
另一種方法是使用參數dfs_query_then_search(dfs =分布式頻率搜索)與搜索請求,它首先計算所有分片的本地idf,然后組合這些本地idf值以計算整個索引的全局idf,然后返回結果使用全局idf計算相關性分數。這在生產中不推薦,并且具有足夠的數據將確保術語頻率分布良好。
參考
-
https://blog.insightdatascience.com/anatomy-of-an-elasticsearch-cluster-part-i-7ac9a13b05db
-
https://blog.insightdatascience.com/anatomy-of-an-elasticsearch-cluster-part-ii-6db4e821b571
-
https://blog.insightdatascience.com/anatomy-of-an-elasticsearch-cluster-part-iii-8bb6ac84488d
-
https://www.elastic.co/blog/index-vs-type
總結
以上是生活随笔為你收集整理的Elasticsearch实现原理分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 牙缝怎么治
- 下一篇: 一文直击Graph Embedding图