玩轉Elasticsearch routing功能

  • 2019 年 12 月 1 日
  • 筆記

Elasticsearch是一個搭建在Lucene搜索引擎庫基礎之上的搜索服務平台。它在單機的Lucene搜索引擎庫基礎之上增加了分佈式設計,translog等特性,增強了搜索引擎的性能,高可用性,高可擴性等。

Elasticsearch分佈式設計的基本思想是Elasticsearch集群由多個服務器節點組成,集群中的一個索引分為多個分片,每個分片可以分配在不同的節點上。其中每個分片都是一個單獨的功能完成的Lucene實例,可以獨立地進行寫入和查詢服務,ES中存儲的數據分佈在集群分片的一個或多個上,其結構簡單描述為下圖。

在上面的架構圖中,集群由於三個節點組成,每個節點上又與兩個分片,想要讀寫文檔就必須知道文檔被分配在哪個分片上,這也正是本文要講的routing功能的作用。

1. 工作原理

1.1 routing參數

routing參數是一個可選參數,默認使用文檔的_id值,可以用在INDEX, UPDATE,GET, SEARCH, DELETE等各種操作中。在寫入(包括更新)時,用於計算文檔所屬分片,在查詢(GET請求或指定了routing的查詢)中用於限制查詢範圍,提高查詢速度。

1.2 計算方法

ES中shardId的計算公式如下:

shardId = hash(_routing) % num_primary_shards

通過該公式可以保證使用相同routing的文檔被分配到同一個shard上,當然在默認情況下使用_id作為routing起到將文檔均勻分佈到多個分片上防止數據傾斜的作用。

1.3 routing_partition_size參數

使用了routing參數可以讓routing值相同的文檔分配到同一個分片上,從而減少查詢時需要查詢的shard數,提高查詢效率。但是使用該參數容易導致數據傾斜。為此,ES還提供了一個index.routing_partition_size參數(僅當使用routing參數時可用),用於將routing相同的文檔映射到集群分片的一個子集上,這樣一方面可以減少查詢的分片數,另一方面又可以在一定程度上防止數據傾斜。引入該參數後計算公式如下

shard_num = (hash(_routing) + hash(_id) % routing_partition_size) % num_primary_shards

1.4 源碼解讀

如下為計算文檔歸屬分片的源碼,從源碼中我們可以看到ES的哈希算法使用的是Murmur3,取模使用的是java的floorMod

version: 6.5  path: orgelasticsearchclusterroutingOperationRouting.java    public static int generateShardId(IndexMetaData indexMetaData, @Nullable String id, @Nullable String routing) {      final String effectiveRouting;      final int partitionOffset;        if (routing == null) {          assert(indexMetaData.isRoutingPartitionedIndex() == false) : "A routing value is required for gets from a partitioned index";          effectiveRouting = id; //默認使用id      } else {          effectiveRouting = routing;      }        if (indexMetaData.isRoutingPartitionedIndex()) {//使用了routing_partition_size參數          partitionOffset = Math.floorMod(Murmur3HashFunction.hash(id), indexMetaData.getRoutingPartitionSize());      } else {          // we would have still got 0 above but this check just saves us an unnecessary hash calculation          partitionOffset = 0;      }        return calculateScaledShardId(indexMetaData, effectiveRouting, partitionOffset);  }    private static int calculateScaledShardId(IndexMetaData indexMetaData, String effectiveRouting, int partitionOffset) {      final int hash = Murmur3HashFunction.hash(effectiveRouting) + partitionOffset;        // we don't use IMD#getNumberOfShards since the index might have been shrunk such that we need to use the size      // of original index to hash documents      return Math.floorMod(hash, indexMetaData.getRoutingNumShards()) / indexMetaData.getRoutingFactor();  }

2. 存在的問題及解決方案

2.1 數據傾斜

如前面所述,用戶使用自定義routing可以控制文檔的分配位置,從而達到將相似文檔放在同一個或同一批分片的目的,減少查詢時的分片個數,提高查詢速度。然而,這也意味着數據無法像默認情況那麼均勻的分配到各分片和各節點上,從而會導致各節點存儲和讀寫壓力分佈不均,影響系統的性能和穩定性。對此可以從以下兩個方面進行優化

  1. 使用routing_partition_size參數 如前面所述,該參數可以使routing相同的文檔分配到一批分片(集群分片的子集)而不是一個分片上,從而可以從一定程度上減輕數據傾斜的問題。該參數的效果與其值設置的大小有關,當該值等於number_of_shard時,routing將退化為與未指定一樣。當然該方法只能減輕數據傾斜,並不能徹底解決。
  2. 合理劃分數據和設置routing值 從前面的分析,我們可以得到文檔分片計算的公式,公式中的hash算法和取模算法也已經通過源碼獲取。因此用戶在劃分數據時,可以首先明確數據要劃分為幾類,每一類數據準備劃分到哪部分分片上,再結合分片計算公式計算出合理的routing值,當然也可以在routing參數設置之前設置一個自定義hash函數來實現,從而實現數據的均衡分配。
  3. routing前使用自定義hash函數 很多情況下,用戶並不能提前確定數據的分類值,為此可以在分類值和routing值之間設置一個hash函數,保證分類值散列後的值更均勻,使用該值作為routing,從而防止數據傾斜。

2.2 異常行為

ES的id去重是在分片維度進行的,之所以這樣做是ES因為默認情況下使用_id作為routing值,這樣id相同的文檔會被分配到相同的分片上,因此只需要在分片維度做id去重即可保證id的唯一性。

然而當使用了自定義routing後,id相同的文檔如果指定了不同的routing是可能被分配到不同的分片上的,從而導致同一個索引中出現兩個id一樣的文檔,這裡之所以說「可能」是因為如果不同的routing經過計算後仍然被映射到同一個分片上,去重還是可以生效的。因此這裡會出現一個不穩定的情況,即當對id相同routing不同的文檔進行寫入操作時,有的時候被更新,有的時候會生成兩個id相同的文檔,具體可以使用下面的操作復現

# 出現兩個id一樣的情況  POST _bulk  {"index":{"_index":"routing_test","_id":"123","routing":"abc"}}  {"name":"zhangsan","age":18}  {"index":{"_index":"routing_test","_id":"123","routing":"xyz"}}  {"name":"lisi","age":22}    GET routing_test/_search    # 相同id被更新的情況  POST _bulk  {"index":{"_index":"routing_test_2","_id":"123","routing":"123"}}  {"name":"zhangsan","age":18}  {"index":{"_index":"routing_test_2","_id":"123","routing":"123456"}}  {"name":"lisi","age":22}    GET routing_test_2/_search

以上測試場景在5.6.4, 6.4.3, 6.8.2集群上均驗證會出現,在7.2.1集群上沒有出現(可能是id去重邏輯發生了變化,這個後續研究一下後更新)。

對於這種場景,雖然在響應行為不一致,但是由於屬於未按正常使用方式使用(id相同的文檔應該使用相同的routing),也屬於可以理解的情況,官方文檔上也有對應描述, 參考地址

3. 常規用法

3.1 文檔劃分及routing指定

  • 明確文檔劃分 使用routing是為了讓查詢時有可能出現在相同結果集的文檔被分配到一個或一批分片上。因此首先要先明確哪些文檔應該被分配在一起,對於這些文檔使用相同的routing值,常規的一些自帶分類信息的文檔,如學生的班級屬性,產品的分類等都可以作為文檔劃分的依據。
  • 確定各類別的目標分片 當然這一步不是必須的,但是合理設置各類數據的目標分片,讓他們盡量均勻分配,可以防止數據傾斜。因此建議在使用前就明確哪一類數據準備分配在哪一個或一批分片上,然後通過計算給出這類文檔的合理routing值
  • routing分佈均勻 在很多場景下分類有哪些值不確定,因此無法明確劃分各類數據的分片歸屬並計算出routing值,對於這種情況,建議可以在routing之前增加一個hash函數,讓不同文檔分類的值通過哈希盡量散列得更均勻一些,從而保證數據分佈平衡。

3.2 routing的使用

  • 寫入操作 文檔的PUT, POST, BULK操作均支持routing參數,在請求中帶上routing=xxx即可。使用了routing值即可保證使用相同routing值的文檔被分配到一個或一批分片上。 GET my_index/_search { "query": { "terms": { "_routing": [ "user1" ] } } }
  • GET操作 對於使用了routing寫入的文檔,在GET時必須指定routing,否則可能導致404,這與GET的實現機制有關,GET請求會先根據routing找到對應的分片再獲取文檔,如果對寫入使用routing的文檔GET時沒有指定routing,那麼會默認使用id進行routing從而大概率無法獲得文檔。
  • 查詢操作 查詢操作可以在body中指定_routing參數(可以指定多個)來進行查詢。當然不指定_routing也是可以查詢出結果的,不過是遍歷所有的分片,指定了_routing後,查詢僅會對routing對應的一個或一批索引進行檢索,從而提高查詢效率,這也是很多用戶使用routing的主要目的,查詢操作示例如下:
  • UPDATE或DELETE操作 UPDATE或DELETE操作與GET操作類似,也是先根據routing確定分片,再進行更新或刪除操作,因此對於寫入使用了routing的文檔,必須指定routing,否則會報404響應。

3.3 設置routing為必選參數

從3.2的分析可以看出對於使用routing寫入的文檔,在進行GET,UPDATE或DELETE操作時如果不指定routing參數會出現異常。為此ES提供了一個索引mapping級別的設置,_routing.required, 來強制用戶在INDEX,GET, DELETE,UPDATA一個文檔時必須使用routing參數。當然查詢時不受該參數的限制的。該參數的設置方式如下:

PUT my_index  {    "mappings": {      "_doc": {        "_routing": {          "required": true        }      }    }  }

3.4 routing結合別名使用

別名功能支持設置routing, 如下:

POST /_aliases  {      "actions" : [          {              "add" : {                   "index" : "test",                   "alias" : "alias1",                   "routing" : "1"              }          }      ]  }

還支持查詢和寫入使用不同的routing,[詳情參考]

將routing和別名結合,可以對使用者屏蔽讀寫時使用routing的細節,降低誤操作的風險,提高操作的效率。

routing是ES中相對高階一些的用法,在用戶了解業務數據分佈和查詢需求的基礎之上,可以對查詢性能進行優化,然而使用不當會導致數據傾斜,重複ID等問題。本文介紹了routing的原理,問題及使用技巧,希望對大家有幫助,歡迎評論討論(微信公眾號無法評論,可以點擊原文進行評論)

歡迎關注公眾號Elastic慕容,和我一起進入Elastic的奇妙世界吧