一條數據是如何落地到對應的shard上的?html
當索引一個文檔的時候,文檔會被存儲到一個主分片中。 Elasticsearch 如何知道一個文檔應該存放到哪一個分片中呢?java
首先這確定不會是隨機的,不然未來要獲取文檔的時候咱們就不知道從何處尋找了。實際上,這個過程是根據下面這個算法決定的:node
shard_num = hash(_routing) % num_primary_shards
其中 _routing 是一個可變值,默認是文檔的 _id 的值 ,也能夠設置成一個自定義的值。 _routing 經過 hash 函數生成一個數字,而後這個數字再除以 num_of_primary_shards (主分片的數量)後獲得餘數 。這個分佈在 0 到 number_of_primary_shards-1 之間的餘數,就是咱們所尋求的文檔所在分片的位置。這就解釋了爲何咱們要在建立索引的時候就肯定好主分片的數量 而且永遠不會改變這個數量:由於若是數量變化了,那麼全部以前路由的值都會無效,文檔也再也找不到了。算法
假設你有一個100個分片的索引。當一個請求在集羣上執行時會發生什麼呢?數據庫
1. 這個搜索的請求會被髮送到一個節點
2. 接收到這個請求的節點,將這個查詢廣播到這個索引的每一個分片上(多是主分片,也多是複本分片)
3. 每一個分片執行這個搜索查詢並返回結果
4. 結果在通道節點上合併、排序並返回給用戶
由於默認狀況下,Elasticsearch使用文檔的ID(相似於關係數據庫中的自增ID),若是插入數據量比較大,文檔會平均的分佈於全部的分片上,這致使了Elasticsearch不能肯定文檔的位置,網絡
因此它必須將這個請求廣播到全部的N個分片上去執行 這種操做會給集羣帶來負擔,增大了網絡的開銷;架構
自定義路由的方式很是簡單,只須要在插入數據的時候指定路由的key便可。雖然使用簡單,但有許多的細節須要注意。咱們從一個例子看起(注:本文關於ES的命令都是在Kibana dev tool中執行的):app
// 步驟1:先建立一個名爲route_test的索引,該索引有3個shard,0個副本 PUT route_test/ { "settings": { "number_of_shards": 2, "number_of_replicas": 0 } } // 步驟2:查看shard GET _cat/shards/route_test?v index shard prirep state docs store ip node route_test 1 p STARTED 0 230b 172.19.0.2 es7_02 route_test 0 p STARTED 0 230b 172.19.0.5 es7_01 // 步驟3:插入第1條數據 PUT route_test/_doc/a?refresh { "data": "A" } // 步驟4:查看shard GET _cat/shards/route_test?v index shard prirep state docs store ip node route_test 1 p STARTED 0 230b 172.19.0.2 es7_02 route_test 0 p STARTED 1 3.3kb 172.19.0.5 es7_01 // 步驟5:插入第2條數據 PUT route_test/_doc/b?refresh { "data": "B" } // 步驟6:查看數據 GET _cat/shards/route_test?v index shard prirep state docs store ip node route_test 1 p STARTED 1 3.3kb 172.19.0.2 es7_02 route_test 0 p STARTED 1 3.3kb 172.19.0.5 es7_01 // 步驟7:查看此時索引裏面的數據 GET route_test/_search { "took" : 5, "timed_out" : false, "_shards" : { "total" : 2, "successful" : 2, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 2, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "route_test", "_type" : "_doc", "_id" : "a", "_score" : 1.0, "_source" : { "data" : "A" } }, { "_index" : "route_test", "_type" : "_doc", "_id" : "b", "_score" : 1.0, "_source" : { "data" : "B" } } ] } }
上面這個例子比較簡單,先建立了一個擁有2個shard,0個副本(爲了方便觀察)的索引 route_test 。建立完以後查看兩個shard的信息,此時shard爲空,裏面沒有任何文檔( docs 列爲0)。接着咱們插入了兩條數據,每次插完以後,都檢查shard的變化。經過對比能夠發現 docid=a 的第一條數據寫入了0號shard,docid=b 的第二條數據寫入了1號 shard。須要注意的是這裏的doc id我選用的是字母"a"和"b",而非數字。緣由是連續的數字很容易路由到一個shard中去。以上的過程就是不指定routing時候的默認行爲。函數
接着,咱們指定routing,看一些有趣的變化:測試
// 步驟8:插入第3條數據 PUT route_test/_doc/c?routing=key1&refresh { "data": "C" } // 步驟9:查看shard GET _cat/shards/route_test?v index shard prirep state docs store ip node route_test 1 p STARTED 1 3.4kb 172.19.0.2 es7_02 route_test 0 p STARTED 2 6.9kb 172.19.0.5 es7_01 // 步驟10:查看索引數據 GET route_test/_search { "took" : 5, "timed_out" : false, "_shards" : { "total" : 2, "successful" : 2, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "route_test", "_type" : "_doc", "_id" : "a", "_score" : 1.0, "_source" : { "data" : "A" } }, { "_index" : "route_test", "_type" : "_doc", "_id" : "c", "_score" : 1.0, "_routing" : "key1", "_source" : { "data" : "C" } }, { "_index" : "route_test", "_type" : "_doc", "_id" : "b", "_score" : 1.0, "_source" : { "data" : "B" } } ] } }
咱們又插入了1條 docid=c 的新數據,但此次咱們指定了路由,路由的值是一個字符串"key1". 經過查看shard信息,能看出這條數據路由到了0號shard。也就是說用"key1"作路由時,文檔會寫入到0號shard。
接着咱們使用該路由再插入兩條數據,但這兩條數據的 docid 分別爲以前使用過的 "a"和"b",你猜一下最終結果會是什麼樣?
// 步驟11:插入 docid=a 的數據,並指定 routing=key1 PUT route_test/_doc/a?routing=key1&refresh { "data": "A with routing key1" } // es的返回信息爲: { "_index" : "route_test", "_type" : "_doc", "_id" : "a", "_version" : 2, "result" : "updated", // 注意此處爲updated,以前的三次插入返回都爲created "forced_refresh" : true, "_shards" : { "total" : 1, "successful" : 1, "failed" : 0 }, "_seq_no" : 2, "_primary_term" : 1 } // 步驟12:查看shard GET _cat/shards/route_test?v index shard prirep state docs store ip node route_test 1 p STARTED 1 3.4kb 172.19.0.2 es7_02 route_test 0 p STARTED 2 10.5kb 172.19.0.5 es7_01 // 步驟13:查詢索引 GET route_test/_search { "took" : 6, "timed_out" : false, "_shards" : { "total" : 2, "successful" : 2, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "route_test", "_type" : "_doc", "_id" : "c", "_score" : 1.0, "_routing" : "key1", "_source" : { "data" : "C" } }, { "_index" : "route_test", "_type" : "_doc", "_id" : "a", "_score" : 1.0, "_routing" : "key1", "_source" : { "data" : "A with routing key1" } }, { "_index" : "route_test", "_type" : "_doc", "_id" : "b", "_score" : 1.0, "_source" : { "data" : "B" } } ] } }
以前 docid=a 的數據就在0號shard中,此次依舊寫入到0號shard中了,由於docid重複,因此文檔被更新了。而後再插入 docid=b 的數據:
// 步驟14:插入 docid=b的數據,使用key1做爲路由字段的值 PUT route_test/_doc/b?routing=key1&refresh { "data": "B with routing key1" } // es返回的信息 { "_index" : "route_test", "_type" : "_doc", "_id" : "b", "_version" : 1, "result" : "created", // 注意這裏不是updated "forced_refresh" : true, "_shards" : { "total" : 1, "successful" : 1, "failed" : 0 }, "_seq_no" : 3, "_primary_term" : 1 } // 步驟15:查看shard信息 GET _cat/shards/route_test?v index shard prirep state docs store ip node route_test 1 p STARTED 1 3.4kb 172.19.0.2 es7_02 route_test 0 p STARTED 3 11kb 172.19.0.5 es7_01 // 步驟16:查詢索引內容 { "took" : 6, "timed_out" : false, "_shards" : { "total" : 2, "successful" : 2, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 4, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "route_test", "_type" : "_doc", "_id" : "c", "_score" : 1.0, "_routing" : "key1", "_source" : { "data" : "C" } }, { "_index" : "route_test", "_type" : "_doc", "_id" : "a", "_score" : 1.0, "_routing" : "key1", "_source" : { "data" : "A with routing key1" } }, { "_index" : "route_test", "_type" : "_doc", "_id" : "b", "_score" : 1.0, "_routing" : "key1", // 和下面的 id=b 的doc相比,多了一個這個字段 "_source" : { "data" : "B with routing key1" } }, { "_index" : "route_test", "_type" : "_doc", "_id" : "b", "_score" : 1.0, "_source" : { "data" : "B" } } ] } }
和步驟11插入docid=a 的那條數據相比,此次這個有些不一樣,咱們來分析一下。步驟11中插入 docid=a 時,es返回的是updated,也就是更新了步驟2中插入的docid爲a的數據,步驟12和13中查詢的結果也能看出,並無新增數據,route_test中仍是隻有3條數據。而步驟14插入 docid=b 的數據時,es返回的是created,也就是新增了一條數據,而不是updated原來docid爲b的數據,步驟15和16的確也能看出多了一條數據,如今有4條數據。並且從步驟16查詢的結果來看,有兩條docid爲b的數據,但一個有routing,一個沒有。並且也能分析出有routing的在0號shard上面,沒有的那個在1號shard上。
這個就是咱們自定義routing後會致使的一個問題:docid再也不全局惟一。ES shard的實質是Lucene的索引,因此其實每一個shard都是一個功能完善的倒排索引。ES能保證docid全局惟一是採用do id做爲了路由,因此一樣的docid確定會路由到同一個shard上面,若是出現docid重複,就會update或者拋異常,從而保證了集羣內docid惟一標識一個doc。但若是咱們換用其它值作routing,那這個就保證不了了,若是用戶還須要docid的全局惟一性,那隻能本身保證了。由於docid再也不全局惟一,因此doc的增刪改查API就可能產生問題,好比下面的查詢:
GET route_test/_doc/b // es返回 { "_index" : "route_test", "_type" : "_doc", "_id" : "b", "_version" : 1, "_seq_no" : 0, "_primary_term" : 1, "found" : true, "_source" : { "data" : "B" } } GET route_test/_doc/b?routing=key1 // es返回 { "_index" : "route_test", "_type" : "_doc", "_id" : "b", "_version" : 1, "_seq_no" : 3, "_primary_term" : 1, "_routing" : "key1", "found" : true, "_source" : { "data" : "B with routing key1" } }
上面兩個查詢,雖然指定的docid都是b,但返回的結果是不同的。因此,若是自定義了routing字段的話,通常doc的增刪改查接口都要加上routing參數以保證一致性。注意這裏的【通常】指的是查詢,並非全部查詢接口都要加上routing。
爲此,ES在mapping中提供了一個選項,能夠強制檢查doc的增刪改查接口是否加了routing參數,若是沒有加,就會報錯。設置方式以下:
PUT <索引名>/ { "settings": { "number_of_shards": 2, "number_of_replicas": 0 }, "mappings": { "_routing": { "required": true // 設置爲true,則強制檢查;false則不檢查,默認爲false } } }
舉個例子:
PUT route_test1/ { "settings": { "number_of_shards": 3, "number_of_replicas": 0 }, "mappings": { "_routing": { "required": true } } } // 寫入一條數據 PUT route_test1/_doc/b?routing=key1 { "data": "b with routing" } // 如下的增刪改查都會抱錯 GET route_test1/_doc/b PUT route_test1/_doc/b { "data": "B" } DELETE route_test1/_doc/b // 錯誤信息 "error": { "root_cause": [ { "type": "routing_missing_exception", "reason": "routing is required for [route_test1]/[_doc]/[b]", "index_uuid": "_na_", "index": "route_test1" } ], "type": "routing_missing_exception", "reason": "routing is required for [route_test1]/[_doc]/[b]", "index_uuid": "_na_", "index": "route_test1" }, "status": 400 }
固然,不少時候自定義路由是爲了減小查詢時掃描shard的個數,從而提升查詢效率。默認查詢接口會搜索全部的shard,但也能夠指定routing字段,這樣就只會查詢routing計算出來的shard,提升查詢速度。
使用方式也很是簡單,只需在查詢語句上面指定routing便可,容許指定多個:
-- 查詢全部分區 GET route_test/_search { "query": { "match": { "data": "b" } } } -- 查詢指定分區 GET route_test/_search?routing=key1,key2 { "query": { "match": { "data": "b" } } }
另外,指定routing還有個弊端就是容易形成負載不均衡。因此ES提供了一種機制能夠將數據路由到一組shard上面,而不是某一個。只需在建立索引時(也只能在建立時)設置index.routing_partition_size
,默認值是1,即只路由到1個shard,能夠將其設置爲大於1且小於索引shard總數的某個值,就能夠路由到一組shard了。值越大,數據越均勻。固然,從設置就能看出來,這個設置是針對單個索引的,能夠加入到動態模板中,以對多個索引生效。指定後,shard的計算方式變爲:
shard_num = (hash(_routing) + hash(_id) % routing_partition_size) % num_primary_shards
對於同一個routing值,hash(_routing)
的結果固定的,hash(_id) % routing_partition_size
的結果有 routing_partition_size 個可能的值,兩個組合在一塊兒,對於同一個routing值的不一樣doc,也就能計算出 routing_partition_size 可能的shard num了,即一個shard集合。但要注意這樣作之後有兩個限制:
_routing
的required
必須設置爲true。可是對於第2點我測試了一下,若是不寫mapping,是能夠的,此時_routing
的required
默認值實際上是false的。但若是顯式的寫了,就必須設置爲true,不然建立索引會報錯。
// 不顯式的設置mapping,能夠成功建立索引 PUT route_test_3/ { "settings": { "number_of_shards": 2, "number_of_replicas": 0, "routing_partition_size": 2 } } // 查詢也能夠不用帶routing,也能夠正確執行,增刪改也同樣 GET route_test_3/_doc/a // 若是顯式的設置了mappings域,且required設置爲false,建立索引就會失敗,必須改成true PUT route_test_4/ { "settings": { "number_of_shards": 2, "number_of_replicas": 0, "routing_partition_size": 2 }, "mappings": { "_routing": { "required": false } } }
不知道這算不算一個bug。
ElasticSearch的routing算是一個高級用法,但的確很是有用。在咱們公司的訂單數據,就用merchant_no做爲routing,這樣就能保證同一個商戶的數據所有保存到同一個shard去,後面檢索的時候,一樣使用merchant_no做爲routing,就能夠精準的從某個shard獲取數據了。對於超大數據量的搜索,routing再配合hot&warm的架構,是很是有用的一種解決方案。並且同一種屬性的數據寫到同一個shard還有不少好處,好比能夠提升aggregation的準確性。
注1:本文例子中routing=key1,這裏的key1是具體的值,而不是字段名稱; 注2:經過JavaAPI建立 IndexRequest 時,經過 routing(java.lang.String routing) 方法指定routing值,注意這裏是具體的值,而不是字段名稱; 注3:本文的全部測試基於ES 7.1.0版本。
hot&warm的架構,參考我另外一篇文章:http://www.javashuo.com/article/p-bkjvpufa-hx.html