一、背景

  分布式的集群通常包含非常多的机器,由于受到机架槽位和交换机网口的限制,通常大型的分布式集群都会跨好几个机架,由多个机架上的机器共同组成一个分布式集群。机架内的机器之间的网络速度通常都会高于跨机架机器之间的网络速度,并且机架之间机器的网络通信通常受到上层交换机间网络带宽的限制。

Hadoop在设计时考虑到数据的安全与高效,数据文件默认在HDFS上存放三份,存储策略为:

第一个block副本放在客户端所在的数据节点里(如果客户端不在集群范围内,则从整个集群中随机选择一个合适的数据节点来存放)。

第二个副本放置在与第一个副本所在节点相同机架内的其它数据节点上

第三个副本放置在不同机架的节点上

 

这样如果本地数据损坏,节点可以从同一机架内的相邻节点拿到数据,速度肯定比从跨机架节点上拿数据要快;
同时,如果整个机架的网络出现异常,也能保证在其它机架的节点上找到数据。
为了降低整体的带宽消耗和读取延时,HDFS会尽量让读取程序读取离它最近的副本。
如果在读取程序的同一个机架上有一个副本,那么就读取该副本。
如果一个HDFS集群跨越多个数据中心,那么客户端也将首先读本地数据中心的副本。
那么Hadoop是如何确定任意两个节点是位于同一机架,还是跨机架的呢?答案就是机架感知。

默认情况下,hadoop的机架感知是没有被启用的。所有的机器hadoop都默认在同一个默认的机架下,名为 “/default-rack”,这种情况下,任何一台datanode机器,不管物理上是否属于同一个机架,都会被认为是在同一个机架下,此时,就很容易出现增添机架间网络负载的情况。因为此时hadoop集群的HDFS在选机器的时候,是随机选择的,也就是说,
很有可能在写数据时,hadoop将第一块数据block1写到了rack1上,然后随机的选择下将block2写入到了rack2下,
此时两个rack之间产生了数据传输的流量,再接下来,在随机的情况下,又将block3重新又写回了rack1,此时,两个rack之间又产生了一次数据流量。

在job处理的数据量非常的大,或者往hadoop推送的数据量非常大的时候,这种情况会造成rack之间的网络流量成倍的上升,成为性能的瓶颈,
进而影响作业的性能以至于整个集群的服务。

 

二、配置

 

默认情况下,namenode启动时候日志是这样的:
 INFO org.apache.hadoop.net.NetworkTopology: Adding a new node: /default-rack/ 172.16.145.35:50010
每个IP 对应的机架ID都是 /default-rack ,说明hadoop的机架感知没有被启用。
要将hadoop机架感知的功能启用,配置非常简单,在 NameNode所在节点的/etc/hadoop/conf下的core-site.xml配置文件中配置一个选项:

<property> 
<name>topology.script.file.name</name> 
<value>/etc/hadoop/conf/RackAware.py</value> 
</property>

  


这个配置选项的value指定为一个可执行程序,通常为一个脚本,该脚本接受一个参数,输出一个值。
接受的参数通常为某台datanode机器的ip地址,而输出的值通常为该ip地址对应的datanode所在的rack,例如”/rack1”。
Namenode启动时,会判断该配置选项是否为空,如果非空,则表示已经启用机架感知的配置,此时namenode会根据配置寻找该脚本,
并在接收到每一个datanode的heartbeat时,将该datanode的ip地址作为参数传给该脚本运行,并将得到的输出作为该datanode所属的机架ID,保存到内存的一个map中.

至于脚本的编写,就需要将真实的网络拓朴和机架信息了解清楚后,通过该脚本能够将机器的ip地址和机器名正确的映射到相应的机架上去。
一个简单的实现如下:

#!/usr/bin/python
#-*-coding:UTF-8 -*-
import sys

rack = {"NN01":"rack2",
        "NN02":"rack3",
        "DN01":"rack4",
        "DN02":"rack4",
        "DN03":"rack1",
        "DN04":"rack3",
        "DN05":"rack1",
        "DN06":"rack4",
        "DN07":"rack1",
        "DN08":"rack2",
        "DN09":"rack1",
        "DN10":"rack2",
        "172.16.145.32":"rack2",
        "172.16.145.33":"rack3",
        "172.16.145.34":"rack4",
        "172.16.145.35":"rack4",
        "172.16.145.36":"rack1",
        "172.16.145.37":"rack3",
        "172.16.145.38":"rack1",
        "172.16.145.39":"rack4",
        "172.16.145.40":"rack1",
        "172.16.145.41":"rack2",
        "172.16.145.42":"rack1",
        "172.16.145.43":"rack2",
        }

if __name__=="__main__":
    print "/" + rack.get(sys.argv[1],"rack0")

  这样配置后,namenode启动时候日志是这样的:
 INFO org.apache.hadoop.net.NetworkTopology: Adding a new node: /rack4/ 172.16.145.35:50010
说明hadoop的机架感知已经被启用了。
查看HADOOP机架信息命令: hdfs  dfsadmin  -printTopology

[hadoop@NN01 hadoop-hdfs]$ hdfs  dfsadmin  -printTopology
Rack: /rack1
   172.16.145.36:50010 (DN03)
   172.16.145.38:50010 (DN05)
   172.16.145.40:50010 (DN07)
   172.16.145.42:50010 (DN09)
   172.16.145.44:50010 (DN11)
   172.16.145.54:50010 (DN17)
   172.16.145.56:50010 (DN19)
   172.16.145.58:50010 (DN21)

Rack: /rack2
   172.16.145.41:50010 (DN08)
   172.16.145.43:50010 (DN10)
   172.16.145.45:50010 (DN12)
   172.16.145.60:50010 (DN23)
   172.16.145.62:50010 (DN25)

Rack: /rack3
   172.16.145.37:50010 (DN04)
   172.16.145.51:50010 (DN14)
   172.16.145.53:50010 (DN16)
   172.16.145.55:50010 (DN18)
   172.16.145.57:50010 (DN20)

Rack: /rack4
   172.16.145.34:50010 (DN01)
   172.16.145.35:50010 (DN02)
   172.16.145.39:50010 (DN06)
   172.16.145.50:50010 (DN13)
   172.16.145.52:50010 (DN15)
   172.16.145.59:50010 (DN22)
   172.16.145.61:50010 (DN24)

 hdfs 三个副本的这种存放策略减少了机架间的数据传输,提高了写操作的效率。机架的错误远远比节点的错误少,所以这种策略不会影响到数据的可靠性和可用性。与此同时,因为数据块只存放在两个不同的机架上,所以此策略减少了读取数据时需要的网络传输总带宽。在这种策略下,副本并不是均匀的分布在不同的机架上:三分之一的副本在一个节点上,三分之二的副本在一个机架上,其它副本均匀分布在剩下的机架中,这种策略在不损害数据可靠性和读取性能的情况下改进了写的性能。

三、网络拓扑机器之间的距离

这里基于一个网络拓扑案例,介绍在复杂的网络拓扑中hadoop集群每台机器之间的距离

 

hadoop 机架感知 生效 hdfs 机架_hdfs 机架感知

有了机架感知,NameNode就可以画出上图所示的datanode网络拓扑图。D1,R1都是交换机,最底层是datanode。则H1的rackid=/D1/R1/H1,H1的parent是R1,R1的是D1。这些rackid信息可以通过topology.script.file.name配置。有了这些rackid信息就可以计算出任意两台datanode之间的距离。

distance(/D1/R1/H1,/D1/R1/H1)=0  相同的datanode
distance(/D1/R1/H1,/D1/R1/H2)=2  同一rack下的不同datanode
distance(/D1/R1/H1,/D1/R1/H4)=4  同一IDC下的不同datanode
distance(/D1/R1/H1,/D2/R3/H7)=6  不同IDC下的datanode

四、如何判断是否是合适的数据节点

上面说到如果客户端是数据节点,则会把正在写入的数据的一个副本保存在这个客户端的数据节点上。我们把它看做是本地节点。但是如果这个客户端上的数据节点空间不足或者是当前负载过重,则应该从该数据节点所在的机架中选择一个合适的数据节点作为此时这个数据块的本地节点。另外,如果客户端上没有一个数据节点的话,则从整个集群中随机选择一个合适的数据节点作为此时这个数据块的本地节点。那么,如何判定一个数据节点合不合适呢,通过查看源码知道它是通过isGoodTarget方法来确定的:

private boolean isGoodTarget(DatanodeStorageInfo storage,
                               long blockSize, int maxTargetPerRack,
                               boolean considerLoad,
                               List<DatanodeStorageInfo> results,
                               boolean avoidStaleNodes,
                               StorageType storageType) {
    if (storage.getStorageType() != storageType) {
      logNodeIsNotChosen(storage,
          "storage types do not match, where the expected storage type is "
              + storageType);
      return false;
    }
    if (storage.getState() == State.READ_ONLY_SHARED) {
      logNodeIsNotChosen(storage, "storage is read-only");
      return false;
    }
    DatanodeDescriptor node = storage.getDatanodeDescriptor();
    // check if the node is (being) decommissioned   //判断节点是否退役(不可用)
    if (node.isDecommissionInProgress() || node.isDecommissioned()) {
      logNodeIsNotChosen(storage, "the node is (being) decommissioned ");
      return false;
    }

    if (avoidStaleNodes) {
      if (node.isStale(this.staleInterval)) {
        logNodeIsNotChosen(storage, "the node is stale ");
        return false;
      }
    }
    
    final long requiredSize = blockSize * HdfsConstants.MIN_BLOCKS_FOR_WRITE;
    final long scheduledSize = blockSize * node.getBlocksScheduled();
	//节点磁盘剩余空间够不够
    if (requiredSize > storage.getRemaining() - scheduledSize) {
      logNodeIsNotChosen(storage, "the node does not have enough space ");
      return false;
    }

    // check the communication traffic of the target machine
    if (considerLoad) {
      final double maxLoad = 2.0 * stats.getInServiceXceiverAverage();
      final int nodeLoad = node.getXceiverCount();
	  //节点当前的负载情况
      if (nodeLoad > maxLoad) {
        logNodeIsNotChosen(storage,
            "the node is too busy (load:"+nodeLoad+" > "+maxLoad+") ");
        return false;
      }
    }
      
    // check if the target rack has chosen too many nodes
    String rackname = node.getNetworkLocation();
    int counter=1;
    for(DatanodeStorageInfo resultStorage : results) {
      if (rackname.equals(
          resultStorage.getDatanodeDescriptor().getNetworkLocation())) {
        counter++;
      }
    }
	// 该节点所在的机架被选择存放当前数据块副本的数据节点过多
    if (counter>maxTargetPerRack) {
      logNodeIsNotChosen(storage, "the rack has too many chosen nodes ");
      return false;
    }
    return true;
  }