本文探討Elasticsearch的資料請求、路由和寫入過程的原理,主要涉及ES的分散式儲存架構、節點和副本的寫入過程、近實時搜尋的原因、持久化機制等。
4.1 ES儲存架構
我們經常說,看一件事情千萬不要直接陷入細節裡,應該先鳥瞰全貌,這樣才有助於從高維度理解問題。分析ES的索引原理和寫入過程也是一樣,首先需要了解ES的儲存架構。
4.1.1 叢集、節點、分片
ES天生就是分散式架構的。ES的底層是Lucene,而Lucene只是一個搜尋引擎庫,沒有併發設計 ,沒有分散式相關的設計,因此要想使用Lucene來處理海量資料,並利用分散式的能力,就需要在其之上進行分散式的相關設計。ES就是這樣一款建立在Lucene基礎之上,賦予其分散式能力的儲存引擎,說成天生就是分散式架構的一點也不過分。
叢集是有多個節點組成的,在上圖中可以看到叢集中有多個不同種型別的節點。
節點是一個Elasticsearch的例項,本質上是一個Java程式。每個節點上面都儲存著叢集的狀態資訊,包括所有的節點資訊、所有的索引和相關的Mapping於Setting資訊和分片的路由資訊等。節點按照角色可以劃分為主節點、資料節點、協調節點和預處理節點等。
Master節點負責管理叢集狀態資訊,包括處理建立、刪除索引等請求,決定分片被分配到哪個節點,維護和更新叢集狀態。值得注意的是,只有Master節點才能修改叢集的狀態資訊,並負責同步給其他節點。可見,Master節點非常重要,在部署上需要考慮單點風險。
協調節點負責接收客戶端的請求,將請求路由到到合適的節點,並將結果彙集到一起。
資料節點是儲存資料的節點,增加資料節點可以解決水平擴充套件和解決資料單點的問題。
預處理節點是資料前置處理轉換的節點,支援 pipeline管道設定,可以對資料進行過濾、轉換等操作。
更多關於節點內容參考:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/modules-node.html
分片是ES分散式儲存的基石,是底層的基本讀寫單元。分片的目的是分割巨大的索引,將資料分散到叢集內各處。分片分為主分片和副本分片,一般情況,一個主分片有多個副本分片。主分片負責處理寫入請求和儲存資料,副本分片只負責儲存資料,是主分片的拷貝,文件會儲存在具體的某個主分片和副本分片上。
資料分片技術是指分散式儲存系統按照一定的規則將資料儲存到對應的儲存節點中,或者到對應的儲存節點中獲取想要的資料。
資料複製技術是指將資料進行備份,使得多個節點都儲存該資料,提高系統可用性和可靠性。
4.2 寫入過程分析
4.2.1 路由和讀寫過程
ES叢集中的協調節點負責接收來自客戶端的讀寫請求,當協調節點收到請求時,ES通過文件到分片的對映演算法找到對應的主分片,將請求轉發給主分片處理,主分片完成寫入之後,將寫入同時傳送給副本分片,副本分片執行寫入操作後返回主分片,主分片再將結果返回給協調節點,協調節點將結果返回給客戶端,完成一次完整的寫入過程。
4.2.2 文件到分片的路由演算法
一個優秀的對映演算法,需要將文件均勻分佈在所有分片上面,並且充分利用硬體資源。
根據歷史經驗判斷,潛在的對映演算法大概有這幾種:
- 隨機演算法、輪詢演算法
如果ES參考Nginx,採用這兩種演算法,寫入的時候比較簡單,也可以使得資料均勻分佈。但是查詢某個特定資料的時候,無法知道資料儲存在哪個分片上面,需要遍歷所有分片查詢,效率會很低,當分片數多的時候,對效能的影響效果會愈發明顯。所以ES沒有采用隨機演算法和輪詢演算法。
- 關鍵字雜湊演算法,空間換時間
如果使用表記錄文件和分片的對映關係,貌似可以達到空間換時間的效果,但是一旦文件和分片的對映關係發生改變(例如增加分片),就要修改對映關係表。如果是單執行緒操作表,所有操作都要序列執行,如果是多執行緒操作表,就涉及到加鎖開銷。另外,由於整個叢集的文件數量是無法預估的,資料非常多的情況下, 如果ES直接記錄對映關係,整個對映表會非常龐大,這個對映表儲存在服務端會佔用很大的空間。所以ES也沒有采用該演算法。
- 關鍵字雜湊演算法,實時計算
這種演算法是根據關鍵字(如文件id)自動計算出需要去哪個分片上面去寫入和查詢。該演算法相當於消耗了很少的CPU資源,不但讓資料分佈更加均衡,還可以讓省去對映表儲存和維護的成本,是個聰明的選擇。沒錯,ES採用的就是這種方式實現從文件到分片的路由的。具體的路由演算法如下:shard_num=hash(_routing) % num_primary_shards
- 預設的
_routing
值是文件id - 可以自行設定routing數值,通過API中的_routing引數指定
- 建立索引時,主分片數一經設定,無法隨意更改的原因;如果修改,將重建索引
注:
- ES最新版本7.15將演算法進行優化為:
關於更多_routing引數內容可參考:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-routing-field.html#mapping-routing-field
- 許多程式語言內建的簡單雜湊函式可能並不適合分片,例如,Java的
Object.hashCode
和Ruby的Object#hash
,同一鍵在不同的程式中可能返回不同的雜湊值。許多資料系統使用MurmurHash演算法進行雜湊計算,例如Redis、Memcached等。
4.2.3 寫入資料與持久化過程
當有新資料寫入的時候,ES首先寫入Index Buffer區域,此時是無法檢索的。
預設情況下ES每秒鐘進行一次Refresh操作,將Index Buffer中的index重新整理到檔案系統快取,在檔案系統快取中,是以Segment進行儲存的,而且是可以被搜尋到的,這就是ES實現近實時搜尋:文件的更改無法立即被搜尋到,但是在一定時間會變得可見。
By default, Elasticsearch periodically refreshes indices every second, but only on indices that have received one search request or more in the last 30 seconds. This is why we say that Elasticsearch has near real-time search: document changes are not visible to search immediately, but will become visible within this timeframe.
需要說明的是,Refresh 觸發的情況有3種:
- 按照時間頻率觸發,預設情況是每 1 秒觸發 1 次 Refresh,可通過
index.refresh_interval
設定; - 當Index Buffer 被佔滿的時候,會觸發 Refresh,Index Buffer 的大小預設值是 JVM 所佔記憶體容量的 10%;
- 手動呼叫呼叫Refresh API。
由於Refresh操作預設間隔為1s,因此會產生大量的小Segment,ES查詢時會同時查詢所有的Segment,並對結果進行彙總,大量小Segment會使效能變差。因此ES會對小Segment進行段合併(Merge),合併操作會丟棄掉重複的鍵,並只保留每個鍵最近的更新。段合併之後搜尋請求可以直接訪問合併之後的Segment,從而提高搜尋效能。
Merge觸發的情況有2種:
- ES自動啟動後臺任務進行Merge;
- 手動呼叫_forcemerge API主動觸發。
在段合併完成之後,ES會將Segment檔案Flush到磁碟中,並建立一個Commit Point檔案,用來標識被Flush到磁碟的Segment。Commit Point其實是記錄所有的Segment資訊,關於移除的Segment的資訊會記錄在“.del”檔案中,查詢結果後會從該檔案中進行過濾。
Flush操作是將Segment從檔案系統快取寫入到磁碟進行持久化,在執行 Flush 的時候會依次執行下面操作:
- 清空Index Buffer
- 記錄 Commit Point
- 重新整理Segment到磁碟
- 刪除translog
4.2.4 Translog機制
為了保障資料安全,ES增加了Translog, 在資料寫入Index Buffer的同時,寫入一份到Translog。預設每個寫入請求,Translog會追加寫入磁碟的,這樣就可以防止伺服器當機後資料丟失。如果對可靠性要求不是很高,也可以設定非同步落盤,由配置引數 index.translog.durability
和 index.translog.sync_interval
控制。
index.translog.durability
:預設是request,每個請求都落盤;設定成async,可非同步寫入
index.translog.sync_interval
:預設5s,不能小於100ms
官方文件地址:https://www.elastic.co/guide/en/elasticsearch/reference/7.10/index-modules-translog.html
Translog落盤有2種情況:
- 每個請求同步或者非同步落盤
- Flush的時候,記憶體中的Segment和Translog同時落盤
4.2.5 併發控制
針對寫入和更新時可能出現的併發問題,ES是通過文件版本號來解決的。當使用者對文件進行操作時,並不需要對文件加鎖和解鎖操作,只需要帶著版本號。當版本號衝突的時候,ES會提示衝突並丟擲異常,並進行重試,重試次數可以通過引數retry_on_conflict
進行設定。
4.3 總結
4.3.1 分散式儲存系統資料分割槽
文件到分片的路由演算法,從本質上講,其實是分散式儲存系統資料分割槽的問題。如果資料量太大,單臺機器進行儲存和處理就會成為瓶頸,因此需要引入資料分割槽機制。分割槽的目的是通過多臺機器均勻分佈資料和查詢負載,避免出現熱點。這需要選擇合適的資料分割槽方案,在節點新增或刪除時重新動態平衡分割槽。
資料分割槽方式主要有兩種,一種是順序分佈,另外一種是雜湊分佈。
順序分佈具體做法是對關鍵字進行排序,每個分割槽值負責一段包含最小到最大關鍵字範圍的一段關鍵字。對關鍵字排序的優點是可以支援高效的區間查詢。
雜湊分佈具體做法是根據資料的某個關鍵字計算雜湊值,並將雜湊值與叢集中的伺服器建立關係,從而將不同雜湊值的資料分佈到不同的伺服器上。傳統雜湊演算法是將雜湊值和伺服器個數做除法取模對映。這種方法的優點是計算方式簡單;缺點是當伺服器數量改變時,資料對映會被完全打亂,資料需要重新分佈和遷移,頻繁的遷移會大大增加再平衡的成本。還有一點,通過關鍵字雜湊分割槽,喪失了良好的區間查詢特性。
對於分割槽再平衡資料遷移,解決思路是引入中間層,用中間層來維護雜湊值和伺服器節點之間的對映關係。有一個簡單的解決方案是這樣的,首先建立遠超實際節點數的分割槽數,然後為每個節點分配多個分割槽。這種方案維持分割槽總數不變,也不改變關鍵字到分割槽的對映關係,僅需要調整的是分割槽和節點的對應關係。
Elasticsearch支援這種動態平衡方法。使用該策略時,分割槽數量一般在建立之初就確定好了,之後不會改變。
4.3.2 分散式儲存系統資料複製
為了保證分散式系統的高可用,資料在系統中一般儲存多個副本。當某個副本所在的儲存節點發生故障的時候,分散式系統能夠自動將服務切換到其他的副本,從而實現自動容錯。
當資料寫入主副本的時候,由主副本進行寫入操作,並複製到其他副本。如果主副本和副本分片都寫入成功才返回客戶端寫入成功,是同步複製技術,屬於強一致性。強一致性的好處在於如果主副本出現故障,至少有一個備副本擁有完整資料,系統可以自動進行切換而不必擔心資料丟失。但是如果副本寫入出現問題將阻塞主副本的寫操作,系統可用性變差。
如果主副本寫入成功,立刻返回客戶端寫入成功,採用非同步的方式進行資料同步,而不等待副本寫入成功,通過定時任務補償等手段達到最終一致性。最終一致性好處是系統可用性較好,但是一致性較差,如果主副本發生不可恢復故障,可能丟失最後一次更新的資料。
一致性 | 可用性 | 應用場景 | |
---|---|---|---|
同步複製技術 | 強 | 弱 | 適用於對資料一致性有嚴格要求的場景 |
非同步複製技術 | 弱 | 強 | 適用於對行囊夠要求很高的場景 |
半同步複製技術 | 較強 | 較弱 | 適用於大多數的分散式場景 |
Elasticsearch天生是分散式架構的,滿足分割槽容錯性,在資料複製寫入時,在CP和AP之間做了取捨,選擇了CP,做到資料寫入不丟失,而丟失了高可用。