数据节点 DataNode 在运行中会与三种对端有互动。第一种是 NameNode ,如前所述,对于数据块的存储地点,虽然最初是由 NameNode 分配和指定的,但相关的信息最终来自DataNode 的报告。第二种是用户的 App (包括 Shell ),用户的 App 可以存在于集群内的任何节点上,不过那是在独立的 JVM 上,即使与 DataNode 同在一个节点上也互相独立;然而真正把数据存储在 DataNode 上或从 DataNode 读取数据的却是 App (或 Shell )。第三种是集群中别的 DataNode ,就是说 DataNode 与 DataNode 之间也会有通信和互动,这主要来自数据块复份 replica 的传输和转储。

hadoop Hue 管理节点 hadoop 数据节点_数据块

1. 发起通信连接

hadoop-hdfs\src\main\java\org\apache\hadoop\hdfs\server\datanode\DataNode.java

DataNode 与 NameNode 之 间 基 本 的 通 信 手 段 就 是 RPC 。

void startDataNode(Configuration conf, List<StorageLocation> dataDirs, SecureResources resources )  {
    //建立基于 ProtoBuf 的 RPC 层通信机制
    initIpcServer(conf);
    //创建本地的 BlockPoolManager 对象
    blockPoolManager = new BlockPoolManager(this);
    //建立或重建与 NameNode 的连接
    blockPoolManager.refreshNamenodes(conf);
}

hadoop-hdfs\src\main\java\org\apache\hadoop\hdfs\server\datanode\BlockPoolManager.java

通常一个集群中只有一种 nameservice ,但是如果把集群配置成联邦(federation)模式,那就可以有多个 nameservice

private void doRefreshNamenodes(
      Map<String, Map<String, InetSocketAddress>> addrMap,
      Map<String, Map<String, InetSocketAddress>> lifelineAddrMap) {
   

    Set<String> toRefresh = Sets.newLinkedHashSet();
    Set<String> toAdd = Sets.newLinkedHashSet();
    Set<String> toRemove;
    
    synchronized (this) {
     
      for (String nameserviceId : addrMap.keySet()) {
        if (bpByNameserviceId.containsKey(nameserviceId)) {
          //原来就有连接的是 toRefresh
          toRefresh.add(nameserviceId);
        } else {
          //原来没有连接的就是 toAdd
          toAdd.add(nameserviceId);
        }
      }
        //对于需要新建连接的每个 nameservice 
        for (String nsToAdd : toAdd) {
          Map<String, InetSocketAddress> nnIdToAddr = addrMap.get(nsToAdd);
          Map<String, InetSocketAddress> nnIdToLifelineAddr =
              lifelineAddrMap.get(nsToAdd);
          ArrayList<InetSocketAddress> addrs =
              Lists.newArrayListWithCapacity(nnIdToAddr.size());
          ArrayList<InetSocketAddress> lifelineAddrs =
              Lists.newArrayListWithCapacity(nnIdToAddr.size());
          for (String nnId : nnIdToAddr.keySet()) {
            addrs.add(nnIdToAddr.get(nnId));
            lifelineAddrs.add(nnIdToLifelineAddr != null ?
                nnIdToLifelineAddr.get(nnId) : null);
          }
          //创建一个专门为其服务的 BPOfferService 对象, 对每个NameNode创建BPServiceActor
          BPOfferService bpos = createBPOS(nsToAdd, addrs, lifelineAddrs);
          bpByNameserviceId.put(nsToAdd, bpos);
          offerServices.add(bpos);
        }
      }
      //以具体用户的身份启动各个 BPServiceActor 对象
      startAll(); 
    }
  }

所以在每个 BPOfferService 内部要为这个 nameservice 的每个 NameNode 都建立连接,并为之创建一个 BPServiceActor 线程专门绑定于这个特定的 NameNode 。总之,对于集群中的每个 NameNode ,每个 DataNode 上都有一个 BPServiceActor 线程专门为其服务。

class BPOfferService {

	 NamespaceInfo bpNSInfo; // 关于目标 Namespace 即 nameservice 的信息
	 volatile DatanodeRegistration bpRegistration; //本节点向该 nameservice 的登记信息
	 private final DataNode dn; //所在节点
	 private BPServiceActor bpServiceToActive = null;//通向当前活跃 NameNode 的 BPServiceActor
	 private final List<BPServiceActor> bpServices //属于同一 nameservice 的所有 BPServiceActor

}
class BPServiceActor implements Runnable {

   InetSocketAddress nnAddr //所连接的 NameNode 的地址
   BPOfferService bpos //所属的 BPOfferService
   DatanodeProtocolClientSideTranslatorPB bpNamenode  // NameNode 的远程代理 proxy
   //有待向 NameNode 报告的存储变化, IBR 为增量块报告之意
   Map<DatanodeStorage , PerStoragePendingIncrementalBR> pendingIncrementalBRperStorage
   DataNode dn //所在的 DataNode
   DatanodeRegistration bpRegistration  //本节点向该 NameNode 的登记信息

   @Override
   public void run() {
    LOG.info(this + " starting to offer service");

    try {
      while (true) {
        // init stuff
        try {
          // 握手连接
          connectToNNAndHandshake();
          break;
        }
      }

      while (shouldRun()) {
        try {
          //向 NameNode 发送心跳和报告,并执行 NameNode 发回的命令
          offerService();
        }
      }
      runningState = RunningState.EXITED;
    }
  }
  
}

2. 发送心跳报告

private void offerService() throws Exception {
    long fullBlockReportLeaseId = 0;
    while (shouldRun()) {
      try {
        final long startTime = scheduler.monotonicNow();
        final boolean sendHeartbeat = scheduler.isHeartbeatDue(startTime);
        HeartbeatResponse resp = null;
        if (sendHeartbeat) {
          boolean requestBlockReportLease = (fullBlockReportLeaseId == 0) &&
                  scheduler.isBlockReportDue(startTime);
          if (!dn.areHeartbeatsDisabledForTests()) {
            resp = sendHeartBeat(requestBlockReportLease);
          }
        }
         if ((fullBlockReportLeaseId != 0) || forceFullBr) {
          //发送数据块报告
          cmds = blockReport(fullBlockReportLeaseId);
          fullBlockReportLeaseId = 0;
        }
    } // while (shouldRun())
  } // offerService
HeartbeatResponse sendHeartBeat(boolean requestBlockReportLease) {
    scheduler.scheduleNextHeartbeat();
    //从 FsVolumeImpl 中提取信息, 填写 StorageReport 数组 reports ,这个数组是心跳报文的核心
    StorageReport[] reports =
        dn.getFSDataset().getStorageReports(bpos.getBlockPoolId());
    scheduler.updateLastHeartbeatTime(monotonicNow());
    VolumeFailureSummary volumeFailureSummary = dn.getFSDataset()
        .getVolumeFailureSummary();
    int numFailedVolumes = volumeFailureSummary != null ?
        volumeFailureSummary.getFailedStorageLocations().length : 0;
    //通过 PB 层发送心跳报文,实质上是对 NameNode 的 RPC 调用
    //对应的报文为 : DatanodeProtocolServerSideTranslatorPB
    return bpNamenode.sendHeartbeat(bpRegistration,
        reports,
        dn.getFSDataset().getCacheCapacity(),
        dn.getFSDataset().getCacheUsed(),
        dn.getXmitsInProgress(),
        dn.getXceiverCount(),
        numFailedVolumes,
        volumeFailureSummary,
        requestBlockReportLease);
  }

NameNode端的real

public class NameNodeRpcServer implements NamenodeProtocols {
  public HeartbeatResponse sendHeartbeat(DatanodeRegistration nodeReg,
      StorageReport[] report, long dnCacheCapacity, long dnCacheUsed,
      int xmitsInProgress, int xceiverCount,
      int failedVolumes, VolumeFailureSummary volumeFailureSummary,
      boolean requestFullBlockReportLease) throws IOException {
    checkNNStartup();
    verifyRequest(nodeReg); //验证心跳来源的有效性
    //由 DatanodeManager 加以处理,并返回一组对于 DataNode 的命令
    //创建一个响应报文,将命令搭载在上面,返回后由 PB 层发回 DataNode
    return namesystem.handleHeartbeat(nodeReg, report,
        dnCacheCapacity, dnCacheUsed, xceiverCount, xmitsInProgress,
        failedVolumes, volumeFailureSummary, requestFullBlockReportLease);
  }
}

返回的报文由DatanodeManager处理

public class DatanodeManager {
  public DatanodeCommand[] handleHeartbeat(......) throws IOException {
      //更新心跳, DataNode 已经登记,根据心跳报告更新有关具体 DataNode 的种种信息和统计
      //根据心跳报告的内容更新关于该 DataNode 的记载
      //设置缓存,更新该存储设备的容量信息
      heartbeatManager.updateHeartbeat(nodeinfo, reports,cacheCapacity, cacheUsed,
                                         xceiverCount, failedVolumes,
                                         volumeFailureSummary);
  }
}

3. 发送数据块报告

块报告 blockReport ,内容来源于 FsDatasetImpl.volumes, FsDatasetImpl 中的 volumes 是个FsVolumeList ,其中按本节点上不同的文件卷记载着每个数据块复份的 ReplicaInfo。

List<DatanodeCommand> blockReport(long fullBrLeaseId) throws IOException {
 
    // 收集数据块报告
    int i = 0;
    int totalBlockCount = 0;
    StorageBlockReport reports[] =
        new StorageBlockReport[perVolumeBlockLists.size()];

    for(Map.Entry<DatanodeStorage, BlockListAsLongs> kvPair : perVolumeBlockLists.entrySet()) {
      BlockListAsLongs blockList = kvPair.getValue();
      reports[i++] = new StorageBlockReport(kvPair.getKey(), blockList);
      totalBlockCount += blockList.getNumberOfBlocks();
    }

    // Send the reports to the NN.
    int numReportsSent = 0;
    int numRPCs = 0;
    boolean success = false;
    long brSendStartTime = monotonicNow();
    long reportId = generateUniqueBlockReportId();
    try {
      if (totalBlockCount < dnConf.blockReportSplitThreshold) {
        // Below split threshold, send all reports in a single message.
        DatanodeCommand cmd = bpNamenode.blockReport(
            bpRegistration, bpos.getBlockPoolId(), reports,
              new BlockReportContext(1, 0, reportId, fullBrLeaseId));
        numRPCs = 1;
        numReportsSent = reports.length;
        if (cmd != null) {
          cmds.add(cmd);
        }
      } else {
        // Send one block report per message.
        for (int r = 0; r < reports.length; r++) {
          StorageBlockReport singleReport[] = { reports[r] };
          //通过 NameNode 的 proxy 发送 RPC 请求,调用 NameNode 上的 blockReport ()
          DatanodeCommand cmd = bpNamenode.blockReport(
              bpRegistration, bpos.getBlockPoolId(), singleReport,
              new BlockReportContext(reports.length, r, reportId,
                  fullBrLeaseId));
          numReportsSent++;
          numRPCs++;
          if (cmd != null) {
            cmds.add(cmd);
          }
        }
      }
      success = true;
    } 
    //安排下次提供块报告的时间
    scheduler.updateLastBlockReportTime(monotonicNow());
    scheduler.scheduleNextBlockReport();
    return cmds.size() == 0 ? null : cmds;
  }

NameNode 上 出 面 接 受 和 处 理 心 跳 的 是 HeartbeatManager ,对 于 blockReport 则 是BlockManager ,这里调用的是 BlockManager. processReport ()。返回后由BPOfferService.processCommandFromActive ()来处理。NameNode 指定将一个块的若干复份( replica )分别存储在什么节点上,但是它本身不会把数据块发送给 DataNode 。实际上 NameNode 根本就不过手任何数据块,它只是处理元数据。把数据块发送给 DataNode 的是用户的 App 或 Shell ,还有就是 DataNode 之间的互相发送。

4. 数据传输

NameNode根本不处理实际的数据块,NomeNode提供了一个统一抽象的文件系统框架来管理来自不同文件系统的数据。用户通过APP或Shell将数据发送给DataNode,然后DataNode与DataNode之间通过一对 Xceiver来收发数据块,前面文章中提到的DataNode的成分DataXceiverServer就是用于数据收发。 DataNode 的数据收发服务,与其连接的请求只能来自集群中别的DataNode 或 App ,而不可能是 NameNode。

E:\hadoop-2.8.5\hadoop-hdfs-project\hadoop-hdfs\src\main\java\org\apache\hadoop\hdfs\protocol\datatransfer\Receiver.java

Receiver 实现了RPC数据传输通讯协议

public abstract class Receiver implements DataTransferProtocol {
 //Receiver 的输入流,这是 Receiver 下面的 TCP 传输层
 protected DataInputStream in; 
 //执行 Op 所规定的操作,根据 Op 调用不同的方法 
 protected final void processOp(Op op) throws IOException {
    switch(op) {
    case READ_BLOCK:
      //从本节点读取一个数据块
      opReadBlock();
      break;
    case WRITE_BLOCK:
      //将一个数据块写入本节点以及流水线中的后续节点
      opWriteBlock(in);
      break;
    case REPLACE_BLOCK:
      //将复份换个地方,从一个节点换到另一个节点
      opReplaceBlock(in);
      break;
    case COPY_BLOCK:
      //拷贝一个数据块复份
      opCopyBlock(in);
      break;
    case BLOCK_CHECKSUM:
      //对一个数据块复份实施 Checksum 检查
      opBlockChecksum(in);
      break;
    case TRANSFER_BLOCK:
      //将数据块复份发送到别的节点
      opTransferBlock(in);
      break;
    }
  }
 
}

E:\hadoop-2.8.5\hadoop-hdfs-project\hadoop-hdfs\src\main\java\org\apache\hadoop\hdfs\server\datanode\DataXceiver.java

DataXceiver 是对于 Receiver 的扩充

class DataXceiver extends Receiver implements Runnable {

  private Peer peer; //代表着与对方的连接
  private final String remoteAddress; // address of remote side
  private final String remoteAddressWithoutPort; // only the address, no port
  private final String localAddress;  // local address of this daemon
  private final DataNode datanode;
  private final DNConf dnConf;
  private final DataXceiverServer dataXceiverServer;
  private final boolean connectToDnViaHostname;
  private long opStartTime; //the start time of receiving an Op
  private final InputStream socketIn;
  private OutputStream socketOut;
  private BlockReceiver blockReceiver = null; //数据块接收器
  
  public void run() {
    op = readOp(); ⇒ processOp(Op op) //调用Receiver.processOp()
  }
}

E:\hadoop-2.8.5\hadoop-hdfs-project\hadoop-hdfs-client\src\main\java\org\apache\hadoop\hdfs\protocol\datatransfer\Sender.java

与 DataXceiver 对接、能够发起 DataTransferProtocol 通信的是发送器 Sender,DataXceiver 之所谓只能被动接受来自上游的操作请求,其实只是针对上游节点而言,实际上它很可能还需要在一个流水线中起中继转发的作用。
DataXceiver 在接收到上游的请求后由Serder转发相应的操作。

public class Sender implements DataTransferProtocol {
  private final DataOutputStream out; //Socket输出
  //发送一个操作请求
  private static void send(final DataOutputStream out, final Op opcode,
      final Message proto) throws IOException {
    op(out, opcode); //操作请求的尾部是版本号 + 操作码
    proto.writeDelimitedTo(out);
    out.flush();
  }
  
  //读取数据块
  public void readBlock(......)
  //写数据块
  public void writeBlock(......)
  
}

E:\hadoop-2.8.5\hadoop-hdfs-project\hadoop-hdfs\src\main\java\org\apache\hadoop\hdfs\server\datanode\DataXceiver.java

public void writeBlock(......){

//根据需要创建 BlockReceiver 对象
lockReceiver=newBlockReceiver ( block , storageType , in ,peer.getRemoteAddressString (),…)
//根据需要建立与下游节点的连接并转发
new Sender(mirrorOut).writeBlock(......);
//如果是数据块传输阶段,就接收、存储并转发数据内容
blockReceiver.receiveBlock(mirrorOut, mirrorIn, replyOut,mirrorAddr, null, targets, false);

}

DataNode 向 NameNode 发出心跳报告以后,NameNode 的响应中有可能搭载着命令 DNA _ TRANSFER ,要求这个 DataNode 将保存在当地的某个数据块复份发送给别的节点,在那里增添一个复份。而 这个 DataNode 上 与此 NameNode 相对 应的BPOfferService ,则在 processCommandFromActive ()中因此而调用 dn. transferBlocks (),即
DataNode. transferBlocks (),以完成跨节点的数据块复制。