本篇文章主要講解 ElasticSearch 中分布式系統的概念,包括節點、分片和并發控制等,同時還會提到分頁遍歷和深度遍歷問題的解決方案。
節點
- 節點是一個 ElasticSearch 示例
- 其本質就是一個 Java 進程
- 一個機器上可以運行多個示例但生產環境推薦只運行一個
- 每一個節點都有名字,通過配置文件配置
- 每一個節點啟動后都會分配一個 UID,保存在 data 目錄下
Coordinating Node
- 處理請求的節點,叫 Coordinating Node
- 路由請求到正確的節點,例如創建索引的請求,需要路由到 Master
- 所有節點默認都是 Coordinating Node
- 通過將其他類型設置成 False,使其成為 Dedicated Coordinating Node
Data Node
- 可以保存數據的節點,叫做 Data Node
- 節點啟動后,默認就是數據節點。可以設置 node.data:false 禁止
- Data Node 的職責
- 保存分片數據。在數據擴展上起到了至關重要的作用(由 Master Node 決定如何把分片分發到數據節點上)
- 通過增加數據節點
- 可以解決數據水平擴展和解決數據單點問題
Master Node
- Master Node 的職責
- 處理創建,刪除索引等請求/決定分片被分配到哪個節點 /負責索引的創建與刪除
- 維護并且更新 Cluster State
- Master Node 的最佳實踐
- Master 節點非常重要,在部署上需要考慮解決單點的問題
- 為一個集群設置多個 Master 節點/每個節點只承擔 Master 的單一角色
Master Eligible Nodes
- 一個集群,支持配置多個 Master Eligible 節點。這些節點可以在必要時(如 Master 節點出現故障,網絡故障時)參與選主流程,成為 Master 節點
- 每個節點啟動后,默認就是一個 Master Eligible 節點
- 可以設置 node.master: false 禁止
- 當集群內第一個 Master Eligible 節點啟動時候,它會將自己選舉成 Master 節點
選主過程
- 互相 Ping 對方,Node ld 低的會成為被選舉的節點
- 其他節點會加入集群,但是不承擔 Master 節點的角色。一旦發現被選中的主節點丟失,就會選舉出新的 Master 節點
腦裂問題
- Split-Brain,分布式系統的經典網絡問題,當出現網絡問題,一個節點和其他節點無法連接
- Node 2 和 Node 3 會重新選舉 Master
- Node 1 自己還是作為 Master 組成一個集群,同時更新 Cluster State
- 導致 2 個 Master 維護不同的 Cluster State,當網絡恢復時,無法選擇正確恢復
解決方法
- 限定選舉條件,設置 quorum(仲裁),只有當 Master Eligible 節點數大于 quorum 時才能進行選舉
- 7.0 后無需配置
分片
Primary Shard
- 分片是 ElasticSearch 分布式存儲的基石(主分片 / 副本分片)
- 通過主分片,將數據分布在所有節點上
- Primary Shard 可以將一份索引的數據分散在多個 Data Node 上,實現存儲的水平擴展
- 主分片數在索引創建時候指定,后續默認不能修改,如要修改需重建索引
Replica Shard
- 數據可用性
- 通過引入副本分片(Replica Shard)提高數據的可用性。一旦主分片丟失,副本分片可以 Promote 成主分片。副本分片數可以動態調整。每個節點上都有完備的數據。如果不設置副本分片,一旦出現節點硬件故障,就有可能造成數據丟失
- 提升系統的讀取性能
- 副本分片由主分片(Primary Shard)同步。通過支持增加 Replica 個數,一定程度可以提高讀取的吞吐量
分片數的設定
- 如何規劃一個索引的主分片數和副本分片數
- 主分片數過小:例如創建了 1 個 Primary Shard 的 Index。如果該索引增長很快,集群無法通過增加節點實現對這個索引的數據擴展
- 主分片數設置過大:導致單個 Shard 容量很小,引發一個節點上有過多分片,影響性能
- 副本分片數設置過多,會降低集群整體的寫入性能
集群健康狀態
GET /_cluster/health{"cluster_name" : "lanlance","status" : "green","timed_out" : false,"number_of_nodes" : 2,"number_of_data_nodes" : 2,"active_primary_shards" : 21,"active_shards" : 42,"relocating_shards" : 0,"initializing_shards" : 0,"unassigned_shards" : 0,"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" : 100.0
}
- Green:健康狀態,所有的主分片和副本分片都可用
- Yellow:亞健康,所有的主分片可用,部分副本分片不可用
- Red:不健康狀態,部分主分片不可用
文檔到分片的路由算法
- s h a r d = h a s h ( r o u t i n g ) / 主分片數 shard = hash(routing) / 主分片數 shard=hash(routing)/主分片數
- Hash 算法確保文檔均勻分散到分片中
- 默認 routing 值是文檔 id
- 可以自行制定 routing 值,與業務邏輯綁定也可以
- 是 Primary Shard 數不能修改的根本原因
刪除一個文檔的流程
分片的內部原理
倒排索引的不可變性
倒排索引采用 Immutable Design,一旦生成,不可更改
不可變性,帶來了的好處如下:
- 無需考慮并發寫文件的問題,避免了鎖機制帶來的性能問題
- 一旦讀入內核的文件系統緩存,便留在哪里。只要文件系統存有足夠的空間,大部分請求就會直接請求內存,不會命中磁盤,提升了很大的性能
- 緩存容易生成和維護/數據可以被壓縮
但壞處是如果需要讓一個新的文檔可以被搜索,需要重建整個索引。
Lucene Index
- 在 Lucene 中,單個倒排索引文件被稱為 Segment。Segment 是自包含的,不可變更的。多個 Segments 匯總在一起稱為 Lucene 的 Index,其對應的就是 ES 中的 Shard
- 當有新文檔寫入時,會生成新 Segment,查詢時會同時查詢所有 Segments,并且對結果匯總。Lucene 中有一個文件用來記錄所有 Segments 信息,叫做 Commit Point
Refresh
- 將 Index buffer 寫入 Segment 的過程叫 Refresh。Refresh 不執行 fsync 操作
- Refresh 默認 1 秒發生一次,可通過 index.refresh_interval 配置。Refresh 后數據就可以被搜索到了。這也是為什么 ElasticSearch 被稱為近實時搜索
- 如果系統有大量的數據寫入,那就會產生很多的 Segment
- Index Buffer 被占滿時會觸發 Refresh,默認值是 JVM 的 10%
Transaction Log
- Segment 寫入磁盤的過程相對耗時,借助文件系統緩存,Refresh 時先將 Segment 寫入緩存以開放查詢
- 為了保證數據不會丟失,所以在 Index 文檔時同時寫 Transaction Log,高版本開始 Transaction Log 默認落盤。每個分片有一個 Transaction Log
- 在 ES Refresh 時 Index Buffer 被清空,Transaction log 不會清空
Flush
- 調用 Refresh,清空 Index Buffer
- 調用 fsync,將緩存中的 Segments 寫入磁盤
- 清空 Transaction Log
默認 30 分鐘調用一次,當 Transaction Log 滿時(默認 512 MB)也會調用
Merge
- Segment 很多,需要被定期合并
- 減少 Segments / 真正刪除已經刪除的文檔
- ES 和 Lucene 會自動進行 Merge 操作
- POST my_index / _forcemerge
分布式搜索的運行機制
ElasticSearch 的搜索會分為 Query 和 Fetch 兩階段進行。
Query
- 用戶發出搜索請求到 ES 節點。節點收到請求后,會以 Coordinating 節點的身份,在 6 個主副分片中隨機選擇 3 個分片,發送查詢請求。
- 被選中的分片執行查詢,進行排序。每個分片都會返回 From+Size 個排序后的文檔 Id 和排序值給 Coordinating 節點。
Fetch
- Coordinating Node 會將 Query 階段從每個分片獲取的排序后的文檔 Id 列表重新進行排序。選取 From 到 From+Size 個文檔的 Id。
- 以 multiget 請求的方式到相應的分片獲取詳細的文檔數據。
潛在有性能不好和相關性算分不準的問題。
解決算分不準的問題
- 數據量不大的時候主分片數設置為 1,數據量大的時候保證文檔均勻分散在各個分片上。
- 使用 DFS Query Then Fetch。會進行一次完整的相關性算法,耗費更多資源,性能不好。
排序
- 排序是針對字段原始內容進行的,倒排索引無法發揮作用,需要正排索引。
- ElasticSearch 中有兩種實現方法。
- FieldData
- Doc Values(列式存儲,對 Text 類型無效)
Doc Values 和 Field Data 比較:
特性 | Doc Values | Field Data |
---|---|---|
存儲位置 | 磁盤 (內存映射訪問) | 堆內存 (JVM Heap) |
加載時機 | 按需加載 (惰性加載到 OS 緩存) | 按需構建 (首次用于聚合/排序時構建在內存中) |
數據結構 | 列式存儲 (按文檔 ID 組織值) | 列式存儲 (按段構建) |
適用字段 | keyword , numeric , date , ip , boolean | text (默認關閉),其他字段類型 (已廢棄) |
默認啟用 | 是 (對于支持它的字段類型) | 否 (尤其對于 text 字段,7.0+ 默認關閉) |
內存占用 | 低 (利用 OS 文件緩存,不直接占用 JVM 堆) | 高 (直接占用 JVM 堆內存) |
垃圾回收 | 無影響 (由 OS 管理緩存) | 顯著影響 (對象在堆上,易引發 GC 壓力) |
適用操作 | 聚合、排序、腳本 (高效) | text 字段聚合 (分詞后的詞條) |
安全性 | 高 (不易引發 OOM) | 低 (不當配置易導致節點 OOM) |
版本趨勢 | 推薦并默認 | 僅限 text 字段聚合需求 (其他字段已棄用) |
分頁和遍歷
分布式系統中深度分頁的問題
- ES 天生就是分布式的。查詢信息同時數據保存在多個分片、多臺機器上,ES 天生就需要滿足排序的需要(按照相關性算分)。
- 當一個查詢:From=990,Size =10。會在每個分片上先都獲取 1000 個文檔。通過 Coordinating Node 聚合所有結果。最后再通過排序選取前 1000 個文檔。
- 頁數越深,占用內存越多。為了避免深度分頁帶來的內存開銷。ES 有一個設定,默認限定到 10000 個文檔。
使用 Search After 避免深度分頁問題
- 避免深度分頁的性能問題,可以實時獲取下一頁文檔信息
- 不支持指定頁數 (From)
- 只能往下翻
- 第一步搜索需要指定 sort,并且保證值是唯一的 (可以通過加入 id 保證唯一性)
- 然后使用上一次最后一個文檔的 sort 值進行查詢。
示例
1、插入數據
POST users/_doc
{"name":"user1","age":10}
POST users/_doc
{"name":"user2","age":11}
POST users/_doc
{"name":"user2","age":12}
POST users/_doc
{"name":"user2","age":13}
2、執行查詢
POST users/_search
{"size": 1,"query": {"match_all": {}},"sort": [{"age": "desc"} ,{"_id": "asc"} ]
}POST users/_search
{"size": 1,"query": {"match_all": {}},"search_after":[10,"ZQ0vYGsBrR8X3IP75QqX"],"sort": [{"age": "desc"} ,{"_id": "asc"} ]
}
Scroll API
Scroll API 是 Elasticsearch 為大數據集深度遍歷設計的查詢機制,通過創建快照式上下文(Snapshot Context)保證分頁一致性,適用于離線導出、全量遷移等場景。
示例
DELETE users
POST users/_doc
{"name":"user1","age":10}
POST users/_doc
{"name":"user2","age":20}
POST users/_doc
{"name":"user3","age":30}
POST users/_doc
{"name":"user4","age":40}POST /users/_search?scroll=5m
{"size": 1,"query": {"match_all" : {}}
}// 這條數據無法查到
POST users/_doc
{"name":"user5","age":50}POST /_search/scroll
{"scroll" : "1m","scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAWAWbWdoQXR2d3ZUd2kzSThwVTh4bVE0QQ=="
}
Scroll API 與 Search After 的對比
特性 | Search After | Scroll API |
---|---|---|
設計目標 | 實時深度分頁(用戶交互場景) | 大數據集離線遍歷(導出/遷移) |
實時性 | 基于當前索引狀態(實時可見變更) | 快照凍結(創建后索引變更不可見) |
內存消耗 | 低(無服務端狀態) | 高(服務端維護上下文,占用堆內存) |
分頁一致性 | 依賴 PIT 保障一致性 | 天然一致性(快照隔離) |
適用場景 | 用戶界面逐頁瀏覽(如訂單列表翻頁) | 全量數據導出、ETL 遷移、離線分析 |
是否支持跳頁 | ? 僅順序連續分頁 | ? 僅順序連續遍歷 |
資源釋放 | 無狀態(客戶端自主管理游標) | 需顯式刪除 Scroll ID(否則超時釋放) |
性能開銷 | 低(分片級游標定位) | 中(維護上下文,但比 from/size 高效) |
最大深度 | 僅受文檔總數限制 | 同左 |
推薦排序方式 | 業務字段 + _id (確保唯一性) | ["_doc"] (最高效,避免排序計算) |
版本演進 | 主流實時分頁方案(結合 PIT 使用) | 逐漸被 Async Search 替代(大數據異步查詢) |
并發控制
ES 使用樂觀鎖進行并發控制。
ES 的樂觀并發控制
ES 中的文檔是不可變更的。如果你更新一個文檔,會將就文檔標記為刪除,同時增加一個全新的文檔。同時文檔的 version 字段加 1。
示例
DELETE products
PUT products
PUT products/_doc/1
{"title":"iphone","count":100
}// success
PUT products/_doc/1?if_seq_no=1&if_primary_term=1
{"title":"iphone","count":100
}// fail
PUT products/_doc/1?if_seq_no=1&if_primary_term=1
{"title":"iphone","count":102
}// success
PUT products/_doc/1?version=30000&version_type=external
{"title":"iphone","count":100
}
寫在最后
這是該系列的第七篇,主要講解 ElasticSearch 中分布式系統的概念,包括節點、分片和并發控制等,同時提到了分頁遍歷和深度遍歷問題的解決方案。可以自己去到 Kibana 的 Dev Tool 實戰操作,未來會持續更新該系列,歡迎關注👏🏻。
同時歡迎關注小紅書:LanLance。不定時分享職場思考、大廠方法論和后端經驗??
參考
- https://github.com/onebirdrocks/geektime-ELK/
- https://www.elastic.co/elasticsearch/