本文探討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,做到數據寫入不丟失,而丟失了高可用。