Elasticsearch 技術分析(四): 分佈式工作原理


|0前言

 

通過前面章節的瞭解,我們已經知道 Elasticsearch 是一個實時的分佈式搜索分析引擎,它能讓你以一個之前從未有過的速度和規模,去探索你的數據。它被用作全文檢索、結構化搜索、分析以及這三個功能的組合。 Elasticsearch 可以橫向擴展至數百(甚至數千)的服務器節點,同時可以處理PB級數據。

雖然說 Elasticsearch 是分佈式的,但是對於我們開發者來說並未過多的參與其中,我們只需啓動對應數量的 ES 實例(即節點),並給它們分配相同的 cluster.name 讓它們歸屬於同一個集羣,創建索引的時候只需指定索引 分片數 和 副本數 即可,其他的都交給了 ES 內部自己去實現。

這和數據庫的分佈式和 同源的 solr 實現分佈式都是有區別的,數據庫分佈式(分庫分表)需要我們指定路由規則和數據同步策略等,solr的分佈式也需依賴 zookeeper,但是 Elasticsearch 完全屏蔽了這些。

所以我們說,Elasticsearch 天生就是分佈式的,並且在設計時屏蔽了分佈式的複雜性。Elasticsearch 在分佈式方面幾乎是透明的。我們可以使用筆記本上的單節點輕鬆地運行Elasticsearch 的程序,但如果你想要在 100 個節點的集羣上運行程序,一切也依然順暢。

Elasticsearch 儘可能地屏蔽了分佈式系統的複雜性。這裏列舉了一些在後臺自動執行的操作:

  • 分配文檔到不同的容器 或 分片 中,文檔可以儲存在一個或多個節點中。
  • 按集羣節點來均衡分配這些分片,從而對索引和搜索過程進行負載均衡。
  • 複製每個分片以支持數據冗餘,從而防止硬件故障導致的數據丟失。
  • 將集羣中任一節點的請求路由到存有相關數據的節點。
  • 集羣擴容時無縫整合新節點,重新分配分片以便從離羣節點恢復。

雖然我們可以不瞭解 Elasticsearch 分佈式內部實現機制也能將Elasticsearch使用的很好,但是瞭解它們將會從另一個角度幫助我們更完整的學習和理解 Elasticsearch 知識。接下里我們從以下幾個部分來詳細講解 Elasticsearch 分佈式的內部實現機制。

2|0集羣的原理

 

對於我們之前的分佈式經驗,我們知道,提升分佈式性能可以通過購買性能更強大( 垂直擴容 ,或 縱向擴容 ) 或者數量更多的服務器( 水平擴容 ,或 橫向擴容 )來實現。

雖然Elasticsearch 可以獲益於更強大的硬件設備,例如將存儲硬盤設爲SSD,但是 垂直擴容 由於硬件設備的技術和價格限制,垂直擴容 是有極限的。真正的擴容能力是來自於 水平擴容 --爲集羣添加更多的節點,並且將負載壓力和穩定性分散到這些節點中。

對於大多數的數據庫而言,通常需要對應用程序進行非常大的改動,才能利用上橫向擴容的新增資源。 與之相反的是,ElastiSearch天生就是 分佈式的 ,它知道如何通過管理多節點來提高擴容性和可用性。 這也意味着你的應用無需關注這個問題。那麼它是如何管理的呢?

2|1主節點

 

啓動一個 ES 實例就是一個節點,節點加入集羣是通過配置文件中設置相同的 cluste.name 而實現的。所以集羣是由一個或者多個擁有相同 cluster.name 配置的節點組成, 它們共同承擔數據和負載的壓力。當有節點加入集羣中或者從集羣中移除節點時,集羣將會重新平均分佈所有的數據。

與其他組件集羣(mysql,redis)的 master-slave模式一樣,ES集羣中也會選舉一個節點成爲主節點,主節點它的職責是維護全局集羣狀態,在節點加入或離開集羣的時候重新分配分片。具體關於主節點選舉的內容可以閱讀選舉主節點

所有主要的文檔級別API(索引,刪除,搜索)都不與主節點通信,主節點並不需要涉及到文檔級別的變更和搜索等操作,所以當集羣只擁有一個主節點的情況下,即使流量的增加它也不會成爲瓶頸。 任何節點都可以成爲主節點。如果集羣中就只有一個節點,那麼它同時也就是主節點。

所以如果我們使用 kibana 來作爲視圖操作工具的話,我們只需在kibana.yml的配置文件中,將elasticsearch.url: "http://localhost:9200"設置爲主節點就可以了,通過主節點 ES 會自動關聯查詢所有節點和分片以及副本的信息。所以 kibana 一般都和主節點在同一臺服務器上。

作爲用戶,我們可以將請求發送到 集羣中的任何節點 ,包括主節點。 每個節點都知道任意文檔所處的位置,並且能夠將我們的請求直接轉發到存儲我們所需文檔的節點。 無論我們將請求發送到哪個節點,它都能負責從各個包含我們所需文檔的節點收集回數據,並將最終結果返回給客戶端。 Elasticsearch 對這一切的管理都是透明的。

2|2發現機制

 

ES 是如何實現只需要配置相同的cluste.name就將節點加入同一集羣的呢?答案是發現機制(discovery module)。

發現機制 負責發現集羣中的節點,以及選擇主節點。每次集羣狀態發生更改時,集羣中的其他節點都會知道狀態(具體方式取決於使用的是哪一種發現機制)。

ES目前主要推薦的自動發現機制,有如下幾種:

  1. Azure classic discovery 插件方式,多播
  2. EC2 discovery 插件方式,多播
  3. Google Compute Engine (GCE) discovery 插件方式,多播
  4. Zen discovery 默認實現,多播/單播

這裏額外介紹下單播,多播,廣播的定義和區別,方便我們更好的理解發現機制。

單播,多播,廣播的區別:

  • 單播(unicast):網絡節點之間的通信就好像是人們之間的對話一樣。如果一個人對另外一個人說話,那麼用網絡技術的術語來描述就是“單播”,此時信息的接收和傳遞只在兩個節點之間進行。例如,你在收發電子郵件、瀏覽網頁時,必須與郵件服務器、Web服務器建立連接,此時使用的就是單播數據傳輸方式。

  • 多播(multicast):“多播”也可以稱爲“組播”,多播”可以理解爲一個人向多個人(但不是在場的所有人)說話,這樣能夠提高通話的效率。因爲如果採用單播方式,逐個節點傳輸,有多少個目標節點,就會有多少次傳送過程,這種方式顯然效率極低,是不可取的。如果你要通知特定的某些人同一件事情,但是又不想讓其他人知道,使用電話一個一個地通知就非常麻煩。多播方式,既可以實現一次傳送所有目標節點的數據,也可以達到只對特定對象傳送數據的目的。多播在網絡技術的應用並不是很多,網上視頻會議、網上視頻點播特別適合採用多播方式。

  • 廣播(broadcast):可以理解爲一個人通過廣播喇叭對在場的全體說話,這樣做的好處是通話效率高,信息一下子就可以傳遞到全體,廣播是不區分目標、全部發送的方式,一次可以傳送完數據,但是不區分特定數據接收對象。

上面列舉的發現機制中, Zen Discovery 是 ES 默認內建發現機制。它提供單播多播的發現方式,並且可以擴展爲通過插件支持雲環境和其他形式的發現。所以我們接下來重點介紹下 Zen Discovery是如何在Elasticsearch中使用的。

集羣是由相同cluster.name的節點組成的。當你在同一臺機器上啓動了第二個節點時,只要它和第一個節點有同樣的 cluster.name 配置,它就會自動發現集羣並加入到其中。但是在不同機器上啓動節點的時候,爲了加入到同一集羣,你需要配置一個可連接到的單播主機列表。

單播主機列表通過discovery.zen.ping.unicast.hosts來配置。這個配置在 elasticsearch.yml 文件中:

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

具體的值是一個主機數組或逗號分隔的字符串。每個值應採用host:porthost的形式(其中port默認爲設置transport.profiles.default.port,如果未設置則返回transport.tcp.port)。請注意,必須將IPv6主機置於括號內。此設置的默認值爲127.0.0.1,[:: 1]

Elasticsearch 官方推薦我們使用 單播 代替 組播。而且 Elasticsearch 默認被配置爲使用 單播 發現,以防止節點無意中加入集羣。只有在同一臺機器上運行的節點纔會自動組成集羣。

雖然 組播 仍然作爲插件提供, 但它應該永遠不被使用在生產環境了,否則你得到的結果就是一個節點意外的加入到了你的生產環境,僅僅是因爲他們收到了一個錯誤的 組播 信號。對於 組播 本身並沒有錯,組播會導致一些愚蠢的問題,並且導致集羣變的脆弱(比如,一個網絡工程師正在搗鼓網絡,而沒有告訴你,你會發現所有的節點突然發現不了對方了)。

使用單播,你可以爲 Elasticsearch 提供一些它應該去嘗試連接的節點列表。當一個節點聯繫到單播列表中的成員時,它就會得到整個集羣所有節點的狀態,然後它會聯繫 master 節點,並加入集羣。

這意味着你的單播列表不需要包含你的集羣中的所有節點,它只是需要足夠的節點,當一個新節點聯繫上其中一個並且說上話就可以了。如果你使用 master 候選節點作爲單播列表,你只要列出三個就可以了。

關於 Elasticsearch 節點發現的詳細信息,請參閱 Zen Discovery

3|0應對故障

 

對於分佈式系統的熟悉,我們應該知道分佈式系統設計的目的是爲了提高可用性和容錯性。在單點系統中的問題在 ES 中同樣也會存在。

3|1單節點的問題

 

如果我們啓動了一個單獨的節點,裏面不包含任何的數據和索引,那我們的集羣就是一個包含空內容節點的集羣,簡稱空集羣

當集羣中只有一個節點在運行時,意味着會有一個單點故障問題——沒有冗餘。單點的最大問題是系統容錯性不高,當單節點所在服務器發生故障後,整個 ES 服務就會停止工作。

讓我們在包含一個空節點的集羣內創建名爲 user 的索引。索引在默認情況下會被分配5個主分片和每個主分片的1個副本, 但是爲了演示目的,我們將分配3個主分片和一份副本(每個主分片擁有一個副本分片):

<span style="color:#222222"><code>PUT /user
{
   <span style="color:#b9b9b9 !important">"settings"</span> : {
      <span style="color:#b9b9b9 !important">"number_of_shards"</span> : <span style="color:#6896ba !important">3</span>,
      <span style="color:#b9b9b9 !important">"number_of_replicas"</span> : <span style="color:#6896ba !important">1</span>
   }
}</code></span>

我們的集羣現在是下圖所示情況,所有3個主分片都被分配在 Node 1 。

擁有一個索引的單節點集羣

此時檢查集羣的健康狀況GET /_cluster/health,我們會發現:

<span style="color:#222222"><code>{
  <span style="color:#b9b9b9 !important">"cluster_name"</span>: <span style="color:#6a8759 !important">"elasticsearch"</span>,
  <span style="color:#b9b9b9 !important">"status"</span>: <span style="color:#6a8759 !important">"yellow"</span>,                     # 1
  <span style="color:#b9b9b9 !important">"timed_out"</span>: <span style="color:#6896ba !important">false</span>,
  <span style="color:#b9b9b9 !important">"number_of_nodes"</span>: <span style="color:#6896ba !important">1</span>,
  <span style="color:#b9b9b9 !important">"number_of_data_nodes"</span>: <span style="color:#6896ba !important">1</span>,
  <span style="color:#b9b9b9 !important">"active_primary_shards"</span>: <span style="color:#6896ba !important">3</span>,
  <span style="color:#b9b9b9 !important">"active_shards"</span>: <span style="color:#6896ba !important">3</span>,
  <span style="color:#b9b9b9 !important">"relocating_shards"</span>: <span style="color:#6896ba !important">0</span>,
  <span style="color:#b9b9b9 !important">"initializing_shards"</span>: <span style="color:#6896ba !important">0</span>,
  <span style="color:#b9b9b9 !important">"unassigned_shards"</span>: <span style="color:#6896ba !important">3</span>,                 # 2
  <span style="color:#b9b9b9 !important">"delayed_unassigned_shards"</span>: <span style="color:#6896ba !important">0</span>,
  <span style="color:#b9b9b9 !important">"number_of_pending_tasks"</span>: <span style="color:#6896ba !important">0</span>,
  <span style="color:#b9b9b9 !important">"number_of_in_flight_fetch"</span>: <span style="color:#6896ba !important">0</span>,
  <span style="color:#b9b9b9 !important">"task_max_waiting_in_queue_millis"</span>: <span style="color:#6896ba !important">0</span>,
  <span style="color:#b9b9b9 !important">"active_shards_percent_as_number"</span>: <span style="color:#6896ba !important">50</span>
}</code></span>

#1 集羣的狀態值是 yellow
#2 未分配的副本數是 3

集羣的健康狀況爲 yellow 則表示全部 主 分片都正常運行(集羣可以正常服務所有請求),但是 副本 分片沒有全部處在正常狀態。 實際上,所有3個副本分片都是 unassigned —— 它們都沒有被分配到任何節點。 在同一個節點上既保存原始數據又保存副本是沒有意義的,因爲一旦失去了那個節點,我們也將丟失該節點上的所有副本數據。

主分片和對應的副本分片是不會在同一個節點上的。所以副本分片數的最大值是 n -1(其中n 爲節點數)。

雖然當前我們的集羣是正常運行的,但是在硬件故障時有丟失數據的風險。

3|2水平擴容

 

既然單點是有問題的,那我們只需再啓動幾個節點並加入到當前集羣中,這樣就可以提高可用性並實現故障轉移,這種方式即 水平擴容

擁有兩個節點的集羣

還以上面的 user 爲例,我們新增一個節點後,新的集羣如上圖所示。

當第二個節點加入到集羣后,3個 副本分片 將會分配到這個節點上——每個主分片對應一個副本分片。 這意味着當集羣內任何一個節點出現問題時,我們的數據都完好無損。

所有新近被索引的文檔都將會保存在主分片上,然後被並行的複製到對應的副本分片上。這就保證了我們既可以從主分片又可以從副本分片上獲得文檔。

cluster-health現在展示的狀態爲 green ,這表示所有6個分片(包括3個主分片和3個副本分片)都在正常運行。我們的集羣現在不僅僅是正常運行的,並且還處於 始終可用 的狀態。

3|3動態擴容

 

產品不斷升級,業務不斷增長,用戶數也會不斷新增,也許我們之前設計的索引容量(3個主分片和3個副本分片)已經不夠使用了,用戶數據的不斷增加,每個主分片和副本分片的數據不斷累積,達到一定程度之後也會降低搜索性能。那麼怎樣爲我們的正在增長中的應用程序按需擴容呢?

我們將之前的兩個節點繼續水平擴容,再增加一個節點,此時集羣狀態如下圖所示:

爲了分散負載,ES 會對分片進行重新分配。Node 1 和 Node 2 上各有一個分片被遷移到了新的 Node 3 節點,現在每個節點上都擁有2個分片,而不是之前的3個。 這表示每個節點的硬件資源(CPU, RAM, I/O)將被更少的分片所共享,每個分片的性能將會得到提升。

分片是一個功能完整的搜索引擎,它擁有使用一個節點上的所有資源的能力。 我們這個擁有6個分片(3個主分片和3個副本分片)的索引可以最大擴容到6個節點,每個節點上存在一個分片,並且每個分片擁有所在節點的全部資源。

但是如果我們想要擴容超過6個節點怎麼辦呢?

主分片的數目在索引創建時 就已經確定了下來。實際上,這個數目定義了這個索引能夠 存儲 的最大數據量。(實際大小取決於你的數據、硬件和使用場景。) 但是,讀操作——搜索和返回數據——可以同時被主分片 或 副本分片所處理,所以當你擁有越多的副本分片時,也將擁有越高的吞吐量。

索引的主分片數這個值在索引創建後就不能修改了(默認值是 5),但是每個主分片的副本數(默認值是 1 )對於活動的索引庫,這個值可以隨時修改的。至於索引的主分片數爲什麼在索引創建之後就不能修改了,我們在下面的文檔存儲原理章節中說明。

既然在運行中的集羣上是可以動態調整副本分片數目的 ,那麼我們可以按需伸縮集羣。讓我們把副本數從默認的 1 增加到 2 :

<span style="color:#222222"><code>PUT /user/_settings
{
   <span style="color:#b9b9b9 !important">"number_of_replicas"</span> : <span style="color:#6896ba !important">2</span>
}</code></span>

如下圖 所示, user 索引現在擁有9個分片:3個主分片和6個副本分片。 這意味着我們可以將集羣擴容到9個節點,每個節點上一個分片。相比原來3個節點時,集羣搜索性能可以提升 3 倍。

當然,如果只是在相同節點數目的集羣上增加更多的副本分片並不能提高性能,因爲每個分片從節點上獲得的資源會變少。 你需要增加更多的硬件資源來提升吞吐量。

但是更多的副本分片數提高了數據冗餘量:按照上面的節點配置,我們可以在失去2個節點的情況下不丟失任何數據。

3|4節點故障

 

如果我們某一個節點發生故障,節點服務器宕機或網絡不可用,這裏假設主節點1發生故障,這時集羣的狀態爲:

此時我們檢查一下集羣的健康狀況,可以發現狀態爲 red,表示不是所有主分片都在正常工作。

我們關閉的節點是一個主節點。而集羣必須擁有一個主節點來保證正常工作,所以發生的第一件事情就是選舉一個新的主節點: Node 2 。

在我們關閉 Node 1 的同時也失去了主分片 1 和 2 ,並且在缺失主分片的時候索引也不能正常工作。

幸運的是,在其它節點上存在着這兩個主分片的完整副本, 所以新的主節點立即將這些分片在 Node 2 和 Node 3 上對應的副本分片提升爲主分片, 此時集羣的狀態將會爲 yellow。 這個提升主分片的過程是瞬間發生的,如同按下一個開關一般。

爲什麼我們集羣狀態是 yellow 而不是 green 呢? 雖然我們擁有所有的三個主分片,但是同時設置了每個主分片需要對應2份副本分片,而此時只存在一份副本分片。 所以集羣不能爲 green 的狀態,不過我們不必過於擔心:如果我們同樣關閉了 Node 2 ,我們的程序 依然 可以保持在不丟任何數據的情況下運行,因爲 Node 3 爲每一個分片都保留着一份副本。

如果我們重新啓動 Node 1 ,集羣可以將缺失的副本分片再次進行分配,那麼集羣的狀態又將恢復到原來的正常狀態。 如果 Node 1 依然擁有着之前的分片,它將嘗試去重用它們,同時僅從主分片複製發生了修改的數據文件。

4|0處理併發衝突

 

分佈式系統中最麻煩的就是併發衝突,既然 ES 也是分佈式的那它是如何處理併發衝突的呢?

通常當我們使用 索引 API 更新文檔時 ,可以一次性讀取原始文檔,做我們的修改,然後重新索引 整個文檔 。 最近的索引請求將獲勝:無論最後哪一個文檔被索引,都將被唯一存儲在 Elasticsearch 中。如果其他人同時更改這個文檔,他們的更改將丟失。

很多時候這是沒有問題的。也許我們的主數據存儲是一個關係型數據庫,我們只是將數據複製到 Elasticsearch 中並使其可被搜索。也許兩個人同時更改相同的文檔的機率很小。或者對於我們的業務來說偶爾丟失更改並不是很嚴重的問題。

但有時丟失了一個變更就是非常嚴重的 。試想我們使用 Elasticsearch 存儲我們網上商城商品庫存的數量, 每次我們賣一個商品的時候,我們在 Elasticsearch 中將庫存數量減少。

有一天,管理層決定做一次促銷。突然地,我們一秒要賣好幾個商品。 假設有兩個 web 程序並行運行,每一個都同時處理所有商品的銷售,那麼會造成庫存結果不一致的情況。

變更越頻繁,讀數據和更新數據的間隙越長,也就越可能丟失變更。

4|1樂觀併發控制 - 版本號

 

在數據庫領域中,有兩種方法通常被用來確保併發更新時變更不會丟失:

  • 悲觀鎖
    這種方法被關係型數據庫廣泛使用,它假定有變更衝突可能發生,因此阻塞訪問資源以防止衝突。 一個典型的例子是讀取一行數據之前先將其鎖住,確保只有放置鎖的線程能夠對這行數據進行修改。

  • 樂觀鎖
    Elasticsearch 中使用的這種方法假定衝突是不可能發生的,並且不會阻塞正在嘗試的操作。然而,如果源數據在讀寫當中被修改,更新將會失敗。應用程序接下來將決定該如何解決衝突。例如,可以重試更新、使用新的數據、或者將相關情況報告給用戶。

Elasticsearch 中對文檔的 index , GET 和 delete 請求時,我們指出每個文檔都有一個 _version (版本)號,當文檔被修改時版本號遞增。

Elasticsearch 使用這個 _version 號來確保變更以正確順序得到執行。如果舊版本的文檔在新版本之後到達,它可以被簡單的忽略。

我們可以利用 _version 號來確保應用中相互衝突的變更不會導致數據丟失。我們通過指定想要修改文檔的 version 號來達到這個目的。 如果該版本不是當前版本號,我們的請求將會失敗。

所有文檔的更新或刪除 API,都可以接受 version 參數,這允許你在代碼中使用樂觀的併發控制,這是一種明智的做法。

4|2樂觀併發控制 - 外部系統

 

版本號(version)只是其中一個實現方式,我們還可以藉助外部系統使用版本控制,一個常見的設置是使用其它數據庫作爲主要的數據存儲,使用 Elasticsearch 做數據檢索, 這意味着主數據庫的所有更改發生時都需要被複制到 Elasticsearch ,如果多個進程負責這一數據同步,你可能遇到類似於之前描述的併發問題。

如果你的主數據庫已經有了版本號,或一個能作爲版本號的字段值比如 timestamp,那麼你就可以在 Elasticsearch 中通過增加 version_type=external到查詢字符串的方式重用這些相同的版本號,版本號必須是大於零的整數, 且小於 9.2E+18(一個 Java 中 long 類型的正值)。

外部版本號的處理方式和我們之前討論的內部版本號的處理方式有些不同, Elasticsearch 不是檢查當前 _version 和請求中指定的版本號是否相同,而是檢查當前_version 是否小於指定的版本號。如果請求成功,外部的版本號作爲文檔的新_version 進行存儲。

外部版本號不僅在索引和刪除請求是可以指定,而且在創建新文檔時也可以指定。

例如,要創建一個新的具有外部版本號 5 的博客文章,我們可以按以下方法進行:

<span style="color:#222222"><code>PUT /website/blog/<span style="color:#6896ba !important">2</span>?version=<span style="color:#6896ba !important">5</span>&version_type=external
{
  <span style="color:#b9b9b9 !important">"title"</span>: <span style="color:#6a8759 !important">"My first external blog entry"</span>,
  <span style="color:#b9b9b9 !important">"text"</span>:  <span style="color:#6a8759 !important">"Starting to get the hang of this..."</span>
}</code></span>

在響應中,我們能看到當前的 _version 版本號是 5 :

<span style="color:#222222"><code>{
  <span style="color:#b9b9b9 !important">"_index"</span>:   <span style="color:#6a8759 !important">"website"</span>,
  <span style="color:#b9b9b9 !important">"_type"</span>:    <span style="color:#6a8759 !important">"blog"</span>,
  <span style="color:#b9b9b9 !important">"_id"</span>:      <span style="color:#6a8759 !important">"2"</span>,
  <span style="color:#b9b9b9 !important">"_version"</span>: <span style="color:#6896ba !important">5</span>,
  <span style="color:#b9b9b9 !important">"created"</span>:  <span style="color:#6896ba !important">true</span>
}</code></span>

現在我們更新這個文檔,指定一個新的 version 號是 10 :

<span style="color:#222222"><code>PUT /website/blog/<span style="color:#6896ba !important">2</span>?version=<span style="color:#6896ba !important">10</span>&version_type=external
{
  <span style="color:#b9b9b9 !important">"title"</span>: <span style="color:#6a8759 !important">"My first external blog entry"</span>,
  <span style="color:#b9b9b9 !important">"text"</span>:  <span style="color:#6a8759 !important">"This is a piece of cake..."</span>
}</code></span>

請求成功並將當前 _version 設爲 10 :

<span style="color:#222222"><code>{
  <span style="color:#b9b9b9 !important">"_index"</span>:   <span style="color:#6a8759 !important">"website"</span>,
  <span style="color:#b9b9b9 !important">"_type"</span>:    <span style="color:#6a8759 !important">"blog"</span>,
  <span style="color:#b9b9b9 !important">"_id"</span>:      <span style="color:#6a8759 !important">"2"</span>,
  <span style="color:#b9b9b9 !important">"_version"</span>: <span style="color:#6896ba !important">10</span>,
  <span style="color:#b9b9b9 !important">"created"</span>:  <span style="color:#6896ba !important">false</span>
}</code></span>

如果你要重新運行此請求時,它將會失敗,並返回像我們之前看到的同樣的衝突錯誤,因爲指定的外部版本號不大於 Elasticsearch 的當前版本號。

5|0文檔存儲原理

 

創建索引的時候我們只需要指定分片數和副本數,ES 就會自動將文檔數據分發到對應的分片和副本中。那麼文件究竟是如何分佈到集羣的,又是如何從集羣中獲取的呢? Elasticsearch 雖然隱藏這些底層細節,讓我們好專注在業務開發中,但是我們深入探索這些核心的技術細節,這能幫助你更好地理解數據如何被存儲到這個分佈式系統中。

5|1文檔是如何路由到分片中的

 

當索引一個文檔的時候,文檔會被存儲到一個主分片中。 Elasticsearch 如何知道一個文檔應該存放到哪個分片中呢?當我們創建文檔時,它如何決定這個文檔應當被存儲在分片 1 還是分片 2 中呢?

首先這肯定不會是隨機的,否則將來要獲取文檔的時候我們就不知道從何處尋找了。實際上,這個過程是根據下面這個公式決定的:

shard = hash(routing) % number_of_primary_shards

routing 是一個可變值,默認是文檔的 _id ,也可以設置成一個自定義的值。 routing 通過 hash 函數生成一個數字,然後這個數字再除以 number_of_primary_shards (主分片的數量)後得到 餘數 。這個分佈在 0 到 number_of_primary_shards-1 之間的餘數,就是我們所尋求的文檔所在分片的位置。

這就解釋了爲什麼我們要在創建索引的時候就確定好主分片的數量 並且永遠不會改變這個數量:因爲如果數量變化了,那麼所有之前路由的值都會無效,文檔也再也找不到了。

你可能覺得由於 Elasticsearch 主分片數量是固定的會使索引難以進行擴容,所以在創建索引的時候合理的預分配分片數是很重要的。

所有的文檔 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一個叫做 routing 的路由參數 ,通過這個參數我們可以自定義文檔到分片的映射。一個自定義的路由參數可以用來確保所有相關的文檔——例如所有屬於同一個用戶的文檔——都被存儲到同一個分片中。更多路由相關的內容可以訪問這裏

5|2主分片和副本分片如何交互

 

上面介紹了一個文檔是如何路由到一個分片中的,那麼主分片是如何和副本分片交互的呢?

假設有個集羣由三個節點組成, 它包含一個叫 user 的索引,有兩個主分片,每個主分片有兩個副本分片。相同分片的副本不會放在同一節點,所以我們的集羣看起來如下圖所示:

我們可以發送請求到集羣中的任一節點。每個節點都有能力處理任意請求。每個節點都知道集羣中任一文檔位置,所以可以直接將請求轉發到需要的節點上。 在下面的例子中,將所有的請求發送到 Node 1 ,我們將其稱爲 協調節點(coordinating node)

當發送請求的時候,爲了擴展負載,更好的做法是輪詢集羣中所有的節點。

對文檔的新建、索引和刪除請求都是寫操作,必須在主分片上面完成之後才能被複制到相關的副本分片。

以下是在主副分片和任何副本分片上面 成功新建,索引和刪除文檔所需要的步驟順序:

  1. 客戶端向 Node 1 發送新建、索引或者刪除請求。
  2. 節點使用文檔的 _id 確定文檔屬於分片 0 。請求會被轉發到 Node 3,因爲分片 0 的主分片目前被分配在 Node 3 上。
  3. Node 3 在主分片上面執行請求。如果成功了,它將請求並行轉發到 Node1 和 Node2 的副本分片上。一旦所有的副本分片都報告成功,Node 3 將向協調節點報告成功,協調節點向客戶端報告成功。

在客戶端收到成功響應時,文檔變更已經在主分片和所有副本分片執行完成,變更是安全的。

在處理讀取請求時,協調結點在每次請求的時候都會通過輪詢所有的副本分片來達到負載均衡。

在文檔被檢索時,已經被索引的文檔可能已經存在於主分片上但是還沒有複製到副本分片。在這種情況下,副本分片可能會報告文檔不存在,但是主分片可能成功返回文檔。一旦索引請求成功返回給用戶,文檔在主分片和副本分片都是可用的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章