elasticsearch之Routing


前言

當索引一個文檔的時候,文檔會被存儲到一個主分片中。那么,elasticsearch如何知道一個文檔應該存放到哪個分片中呢?

首先這肯定不是隨機的,否則在檢索文檔時就不知道該從哪去尋找它了。實際上這個過程是根據下面公式決定的:

shard = hash(routing) % number_of_primary_shards

routing是一個可變值,默認是文檔的_id,也可以是自定義的值。hash函數將routing值哈希后生成一個數字,然后這個數字再除以number_of_primary_shards(主分片的數量)得到余數,這個分布在0number_of_primary_shards減一(計數從0開始,比如5個主分片,那么范圍就是0~4)之間的余數,就是文檔存放的分片位置。
比如一篇文檔的id為123,那么它就應該存在:

>>> hash(123) % 5
3

這篇文檔就存在P3主分片上。

這也就解釋了為什么在創建索引時,主分片的數量一經定義就不能改變,因為如果數量變化了,那么之前所有的路由(routing)值都會無效,文檔就再也找不到了。

一般的,elasticsearch的默認路由算法都會根據文檔的id值作為依據將其哈希到相應的主分片上,該算法基本上會將所有的文檔平均分布在所有的主分片上,而不會產生某個分片數據過大而導致集群不平衡的情況。
那么我們在向一個有100個主分片的索引發送查詢某篇文檔的請求時,該請求發送到集群,集群干了什么呢?

  • 這個請求會被集群交給主節點。
  • 主節點接收這個請求后,將這個查詢請求廣播到這個索引的每個分片上(包含主、復制分片)。
  • 每個分片執行這個搜索請求,並將結果返回。
  • 結果在主節點上合並、排序后返回給用戶。

這里面就有些問題了。因為在存儲文檔時,通過hash算法將文檔平均分布在各分片上,這就導致了elasticsearch也不確定文檔的位置,所以它必須將這個請求廣播到所有的分片上去執行。
為了避免不必要的查詢,我們使用自定義的路由模式,這樣可以使我們的查詢更具目的性。比如之前的查詢是這樣的:

請求來了,你們(索引下的所有分片)都要檢查一下自己是否有符合條件的文檔

當能自定義路由后的查詢變成了:

請求來了,分片3、5你倆把文檔給我返回

自定義路由

所有的文檔 API( getindexdeletebulkupdate 以及 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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM