經過前面章節的瞭解,咱們已經知道 Elasticsearch 是一個實時的分佈式搜索分析引擎,它能讓你以一個以前從未有過的速度和規模,去探索你的數據。它被用做全文檢索、結構化搜索、分析以及這三個功能的組合。 Elasticsearch 能夠橫向擴展至數百(甚至數千)的服務器節點,同時能夠處理PB級數據。html
雖說 Elasticsearch 是分佈式的,可是對於咱們開發者來講並未過多的參與其中,咱們只需啓動對應數量的 ES 實例(即節點),並給它們分配相同的 cluster.name
讓它們歸屬於同一個集羣,建立索引的時候只需指定索引 分片數 和 副本數 便可,其餘的都交給了 ES 內部本身去實現。node
這和數據庫的分佈式和 同源的 solr 實現分佈式都是有區別的,數據庫分佈式(分庫分表)須要咱們指定路由規則和數據同步策略等,solr的分佈式也需依賴 zookeeper,可是 Elasticsearch 徹底屏蔽了這些。mysql
因此咱們說,Elasticsearch 天生就是分佈式的,而且在設計時屏蔽了分佈式的複雜性。Elasticsearch 在分佈式方面幾乎是透明的。咱們可使用筆記本上的單節點輕鬆地運行Elasticsearch 的程序,但若是你想要在 100 個節點的集羣上運行程序,一切也依然順暢。web
Elasticsearch 儘量地屏蔽了分佈式系統的複雜性。這裏列舉了一些在後臺自動執行的操做:redis
雖然咱們能夠不瞭解 Elasticsearch 分佈式內部實現機制也能將Elasticsearch使用的很好,可是瞭解它們將會從另外一個角度幫助咱們更完整的學習和理解 Elasticsearch 知識。接下里咱們從如下幾個部分來詳細講解 Elasticsearch 分佈式的內部實現機制。sql
對於咱們以前的分佈式經驗,咱們知道,提高分佈式性能能夠經過購買性能更強大( 垂直擴容 ,或 縱向擴容 ) 或者數量更多的服務器( 水平擴容 ,或 橫向擴容 )來實現。數據庫
雖然Elasticsearch 能夠獲益於更強大的硬件設備,例如將存儲硬盤設爲SSD,可是 垂直擴容 因爲硬件設備的技術和價格限制,垂直擴容 是有極限的。真正的擴容能力是來自於 水平擴容 --爲集羣添加更多的節點,而且將負載壓力和穩定性分散到這些節點中。json
對於大多數的數據庫而言,一般須要對應用程序進行很是大的改動,才能利用上橫向擴容的新增資源。 與之相反的是,ElastiSearch天生就是 分佈式的 ,它知道如何經過管理多節點來提升擴容性和可用性。 這也意味着你的應用無需關注這個問題。那麼它是如何管理的呢?數組
啓動一個 ES 實例就是一個節點,節點加入集羣是經過配置文件中設置相同的 cluste.name
而實現的。因此集羣是由一個或者多個擁有相同 cluster.name
配置的節點組成, 它們共同承擔數據和負載的壓力。當有節點加入集羣中或者從集羣中移除節點時,集羣將會從新平均分佈全部的數據。安全
與其餘組件集羣(mysql,redis)的 master-slave模式同樣,ES集羣中也會選舉一個節點成爲主節點,主節點它的職責是維護全局集羣狀態,在節點加入或離開集羣的時候從新分配分片。具體關於主節點選舉的內容能夠閱讀選舉主節點。
全部主要的文檔級別API(索引,刪除,搜索)都不與主節點通訊,主節點並不須要涉及到文檔級別的變動和搜索等操做,因此當集羣只擁有一個主節點的狀況下,即便流量的增長它也不會成爲瓶頸。 任何節點均可以成爲主節點。若是集羣中就只有一個節點,那麼它同時也就是主節點。
因此若是咱們使用 kibana 來做爲視圖操做工具的話,咱們只需在kibana.yml
的配置文件中,將elasticsearch.url: "http://localhost:9200"
設置爲主節點就能夠了,經過主節點 ES 會自動關聯查詢全部節點和分片以及副本的信息。因此 kibana 通常都和主節點在同一臺服務器上。
做爲用戶,咱們能夠將請求發送到 集羣中的任何節點 ,包括主節點。 每一個節點都知道任意文檔所處的位置,而且可以將咱們的請求直接轉發到存儲咱們所需文檔的節點。 不管咱們將請求發送到哪一個節點,它都能負責從各個包含咱們所需文檔的節點收集回數據,並將最終結果返回給客戶端。 Elasticsearch 對這一切的管理都是透明的。
ES 是如何實現只須要配置相同的cluste.name
就將節點加入同一集羣的呢?答案是發現機制(discovery module)。
發現機制 負責發現集羣中的節點,以及選擇主節點。每次集羣狀態發生更改時,集羣中的其餘節點都會知道狀態(具體方式取決於使用的是哪種發現機制)。
ES目前主要推薦的自動發現機制,有以下幾種:
這裏額外介紹下單播,多播,廣播的定義和區別,方便咱們更好的理解發現機制。
單播,多播,廣播的區別:
單播(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:port
或host
的形式(其中port
默認爲設置transport.profiles.default.port
,若是未設置則返回transport.tcp.port
)。請注意,必須將IPv6主機置於括號內。此設置的默認值爲127.0.0.1,[:: 1]
。
Elasticsearch 官方推薦咱們使用 單播 代替 組播。並且 Elasticsearch 默認被配置爲使用 單播 發現,以防止節點無心中加入集羣。只有在同一臺機器上運行的節點纔會自動組成集羣。
雖然 組播 仍然做爲插件提供, 但它應該永遠不被使用在生產環境了,不然你獲得的結果就是一個節點意外的加入到了你的生產環境,僅僅是由於他們收到了一個錯誤的 組播 信號。對於 組播 自己並無錯,組播會致使一些愚蠢的問題,而且致使集羣變的脆弱(好比,一個網絡工程師正在搗鼓網絡,而沒有告訴你,你會發現全部的節點忽然發現不了對方了)。
使用單播,你能夠爲 Elasticsearch 提供一些它應該去嘗試鏈接的節點列表。當一個節點聯繫到單播列表中的成員時,它就會獲得整個集羣全部節點的狀態,而後它會聯繫 master 節點,並加入集羣。
這意味着你的單播列表不須要包含你的集羣中的全部節點,它只是須要足夠的節點,當一個新節點聯繫上其中一個而且說上話就能夠了。若是你使用 master 候選節點做爲單播列表,你只要列出三個就能夠了。
關於 Elasticsearch 節點發現的詳細信息,請參閱 Zen Discovery。
對於分佈式系統的熟悉,咱們應該知道分佈式系統設計的目的是爲了提升可用性和容錯性。在單點系統中的問題在 ES 中一樣也會存在。
若是咱們啓動了一個單獨的節點,裏面不包含任何的數據和索引,那咱們的集羣就是一個包含空內容節點的集羣,簡稱空集羣。
當集羣中只有一個節點在運行時,意味着會有一個單點故障問題——沒有冗餘。單點的最大問題是系統容錯性不高,當單節點所在服務器發生故障後,整個 ES 服務就會中止工做。
讓咱們在包含一個空節點的集羣內建立名爲 user 的索引。索引在默認狀況下會被分配5個主分片和每一個主分片的1個副本, 可是爲了演示目的,咱們將分配3個主分片和一份副本(每一個主分片擁有一個副本分片):
PUT /user
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
複製代碼
咱們的集羣如今是下圖所示狀況,全部3個主分片都被分配在 Node 1 。
此時檢查集羣的健康情況GET /_cluster/health
,咱們會發現:
{
"cluster_name": "elasticsearch",
"status": "yellow", # 1
"timed_out": false,
"number_of_nodes": 1,
"number_of_data_nodes": 1,
"active_primary_shards": 3,
"active_shards": 3,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 3, # 2
"delayed_unassigned_shards": 0,
"number_of_pending_tasks": 0,
"number_of_in_flight_fetch": 0,
"task_max_waiting_in_queue_millis": 0,
"active_shards_percent_as_number": 50
}
複製代碼
#1 集羣的狀態值是 yellow #2 未分配的副本數是 3
集羣的健康情況爲 yellow 則表示所有 主 分片都正常運行(集羣能夠正常服務全部請求),可是 副本 分片沒有所有處在正常狀態。 實際上,全部3個副本分片都是 unassigned —— 它們都沒有被分配到任何節點。 在同一個節點上既保存原始數據又保存副本是沒有意義的,由於一旦失去了那個節點,咱們也將丟失該節點上的全部副本數據。
主分片和對應的副本分片是不會在同一個節點上的。因此副本分片數的最大值是 n -1(其中n 爲節點數)。
雖然當前咱們的集羣是正常運行的,可是在硬件故障時有丟失數據的風險。
既然單點是有問題的,那咱們只需再啓動幾個節點並加入到當前集羣中,這樣就能夠提升可用性並實現故障轉移,這種方式即 水平擴容。
還以上面的 user 爲例,咱們新增一個節點後,新的集羣如上圖所示。
當第二個節點加入到集羣后,3個 副本分片 將會分配到這個節點上——每一個主分片對應一個副本分片。 這意味着當集羣內任何一個節點出現問題時,咱們的數據都無缺無損。
全部新近被索引的文檔都將會保存在主分片上,而後被並行的複製到對應的副本分片上。這就保證了咱們既能夠從主分片又能夠從副本分片上得到文檔。
cluster-health
如今展現的狀態爲 green
,這表示全部6個分片(包括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 :
PUT /user/_settings
{
"number_of_replicas" : 2
}
複製代碼
以下圖 所示, user 索引如今擁有9個分片:3個主分片和6個副本分片。 這意味着咱們能夠將集羣擴容到9個節點,每一個節點上一個分片。相比原來3個節點時,集羣搜索性能能夠提高 3 倍。
固然,若是隻是在相同節點數目的集羣上增長更多的副本分片並不能提升性能,由於每一個分片從節點上得到的資源會變少。 你須要增長更多的硬件資源來提高吞吐量。
可是更多的副本分片數提升了數據冗餘量:按照上面的節點配置,咱們能夠在失去2個節點的狀況下不丟失任何數據。
若是咱們某一個節點發生故障,節點服務器宕機或網絡不可用,這裏假設主節點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 依然擁有着以前的分片,它將嘗試去重用它們,同時僅從主分片複製發生了修改的數據文件。
分佈式系統中最麻煩的就是併發衝突,既然 ES 也是分佈式的那它是如何處理併發衝突的呢?
一般當咱們使用 索引 API 更新文檔時 ,能夠一次性讀取原始文檔,作咱們的修改,而後從新索引 整個文檔 。 最近的索引請求將獲勝:不管最後哪個文檔被索引,都將被惟一存儲在 Elasticsearch 中。若是其餘人同時更改這個文檔,他們的更改將丟失。
不少時候這是沒有問題的。也許咱們的主數據存儲是一個關係型數據庫,咱們只是將數據複製到 Elasticsearch 中並使其可被搜索。也許兩我的同時更改相同的文檔的概率很小。或者對於咱們的業務來講偶爾丟失更改並非很嚴重的問題。
但有時丟失了一個變動就是很是嚴重的 。試想咱們使用 Elasticsearch 存儲咱們網上商城商品庫存的數量, 每次咱們賣一個商品的時候,咱們在 Elasticsearch 中將庫存數量減小。
有一天,管理層決定作一次促銷。忽然地,咱們一秒要賣好幾個商品。 假設有兩個 web 程序並行運行,每個都同時處理全部商品的銷售,那麼會形成庫存結果不一致的狀況。
變動越頻繁,讀數據和更新數據的間隙越長,也就越可能丟失變動。
在數據庫領域中,有兩種方法一般被用來確保併發更新時變動不會丟失:
悲觀鎖 這種方法被關係型數據庫普遍使用,它假定有變動衝突可能發生,所以阻塞訪問資源以防止衝突。 一個典型的例子是讀取一行數據以前先將其鎖住,確保只有放置鎖的線程可以對這行數據進行修改。
樂觀鎖 Elasticsearch 中使用的這種方法假定衝突是不可能發生的,而且不會阻塞正在嘗試的操做。然而,若是源數據在讀寫當中被修改,更新將會失敗。應用程序接下來將決定該如何解決衝突。例如,能夠重試更新、使用新的數據、或者將相關狀況報告給用戶。
Elasticsearch 中對文檔的 index , GET 和 delete 請求時,咱們指出每一個文檔都有一個 _version (版本)號,當文檔被修改時版本號遞增。
Elasticsearch 使用這個 _version 號來確保變動以正確順序獲得執行。若是舊版本的文檔在新版本以後到達,它能夠被簡單的忽略。
咱們能夠利用 _version 號來確保應用中相互衝突的變動不會致使數據丟失。咱們經過指定想要修改文檔的 version 號來達到這個目的。 若是該版本不是當前版本號,咱們的請求將會失敗。
全部文檔的更新或刪除 API,均可以接受 version
參數,這容許你在代碼中使用樂觀的併發控制,這是一種明智的作法。
版本號(version)只是其中一個實現方式,咱們還能夠藉助外部系統使用版本控制,一個常見的設置是使用其它數據庫做爲主要的數據存儲,使用 Elasticsearch 作數據檢索, 這意味着主數據庫的全部更改發生時都須要被複制到 Elasticsearch ,若是多個進程負責這一數據同步,你可能遇到相似於以前描述的併發問題。
若是你的主數據庫已經有了版本號,或一個能做爲版本號的字段值好比 timestamp
,那麼你就能夠在 Elasticsearch 中經過增長 version_type=external
到查詢字符串的方式重用這些相同的版本號,版本號必須是大於零的整數, 且小於 9.2E+18
(一個 Java 中 long 類型的正值)。
外部版本號的處理方式和咱們以前討論的內部版本號的處理方式有些不一樣, Elasticsearch 不是檢查當前 _version
和請求中指定的版本號是否相同,而是檢查當前_version
是否小於指定的版本號。若是請求成功,外部的版本號做爲文檔的新_version
進行存儲。
外部版本號不只在索引和刪除請求是能夠指定,並且在建立新文檔時也能夠指定。
例如,要建立一個新的具備外部版本號 5 的博客文章,咱們能夠按如下方法進行:
PUT /website/blog/2?version=5&version_type=external
{
"title": "My first external blog entry",
"text": "Starting to get the hang of this..."
}
複製代碼
在響應中,咱們能看到當前的 _version 版本號是 5 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 5,
"created": true
}
複製代碼
如今咱們更新這個文檔,指定一個新的 version 號是 10 :
PUT /website/blog/2?version=10&version_type=external
{
"title": "My first external blog entry",
"text": "This is a piece of cake..."
}
複製代碼
請求成功並將當前 _version 設爲 10 :
{
"_index": "website",
"_type": "blog",
"_id": "2",
"_version": 10,
"created": false
}
複製代碼
若是你要從新運行此請求時,它將會失敗,並返回像咱們以前看到的一樣的衝突錯誤,由於指定的外部版本號不大於 Elasticsearch 的當前版本號。
建立索引的時候咱們只須要指定分片數和副本數,ES 就會自動將文檔數據分發到對應的分片和副本中。那麼文件到底是如何分佈到集羣的,又是如何從集羣中獲取的呢? Elasticsearch 雖然隱藏這些底層細節,讓咱們好專一在業務開發中,可是咱們深刻探索這些核心的技術細節,這能幫助你更好地理解數據如何被存儲到這個分佈式系統中。
當索引一個文檔的時候,文檔會被存儲到一個主分片中。 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
的路由參數 ,經過這個參數咱們能夠自定義文檔到分片的映射。一個自定義的路由參數能夠用來確保全部相關的文檔——例如全部屬於同一個用戶的文檔——都被存儲到同一個分片中。更多路由相關的內容能夠訪問這裏。
上面介紹了一個文檔是如何路由到一個分片中的,那麼主分片是如何和副本分片交互的呢?
假設有個集羣由三個節點組成, 它包含一個叫 user 的索引,有兩個主分片,每一個主分片有兩個副本分片。相同分片的副本不會放在同一節點,因此咱們的集羣看起來以下圖所示:
咱們能夠發送請求到集羣中的任一節點。每一個節點都有能力處理任意請求。每一個節點都知道集羣中任一文檔位置,因此能夠直接將請求轉發到須要的節點上。 在下面的例子中,將全部的請求發送到 Node 1 ,咱們將其稱爲 協調節點(coordinating node)。
當發送請求的時候,爲了擴展負載,更好的作法是輪詢集羣中全部的節點。
對文檔的新建、索引和刪除請求都是寫操做,必須在主分片上面完成以後才能被複制到相關的副本分片。
如下是在主副分片和任何副本分片上面 成功新建,索引和刪除文檔所須要的步驟順序:
在客戶端收到成功響應時,文檔變動已經在主分片和全部副本分片執行完成,變動是安全的。
在處理讀取請求時,協調結點在每次請求的時候都會經過輪詢全部的副本分片來達到負載均衡。
在文檔被檢索時,已經被索引的文檔可能已經存在於主分片上可是尚未複製到副本分片。在這種狀況下,副本分片可能會報告文檔不存在,可是主分片可能成功返回文檔。一旦索引請求成功返回給用戶,文檔在主分片和副本分片都是可用的。
我的公衆號:JaJian
歡迎長按下圖關注公衆號:JaJian!
按期爲你奉上分佈式,微服務等一線互聯網公司相關技術的講解和分析。