前面几篇文章在代码层面介绍了elasticsearch内部是怎么实现shard split的,这篇文章主要回答两个问题:
- 为什么不能只split 一个shard
- 怎么保证split 后数据分布式一致的,即不用对原有的数据重新hash
首先,elasticsearch是通过hash的方式确定每个文档所属的分片的。
hash(docId) % numShards
其中docId表示某个文档的id,numShards表示索引有多少个shard。
那么问题来了,split后shard个数变了,每个文档hash后再对shard个数取模值肯定也不一样了。举个列子:假设原来有两个shard,有四篇文档,hash后的文档id分别为0,1,2,3。那么这四篇文档存入elasticsearch后会是这种情况
Sshard0: 0, 2
Sshard1: 1, 3
每个shard包含两个文档。如果现在split成四个shard,即源索引的每个shard split成目标索引的两个shard。那么目标shard 和源shard 对应的关系如下:
Tshard0 -> Sshard0
Tshard1 -> Sshard0
Tshard2 ->Sshard1
Tshard3 -> Sshard1
其中Tshard表示目标shard, Sshard表示源shard。因为从前几篇文章我们了解到选择目标shard的源shard算法为:
Sshard = Tshard / routingFactor
routingFactor = numTargetShard / numSourceShard
在这种情况下,routingFactor = 2。但这样split好像行不通啊!如果按哈希取模确定shard,那么文档1在目标索引中所属的shard应该为 1 % 4 = 1。分配情况如下:
Tshard0: 0
Tshard1: 1
Tshard: 2
Tshard: 3
即文档1应该在目标Tshard1中,而Tshard1又是通过Sshard0 split得到。但是Sshard0中并没有包含文档1。那么elasticsearch是怎么解决这个问题的?
还记得在在分析一种我们就讲过,如果要支持split有两个限制条件,其一就是在创建索引的时候必须指定number_of_routing_shards参数。那么这个参数具体有什么作用呢?
先看下背后生成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);
}
这个函数是生成shardId的算法,id参数就是文档id,routing参数是自定义路由的参数。这里可以看到如果自定义路由,将不会使用文档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();
}
这里发现对hash值取模后还除了一个routingFactor。而且不是对numShards取模,而是对routingNumShards取模。IndexMetaData.getRoutigNumShards返回的就是创建索引指定的number_of_routing_shards的值,而IndexMetaData.getRoutingFactor的值其实是routingNumShards / numberOfShards。
这里其实用的是一致性哈希算法,哈希空间由routingNumShards参数指定,如果创建索引的时候没有指定number_of_routing_shards参数,则routingNumShards的值就等于numberOfShards。所以也明白了为什么split必须要指定number_of_routing_shards参数,而且split最大次数受限于该参数。
现在反过来再看一次上面的例子,假设创建索引时指定了number_of_routing_shards等于8,shard数等于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。然后现在需要分裂,还是分裂成四个shard。注意,分裂时也有个routingFactor,要搞清两个routingFactor的区别。分裂后shard之间的对应关系还是没变。
Tshard0 -> Sshard0
Tshard1 -> Sshard0
Tshard2 ->Sshard1
Tshard3 -> Sshard1
分裂后目标索引的routingNumShards值等于源索引,所以也是8,单目标索引的shard数变量,最终目标索引分配情况如下:
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就没问题了。虽然解决了这个问题,但这种方法也就导致了在split的时候只能源索引的每个shard都参与split,而不支持单独某个shard split。因为在计算shardId的时候对取模的结果除了一个routingFactor,相当于每个shard平均分配hash空间里的每个桶,所以不能出现某个shard占的hash桶比较多的情况。