分散式搜尋引擎Elasticsearch的架構分析

vivo網際網路技術發表於2020-12-08

一、寫在前面

 ES(Elasticsearch下文統一稱為ES)越來越多的企業在業務場景是使用ES儲存自己的非結構化資料,例如電商業務實現商品站內搜尋,資料指標分析,日誌分析等,ES作為傳統關係型資料庫的補充,提供了關係型資料庫不具備的一些能力。

ES最先進入大眾視野的是其能夠實現全文搜尋的能力,也是由於基於Lucene的實現,內部有一種倒排索引的資料結構。

本文作者將介紹ES的分散式架構,以及ES的儲存索引機制,本文不會詳細介紹ES的API,會從整體架構層面進行分析,後續作者會有其他文章對ES的使用進行介紹。

二、什麼是倒排索引

要講明白什麼是倒排索引,首先我們先梳理下什麼索引,比如一本書,書的目錄頁,有章節,章節名稱,我們想看哪個章節,我們通過目錄頁,查到對應章節和頁碼,就能定位到具體的章節內容,通過目錄頁的章節名稱查到章節的頁碼,進而看到章節內容,這個過程就是一個索引的過程,那麼什麼是倒排索引呢?

比如查詢《java程式設計思想》這本書的文章,翻開書本可以看到目錄頁,記錄這個章節名字和章節地址頁碼,通過查詢章節名字“繼承”可以定位到“繼承”這篇章節的具體地址,檢視到文章的內容,我們可以看到文章內容中包含很多“物件”這個詞。

那麼如果我們要在這本書中查詢所有包含有“物件”這個詞的文章,那該怎麼辦呢?

按照現在的索引方式無疑大海撈針,假設我們有一個“物件”--→文章的對映關係,不就可以了嗎?類似這樣的反向建立對映關係的就叫倒排索引。

如圖1所示,將文章進行分詞後得到關鍵詞,在根據關鍵詞建立倒排索引,關鍵詞構建成一個詞典,詞典中存放著一個個詞條(關鍵詞),每個關鍵詞都有一個列表與其對應,這個列表就是倒排表,存放的是章節文件編號和詞頻等資訊,倒排列表中的每個元素就是一個倒排項,最後可以看到,整個倒排索引就像一本新華字典,所有單詞的倒排列表往往順序地儲存在磁碟的某個檔案裡,這個檔案被稱之為倒排檔案。

bbbc78e45f794b4e51d68fae680c6c97.webp
(圖1)

 

詞典和倒排檔案是Lucene的兩種基本資料結構,但是儲存方式不同,詞典在記憶體中儲存,倒排檔案在磁碟上。本文不會去介紹分詞,tf-idf,BM25,向量空間相似度等構建倒排索引和查詢倒排索引所用到的技術,讀者只需要對倒排索引有個基本的認識即可。

三、ES的叢集架構

1. 叢集節點

一個ES叢集可以有多個節點構成,一個節點就是一個ES服務例項,通過配置叢集名稱cluster.name加入叢集。那麼節點是如何通過配置相同的叢集名稱加入叢集的呢?要搞明白這個問題,我們必須先搞清楚ES叢集中節點的角色。

ES中節點有角色的區分的,通過配置檔案conf/elasticsearch.yml中配置以下配置進行角色的設定。

node.master: true/false
node.data: true/false

叢集中單個節點既可以是候選主節點也可以是資料節點,通過上面的配置可以進行兩兩組合形成四大分類:

(1)僅為候選主節點
(2)既是候選主節點也是資料節點
(3)僅為資料節點
(4)既不是候選主節點也不是資料節點

候選主節點:只有是候選主節點才可以參與選舉投票,也只有候選主節點可以被選舉為主節點。

主節點:負責索引的新增、刪除,跟蹤哪些節點是群集的一部分,對分片進行分配、收集叢集中各節點的狀態等,穩定的主節點對叢集的健康是非常重要。

資料節點:負責對資料的增、刪、改、查、聚合等操作,資料的查詢和儲存都是由資料節點負責,對機器的CPU,IO以及記憶體的要求比較高,一般選擇高配置的機器作為資料節點。

此外還有一種節點角色叫做協調節點,其本身不是通過設定來分配的,使用者的請求可以隨機發往任何一個節點,並由該節點負責分發請求、收集結果等操作,而不需要主節點轉發。這種節點可稱之為協調節點,叢集中的任何節點都可以充當協調節點的角色。每個節點之間都會保持聯絡。

b2f39580e397bcf539fcef6d20934619.webp
(圖2)

 

2. 發現機制

前文說到通過設定一個叢集名稱,節點就可以加入叢集,那麼ES是如何做到這一點的呢?

這裡就要講一講ES特殊的發現機制ZenDiscovery。

ZenDiscovery是ES的內建發現機制,提供單播和多播兩種發現方式,主要職責是叢集中節點的發現以及選舉Master節點。

多播也叫組播,指一個節點可以向多臺機器傳送請求。生產環境中ES不建議使用這種方式,對於一個大規模的叢集,組播會產生大量不必要的通訊。

單播,當一個節點加入一個現有叢集,或者組建一個新的叢集時,請求傳送到一臺機器。當一個節點聯絡到單播列表中的成員時,它就會得到整個叢集所有節點的狀態,然後它會聯絡Master節點,並加入叢集。

只有在同一臺機器上執行的節點才會自動組成叢集。ES 預設被配置為使用單播發現,單播列表不需要包含叢集中的所有節點,它只是需要足夠的節點,當一個新節點聯絡上其中一個並且通訊就可以了。如果你使用 Master 候選節點作為單播列表,你只要列出三個就可以了。

這個配置在 elasticsearch.yml 檔案中:

discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]

叢集資訊收集階段採用了 Gossip 協議,上面配置的就相當於一個seed nodes,Gossip協議這裡就不多做贅述了。

ES官方建議unicast.hosts配置為所有的候選主節點,ZenDiscovery 會每隔ping_interval(配置項)ping一次,每次超時時間是discovery.zen.ping_timeout(配置項),3次(ping_retries配置項)ping失敗則認為節點當機,當機的情況下會觸發failover,會進行分片重分配、複製等操作。

如果當機的節點不是Master,則Master會更新叢集的元資訊,Master節點將最新的叢集元資訊釋出出去,給其他節點,其他節點回復Ack,Master節點收到discovery.zen.minimum_master_nodes的值-1個 候選主節點的回覆,則傳送Apply訊息給其他節點,叢集狀態更新完畢。如果當機的節點是Master,則其他的候選主節點開始Master節點的選舉流程。

2.1 選主

Master的選主過程中要確保只有一個master,ES通過一個引數quorum的代表多數派閾值,保證選舉出的master被至少quorum個的候選主節點認可,以此來保證只有一個master。

選主的發起由候選主節點發起,當前候選主節點發現自己不是master節點,並且通過ping其他節點發現無法聯絡到主節點,並且包括自己在內已經有超過minimum_master_nodes個節點無法聯絡到主節點,那麼這個時候則發起選主。

選主流程圖

a5272b335bc7d2b9ee5cf60f4a4ce215.webp
(圖3)

 

選主的時候按照叢集節點的引數<stateVersion, id> 排序。stateVersion從大到小排序,以便選出叢集元資訊較新的節點作為Master,id從小到大排序,避免在stateVersion相同時發生分票無法選出 Master。

排序後第一個節點即為Master節點。當一個候選主節點發起一次選舉時,它會按照上述排序策略選出一個它認為的Master。     

2.2 腦裂

提到分散式系統選主,不可避免的會提到腦裂這樣一個現象,什麼是腦裂呢?如果叢集中選舉出多個Master節點,使得資料更新時出現不一致,這種現象稱之為腦裂。

簡而言之叢集中不同的節點對於 Master的選擇出現了分歧,出現了多個Master競爭。

  一般而言腦裂問題可能有以下幾個原因造成:

  • 網路問題:叢集間的網路延遲導致一些節點訪問不到Master,認為Master 掛掉了,而master其實並沒有當機,而選舉出了新的Master,並對Master上的分片和副本標紅,分配新的主分片。

  • 節點負載:主節點的角色既為Master又為Data,訪問量較大時可能會導致 ES 停止響應(假死狀態)造成大面積延遲,此時其他節點得不到主節點的響應認為主節點掛掉了,會重新選取主節點。

  • 記憶體回收:主節點的角色既為Master又為Data,當Data節點上的ES程式佔用的記憶體較大,引發JVM的大規模記憶體回收,造成ES程式失去響應。

如何避免腦裂:我們可以基於上述原因,做出優化措施:

  • 適當調大響應超時時間,減少誤判。通過引數 discovery.zen.ping_timeout 設定節點ping超時時間,預設為 3s,可以適當調大。

  • 選舉觸發,我們需要在候選節點的配置檔案中設定引數 discovery.zen.munimum_master_nodes 的值。這個參數列示在選舉主節點時需要參與選舉的候選主節點的節點數,預設值是 1,官方建議取值(master_eligibel_nodes/2)+1,其中 master_eligibel_nodes 為候選主節點的個數。這樣做既能防止腦裂現象的發生,也能最大限度地提升叢集的高可用性,因為只要不少於 discovery.zen.munimum_master_nodes 個候選節點存活,選舉工作就能正常進行。當小於這個值的時候,無法觸發選舉行為,叢集無法使用,不會造成分片混亂的情況。

  • 角色分離,即是上面我們提到的候選主節點和資料節點進行角色分離,這樣可以減輕主節點的負擔,防止主節點的假死狀態發生,減少對主節點當機的誤判。

四、索引如何寫入的

1.  寫索引原理

1.1 分片

ES支援PB級全文搜尋,通常我們資料量很大的時候,查詢效能都會越來越慢,我們能想到的一個方式的將資料分散到不同的地方儲存,ES也是如此,ES通過水平拆分的方式將一個索引上的資料拆分出來分配到不同的資料塊上,拆分出來的資料庫塊稱之為一個分片Shard,很像MySQL的分庫分表。

不同的主分片分佈在不同的節點上,那麼在多分片的索引中資料應該被寫入哪裡?肯定不能隨機寫,否則查詢的時候就無法快速檢索到對應的資料了,這需要有一個路由策略來確定具體寫入哪一個分片中,怎麼路由我們下文會介紹。在建立索引的時候需要指定分片的數量,並且分片的數量一旦確定就不能修改。

1.2 副本

副本就是對分片的複製,每個主分片都有一個或多個副本分片,當主分片異常時,副本可以提供資料的查詢等操作。主分片和對應的副本分片是不會在同一個節點上的,避免資料的丟失,當一個節點當機的時候,還可以通過副本查詢到資料,副本分片數的最大值是 N-1(其中 N 為節點數)。

對doc的新建、索引和刪除請求都是寫操作,這些寫操作是必須在主分片上完成,然後才能被複制到對應的副本上。ES為了提高寫入的能力這個過程是併發寫的,同時為了解決併發寫的過程中資料衝突的問題,ES通過樂觀鎖的方式控制,每個文件都有一個 _version號,當文件被修改時版本號遞增。

一旦所有的副本分片都報告寫成功才會向協調節點報告成功,協調節點向客戶端報告成功。

77b1b94a96ca267e7661539e39364f6b.webp
(圖4)

 

1.3 Elasticsearch 的寫索引流程

上面提到了寫索引是隻能寫在主分片上,然後同步到副本分片,那麼如圖4所示,這裡有四個主分片分別是S0、S1、S2、S3,一條資料是根據什麼策略寫到指定的分片上呢?這條索引資料為什麼被寫到S0上而不寫到 S1 或 S2 上?這個過程是根據下面這個公式決定的。

shard = hash(routing) % number_of_primary_shards

以上公式的值是在0到number_of_primary_shards-1之間的餘數,也就是資料檔所在分片的位置。routing通過Hash函式生成一個數字,然後這個數字再除以number_of_primary_shards(主分片的數量)後得到餘數。routing是一個可變值,預設是文件的_id ,也可以設定成一個自定義的值。

在一個寫請求被髮送到某個節點後,該節點按照前文所述,會充當協調節點,會根據路由公式計算出寫哪個分片,當前節點有所有其他節點的分片資訊,如果發現對應的分片是在其他節點上,再將請求轉發到該分片的主分片節點上。

在ES叢集中每個節點都通過上面的公式知道資料的在叢集中的存放位置,所以每個節點都有接收讀寫請求的能力。

那麼為什麼在建立索引的時候就確定好主分片的數量,並且不可修改?因為如果數量變化了,那麼所有之前路由計算的值都會無效,資料也就再也找不到了。

( 圖5)

 

如上圖5所示,當前一個資料通過路由計算公式得到的值是 shard=hash(routing)%4=0,則具體流程如下:

(1)資料寫請求傳送到 node1 節點,通過路由計算得到值為1,那麼對應的資料會應該在主分片S1上。
(2)node1節點將請求轉發到 S1 主分片所在的節點node2,node2 接受請求並寫入到磁碟。
(3)併發將資料複製到三個副本分片R1上,其中通過樂觀併發控制資料的衝突。一旦所有的副本分片都報告成功,則節點 node2將向node1節點報告成功,然後node1節點向客戶端報告成功。

這種模式下,只要有副本在,寫入延時最小也是兩次單分片的寫入耗時總和,效率會較低,但是這樣的好處也很明顯,避免寫入後單個機器硬體故障導致資料丟失,在資料完整性和效能方面,一般都是優先選擇資料,除非一些允許丟資料的特殊場景。

在ES裡為了減少磁碟IO保證讀寫效能,一般是每隔一段時間(比如30分鐘)才會把資料寫入磁碟持久化,對於寫入記憶體,但還未flush到磁碟的資料,如果發生機器當機或者掉電,那麼記憶體中的資料也會丟失,這時候如何保證?

對於這種問題,ES借鑑資料庫中的處理方式,增加CommitLog模組,在ES中叫transLog,在下面的ES儲存原理中會介紹。

2.  儲存原理

上面介紹了在ES內部的寫索引處理流程,資料在寫入到分片和副本上後,目前資料在記憶體中,要確保資料在斷電後不丟失,還需要持久化到磁碟上。

我們知道ES是基於Lucene實現的,內部是通過Lucene完成的索引的建立寫入和搜尋查詢,Lucene 工作原理如下圖所示,當新新增一片文件時,Lucene進行分詞等預處理,然後將文件索引寫入記憶體中,並將本次操作寫入事務日誌(transLog),transLog類似於mysql的binlog,用於當機後記憶體資料的恢復,儲存未持久化資料的操作日誌。

預設情況下,Lucene每隔1s(refresh_interval配置項)將記憶體中的資料重新整理到檔案系統快取中,稱為一個segment(段)。一旦刷入檔案系統快取,segment才可以被用於檢索,在這之前是無法被檢索的。

因此refresh_interval決定了ES資料的實時性,因此說ES是一個準實時的系統。segment 在磁碟中是不可修改的,因此避免了磁碟的隨機寫,所有的隨機寫都在記憶體中進行。隨著時間的推移,segment越來越多,預設情況下,Lucene每隔30min或segment 空間大於512M,將快取中的segment持久化落盤,稱為一個commit point,此時刪掉對應的transLog。

當我們在進行寫操作的測試的時候,可以通過手動重新整理來保障資料能夠被及時檢索到,但是不要在生產環境下每次索引一個文件都去手動重新整理,重新整理操作會有一定的效能開銷。一般業務場景中並不都需要每秒重新整理。

可以通過在 Settings 中調大 refresh_interval = "30s" 的值,來降低每個索引的重新整理頻率,設值時需要注意後面帶上時間單位,否則預設是毫秒。當 refresh_interval=-1 時表示關閉索引的自動重新整理。

 

(圖6)

 

索引檔案分段儲存並且不可修改,那麼新增、更新和刪除如何處理呢?

  • 新增,新增很好處理,由於資料是新的,所以只需要對當前文件新增一個段就可以了。
  • 刪除,由於不可修改,所以對於刪除操作,不會把文件從舊的段中移除而是通過新增一個 .del 檔案,檔案中會列出這些被刪除文件的段資訊,這個被標記刪除的文件仍然可以被查詢匹配到, 但它會在最終結果被返回前從結果集中移除。
  • 更新,不能修改舊的段來進行文件的更新,其實更新相當於是刪除和新增這兩個動作組成。會將舊的文件在 .del 檔案中標記刪除,然後文件的新版本中被索引到一個新的段。可能兩個版本的文件都會被一個查詢匹配到,但被刪除的那個舊版本文件在結果集返回前就會被移除。

segment被設定為不可修改具有一定的優勢也有一定的缺點。

優點:

  • 不需要鎖。如果你從來不更新索引,你就不需要擔心多程式同時修改資料的問題。
  • 一旦索引被讀入核心的檔案系統快取,便會留在哪裡,由於其不變性。只要檔案系統快取中還有足夠的空間,那麼大部分讀請求會直接請求記憶體,而不會命中磁碟。這提供了很大的效能提升.
  • 其它快取(像 Filter 快取),在索引的生命週期內始終有效。它們不需要在每次資料改變時被重建,因為資料不會變化。
  • 寫入單個大的倒排索引允許資料被壓縮,減少磁碟 I/O 和需要被快取到記憶體的索引的使用量。

缺點:

  • 當對舊資料進行刪除時,舊資料不會馬上被刪除,而是在 .del 檔案中被標記為刪除。而舊資料只能等到段更新時才能被移除,這樣會造成大量的空間浪費。
  •  若有一條資料頻繁的更新,每次更新都是新增新的,標記舊的,則會有大量的空間浪費。
  • 每次新增資料時都需要新增一個段來儲存資料。當段的數量太多時,對伺服器的資源例如檔案控制程式碼的消耗會非常大。
  • 在查詢的結果中包含所有的結果集,需要排除被標記刪除的舊資料,這增加了查詢的負擔。

2.1  段合併

由於每當重新整理一次就會新建一個segment(段),這樣會導致短時間內的段數量暴增,而segment數目太多會帶來較大的麻煩。大量的segment會影響資料的讀效能。每一個segment都會消耗檔案控制程式碼、記憶體和CPU 執行週期。

更重要的是,每個搜尋請求都必須輪流檢查每個segment然後合併查詢結果,所以segment越多,搜尋也就越慢。

因此Lucene會按照一定的策略將segment合併,合併的時候會將那些舊的已刪除文件從檔案系統中清除。被刪除的文件不會被拷貝到新的大segment中。

合併的過程中不會中斷索引和搜尋,倒排索引的資料結構使得檔案的合併是比較容易的。

段合併在進行索引和搜尋時會自動進行,合併程式選擇一小部分大小相似的段,並且在後臺將它們合併到更大的段中,這些段既可以是未提交的也可以是已提交的。

合併結束後老的段會被刪除,新的段被重新整理到磁碟,同時寫入一個包含新段且排除舊的和較小的段的新提交點,新的段被開啟,可以用來搜尋。段合併的計算量龐大,而且還要吃掉大量磁碟 I/O,並且段合併會拖累寫入速率,如果任其發展會影響搜尋效能。

ES在預設情況下會對合並流程進行資源限制,所以搜尋效能可以得到保證。

(圖7)

 

五、寫在最後

作者對ES的架構原理和索引儲存和寫機制進行介紹,ES的整體架構體系相對比較巧妙,我們在進行系統設計的時候可以借鑑其設計思路,本文只介紹ES整體架構部分,更多的內容,後續作者會在其他文章中繼續分享。

作者:vivo官網商城開發團隊

相關文章