分佈式搜索引擎 Elasticsearch 的架構分析

1、寫在前面

 ES(Elasticsearch下文統一稱爲ES)愈來愈多的企業在業務場景是使用ES存儲本身的非結構化數據,例如電商業務實現商品站內搜索,數據指標分析,日誌分析等,ES做爲傳統關係型數據庫的補充,提供了關係型數據庫不具有的一些能力。java

ES最早進入大衆視野的是其可以實現全文搜索的能力,也是因爲基於Lucene的實現,內部有一種倒排索引的數據結構。node

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

2、什麼是倒排索引

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

好比查詢《java編程思想》這本書的文章,翻開書本能夠看到目錄頁,記錄這個章節名字和章節地址頁碼,經過查詢章節名字「繼承」能夠定位到「繼承」這篇章節的具體地址,查看到文章的內容,咱們能夠看到文章內容中包含不少「對象」這個詞。數據庫

那麼若是咱們要在這本書中查詢全部包含有「對象」這個詞的文章,那該怎麼辦呢?編程

按照如今的索引方式無疑大海撈針,假設咱們有一個「對象」--→文章的映射關係,不就能夠了嗎?相似這樣的反向創建映射關係的就叫倒排索引。緩存

如圖1所示,將文章進行分詞後獲得關鍵詞,在根據關鍵詞創建倒排索引,關鍵詞構建成一個詞典,詞典中存放着一個個詞條(關鍵詞),每一個關鍵詞都有一個列表與其對應,這個列表就是倒排表,存放的是章節文檔編號和詞頻等信息,倒排列表中的每一個元素就是一個倒排項,最後能夠看到,整個倒排索引就像一本新華字典,全部單詞的倒排列表每每順序地存儲在磁盤的某個文件裏,這個文件被稱之爲倒排文件。服務器

(圖1)網絡

詞典和倒排文件是Lucene的兩種基本數據結構,可是存儲方式不一樣,詞典在內存中存儲,倒排文件在磁盤上。本文不會去介紹分詞,tf-idf,BM25,向量空間類似度等構建倒排索引和查詢倒排索引所用到的技術,讀者只須要對倒排索引有個基本的認識便可。數據結構

3、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以及內存的要求比較高,通常選擇高配置的機器做爲數據節點。

此外還有一種節點角色叫作協調節點,其自己不是經過設置來分配的,用戶的請求能夠隨機發往任何一個節點,並由該節點負責分發請求、收集結果等操做,而不須要主節點轉發。這種節點可稱之爲協調節點,集羣中的任何節點均可以充當協調節點的角色。每一個節點之間都會保持聯繫。

(圖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個節點沒法聯繫到主節點,那麼這個時候則發起選主。

選主流程圖

(圖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 個候選節點存活,選舉工做就能正常進行。當小於這個值的時候,沒法觸發選舉行爲,集羣沒法使用,不會形成分片混亂的狀況。
  • 角色分離,便是上面咱們提到的候選主節點和數據節點進行角色分離,這樣能夠減輕主節點的負擔,防止主節點的假死狀態發生,減小對主節點宕機的誤判。

4、索引如何寫入的

1.  寫索引原理

1.1 分片

ES支持PB級全文搜索,一般咱們數據量很大的時候,查詢性能都會愈來愈慢,咱們能想到的一個方式的將數據分散到不一樣的地方存儲,ES也是如此,ES經過水平拆分的方式將一個索引上的數據拆分出來分配到不一樣的數據塊上,拆分出來的數據庫塊稱之爲一個分片Shard,很像MySQL的分庫分表。

不一樣的主分片分佈在不一樣的節點上,那麼在多分片的索引中數據應該被寫入哪裏?確定不能隨機寫,不然查詢的時候就沒法快速檢索到對應的數據了,這須要有一個路由策略來肯定具體寫入哪個分片中,怎麼路由咱們下文會介紹。在建立索引的時候須要指定分片的數量,而且分片的數量一旦肯定就不能修改。

1.2 副本

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

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

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

(圖4)

1.3 Elasticsearch 的寫索引流程

上面提到了寫索引是隻能寫在主分片上,而後同步到副本分片,那麼如圖4所示,這裏有四個主分片分別是S0、S一、S二、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)

5、寫在最後

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

做者:vivo官網商城開發團隊
相關文章
相關標籤/搜索