解剖 Elasticsearch 集群 - 之二
本篇文章是一系列涵蓋 Elasticsearch 底層架構和原型示例的其中一篇。在本篇文章中,我們會討論 Elasticsearch 是如何處理 3C 問題的(共識性、並發性和一致性)以及分片的內部概念如 translog(Write Ahead Log - WAL)以及 Lucene 的分段(segments)。
在之前的文章中,我們談到了 Elasticsearch 存儲模型以及 CRUD 操作。在本篇文章中,我會分享 Elasticsearch 是如何解決一些分布式系統的基本挑戰的以及分片的一些內部概念。主要會涵蓋以下話題:
- 共識性(Consensus) — 腦裂(split-brain)問題以及選舉(quorum)的重要性
- 並發性(Concurrency)
- 一致性(Consistency):保證寫和讀的一致性
- Translog (Write Ahead Log — WAL)
- Lucene 段
共識性(Consensus) — 腦裂(split-brain)問題以及選舉(quorum)的重要性
共識性(Consensus)是分布式系統基本挑戰的其中一項。它需要系統的所有進程/節點(processes/nodes)都對某一數據值/狀態(value/status)達成一致。有很多達成共識的算法如 Raft、Paxos 等,它們都在數學上被證明是正確的,但是,Elasticsearch 實現了它自己的共識系統(zen discovery),Elasticsearch 的作者 Shay Banon 對此有相關解釋。zen discovery 模塊有兩部分:
- Ping: 進程節點用來發現其他節點
- Unicast: 模塊包含了一個主機名的列表來控制 ping 哪個節點
Elasticsearch 是一個對等網絡(peer-to-peer)系統,所有節點都可以與其他節點通訊,有一個活動的主節點用來更新和控制集群范圍的狀態和操作。新 Elasticsearch 集群將選舉過程是作為 ping 進程的一部分,從所有合法主節點中選取一個主節點,並將其他節點加入到主節點。默認的 ping_interval 是 1 秒,ping_timeout 為 3 sec 。當節點加入時,它們會向主節點發送加入的請求,默認的 join_timeout 是 ping_timeout 的 20 倍。如果主節點失效,集群的節點會重新開始 ping 並發起新一輪的選舉。ping 過程在當一個節點意外誤以為主節點失效並通過其他節點發現主節點時也會有幫助。
NOTE: 默認,客戶端節點和數據節點並不對選舉過程產生任何作用。可以在配置文件 中 elasticsearch.yml 修改配置 discovery.zen.master_election.filter_client 和 discovery.zen.master_election.filter_data 屬性為 False 。
為了進行錯誤檢測,主節點會 ping 所有其他節點來檢查它們是否處於 alive 狀態,所有節點都會回 ping 主節點報告它們處於 alive 狀態。
如果用默認設置,Elasticsearch 會在網絡隔離時出現 腦裂(split-brain),節點會認為主節點死掉並選舉它自己作為主節點,從而導致集群下有多個主節點。這不僅會導致數據丟失還可能會導致數據合並不正確。可以通過設置一下屬性來避免這個問題。
discovery.zen.minimum_master_nodes = int(# of master eligible nodes/2)+1
這個屬性要求活動合法主節點加入到新選舉的主節點標志一次選舉的過程的完成,新主節點接受了它主節點的地位。這是個極度重要的屬性,它保證集群的穩定性,它會隨着繼續大小的變化而動態更新。圖 a 和 b 表明當在設置或不設置 minimum_master_nodes 屬性時,如果網絡發生隔離的情況,分別會發生什么。
NOTE: 對於生產環境的集群,推薦是有 3 個專用主節點,不接受任何客戶端請求,任意時候有 1 個節點作為活動的主節點。
現在我們了解了 Elasticsearch 中的共識性,我們再來看它是如何處理並發的。
並發(Concurrency)
Elasticsearch 是一個分布式系統並支持並發請求。當一個 create/update/delete 請求發送到主分片時,它同時也會以並行的方式發送到備份分片,不過,這些請求的送達可能是無序的。在這種情況下,Elasticsearch 用 樂觀並發控制(optimistic concurrency control) 來保證新版本的文檔不會被舊版本覆蓋掉。
每個索引的文檔都有一個版本號,它會在每次對文檔變更后做自增運算。這些版本號用以保證變更是有序的。為了保證在應用里的更新不會導致數據丟失,Elasticsearch 的 API 允許用戶指定當前版本號的文檔中應該更新的部分。如果請求中指定的版本早於分片里的當前版本,請求會失敗,也就是說文檔以及被另一進程更新了。可以在應用程序層面來處理失敗的請求。Elasticsearch 還提供了一些其他的鎖機制,可以在 這里 進一步了解。
當我們並發請求 Elasticsearch 時,另外一個顧慮就是 — 如何確保這些請求的一致?現在,回答 Elasticsearch 是處於 CAP 三角的哪個地方還處於爭論中,這個不在本篇文章的討論范圍。
現在,我們來看看 Elasticsearch 是如何達到讀寫一致的目標的。
一致性 — 保證讀寫一致
對於寫來說,Elasticsearch 對一致性的支持層級與其他大多數數據庫不一樣,它允許通過使用預檢查來看有多少個分片可供寫入。可選項有 仲裁集(quorum)、一(one) 和 所有(all)。缺省設置是 仲裁集(quorum),這也就意味着只有當大多數分片可用時,才允許被寫入。即使在多數分片可用時,還是會發生向備份分片寫入失敗的情況,這種情況下,備份分片被認為是有錯誤的,分片會在另一個節點上重建。
對於讀來說,新文檔只有在刷新間隔之后才對搜索可見。為了保證搜索請求返回結果是最新版本的文檔,備份可以被設置成為 sync(默認值),當寫操作在主備分片同時完成之后才會返回請求的結果。這樣,無論搜索請求至哪個分片都會返回最新的文檔。甚至如果你的應用要求高索引吞吐率(higher indexing rate)時,replication=async,可以為搜索請求設置 _preference 參數為 primary 。這樣搜索請求會查詢主分片,從而保證結果中的文檔是最新版本。
在理解 Elasticsearch 是如何處理共識性、並發性和一致性的問題后,讓我們再來看看一些關於分片內部構造的重要概念,這些概念使得 Elasticsearch 具備分布式搜索引擎的特性。
Translog
先寫日志(Write Ahead Log - WAL)或事務日志(transaction log - translog)的概念從關系型數據庫開始發展時就已經存在。translog 的底層原理可以在失敗的情況下保證數據的完整性,即預期更改必須在真實更改保存到磁盤之前被記錄保存下來。
當新文檔被索引或舊文檔被更新后,Lucene 索引會發生更改,這些更改會被保存到磁盤。如果在每次寫請求后做這個操作的代價將會很高,所以完成的方式是將多個更改一次寫入到磁盤。正像我們在之前的文章中描述的那樣,flush 操作(Lucene 提交)默認是每 30 分鍾一次,或者是在 translog 過大時(默認 512MB)。在這種情況下,有可能存在丟失兩次 Lucene 提交(Luncene commit)之間的所有數據的情況。為了避免這個問題,Elasticsearch 使用 translog 。所有的索引/刪除/更新(index/delete/update)操作都會被寫入到 translog ,translog 在每次索引/刪除/更新(index/delete/update)操作后(或默認每 5 秒鍾)都會被同步(fsync)從而保證更改以及持久化。在 translog 在所有主從分片上都被同步后,客戶端才會接收到寫的應答。
如果在兩次 Lucene 提交(Lucene commits)之間出現硬件故障或重啟,translog 會進行重放來恢復最近一次 Lucene 提交前丟失的所有數據,所有的更改都會被提交到索引。
NOTE: 在重啟 Elasticsearch 實例之前,推薦使用顯式地將 translog 進行 flush ,這樣需要重放的 translog 會是空的,重啟也會更快。POST /_all/_flush 命令可以用來 flush 集群里的所有索引。
有了 translog 的 flush 操作,在文件系統緩存里的分片會被提交到磁盤完成索引的持久化。現在我們再來看看 Lucene 段的工作方式。
Lucene 的段(Segments)
一個 Lucene 的索引是由多個段構成的,一個段又是一個完整的倒排索引。段是不可變的,使 Lucene 可以將新的文檔以增量的形式加入到索引,無須對索引進行重建,每個段都會消耗 CPU 時鍾,文件句柄和內存。這也意味着段越多,搜索的性能將會越差。
為了解決這個問題,Elasticsearch 會將很多小段合並成大的段(如下圖所示),並將新的合並的段提交到磁盤,然后刪除老的小段。
這個過程是在后台自動執行的,不會影響索引或搜索。因為段合並會占用資源並影響搜索的性能,Elasticsearch 會對合並進程節流,保證搜索有足夠可用的資源。
接下來是什么?
對於搜索請求,Elasticsearch 索引內分片里的所有 Lucene 段都會被搜索,但是,如果要讀取所有匹配的文檔或者讀取搜索結果排名中比較深的文檔,對於 Elasticsearch 集群是危險的。在后續的文章中,我們會看看為什么會這樣,同時也會看看以下的話題(包括 Elasticsearch 是如何在低延遲與高相關度結果間做權衡的)
- Elasticsearch 的准實時
- 為什么深度分頁會很危險?
- 計算搜索相關性的權衡
參考
參考來源:
Anatomy of an Elasticsearch Cluster: Part II