【精】HDFS基于磁盘ioutil的选盘策略实现
一、HDFS现有的选盘策略
截止到Hadoop-3.3.1版本,目前HDFS写数据的选盘策略有两种,分别是:RoundRobinVolumeChoosingPolicy
和AvailableSpaceVolumeChoosingPolicy
。
由配置项dfs.datanode.fsdataset.volume.choosing.policy
指定使用的选盘策略,默认是RoundRobinVolumeChoosingPolicy
策略。
RoundRobinVolumeChoosingPolicy
策略思路比较简单,就是用轮询方式依次选择每个volume。
AvailableSpaceVolumeChoosingPolicy
策略考虑了磁盘的剩余可用空间。在可选磁盘列表之中所有的磁盘剩余可用空间相差在配置的某个阈值(默认10GB)内时,说明此时各个磁盘之间数据量比较均匀,于是退化成RoundRobinVolumeChoosingPolicy
。如果相差大于配置的阈值时,则按照一定的概率(实际中一般是大于0.5,小于1)优先选择剩余可用空间大的volume。
二、基于ioutil的选盘策略
设计思路 :
我们在AvailableSpaceVolumeChoosingPolicy
策略的基础上,添加了可插拔的ioutil选盘。具体来讲就是,提供一个配置项用来控制是否启用ioutil选盘策略。如果开启了ioutil选盘策略的话,那我们会在原有逻辑的基础上,再对磁盘列表依据ioutil进行一次过滤,选出ioutil最小的磁盘去写数据。如下图:
接下来,我们来看看一些实现细节。
2.1 如何计算ioutil?
看上面的功能图,DiskIOUtilManager类的作用是用来计算磁盘的ioutil,获得指定磁盘的ioutil。计算磁盘的ioutil的工作主要由DiskIOUtilManager类中的diskStatThread线程去做。
我们知道在linux下,可以通过iostat命令查看磁盘的io情况,iostat命令的最后一列输出就是ioutil。那怎么用java代码计算得到ioutil呢?我们这里参考了iostat工具的源码。
iostat的源码中从/proc/diskstats文件中读取磁盘信息,然后进行计算。
我们先来看一下/proc/diskstats文件中的内容是什么?
# cat /proc/diskstats
1 0 ram0 0 0 0 0 0 0 0 0 0 0 0
1 1 ram1 0 0 0 0 0 0 0 0 0 0 0
1 2 ram2 0 0 0 0 0 0 0 0 0 0 0
1 3 ram3 0 0 0 0 0 0 0 0 0 0 0
1 4 ram4 0 0 0 0 0 0 0 0 0 0 0
1 5 ram5 0 0 0 0 0 0 0 0 0 0 0
1 6 ram6 0 0 0 0 0 0 0 0 0 0 0
1 7 ram7 0 0 0 0 0 0 0 0 0 0 0
1 8 ram8 0 0 0 0 0 0 0 0 0 0 0
1 9 ram9 0 0 0 0 0 0 0 0 0 0 0
1 10 ram10 0 0 0 0 0 0 0 0 0 0 0
1 11 ram11 0 0 0 0 0 0 0 0 0 0 0
1 12 ram12 0 0 0 0 0 0 0 0 0 0 0
1 13 ram13 0 0 0 0 0 0 0 0 0 0 0
1 14 ram14 0 0 0 0 0 0 0 0 0 0 0
1 15 ram15 0 0 0 0 0 0 0 0 0 0 0
7 0 loop0 0 0 0 0 0 0 0 0 0 0 0
7 1 loop1 0 0 0 0 0 0 0 0 0 0 0
7 2 loop2 0 0 0 0 0 0 0 0 0 0 0
7 3 loop3 0 0 0 0 0 0 0 0 0 0 0
7 4 loop4 0 0 0 0 0 0 0 0 0 0 0
7 5 loop5 0 0 0 0 0 0 0 0 0 0 0
7 6 loop6 0 0 0 0 0 0 0 0 0 0 0
7 7 loop7 0 0 0 0 0 0 0 0 0 0 0
202 0 xvda 88773 1108 2010114 551529 31249889 47896636 633188176 338606000 0 56198497 339137808
202 1 xvda1 88624 1108 2008922 551388 31249889 47896636 633188176 338606000 0 56198357 339137671
202 16 xvdb 329 484 2628 305 0 0 0 0 0 304 304
202 17 xvdb1 302 484 2412 296 0 0 0 0 0 296 296
一共14列。每一列的含义如下:
列数 | 含义 |
---|---|
1 | 主设备号 |
2 | 次设备号 |
3 | 设备名称 |
4 | 成功完成读的总次数 |
5 | 合并读次数 |
6 | 读扇区的次数 |
7 | 读花的时间 |
8 | 成功完成写的总次数 |
9 | 合并写次数 |
10 | 写扇区的次数 |
11 | 写花的时间 |
12 | 当前处理的I/O个数 |
13 | 输入输出花的时间(ms) |
14 | 输入/输出操作花费的加权毫秒数 |
可以参考内核文档:
The /proc/diskstats file displays the I/O statistics of block devices. Each line contains the following 14 fields:
1 - major number
2 - minor mumber
3 - device name
4 - reads completed successfully
5 - reads merged
6 - sectors read
7 - time spent reading (ms)
8 - writes completed
9 - writes merged
10 - sectors written
11 - time spent writing (ms)
12 - I/Os currently in progress
13 - time spent doing I/Os (ms)
14 - weighted time spent doing I/Os (ms)
除了第12个字段之外,其他的关于I/O的值都是累加值。
ioutil的计算公式是:两次采集的I/O操作花费的毫秒数之差 / 采集间隔时间。
因此我们就可以通过两次采样的第13列的值相减/两次采样的时间间隔来计算磁盘的ioutil值。
计算出磁盘的ioutil值之后,以第三列设备名为key,添加到Map里,留待后续使用。
二、根据ioutil选盘
解决了磁盘ioutil值的获取问题之后,下一步就是考虑怎么使用这个值来指导datanode选盘写数据。
在DataNode启动的时候,设置好DiskIOUtilManager的storageLocations之后,启动DiskIOUtilManager线程,其中StorageLocation类保存了一个存储目录的URI和Storage Type(SSD啊,还是DISK啊,还是RAM啊等)。核心代码片段如下:
void startDataNode(List<StorageLocation> dataDirectories,
SecureResources resources
) throws IOException {
synchronized (this) {
if (diskIOUtilManager != null) {
// 设置好storageLocations
this.diskIOUtilManager.setStorageLocations(dataDirectories);
// 启动DiskIOUtilManager线程
this.diskIOUtilManager.start();
}
}
}
通过这个StorageLocation对象,我们可以拿到目录的path,通过目录的path我们可以拿到目录对应的设备名(例如/dev/sda、/dev/sdb这种的),然后建立设备名和IOStat对象(存储ioutil值的包装对象)的映射。
启动diskIOUtilManager线程后,根据我们的默认配置,会每隔1s钟进行一次采集,计算一次ioutil值。更新Map数据结构中IOStat对象的值。
在做choose volume的时候,我们会在原有的AvailableSpaceVolumeChoosingPolicy类的算法基础上同时考虑ioutil。算法如下:
一、如果volume列表中的所有volume的可用空间相差小于配置的阈值(10GB),说明磁盘之间数据相对来说还算均衡,则:
1.1 启用IOUtil选盘策略的情况下,返回ioutil最小的volume。如果两个volume的ioutil相等,则返回剩余可用空间最大的那个volume
1.2 未启用IOUtil选盘策略的情况下,退化成roundRobinPolicy轮询的选盘策略。
二、如果volume列表中的所有volume的可用空间相差大于配置的阈值(10GB),说明磁盘之间数据相对来说不是特别均衡。则:
根据配置项:dfs.datanode.available-space-volume-choosing-policy.balanced-space-preference-fraction计算出一个float数scaledPreferencePercent(后文会补充这个配置项的作用)
2.1
如果随机数小于scaledPreferencePercent,开启了ioutil功能,则从剩余空间多的volume里选择ioutil小的磁盘,如果没开启ioutil功能,则在剩余空间多的volume里使用round robin选盘策略。
2.2
如果随机数大于scaledPreferencePercent,开启了ioutil功能,则从剩余空间少的volume里选择ioutil小的磁盘,如果没开启ioutil功能,则在剩余空间少的volume里使用round robin选盘策略。
补充两点内容:
我们自己定义的两个磁盘的排序比较算法:如果ioutil不相等,则优先返回ioutil比较小的volume。如果ioutil相等的话,那就根据磁盘剩余可用空间排序,优先返回可用空间大的磁盘。
@Override
public int compareTo(IOUtilVolumePair o) {
if (this.ioUtil != o.ioUtil) {
return this.ioUtil - o.ioUtil;
} else {
if (this.availableSpace < o.availableSpace) {
return 1;
} else if (this.availableSpace == o.availableSpace) {
return 0;
} else {
return -1;
}
}
}
计算scaledPreferencePercent时,有这样几句代码:
float preferencePercentScaler =
(highAvailableVolumes.size() * balancedPreferencePercent) +
(lowAvailableVolumes.size() * (1 - balancedPreferencePercent));
float scaledPreferencePercent =
(highAvailableVolumes.size() * balancedPreferencePercent) /
preferencePercentScaler;
其中balancedPreferencePercent是由配置项:dfs.datanode.available-space-volume-choosing-policy.balanced-space-preference-fraction
配置的,默认值是0.75f。
这个配置项仅当“dfs.datanode.fsdataset.volume.choosing.policy”设置为“org.apache.hadoop.hdfs.server.datanode.fsdataset.AvailableSpaceVolumeChoosingPolicy”时生效。这个配置项用来控制新块分配将被发送到具有比其他磁盘可用空间更多的磁盘的百分比。以默认值0.75f举例,假设分配了100个新块,那么理论上会有75个块发送到可用空间多的磁盘上。为什么是理论上呢?因为这个值是一个概率,样本够大的时候是符合这个概率的。
这个设置应该在0.0 - 1.0的范围内,但实际上是0.5 - 1.0,因为没有理由让可用磁盘空间更少的卷获得更多的块分配。
我们已将此功能的代码提交到社区。参考链接:https://github.com/apache/hadoop/pull/3960
参考资料:
HDFS-16446. Consider ioutils of disk when choosing volume