前言
當索引一個文檔的時候,文檔會被存儲到一個主分片中。那么,elasticsearch如何知道一個文檔應該存放到哪個分片中呢?
首先這肯定不是隨機的,否則在檢索文檔時就不知道該從哪去尋找它了。實際上這個過程是根據下面公式決定的:
shard = hash(routing) % number_of_primary_shards
routing
是一個可變值,默認是文檔的_id
,也可以是自定義的值。hash函數將routing
值哈希后生成一個數字,然后這個數字再除以number_of_primary_shards
(主分片的數量)得到余數,這個分布在0
到number_of_primary_shards
減一(計數從0開始,比如5個主分片,那么范圍就是0~4)之間的余數,就是文檔存放的分片位置。
比如一篇文檔的id為123,那么它就應該存在:
>>> hash(123) % 5
3
這篇文檔就存在P3
主分片上。
這也就解釋了為什么在創建索引時,主分片的數量一經定義就不能改變,因為如果數量變化了,那么之前所有的路由(routing)值都會無效,文檔就再也找不到了。
一般的,elasticsearch的默認路由算法都會根據文檔的id值作為依據將其哈希到相應的主分片上,該算法基本上會將所有的文檔平均分布在所有的主分片上,而不會產生某個分片數據過大而導致集群不平衡的情況。
那么我們在向一個有100個主分片的索引發送查詢某篇文檔的請求時,該請求發送到集群,集群干了什么呢?
- 這個請求會被集群交給主節點。
- 主節點接收這個請求后,將這個查詢請求廣播到這個索引的每個分片上(包含主、復制分片)。
- 每個分片執行這個搜索請求,並將結果返回。
- 結果在主節點上合並、排序后返回給用戶。
這里面就有些問題了。因為在存儲文檔時,通過hash算法將文檔平均分布在各分片上,這就導致了elasticsearch也不確定文檔的位置,所以它必須將這個請求廣播到所有的分片上去執行。
為了避免不必要的查詢,我們使用自定義的路由模式,這樣可以使我們的查詢更具目的性。比如之前的查詢是這樣的:
請求來了,你們(索引下的所有分片)都要檢查一下自己是否有符合條件的文檔
當能自定義路由后的查詢變成了:
請求來了,分片3、5你倆把文檔給我返回
自定義路由
所有的文檔 API( get
、 index
、 delete
、 bulk
、 update
以及 mget
)都接受一個叫做 routing
的路由參數 ,通過這個參數我們可以自定義文檔到分片的映射。一個自定義的路由參數可以用來確保所有相關的文檔——例如所有屬於同一個用戶的文檔——都被存儲到同一個分片中。
PUT r1/doc/1?routing=user1
{
"title":"論母豬的產前保養"
}
PUT r1/doc/2?routing=user1
{
"title":"論母豬的產后護理"
}
上例中,該文檔使用user1
作為路由值而不是使用_id
。這樣,具有相同user1
的文檔將會被分配同一個分片上。
通過路由查詢文檔
自定義路由可以減少搜索,不需要將搜索請求分發到所有的分片,只需要將請求發送到匹配特定的路由值分片既可。
我們來查詢:
GET r1/doc/1?routing=user1
# 結果如下
{
"_index" : "r1",
"_type" : "doc",
"_id" : "1",
"_version" : 3,
"_routing" : "user1",
"found" : true,
"_source" : {
"title" : "論母豬的產前保養"
}
}
也可通過這個路由值查詢文檔:
GET r1/doc/_search
{
"query": {
"terms": {
"_routing":["user1"]
}
}
}
# 結果如下
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [
{
"_index" : "r1",
"_type" : "doc",
"_id" : "2",
"_score" : 1.0,
"_routing" : "user1",
"_source" : {
"title" : "論母豬的產后護理"
}
},
{
"_index" : "r1",
"_type" : "doc",
"_id" : "1",
"_score" : 1.0,
"_routing" : "user1",
"_source" : {
"title" : "論母豬的產前保養"
}
}
]
}
}
刪除文檔
我們來刪除文檔。
DELETE r1/doc/1
# 結果如下
{
"_index" : "r1",
"_type" : "doc",
"_id" : "1",
"_version" : 1,
"result" : "not_found",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
由上例可見,不提供路由,無法刪除文檔。
DELETE r1/doc/1?routing=user1
# 結果如下
{
"_index" : "r1",
"_type" : "doc",
"_id" : "1",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1
}
給上路由就OK了。
由此可見,在查詢、刪除、更新文檔時都要提供相同的路由值。
查詢多個路由
除了指定查詢單個路由值之外,還可以指定多個路由值查詢:
PUT r2/doc/1?routing=user1
{
"title":"母豬產前保養重點在多喂飼料,輔以人工按摩"
}
PUT r2/doc/2?routing=user2
{
"title":"母豬產后護理重點在母子隔離喂養"
}
此搜索請求將僅在與user1和user2路由值關聯的分片上執行。
GET r2/doc/_search?routing=user1,user2
{
"query": {
"match": {
"title": "母豬"
}
}
}
# 結果如下
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 2,
"successful" : 2,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 0.68324494,
"hits" : [
{
"_index" : "r2",
"_type" : "doc",
"_id" : "2",
"_score" : 0.68324494,
"_routing" : "user2",
"_source" : {
"title" : "母豬產后護理重點在母子隔離喂養"
}
},
{
"_index" : "r2",
"_type" : "doc",
"_id" : "1",
"_score" : 0.5753642,
"_routing" : "user1",
"_source" : {
"title" : "母豬產前保養重點在多喂飼料,輔以人工按摩"
}
}
]
}
}
忘了路由值怎么辦?
由之前的示例可以看到,在自定義的路由中,索引、查詢、刪除、更新文檔時,都要提供路由值。但是我們有可能會忘記路由值,導致文檔在多個分片建立索引:
PUT r3/doc/1?routing=u1
{
"title":"小豬仔真可愛"
}
PUT r3/doc/2
{
"title":"可愛可愛一盤菜"
}
正如上例所示,我們在創建文檔2的時候,忘記路由了,導致這篇文檔被默認分配到別的分片上了。那我們想通過u1
路由查詢就會發現:
GET r3/doc/_search
{
"query": {
"terms": {
"_routing":["u1"]
}
}
}
# 結果如下
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "r3",
"_type" : "doc",
"_id" : "1",
"_score" : 1.0,
"_routing" : "u1",
"_source" : {
"title" : "小豬仔真可愛"
}
}
]
}
}
可以發現,那個文檔2通過這個路由值查詢不到,但是可以通過普通的查詢:
GET r3/doc/_search
這樣,兩篇文檔都會有被返回。
為了避免類似上述的情況出現,我們必須采取安全措施,加個套!在自定義映射關系時,使用_routing
參數生成那個安全套
!
# 以下是6.5.4版本的寫法
PUT r4
{
"mappings": {
"doc":{
"_routing":{
"required": true
}
}
}
}
# 以下是7.0官方文檔的的寫法
PUT my_index2
{
“mappings”:{
“_ usting”:{
“required”:true
}
}
}
在_routing
參數內,將required:true
就表明在對文檔做CURD時需要指定路由。不然就會拋出一個routing_missing_exception
錯誤。就像下面的示例一樣。
PUT r4/doc/1
{
"title":"母豬不懷孕怎么辦?"
}
# 結果是報錯
{
"error": {
"root_cause": [
{
"type": "routing_missing_exception",
"reason": "routing is required for [r4]/[doc]/[1]",
"index_uuid": "_na_",
"index": "r4"
}
],
"type": "routing_missing_exception",
"reason": "routing is required for [r4]/[doc]/[1]",
"index_uuid": "_na_",
"index": "r4"
},
"status": 400
}
有了這種規范,我們在自定義路由時,就可以避免一些不必要的情況發生了。
自定義路由唯一ID
索引指定自定義_routing的文檔時,不能保證索引中所有分片的_id唯一性。 事實上,如果使用不同的_routing值索引,具有相同_id的文檔可能最終會出現在不同的分片上。
我們應確保ID在索引中是唯一的。
路由到索引分區
問題來了,在實際開發中,可能由於業務場景問題碰到某個路由的文檔量非常大,造成該分片非常大,而某些路由的文檔卻非常小,這就會造成數據偏移而導致集群不平衡。我們該如何辦呢?
我們可以配置索引,使得自定義路由值將轉到分片的子集而不是單個分片。這有助於降低上述問題導致集群不平衡的風險,同時仍然可以減少搜索的影響。
這是通過在索引創建時提供索引級別設置index.routing_partition_size
來完成的。隨着分區大小的增加,數據分布越均勻,代價是每個請求必須搜索更多分片。
PUT r6
{
"mappings": {
"doc":{
"_routing":{
"required": true
}
}
},
"settings": {
"index.routing_partition_size": 3
}
}
通俗的說,這是限制文檔分布到指定個數分片上,而不是默認的所有分片上,既提高了請求效率,也減小單一分片數據量過大的問題。
當此設置存在時,計算分片的公式變為:
shard_num = (hash(_routing) + hash(_id) % routing_partition_size) % num_primary_shards
也就是說,_routing字段用於計算索引中的一組分片,然后_id用於選擇該集合中的分片。
要啟用此功能,index.routing_partition_size
應具有大於1且小於index.number_of_shards
的值。
啟用后,分區索引將具有以下限制:
- 無法在其中創建具有join field關系的映射。
- 索引中的所有映射都必須將_routing字段標記為必需。
歡迎斧正,that's all see also:[路由一個文檔到一個分片中](https://www.elastic.co/guide/cn/elasticsearch/guide/current/routing-value.html) | [官方5.6版本的_routing field](https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-routing-field.html) | [官方7.0的_routing field](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-routing-field.html) | [路由文檔到分片](https://www.cnblogs.com/bonelee/p/6055340.html) | [Elasticsearch分片、副本與路由(shard replica routing)](https://www.cnblogs.com/kangoroo/p/7622957.html) | [Elasticsearch路由機制介紹](https://blog.csdn.net/wwd0501/article/details/78109617)