🍊🍉🍋 先說下結論,只要叢集中的工作節點過半,有候選的master節點,掛掉的節點中不同時包含索引的主分片和副分片,那麼ES是可以做到讓業務無感知的進行主副分片切換的。
藍胖子會先講解下ES叢集寫入文件的原理,並對異常情況進行分析,最後來模擬叢集寫入過程中節點當機的情況來對這個問題展開討論。
主副分片的寫入流程
之前我在Elasticsearch 如何保證寫入過程中不丟失資料的 有提到ES 透過translog 保證了segment在寫入完成後即使會在記憶體停留一段時間也不會因為當機而丟失資料。但是沒有提到ES在寫入時,副本和主節點之間的關係,現在把這部分補充完整。
如下圖所示,有3個es節點,es03是整個叢集的master節點,同時也承擔資料節點的角色,es01,es02都是資料節點,同時也可以成為master節點候選者。
es client
客戶端傳送插入文件的請求,預設是隨機選取叢集中的一個節點進行傳送,假設請求傳送給了es02
,由於插入和修改操作只能由主分片進行,此時主分片又是es03
節點,所以es02
這個時候就充當了協調節點的角色,對客戶端的請求進行了轉發,轉發給了es03
。
正常情況下,es03
會在主分片對資料進行寫入,寫入成功後,再將插入請求轉發到副本節點進行復制,等待副本節點回應後(無論成功還是失敗都會回應)會將此次寫入資料的結果返回給協調節點es03
,es03再將結果返回給客戶端。
分片寫入過程中的異常處理流程
上面是一個正常的流程,現在來看一下一個異常的情況,如下圖所示,在協調節點es02轉發請求給es03時,es03 在處理分片寫入過程中當機了,這個時候,客戶端的此次寫入會失敗掉嗎?
es03 節點上的是主分片,這裡有必要對es03節點當機的時機進行分析,
1,es03節點在主分片寫入前當機。
2,es03 節點在主分片寫入成功後,還沒來得及向副分片寫入資料就當機。
3,es03節點在副分片也寫入成功後,就當機。
以上是主分片的3種當機的情況。
接著,回到寫入流程上,es03節點當機了,es02的http 轉發請求將會失敗,此時es02節點在收到失敗後,將會進行重試,並且由於此時是主分片,且是master節點當機,所以,es02節點會等待整個叢集重新選出主分片和主節點後,再會進行重試
。
📢📢📢需要注意的是,如果es03節點當機的時機是主分片寫入前,或者是還沒來得及向副分片進行復制就當機
,那麼重試不會有任何問題,新的主分片將會擁有寫入失敗的資料。如下圖所示,
如果es03節點在當機前已經完成了副分片的複製,es01節點已經擁有了這條插入的資料
,那麼協調節點es02的重試會導致多插入一條資料嗎?
其實是不會的,在Es節點內部,每個文件都會有一個_id
和 version
,_id
標記唯一一個文件,version
用於版本更新,ES的更新是基於樂觀鎖機制,併發更新時有可能返回致版本錯誤,需要業務方進行重試,更新成功後文件的version
欄位會進行加1。
在協調節點轉發插入請求前,便會生成_id
和version
欄位,所以如果es03節點已經將插入的資料複製到了副分片,那麼es02節點在進行重試發請求時,請求到達es01節點,會在文件中發現相同的_id
和version
的文件,則會認為資料已經插入了,忽略掉這次重試的插入請求。ES叢集會正常的進行插入資料,後續客戶端的插入請求都將被轉發到es01節點的主分片進行插入。客戶端除了直接請求es03節點,不會察覺到es03節點掛了。
上面是在文件寫入過程中節點當機的情況,除以以外,如果es03節點如果沒有在處理請求就當機了,那麼叢集會重新選主節點和主分片,如果選舉過程還沒結束,此時客戶端就傳送來插入請求,那麼會等待選舉結束後,es02才會繼續轉發請求到新主分片節點。
所以,你可以看到,Es節點可以透過重試和等待主分片選舉實現讓業務無感知的進行主副分片的切換。
壓測叢集模擬節點當機
下面我用docker compose
啟動一個3節點叢集,然後對叢集進行併發插入,接著模擬節點 當機,主動kill掉一個節點,來看看最後插入的資料是否和併發插入資料是吻合的,並且插入過程沒有報錯。
啟動一個es叢集,
version: '2.2'
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:7.14.2
container_name: es01
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
ports:
- 9200:9200
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:7.14.2
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
ports:
- 9201:9200
es03:
image: docker.elastic.co/elasticsearch/elasticsearch:7.14.2
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
ports:
- 9202:9200
啟動之後,建立了一個名字叫做cd
的索引,
PUT /cd
{
"mappings": {
"properties": {
"info":{
"type": "text"
},
"age":{
"type":"integer"
},
"email":{
"type": "keyword",
"index": false
},
"name":{
"type": "object",
"properties": {
"firstName": {
"type": "keyword"
},
"lastName": {
"type": "keyword"
}
}
}
}
}
}
透過kibana進行檢視,目前的主節點es03
接著,我用golang寫了一個程式總共發出10萬的插入請求,100併發進行發出,程式碼如下,
package main
import (
"fmt"
"github.com/google/uuid"
"io/ioutil"
"net/http"
"strings"
"sync")
func main() {
wg := sync.WaitGroup{}
for i := 1; i <= 100000; i++ {
if i%100 == 0 {
wg.Wait()
}
wg.Add(1)
go func() {
defer wg.Done()
insert()
}()
}
wg.Wait()
}
func insert() {
url := "http://localhost:9201/cd/_doc"
method := "POST"
id := uuid.New().String()
payload := strings.NewReader(`{
"info":"` + id + `",
"email":"12345@136.com", "name":{ "firstName":"張",
"lastName":"四"
}, "age":18}`)
client := &http.Client{}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
fmt.Println(err, "錯誤了", id)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
if res.StatusCode != 201 {
fmt.Println(string(body), res.StatusCode, id)
}
}
程式啟動後,就開始併發的往叢集中進行插入,此時我手動進入es03容器內部,然後kill掉了es程序。接著,此時索引的主副分片發生了變化,如下圖所示,索引的主分片變到了節點es01上,而叢集的master節點變成了es02。
整個程式此時還在進行,並未有任何報錯。
此時我又手動重啟了掛掉的es03,讓其自動進行恢復recovery
,節點狀態變化如下,可以看到es03已經變成副分片了。
最後,等待併發程式結束後,我向叢集中對插入的資料行數進行查詢,的確是10萬條資料,說明資料沒有發生丟失,並且,整個過程,客戶端也沒有報錯。
足以說明,ES的主副分片的確對業務是無感知的了。的確是妙啊。對比起mysql的可靠性優先的主備切換方式,資料庫會有小段的時間不可寫,重試機制由ES幫我們做了。