Elasticsearch 是使用 Java 編寫的一種開源搜尋引擎,它在內部使用 Luence 做索引與搜尋,通過對 Lucene 的封裝,提供了一套簡單一致的 RESTful API。
Elasticsearch 也是一種分散式的搜尋引擎架構,可以很簡單地擴充套件到上百個服務節點,並支援 PB 級別的資料查詢,使系統具備高可用和高併發性。
核心概念
Elasticsearch 的核心概念如下:
-
Cluster:叢集,由一個或多個 Elasticsearch 節點組成。
-
Node:節點,組成 Elasticsearch 叢集的服務單元,同一個叢集內節點的名字不能重複。通常在一個節點上分配一個或者多個分片。
-
Shards:分片,當索引上的資料量太大的時候,我們通常會將一個索引上的資料進行水平拆分,拆分出來的每個資料庫叫作一個分片。
在一個多分片的索引中寫入資料時,通過路由來確定具體寫入那一個分片中,所以在建立索引時需要指定分片的數量,並且分片的數量一旦確定就不能更改。
分片後的索引帶來了規模上(資料水平切分)和效能上(並行執行)的提升。每個分片都是 Luence 中的一個索引檔案,每個分片必須有一個主分片和零到多個副本分片。
-
Replicas:備份也叫作副本,是指對主分片的備份。主分片和備份分片都可以對外提供查詢服務,寫操作時先在主分片上完成,然後分發到備份上。
當主分片不可用時,會在備份的分片中選舉出一個作為主分片,所以備份不僅可以提升系統的高可用效能,還可以提升搜尋時的併發效能。但是若副本太多的話,在寫操作時會增加資料同步的負擔。
-
Index:索引,由一個和多個分片組成,通過索引的名字在叢集內進行唯一標識。
-
Type:類別,指索引內部的邏輯分割槽,通過 Type 的名字在索引內進行唯一標識。在查詢時如果沒有該值,則表示在整個索引中查詢。
-
Document:文件,索引中的每一條資料叫作一個文件,類似於關係型資料庫中的一條資料通過 _id 在 Type 內進行唯一標識。
-
Settings:對叢集中索引的定義,比如一個索引預設的分片數、副本數等資訊。
-
Mapping:類似於關係型資料庫中的表結構資訊,用於定義索引中欄位(Field)的儲存型別、分詞方式、是否儲存等資訊。Elasticsearch 中的 Mapping 是可以動態識別的。
如果沒有特殊需求,則不需要手動建立 Mapping,因為 Elasticsearch 會自動根據資料格式識別它的型別,但是當需要對某些欄位新增特殊屬性(比如:定義使用其他分詞器、是否分詞、是否儲存等)時,就需要手動設定 Mapping 了。一個索引的 Mapping 一旦建立,若已經儲存了資料,就不可修改了。
-
Analyzer:欄位的分詞方式的定義。一個 Analyzer 通常由一個 Tokenizer、零到多個 Filter 組成。
比如預設的標準 Analyzer 包含一個標準的 Tokenizer 和三個 Filter:Standard Token Filter、Lower Case Token Filter、Stop Token Filter。
Elasticsearch 的節點的分類如下:
①主節點(Master Node):也叫作主節點,主節點負責建立索引、刪除索引、分配分片、追蹤叢集中的節點狀態等工作。Elasticsearch 中的主節點的工作量相對較輕。
使用者的請求可以發往任何一個節點,並由該節點負責分發請求、收集結果等操作,而並不需要經過主節點轉發。
通過在配置檔案中設定 node.master=true 來設定該節點成為候選主節點(但該節點不一定是主節點,主節點是叢集在候選節點中選舉出來的),在 Elasticsearch 叢集中只有候選節點才有選舉權和被選舉權。其他節點是不參與選舉工作的。
②資料節點(Data Node):資料節點,負責資料的儲存和相關具體操作,比如索引資料的建立、修改、刪除、搜尋、聚合。
所以,資料節點對機器配置要求比較高,首先需要有足夠的磁碟空間來儲存資料,其次資料操作對系統 CPU、Memory 和 I/O 的效能消耗都很大。
通常隨著叢集的擴大,需要增加更多的資料節點來提高可用性。通過在配置檔案中設定 node.data=true 來設定該節點成為資料節點。
③客戶端節點(Client Node):就是既不做候選主節點也不做資料節點的節點,只負責請求的分發、彙總等,也就是下面要說到的協調節點的角色。
其實任何一個節點都可以完成這樣的工作,單獨增加這樣的節點更多地是為了提高併發性。
可在配置檔案中設定該節點成為資料節點:
node.master=falsenode.data=false
④部落節點(Tribe Node):部落節點可以跨越多個叢集,它可以接收每個叢集的狀態,然後合併成一個全域性叢集的狀態。
它可以讀寫所有叢集節點上的資料,在配置檔案中通過如下設定使節點成為部落節點:
tribe: one: cluster.name: cluster_one two: cluster.name: cluster_two
因為 Tribe Node 要在 Elasticsearch 7.0 以後移除,所以不建議使用。
⑤協調節點(Coordinating Node):協調節點,是一種角色,而不是真實的 Elasticsearch 的節點,我們沒有辦法通過配置項來配置哪個節點為協調節點。叢集中的任何節點都可以充當協調節點的角色。
當一個節點 A 收到使用者的查詢請求後,會把查詢語句分發到其他的節點,然後合併各個節點返回的查詢結果,最好返回一個完整的資料集給使用者。
在這個過程中,節點 A 扮演的就是協調節點的角色。由此可見,協調節點會對 CPU、Memory 和 I/O 要求比較高。
叢集的狀態有 Green、Yellow 和 Red 三種,如下所述:
-
Green:綠色,健康。所有的主分片和副本分片都可正常工作,叢集 100% 健康。
-
Yellow:黃色,預警。所有的主分片都可以正常工作,但至少有一個副本分片是不能正常工作的。此時叢集可以正常工作,但是叢集的高可用性在某種程度上被弱化。
-
Red:紅色,叢集不可正常使用。叢集中至少有一個分片的主分片及它的全部副本分片都不可正常工作。
這時雖然叢集的查詢操作還可以進行,但是也只能返回部分資料(其他正常分片的資料可以返回),而分配到這個分片上的寫入請求將會報錯,最終會導致資料的丟失。
3C 和腦裂
①共識性(Consensus)
共識性是分散式系統中最基礎也最主要的一個元件,在分散式系統中的所有節點必須對給定的資料或者節點的狀態達成共識。
雖然現在有很成熟的共識演算法如 Raft、Paxos 等,也有比較成熟的開源軟體如 Zookeeper。
但是 Elasticsearch 並沒有使用它們,而是自己實現共識系統 zen discovery。
*Elasticsearch 之父 Shay Banon 解釋了其中主要的原因:**“zen discovery是 Elasticsearch 的一個核心的基礎元件,zen discovery 不僅能夠實現共識系統的選擇工作,還能夠很方便地監控叢集的讀寫狀態是否健康。當然,我們也不保證其後期會使用 Zookeeper 代替現在的 zen discovery”。*
zen discovery 模組以“八卦傳播”(Gossip)的形式實現了單播(Unicat):單播不同於多播(Multicast)和廣播(Broadcast)。節點間的通訊方式是一對一的。
②併發(Concurrency)
Elasticsearch 是一個分散式系統。寫請求在傳送到主分片時,同時會以並行的形式傳送到備份分片,但是這些請求的送達時間可能是無序的。
在這種情況下,Elasticsearch 用樂觀併發控制(Optimistic Concurrency Control)來保證新版本的資料不會被舊版本的資料覆蓋。
樂觀併發控制是一種樂觀鎖,另一種常用的樂觀鎖即多版本併發控制(Multi-Version Concurrency Control)。
它們的主要區別如下:
-
樂觀併發控制(OCC):是一種用來解決寫-寫衝突的無鎖併發控制,認為事務間的競爭不激烈時,就先進行修改,在提交事務前檢查資料有沒有變化,如果沒有就提交,如果有就放棄並重試。樂觀併發控制類似於自選鎖,適用於低資料競爭且寫衝突比較少的環境。
-
多版本併發控制(MVCC):是一種用來解決讀-寫衝突的無所併發控制,也就是為事務分配單向增長的時間戳,為每一個修改儲存一個版本,版本與事務時間戳關聯,讀操作只讀該事務開始前的資料庫的快照。
這樣在讀操作不用阻塞操作且寫操作不用阻塞讀操作的同時,避免了髒讀和不可重複讀。
③一致性(Consistency)
Elasticsearch 叢集保證寫一致性的方式是在寫入前先檢查有多少個分片可供寫入,如果達到寫入條件,則進行寫操作,否則,Elasticsearch 會等待更多的分片出現,預設為一分鐘。
有如下三種設定來判斷是否允許寫操作:
-
One:只要主分片可用,就可以進行寫操作。
-
All:只有當主分片和所有副本都可用時,才允許寫操作。
-
Quorum(k-wu-wo/reng,法定人數):是 Elasticsearch 的預設選項。當有大部分的分片可用時才允許寫操作。其中,對“大部分”的計算公式為 int((primary+number_of_replicas)/2)+1。
Elasticsearch 叢集保證讀寫一致性的方式是,為了保證搜尋請求的返回結果是最新版本的文件,備份可以被設定為 Sync(預設值),寫操作在主分片和備份分片同時完成後才會返回寫請求的結果。
這樣,無論搜尋請求至哪個分片都會返回最新的文件。但是如果我們的應用對寫要求很高,就可以通過設定 replication=async 來提升寫的效率,如果設定 replication=async,則只要主分片的寫完成,就會返回寫成功。
④腦裂
在 Elasticsearch 叢集中主節點通過 Ping 命令來檢查叢集中的其他節點是否處於可用狀態,同時非主節點也會通過 Ping 來檢查主節點是否處於可用狀態。
當叢集網路不穩定時,有可能會發生一個節點 Ping 不通 Master 節點,則會認為 Master 節點發生了故障,然後重新選出一個 Master 節點,這就會導致在一個叢集內出現多個 Master 節點。
當在一個叢集中有多個 Master 節點時,就有可能會導致資料丟失。我們稱這種現象為腦裂。
事務日誌
我們在上面瞭解到,Lucene 為了加快寫索引的速度,採用了延遲寫入的策略。
雖然這種策略提高了寫入的效率,但其最大的弊端是,如果資料在記憶體中還沒有持久化到磁碟上時發生了類似斷電等不可控情況,就可能丟失資料。
為了避免丟失資料,Elasticsearch 新增了事務日誌(Translog),事務日誌記錄了所有還沒有被持久化磁碟的資料。
Elasticsearch 寫索引的具體過程如下:首先,當有資料寫入時,為了提升寫入的速度,並沒有資料直接寫在磁碟上,而是先寫入到記憶體中,但是為了防止資料的丟失,會追加一份資料到事務日誌裡。
因為記憶體中的資料還會繼續寫入,所以記憶體中的資料並不是以段的形式儲存的,是檢索不到的。
總之,Elasticsearch 是一個準實時的搜尋引擎,而不是一個實時的搜尋引擎。
此時的狀態如圖 7 所示:
圖 7:Elasticsearch 寫資料的過程
然後,當達到預設的時間(1 秒鐘)或者記憶體的資料達到一定量時,會觸發一次重新整理(Refresh)。
重新整理的主要步驟如下:
-
將記憶體中的資料重新整理到一個新的段中,但是該段並沒有持久化到硬碟中,而是快取在作業系統的檔案快取系統中。雖然資料還在記憶體中,但是記憶體裡的資料和檔案快取系統裡的資料有以下區別。
記憶體使用的是 JVM 的記憶體,而檔案快取系統使用的是作業系統的記憶體;記憶體的資料不是以段的形式儲存的,並且可以繼續向記憶體裡寫資料。檔案快取系統中的資料是以段的形式儲存的,所以只能讀,不能寫;記憶體中的資料是搜尋不到,檔案快取系統中的資料是可以搜尋的。
-
開啟儲存在檔案快取系統中的段,使其可被搜尋。
-
清空記憶體,準備接收新的資料。日誌不做清空處理。
此時的狀態如圖 8 所示:
圖 8:Elasticsearch 寫資料的過程
最後,重新整理(Flush)。當日志資料的大小超過 512MB 或者時間超過 30 分鐘時,需要觸發一次重新整理。
重新整理的主要步驟如下:
-
在檔案快取系統中建立一個新的段,並把記憶體中的資料寫入,使其可被搜尋。
-
清空記憶體,準備接收新的資料。
-
將檔案系統快取中的資料通過 Fsync 函式重新整理到硬碟中。
-
生成提交點。
-
刪除舊的日誌,建立一個空的日誌。
此時的狀態如圖 9 所示:
圖 9:Elasticsearch 寫資料的過程
由上面索引建立的過程可知,記憶體裡面的資料並沒有直接被重新整理(Flush)到硬碟中,而是被重新整理(Refresh)到了檔案快取系統中,這主要是因為持久化資料十分耗費資源,頻繁地呼叫會使寫入的效能急劇下降。
所以 Elasticsearch,為了提高寫入的效率,利用了檔案快取系統和記憶體來加速寫入時的效能,並使用日誌來防止資料的丟失。
在需要重啟時,Elasticsearch 不僅要根據提交點去載入已經持久化過的段,還需要根據 Translog 裡的記錄,把未持久化的資料重新持久化到磁碟上。
根據上面對 Elasticsearch,寫操作流程的介紹,我們可以整理出一個索引資料所要經歷的幾個階段,以及每個階段的資料的儲存方式和作用,如圖 10 所示:
圖 10:Elasticsearch 寫操作流程
在叢集中寫索引
假設我們有如圖 11 所示(圖片來自官網)的一個叢集,該叢集由三個節點組成(Node 1、Node 2 和 Node 3),包含一個由兩個主分片和每個主分片由兩個副本分片組成的索引。
圖 11:寫索引
其中,標星號的 Node 1 是 Master 節點,負責管理整個叢集的狀態;p1 和 p2 是主分片;r0 和 r1 是副本分片。為了達到高可用,Master 節點避免將主分片和副本放在同一個節點。
將資料分片是為了提高可處理資料的容量和易於進行水平擴充套件,為分片做副本是為了提高叢集的穩定性和提高併發量。
在主分片掛掉後,會從副本分片中選舉出一個升級為主分片,當副本升級為主分片後,由於少了一個副本分片,所以叢集狀態會從 Green 改變為 Yellow,但是此時叢集仍然可用。
在一個叢集中有一個分片的主分片和副本分片都掛掉後,叢集狀態會由 Yellow 改變為 Red,叢集狀態為 Red 時叢集不可正常使用。
由上面的步驟可知,副本分片越多,叢集的可用性就越高,但是由於每個分片都相當於一個 Lucene 的索引檔案,會佔用一定的檔案控制程式碼、記憶體及 CPU,並且分片間的資料同步也會佔用一定的網路頻寬,所以,索引的分片數和副本數並不是越多越好。
寫索引時只能寫在主分片上,然後同步到副本上,那麼,一個資料應該被寫在哪個分片上呢?
如圖 10 所示,如何知道一個資料應該被寫在 p0 還是 p1 上呢答案就是路由(routing),路由公式如下:
shard = hash(routing)%number_of_primary_shards
其中,Routing 是一個可選擇的值,預設是文件的 _id(文件的唯一主鍵,文件在建立時,如果文件的 _id 已經存在,則進行更新,如果不存在則建立)。
後面會介紹如何通過自定義 Routing 引數使查詢落在一個分片中,而不用查詢所有的分片,從而提升查詢的效能。
Routing 通過 Hash 函式生成一個數字,將這個數字除以 number_of_primary_shards(分片的數量)後得到餘數。
這個分佈在 0 到 number_of_primary_shards - 1 之間的餘數,就是我們所尋求的文件所在分片的位置。
這也就說明了一旦分片數定下來就不能再改變的原因,因為分片數改變之後,所有之前的路由值都會變得無效,前期建立的文件也就找不到了。
由於在 Elasticsearch 叢集中每個節點都知道叢集中的文件的存放位置(通過路由公式定位),所以每個節點都有處理讀寫請求的能力。
在一個寫請求被髮送到叢集中的一個節點後,此時,該節點被稱為協調點(Coordinating Node),協調點會根據路由公式計算出需要寫到哪個分片上,再將請求轉發到該分片的主分片節點上。
圖 12:寫索引
寫操作的流程如下(參考圖 11,圖片來自官網):
-
客戶端向 Node 1(協調節點)傳送寫請求。
-
Node 1 通過文件的 _id(預設是 _id,但不表示一定是 _id)確定文件屬於哪個分片(在本例中是編號為 0 的分片)。請求會被轉發到主分片所在的節點 Node 3 上。
-
Node 3 在主分片上執行請求,如果成功,則將請求並行轉發到 Node 1 和 Node 2 的副本分片上。
一旦所有的副本分片都報告成功(預設),則 Node 3 將向協調節點報告成功,協調節點向客戶端報告成功。
叢集中的查詢流程
根據 Routing 欄位進行的單個文件的查詢,在 Elasticsearch 叢集中可以在主分片或者副本分片上進行。
圖 13
查詢欄位剛好是 Routing 的分片欄位如“_id”的查詢流程如下(見圖 12,圖片來自官網):
-
客戶端向叢集傳送查詢請求,叢集再隨機選擇一個節點作為協調點(Node 1),負責處理這次查詢。
-
Node 1 使用文件的 routing id 來計算要查詢的文件在哪個分片上(在本例中落在了 0 分片上)分片 0 的副本分片存在所有的三個節點上。
在這種情況下,協調節點可以把請求轉發到任意節點,本例將請求轉發到 Node 2 上。
-
Node 2 執行查詢,並將查詢結果返回給協調節點 Node 1,Node 1 再將文件返回給客戶端。
當一個搜尋請求被髮送到某個節點時,這個節點就變成了協調節點(Node 1)。
協調節點的任務是廣播查詢請求到所有分片(主分片或者副本分片),並將它們的響應結果整合成全域性排序後的結果集合。
由上面步驟 3 所示,預設返回給協調節點並不是所有的資料,而是隻有文件的 id 和得分 score,因為我們最後只返回給使用者 size 條資料,所以這樣做的好處是可以節省很多頻寬,特別是 from 很大時。
協調節點對收集回來的資料進行排序後,找到要返回的 size 條資料的 id,再根據 id 查詢要返回的資料,比如 title、content 等。
圖 14
取回資料等流程如下(見圖 13,圖片來自官網):
-
Node 3 進行二次排序來找出要返回的文件 id,並向相關的分片提交多個獲得文件詳情的請求。
-
每個分片載入文件,並將文件返回給 Node 3。
-
一旦所有的文件都取回了,Node 3 就返回結果給客戶端。
協調節點收集各個分片查詢出來的資料,再進行二次排序,然後選擇需要被取回的文件。
例如,如果我們的查詢指定了{"from": 20, "size": 10},那麼我們需要在每個分片中查詢出來得分最高的 20+10 條資料,協調節點在收集到 30×n(n 為分片數)條資料後再進行排序。
排序位置在 0-20 的結果會被丟棄,只有從第 21 個開始的 10 個結果需要被取回。這些文件可能來自多個甚至全部分片。
由上面的搜尋策略可以知道,在查詢時深翻(Deep Pagination)並不是一種好方法。
因為深翻時,from 會很大,這時的排序過程可能會變得非常沉重,會佔用大量的 CPU、記憶體和頻寬。因為這個原因,所以強烈建議慎重使用深翻。
分片可以減少每個片上的資料量,加快查詢的速度,但是在查詢時,協調節點要在收集數(from+size)×n 條資料後再做一次全域性排序。
若這個資料量很大,則也會佔用大量的 CPU、記憶體、頻寬等,並且分片查詢的速度取決於最慢的分片查詢的速度,所以分片數並不是越多越好。
本作品採用《CC 協議》,轉載必須註明作者和本文連結