前面幾篇文章在代碼層面介紹了elasticsearch內(nèi)部是怎么實(shí)現(xiàn)shard split的,這篇文章主要回答兩個(gè)問題:
- 為什么不能只split 一個(gè)shard
- 怎么保證split 后數(shù)據(jù)分布式一致的,即不用對(duì)原有的數(shù)據(jù)重新hash
首先,elasticsearch是通過hash的方式確定每個(gè)文檔所屬的分片的。
hash(docId) % numShards
其中docId表示某個(gè)文檔的id,numShards表示索引有多少個(gè)shard。
那么問題來了,split后shard個(gè)數(shù)變了,每個(gè)文檔hash后再對(duì)shard個(gè)數(shù)取模值肯定也不一樣了。舉個(gè)列子:假設(shè)原來有兩個(gè)shard,有四篇文檔,hash后的文檔id分別為0,1,2,3。那么這四篇文檔存入elasticsearch后會(huì)是這種情況
Sshard0: 0, 2
Sshard1: 1, 3
每個(gè)shard包含兩個(gè)文檔。如果現(xiàn)在split成四個(gè)shard,即源索引的每個(gè)shard split成目標(biāo)索引的兩個(gè)shard。那么目標(biāo)shard 和源shard 對(duì)應(yīng)的關(guān)系如下:
Tshard0 -> Sshard0
Tshard1 -> Sshard0
Tshard2 ->Sshard1
Tshard3 -> Sshard1
其中Tshard表示目標(biāo)shard, Sshard表示源shard。因?yàn)閺那皫灼恼挛覀兞私獾竭x擇目標(biāo)shard的源shard算法為:
Sshard = Tshard / routingFactor
routingFactor = numTargetShard / numSourceShard
在這種情況下,routingFactor = 2。但這樣split好像行不通?。∪绻垂H∧4_定shard,那么文檔1在目標(biāo)索引中所屬的shard應(yīng)該為 1 % 4 = 1。分配情況如下:
Tshard0: 0
Tshard1: 1
Tshard: 2
Tshard: 3
即文檔1應(yīng)該在目標(biāo)Tshard1中,而Tshard1又是通過Sshard0 split得到。但是Sshard0中并沒有包含文檔1。那么elasticsearch是怎么解決這個(gè)問題的?
還記得在在分析一種我們就講過,如果要支持split有兩個(gè)限制條件,其一就是在創(chuàng)建索引的時(shí)候必須指定number_of_routing_shards參數(shù)。那么這個(gè)參數(shù)具體有什么作用呢?
先看下背后生成shardId的算法
OperationRouting
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;
} else {
effectiveRouting = routing;
}
if (indexMetaData.isRoutingPartitionedIndex()) {
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);
}
這個(gè)函數(shù)是生成shardId的算法,id參數(shù)就是文檔id,routing參數(shù)是自定義路由的參數(shù)。這里可以看到如果自定義路由,將不會(huì)使用文檔id來確定shard。
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();
}
這里發(fā)現(xiàn)對(duì)hash值取模后還除了一個(gè)routingFactor。而且不是對(duì)numShards取模,而是對(duì)routingNumShards取模。IndexMetaData.getRoutigNumShards返回的就是創(chuàng)建索引指定的number_of_routing_shards的值,而IndexMetaData.getRoutingFactor的值其實(shí)是routingNumShards / numberOfShards。
這里其實(shí)用的是一致性哈希算法,哈希空間由routingNumShards參數(shù)指定,如果創(chuàng)建索引的時(shí)候沒有指定number_of_routing_shards參數(shù),則routingNumShards的值就等于numberOfShards。所以也明白了為什么split必須要指定number_of_routing_shards參數(shù),而且split最大次數(shù)受限于該參數(shù)。
現(xiàn)在反過來再看一次上面的例子,假設(shè)創(chuàng)建索引時(shí)指定了number_of_routing_shards等于8,shard數(shù)等于2,那么。那么分配情況如下:
routingNumShards=8,numberOfShards=2, routingFcator = 4
Sshard0: 0,1,2,3 (0 % 8)/ 4 = 0, (1%8) / 4 = 0, (2%8) / 4 = 0 , (3%8) / 4 = 0
Sshard1:
可以看到所有文檔都分配到了Sshard0。然后現(xiàn)在需要分裂,還是分裂成四個(gè)shard。注意,分裂時(shí)也有個(gè)routingFactor,要搞清兩個(gè)routingFactor的區(qū)別。分裂后shard之間的對(duì)應(yīng)關(guān)系還是沒變。
Tshard0 -> Sshard0
Tshard1 -> Sshard0
Tshard2 ->Sshard1
Tshard3 -> Sshard1
分裂后目標(biāo)索引的routingNumShards值等于源索引,所以也是8,單目標(biāo)索引的shard數(shù)變量,最終目標(biāo)索引分配情況如下:
routingNumShards=8,numberOfShards=4, routingFcator = 2
Tshard0: 0, 1 (0 % 8) / 2 = 0, (1 % 8) / 2 = 0
Tshard1: 2, 3 (2 % 8) / 2 = 1, (3 % 8 ) / 2 = 1
Tshard2:
Tshard3:
這樣,Sshard0 分裂成Tshard0, Tshard1就沒問題了。雖然解決了這個(gè)問題,但這種方法也就導(dǎo)致了在split的時(shí)候只能源索引的每個(gè)shard都參與split,而不支持單獨(dú)某個(gè)shard split。因?yàn)樵谟?jì)算shardId的時(shí)候?qū)θ∧5慕Y(jié)果除了一個(gè)routingFactor,相當(dāng)于每個(gè)shard平均分配hash空間里的每個(gè)桶,所以不能出現(xiàn)某個(gè)shard占的hash桶比較多的情況。