背景
HDFS集群规模日益扩大之后,集群中难免会出现一些“慢节点“,主要表现为网络数据传输变慢、磁盘读写变慢。平常这些慢节点很难被发现,只有当业务作业数据读写涉及到这些节点,导致作业运行时间延长,我们才会发现集群读写变慢了,进而去定位具体变慢的节点。
所以慢节点一直是HDFS集群运维中需重点关注的问题,在Hadoop2.9之后,社区支持了从Namenode jmx上查看慢节点的功能。
metrics格式如下,需要注意的是这里最多展示Top5个节点/磁盘:
"SlowPeersReport":[{
"SlowNode":"node4","ReportingNodes":["node1"]},{
"SlowNode":"node2","ReportingNodes":["node1","node3"]},{
"SlowNode":"node1","ReportingNodes":["node2"]}]
"SlowDisksReport":[{
"SlowDiskID":"dn3:disk1","Latencies":{
"WRITE":1000.1}},{
"SlowDiskID":"dn2:disk2","Latencies":{
"WRITE":1000.1}},{
"SlowDiskID":"dn1:disk2","Latencies":{
"READ":1000.3}},{
"SlowDiskID":"dn1:disk1","Latencies":{
"METADATA":1000.1,"READ":1000.8}}]
网络慢监控
原理
监控一个DN网络传输变慢的原理是,记录集群中各个DN间的packet数据传输耗时,找出其中的异常值并作为慢节点上报给NN。正常情况下各节点间的传输速率基本一致,不会相差太多,假如出现了A传B耗时异常,A就往NN上报B是慢节点。
为了计算DN往下游传数据的平均耗时,DN内部维护了一个Map<String, LinkedBlockingDeque<SumAndCount>>
,Map的key为下游DN的ip,value是一个存放SumAndCount对象的队列,SumAndCount对象用于记录往下游DN传输packet的数量与耗时。
DN在发送心跳的时候会判断是否需要生成SlowPeerReport,并将其作为心跳信息的一部分发送给NN。SlowPeerReport的生成周期由dfs.datanode.outliers.report.interval
参数控制,默认30min。首先从队列中取出所有packet传输耗时求平均值averageLatency,然后根据这些averageLatency,计算出慢节点上报阈值upperLimitLatency。当有节点的averageLatency大于upperLimitLatency,即认为该节点属于一个网络慢节点,且由DN1上报。最后生成对应的SlowPeerReport,通过心跳上报给NN。
慢节点阈值upperLimitLatency的计算逻辑
先算出所有下游DN传输耗时的中位数median,再算出中位数绝对偏差mad:
// MAD_MULTIPLIER = 1.4826
mad = median(|list[i]-median(list)|) * MAD_MULTIPLIER
最终upperLimitLatency为:
// lowThresholdMs = 5ms
upperLimitLatency = max(lowThresholdMs, median * 3, median + mad * 3)
代码详情如下:
org.apache.hadoop.hdfs.server.datanode.metrics.OutlierDetector.java
public Map<String, Double> getOutliers(Map<String, Double> stats) {
// minNumResources=10,节点少于10个不参与计算
if (stats.size() < minNumResources) {
LOG.debug("Skipping statistical outlier detection as we don't have " +
"latency data for enough resources. Have {}, need at least {}",
stats.size(), minNumResources);
return ImmutableMap.of();
}
final List<Double> sorted = new ArrayList<>(stats.values());
Collections.sort(sorted);
// 计算中位数median
final Double median = computeMedian(sorted);
// 计算中位数绝对偏差值mad
final Double mad = computeMad(sorted);
// 计算异常阈值upperLimitLatency
Double upperLimitLatency = Math.max(
lowThresholdMs, median * MEDIAN_MULTIPLIER);
upperLimitLatency = Math.max(
upperLimitLatency, median + (DEVIATION_MULTIPLIER * mad));
final Map<String, Double> slowResources = new HashMap<>();
// 找出大于异常阈值的节点
for (Map.Entry<String, Double> entry : stats.entrySet()) {
if (entry.getValue() > upperLimitLatency) {
slowResources.put(entry.getKey(), entry.getValue());
}
}
return slowResources;
}
public static Double computeMad(List<Double> sortedValues) {
...
// 计算出中位数
Double median = computeMedian(sortedValues);
List<Double> deviations = new ArrayList<>(sortedValues);
// 计算绝对偏差值
for (int i = 0; i < sortedValues.size(); ++i) {
deviations.set(i, Math.abs(sortedValues.get(i) - median));
}
Collections.sort(deviations);
// MAD_MULTIPLIER = 1.4826
return computeMedian(deviations) * MAD_MULTIPLIER;
}
public static Double computeMedian(List<Double> sortedValues) {
...
Double median = sortedValues.get(sortedValues.size() / 2);
if (sortedValues.size() % 2 == 0) {
median += sortedValues.get((sortedValues.size() / 2) - 1);
median /= 2