ElasticSearch 文件(document)內部機制詳解

|舊市拾荒|發表於2022-03-13

1、資料路由

1.1 文件儲存怎麼路由到相應分片?

一個文件,最終會落在主分片的一個分片上,到底應該在哪一個分片?這就是資料路由。

1.2 路由演算法

shard = hash(routing) % number_of_primary_shards

簡單來說就是雜湊值對主分片數取模。

舉例:

  對一個文件經行crud時,都會帶一個路由值 routing number。預設為文件_id(可能是手動指定,也可能是自動生成)。

  儲存1號文件,經過雜湊計算,雜湊值為2,此索引有3個主分片,那麼計算2%3=2,就算出此文件在P2分片上。決定一個document在哪個shard上,最重要的一個值就是routing值,預設是_id,也可以手動指定,相同的routing值,每次過來,從hash函式中,產出的hash值一定是相同的。無論hash值是幾,無論是什麼數字,對number_of_primary_shards求餘數,結果一定是在0~number_of_primary_shards-1之間這個範圍內的。

  

1.3 手動指定 routing number

PUT /test_index/_doc/15?routing=num
{
  "num": 0,
  "tags": []
}

場景:在程式中,架構師可以手動指定已有資料的一個屬性為路由值,好處是可以定製一類文件資料儲存到一個分片中。缺點是設計不好,會造成資料傾斜。所以,不同文件儘量放到不同的索引中。剩下的事情交給es叢集自己處理。

1.4 主分片數量不可變

涉及到以往資料的查詢搜尋,所以一旦建立索引,主分片數不可變。

2、文件(Document)的增刪改內部機制(寫資料過程)

增刪改可以看做update,都是對資料的改動。一個改動請求傳送到es叢集,經歷以下四個步驟:

(1)客戶端選擇一個node傳送請求過去,這個node就是coordinating node(協調節點)

(2)coordinating node,對document進行路由,將請求轉發給對應的node(有primary shard)

(3)實際的node上的primary shard處理請求,然後將資料同步到replica node。

(4)coordinating node,如果發現primary node和所有replica node都搞定之後,就返回響應結果給客戶端。

  

  如上圖所示,存在一個book索引,3個主分片,一個副本分片。比如說選擇第一個節點為協調節點,在根據id進行資料路由,判斷出屬於第一個分片,找到對應的主分片完成對應的請求,在去對應的副本分片完成請求,最後在將響應結果返回給客戶端。

3、文件的查詢內部機制(讀資料過程)

1、客戶端傳送請求到任意一個node,成為coordinate node

2、coordinate node對document進行路由,將請求轉發到對應的node,此時會使用round-robin隨機輪詢演算法,在primary shard以及其所有replica中隨機選擇一個,讓讀請求負載均衡

3、接收請求的node返回document給coordinate node

4、coordinate node返回document給客戶端

5、特殊情況:document如果還在建立索引過程中,可能只有primary shard有,任何一個replica shard都沒有,此時可能會導致無法讀取到document,但是document完成索引建立之後,primary shard和replica shard就都有了。

  

  如上圖所示,存在一個book索引,3個主分片,一個副本分片。比如說選擇第一個節點為協調節點,在根據id進行資料路由,判斷出屬於第一個分片,在primary shard以及其所有replica中隨機選擇一個,最後在將響應結果返回給客戶端。

4、文件的搜尋機制(過程)

es 最強大的是做全文檢索,就是比如你有三條資料:

  • java真好玩兒啊
  • java好難學啊
  • j2ee特別牛

你根據 java 關鍵詞來搜尋,將包含 java的 document 給搜尋出來。es 就會給你返回:java真好玩兒啊,java好難學啊。

  • 客戶端傳送請求到一個 coordinate node

  • 協調節點將搜尋請求轉發到所有的 shard 對應的 primary shard 或 replica shard,都可以。

  • query phase:每個 shard 將自己的搜尋結果(其實就是一些 doc id)返回給協調節點,由協調節點進行資料的合併、排序、分頁等操作,產出最終結果。

  • fetch phase:接著由協調節點根據 doc id 去各個節點上拉取實際的 document 資料,最終返回給客戶端。

5、bulk api奇特的json格式

POST /_bulk
{"action": {"meta"}}
{"data"}
{"action": {"meta"}}
{"data"}

[
    {
        "action":{
            "method":"create"
        },
        "data":{
            "id":1,
            "field1":"java",
            "field1":"spring",
        }
    },
      {
        "action":{
            "method":"create"
        },
        "data":{
            "id":2,
            "field1":"java",
            "field1":"spring",
        }
    }       
]

如上所示,為什麼bulk api不採用下面的那種閱讀性非常強的格式而是採用上面那種格式呢?原因有以下3點。

1、bulk中的每個操作都可能要轉發到不同的node的shard去執行

2、如果採用比較良好的json陣列格式,這種格式允許任意的換行,整個可讀性非常棒,讀起來很爽,es拿到這種標準格式的json串以後,要按照下述流程去進行處理

(1)將json陣列解析為JSONArray物件,這個時候,整個資料,就會在記憶體中出現一份一模一樣的拷貝,一份資料是json文字,一份資料是JSONArray物件

(2)解析json陣列裡的每個json,對每個請求中的document進行路由

(3)為路由到同一個shard上的多個請求,建立一個請求陣列。100請求中有10個是到P1.

(4)將這個請求陣列序列化

(5)將序列化後的請求陣列傳送到對應的節點上去

3、耗費更多記憶體,更多的jvm gc開銷。

  一般來說bulk size最佳大小在幾千條左右,然後大小在10MB左右,所以說,可怕的事情來了。假設說現在100個bulk請求傳送到了一個節點上去,然後每個請求是10MB,100個請求,就是1000MB = 1GB,然後每個請求的json都copy一份為jsonarray物件,此時記憶體中的佔用就會翻倍,就會佔用2GB的記憶體,甚至還不止。因為弄成jsonarray之後,還可能會多搞一些其他的資料結構,2GB+的記憶體佔用。佔用更多的記憶體可能就會積壓其他請求的記憶體使用量,比如說最重要的搜尋請求,分析請求,等等,此時就可能會導致其他請求的效能急速下降。另外的話,佔用記憶體更多,就會導致java虛擬機器的垃圾回收次數更多,跟頻繁,每次要回收的垃圾物件更多,耗費的時間更多,導致es的java虛擬機器停止工作執行緒的時間更多。

再看看現在的奇特格式

POST /_bulk
{ "delete": { "_index": "test_index",  "_id": "5" }} 
{ "create": { "_index": "test_index",  "_id": "14" }}
{ "test_field": "test14" }\n
{ "update": { "_index": "test_index",  "_id": "2"} }
{ "doc" : {"test_field" : "bulk test"} }\n

(1)不用將其轉換為json物件,不會出現記憶體中的相同資料的拷貝,直接按照換行符切割json

(2)對每兩個一組的json,讀取meta,進行document路由

(3)直接將對應的json傳送到node上去

這種格式最大的優勢在於,不需要將json陣列解析為一個JSONArray物件,形成一份大資料的拷貝,不至於浪費記憶體空間,也能儘可能地保證效能。

 

 

相關文章