
正如我在 ES shard allocation bug 中提到的一样,当单个 ES 数据节点的 data.path 中的目录数超过 20 个时,会导致同一个索引分配到该节点的多个 shard 全部存储在某个目录下,造成磁盘 io 的倾斜,写入性能产生瓶颈。该问题存在于 ES 的各个版本中。本文针对 7.17 版本的 ES 代码进行分析
ES 数据节点上 shard 对应的 path 分配算法源码如下:
public static ShardPath selectNewPathForShard(
NodeEnvironment env,
ShardId shardId,
IndexSettings indexSettings,
long avgShardSizeInBytes,
Map<Path, Integer> dataPathToShardCount
) throws IOException {
final Path dataPath;
final Path statePath;
if (indexSettings.hasCustomDataPath()) {
dataPath = env.resolveCustomLocation(indexSettings.customDataPath(), shardId);
statePath = env.dataPaths()[0].resolve(shardId);
} else {
// 计算所有 path 对应的剩余可用空间总和。
BigInteger totFreeSpace = BigInteger.ZERO;
for (NodeEnvironment.DataPath nodeDataPath : env.dataPaths()) {
totFreeSpace = totFreeSpace.add(BigInteger.valueOf(nodeDataPath.fileStore.getUsableSpace()));
}
// 预估当前 shard 大小 = max(总可用磁盘空间/20,当前集群平均 shard 大小)
BigInteger estShardSizeInBytes = BigInteger.valueOf(avgShardSizeInBytes).max(totFreeSpace.divide(BigInteger.valueOf(20)));
final NodeEnvironment.DataPath[] paths = env.dataPaths();
// 取剩余磁盘空间最大的 path 作为初始路径
NodeEnvironment.DataPath bestPath = getPathWithMostFreeSpace(env);
if (paths.length != 1) {
// 获取该索引在每个 path 下已分配的 shard 数的对应关系
Map<NodeEnvironment.DataPath, Long> pathToShardCount = env.shardCountPerPath(shardId.getIndex());
// Compute how much space there is on each path
final Map<NodeEnvironment.DataPath, BigInteger> pathsToSpace = new HashMap<>(paths.length);
// 重新计算各 path 对应的可用空间
for (NodeEnvironment.DataPath nodePath : paths) {
FileStore fileStore = nodePath.fileStore;
BigInteger usableBytes = BigInteger.valueOf(fileStore.getUsableSpace());
pathsToSpace.put(nodePath, usableBytes);
}
bestPath = Arrays.stream(paths)
// 注意:这里很重要,过滤去剩余磁盘空间 > estShardSizeInBytes 对应的 path。
// 由于 estShardSizeInBytes = 总的可用磁盘空间 * %5
.filter((path) -> pathsToSpace.get(path).subtract(estShardSizeInBytes).compareTo(BigInteger.ZERO) > 0)
// 将上一步筛选出来的 path 再根据已分配的当前索引 shard 个数按升序排序
.sorted((p1, p2) -> {
int cmp = Long.compare(pathToShardCount.getOrDefault(p1, 0L), pathToShardCount.getOrDefault(p2, 0L));
if (cmp == 0) {
cmp = Integer.compare(
dataPathToShardCount.getOrDefault(p1.path, 0),
dataPathToShardCount.getOrDefault(p2.path, 0)
);
if (cmp == 0) {
cmp = pathsToSpace.get(p2).compareTo(pathsToSpace.get(p1));
}
}
return cmp;
})
// 取 排序完成后的 path 列表中的第一个。这里相当于将同一个索引的shard,按 path 均匀打散
.findFirst()
// 如果没有,则直接使用初始的 bestPath
.orElse(bestPath);
}
statePath = bestPath.resolve(shardId);
dataPath = statePath;
}
return new ShardPath(indexSettings.hasCustomDataPath(), dataPath, statePath, shardId);
}从上面的源码可知,es shard 在 data node 节点的 path 分配算法如下:
BigInteger totFreeSpace = BigInteger.ZERO;
for (NodeEnvironment.DataPath nodeDataPath : env.dataPaths()) {
totFreeSpace = totFreeSpace.add(BigInteger.valueOf(nodeDataPath.fileStore.getUsableSpace()));
}BigInteger estShardSizeInBytes = BigInteger.valueOf(avgShardSizeInBytes).max(totFreeSpace.divide(BigInteger.valueOf(20)));NodeEnvironment.DataPath bestPath = getPathWithMostFreeSpace(env);Map<NodeEnvironment.DataPath, Long> pathToShardCount = env.shardCountPerPath(shardId.getIndex());final Map<NodeEnvironment.DataPath, BigInteger> pathsToSpace = new HashMap<>(paths.length);
for (NodeEnvironment.DataPath nodePath : paths) {
FileStore fileStore = nodePath.fileStore;
BigInteger usableBytes = BigInteger.valueOf(fileStore.getUsableSpace());
pathsToSpace.put(nodePath, usableBytes);
}bestPath = Arrays.stream(paths)
// 注意:这里很重要,过滤去剩余磁盘空间 > estShardSizeInBytes 对应的 path。
// 由于 estShardSizeInBytes = 总的可用磁盘空间 * %5
.filter((path) -> pathsToSpace.get(path).subtract(estShardSizeInBytes).compareTo(BigInteger.ZERO) > 0)
// 将上一步筛选出来的 path 再根据已分配的当前索引 shard 个数按升序排序
.sorted((p1, p2) -> {
int cmp = Long.compare(pathToShardCount.getOrDefault(p1, 0L), pathToShardCount.getOrDefault(p2, 0L));
if (cmp == 0) {
// if the number of shards is equal, tie-break with the number of total shards
cmp = Integer.compare(
dataPathToShardCount.getOrDefault(p1.path, 0),
dataPathToShardCount.getOrDefault(p2.path, 0)
);
if (cmp == 0) {
// if the number of shards is equal, tie-break with the usable bytes
cmp = pathsToSpace.get(p2).compareTo(pathsToSpace.get(p1));
}
}
return cmp;
})
// 取 排序完成后的 path 列表中的第一个。这里相当于将同一个索引的shard,按 path 均匀打散
.findFirst()
// 如果没有,则直接使用初始的 bestPath
.orElse(bestPath);原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。