Hadoop3.2.1 【 HDFS 】原始碼分析 : DataXceiver: 讀取資料塊 解析 [二]

張伯毅發表於2020-11-23

一. 前言

Receiver.processOp()方法用於處理流式介面的請求, 它首先從資料流中讀取序列化後的引數, 對引數反序列化, 然後根據操作碼呼叫DataTransferProtocol中定義的方法, 這些方法都是在DataXceiver中具體實現的。
流式介面中最重要的一個部分就是客戶端從資料節點上讀取資料塊, DataTransferProtocol.readBlock()給出了讀取操作的介面定義, 操作碼是81。 DataXceiver.readBlock()則實現了DataTransferProtocol.readBlock()方法。

客戶端通過呼叫Sender.readBlock()方法從指定資料節點上讀取資料塊, 請求通過IO流到達資料節點後, 資料節點的DataXceiverServer會建立一個DataXceiver物件響應流式介面請求。 DataXceiver.processOp()方法解析操作碼為81(讀請求) , 則呼叫DataXceiver.readBlock()響應這個讀請求。

在這裡插入圖片描述
DataXceiver.readBlock()首先向客戶端回覆一個BlockOpResponseProto響應, 指明請求已經成功接收, 並通過BlockOpResponseProto響應給出Datanode當前使用的校驗方式。 接下來DataXceiver.readBlock()方法會將資料節點上的資料塊(block) 切分成若干個資料包(packet) , 然後依次將資料包傳送給客戶端。 客戶端會在收到每個資料包時進行校驗,如果校驗和錯誤, 客戶端會切斷與當前資料節點的連線, 選擇新的資料節點讀取資料; 如果資料塊內的所有資料包都校驗成功, 客戶端會給資料節點傳送一個Status.CHECKSUM_OK響應, 表明讀取成功。

在這裡插入圖片描述

二. DataXceiver.readBlock()

先看一下DataTransferProtocol.readBlock()方法的定義。


  /**
   * Read a block.
   *
   * @param blk the block being read.
   * @param blockToken security token for accessing the block.
   * @param clientName client's name.
   * @param blockOffset offset of the block.
   * @param length maximum number of bytes for this read.
   * @param sendChecksum if false, the DN should skip reading and sending
   *        checksums
   * @param cachingStrategy  The caching strategy to use.
   *
   * 從當前Datanode讀取指定的資料塊。
   */
  void readBlock(final ExtendedBlock blk,
      final Token<BlockTokenIdentifier> blockToken,
      final String clientName,
      final long blockOffset,
      final long length,
      final boolean sendChecksum,
      final CachingStrategy cachingStrategy) throws IOException;

readBlock()需要傳入的引數主要有如下幾個。
■ ExtendedBlock blk: 要讀取的資料塊。
■ TokenblockToken: 資料塊的訪問令牌。
■ String clientName: 客戶端的名稱。
■ long blockOffset: 要讀取資料在資料塊中的位置。
■ long length: 讀取資料的長度。
■ sendChecksum: Datanode是否傳送校驗資料, 如果為false, 則Datanode不傳送校驗資料。 這裡要特別注意, 資料塊的讀取校驗工作是在客戶端完成的, 客戶端會將校驗結果返回給Datanode。
■ CachingStrategy cachingStrategy: 快取策略, 這裡主要包括兩個重要的欄位——readahead, 預讀取操作, Datanode會在讀取資料塊檔案時預讀取部分資料至作業系統快取中, 以提高讀取檔案效率 ; dropBehind, 如果快取中存放的檔案比較多, 那麼在讀取完資料之後, 就馬上從快取中將資料刪除。

三.readBlock()方法的執行流程

■ 建立BlockSender物件: 首先呼叫getOutputStream()方法獲取Datanode連線到客戶端的IO流, 然後構造BlockSender物件。
■ 成功建立BlockSender物件後, 呼叫writeSuccessWithChecksumInfo()方法傳送BlockOpResponseProto響應給客戶端, 通知客戶端讀請求已經成功接收, 並且告知客戶端當前資料節點的校驗資訊。
■ 呼叫BlockSender.sendBlock()方法將資料塊傳送給客戶端。
■ 當BlockSender完成傳送資料塊的所有內容後, 客戶端會響應一個狀態碼,Datanode需要解析這個狀態碼。

在這裡插入圖片描述
readBlock()的異常處理邏輯也比較簡單, 當客戶端關閉了當前Socket(可能是出現了校驗錯誤) , 或者無法從IO流中成功獲取客戶端發回的響應時, 則直接關閉Datanode到客戶端的底層輸出流。 在readBlock()方法的最後, 會關閉BlockSender類以執行清理操作。


  @Override
  public void readBlock(final ExtendedBlock block, // BP-451827885-192.168.8.156-1584099133244:blk_1073746920_6096
      final Token<BlockTokenIdentifier> blockToken, // Kind: , Service: , Ident:
      final String clientName, //DFSClient_NONMAPREDUCE_368352401_1
      final long blockOffset, // 0
      final long length,  // 1361
      final boolean sendChecksum, // true
      final CachingStrategy cachingStrategy) throws IOException {  // CachingStrategy(dropBehind=null, readahead=null)
    // 客戶端名稱 DFSClient_NONMAPREDUCE_368352401_1
    previousOpClientName = clientName;

    long read = 0;

    updateCurrentThreadName("Sending block " + block);

    OutputStream baseStream = getOutputStream();

    DataOutputStream out = getBufferedOutputStream();

    checkAccess(out, true, block, blockToken, Op.READ_BLOCK, BlockTokenIdentifier.AccessMode.READ);

    // send the block
    BlockSender blockSender = null;

    DatanodeRegistration dnR =   datanode.getDNRegistrationForBP(block.getBlockPoolId());
    // src: /127.0.0.1:9866, dest: /127.0.0.1:51764, bytes: %d, op: HDFS_READ, cliID: DFSClient_NONMAPREDUCE_368352401_1, offset: %d, srvID: 9efa402a-df6b-48cf-9273-5468f68cc42f, blockid: BP-451827885-192.168.8.156-1584099133244:blk_1073746920_6096, duration(ns): %d
    final String clientTraceFmt =
      clientName.length() > 0 && ClientTraceLog.isInfoEnabled()
        ? String.format(DN_CLIENTTRACE_FORMAT, localAddress, remoteAddress,
            "%d", "HDFS_READ", clientName, "%d",
            dnR.getDatanodeUuid(), block, "%d")
        : dnR + " Served block " + block + " to " +
            remoteAddress;

    try {
      try {

        blockSender = new BlockSender(block, blockOffset, length,
            true, false, sendChecksum, datanode, clientTraceFmt,
            cachingStrategy);


      } catch(IOException e) {
        String msg = "opReadBlock " + block + " received exception " + e; 
        LOG.info(msg);
        sendResponse(ERROR, msg);
        throw e;
      }
      
      // send op status
      writeSuccessWithChecksumInfo(blockSender, new DataOutputStream(getOutputStream()));

      long beginRead = Time.monotonicNow();

      // 傳送資料
      read = blockSender.sendBlock(out, baseStream, null); // send data


      long duration = Time.monotonicNow() - beginRead;

      if (blockSender.didSendEntireByteRange()) {
        // If we sent the entire range, then we should expect the client
        // to respond with a Status enum.
        try {
          ClientReadStatusProto stat = ClientReadStatusProto.parseFrom(
              PBHelperClient.vintPrefixed(in));
          if (!stat.hasStatus()) {
            LOG.warn("Client {} did not send a valid status code " +
                "after reading. Will close connection.",
                peer.getRemoteAddressString());
            IOUtils.closeStream(out);
          }
        } catch (IOException ioe) {
          LOG.debug("Error reading client status response. Will close connection.", ioe);
          IOUtils.closeStream(out);
          incrDatanodeNetworkErrors();
        }
      } else {
        IOUtils.closeStream(out);
      }
      datanode.metrics.incrBytesRead((int) read);
      datanode.metrics.incrBlocksRead();
      datanode.metrics.incrTotalReadTime(duration);
    } catch ( SocketException ignored ) {
      LOG.trace("{}:Ignoring exception while serving {} to {}",
          dnR, block, remoteAddress, ignored);
      // Its ok for remote side to close the connection anytime.
      datanode.metrics.incrBlocksRead();
      IOUtils.closeStream(out);
    } catch ( IOException ioe ) {
      /* What exactly should we do here?
       * Earlier version shutdown() datanode if there is disk error.
       */
      if (!(ioe instanceof SocketTimeoutException)) {
        LOG.warn("{}:Got exception while serving {} to {}",
            dnR, block, remoteAddress, ioe);
        incrDatanodeNetworkErrors();
      }
      // Normally the client reports a bad block to the NN. However if the
      // meta file is corrupt or an disk error occurs (EIO), then the client
      // never gets a chance to do validation, and hence will never report
      // the block as bad. For some classes of IO exception, the DN should
      // report the block as bad, via the handleBadBlock() method
      datanode.handleBadBlock(block, ioe, false);
      throw ioe;
    } finally {
      IOUtils.closeStream(blockSender);
    }

    //update metrics
    datanode.metrics.addReadBlockOp(elapsed());
    datanode.metrics.incrReadsFromClient(peer.isLocal(), read);
  }

四. 資料塊的傳輸格式

BlockSender類主要負責從資料節點的磁碟讀取資料塊, 然後傳送資料塊到接收方。需要注意的是, BlockSender傳送的資料是以一定結構組織的。

在這裡插入圖片描述
PacketLength大小為: 4 + CHECKSUMS(校驗資料的大小)+ DATA(真實資料的大小)

BlockSender傳送資料的格式包括兩個部分: 校驗資訊頭(ChecksumHeader) 和資料包序列(packets)

|校驗資訊頭(ChecksumHeader) |資料包序列(packets) |

4.1.校驗資訊頭(ChecksumHeader)

ChecksumHeader是一個校驗資訊頭, 用於描述當前Datanode使用的校驗方式等資訊。

| 1 byte校驗型別(CHECKSUM—TYPE) | 4 byte校驗塊大小(BYTES_PER_CHECKSUM)|

■ 資料校驗型別: 資料校驗型別定義在org.apache.hadoop.util.DataChecksum中, 目前包括三種方式——空校驗(不進行校驗) 、 CRC32以及CRC32C。 這裡使用1byte描述資料校驗型別, 空校驗、 CRC32、 CRC32C、分別對應於值0、 1、 2、3、4。

另外兩種型別 : CHECKSUM_DEFAULT、CHECKSUM_MIXED 對應於值3、4 不能使用者建立 DataChecksum .

■ 校驗塊大小: 校驗資訊頭中的第二個部分是校驗塊的大小, 也就是多少位元組的資料產生一個校驗值。 這裡以CRC32為例, 一般情況下是512位元組的資料產生一個4位元組的校驗和, 我們把這512位元組的資料稱為一個校驗塊(chunk) 。 這個校驗塊的概念非常重要, 它是HDFS中讀取和寫入資料塊操作的最小單元.

4.2.資料包序列

BlockSender會將資料塊切分成若干資料包(packet) 對外傳送, 當資料傳送完成後,會以一個空的資料包作為結束。

每個資料包都包括一個變長的包頭、 校驗資料以及若干位元組的實際資料。

|變長的資料包頭(packetHeader) | |校驗資料 | |實際資料…… |

■ 資料包頭——資料包頭用於描述當前資料包的資訊, 是通過ProtoBuf序列化的,包括4位元組的全包長度, 以及2位元組的包頭長度, 之後緊跟如下資料包資訊。

  • 當前資料包在整個資料塊中的位置。
  • 資料包在管道中的序列號。
  • 當前資料包是不是資料塊中的最後一個資料包。
  • 當前資料包中資料部分的長度。
  • 是否需要DN同步。

■ 校驗資料——校驗資料是對實際資料做校驗操作產生的, 它將實際資料以校驗塊為單位, 每個校驗塊產生一個檢驗和, 校驗資料中包含了所有校驗塊的校驗和。校驗資料的大小為: (實際資料長度+校驗塊大小 - 1) / 校驗塊大小×校驗和長度。
■ 實際資料——資料包中的實際資料就是資料塊檔案中儲存的資料, 實際資料的傳輸是以校驗塊為單位的, 一個校驗塊對應產生一個校驗和的實際資料。 在資料包中會將校驗塊與校驗資料分開傳送, 首先將所有校驗塊的校驗資料傳送出去, 然後再傳送所有的校驗塊。

五. BlockSender實現

資料塊的傳送主要是由BlockSender類執行
BlockSender中資料塊的傳送過程包括: 傳送準備、 傳送資料塊以及清理工作。

5.1.傳送準備——構造方法

BlockSender中傳送資料的準備工作主要是在BlockSender的構造方法中執行的,BlockSender的構造方法執行了以下操作。

■ readahead & dropBehind的處理: 如果使用者通過cachingStrategy設定了這兩個欄位, 則按照這兩個欄位初始化讀取操作。 如果cachingStrategy為Null, 則按照配置檔案設定dropCacheBehindLargeReads為dfs.datanode.drop.cache.behind.reads,設定readaheadLength為dfs.datanode.readahead.bytes, 預設為4MB。
■ 賦值與校驗: 檢查當前Datanode上被讀取資料塊的時間戳、 資料塊檔案的長度等狀態是否正常。
■ 是否開啟transferTo模式: 預設為true, transferTo機制請參考零拷貝資料傳輸小節內容。
■ 獲取checksum資訊: 從Meta檔案中獲取當前資料塊的校驗演算法、 校驗和長度, 以及多少位元組產生一個校驗值, 也就是校驗塊的大小。
■ 計算offset以及endOffset: offset變數用於標識要讀取的資料在資料塊的起始位置,endOffset則用於標識結束的位置。 由於讀取位置往往不會落在某個校驗塊的起始位置, 所以在準備工作中需要確保offset在校驗塊的起始位置, endOffset在校驗塊的結束位置。 這樣讀取時就可以以校驗塊為單位讀取, 方便校驗和的操作。
■ 將資料塊檔案與校驗和檔案的offset都移動到指定位置。

  /**
   * Constructor
   * 
   * @param block Block that is being read
   * @param startOffset starting offset to read from
   * @param length length of data to read
   * @param corruptChecksumOk if true, corrupt checksum is okay
   * @param verifyChecksum verify checksum while reading the data
   * @param sendChecksum send checksum to client.
   * @param datanode datanode from which the block is being read
   * @param clientTraceFmt format string used to print client trace logs
   * @throws IOException
   */
  BlockSender(ExtendedBlock block, long startOffset, long length,
              boolean corruptChecksumOk, boolean verifyChecksum,
              boolean sendChecksum, DataNode datanode, String clientTraceFmt,
              CachingStrategy cachingStrategy)
      throws IOException {

    InputStream blockIn = null;

    DataInputStream checksumIn = null;

    FsVolumeReference volumeRef = null;
    // FileIoprovider@5795  DataNode{data=FSDataset{dirpath='[/opt/tools/hadoop-3.2.1/data/hdfs/data, /opt/tools/hadoop-3.2.1/data/hdfs/data01]'}, localName='192.168.8.188:9866', datanodeUuid='9efa402a-df6b-48cf-9273-5468f68cc42f', xmitsInProgress=0}
    this.fileIoProvider = datanode.getFileIoProvider();
    try {

      this.block = block;

      this.corruptChecksumOk = corruptChecksumOk;

      this.verifyChecksum = verifyChecksum;

      this.clientTraceFmt = clientTraceFmt;

      /*
       * If the client asked for the cache to be dropped behind all reads,
       * we honor that.  Otherwise, we use the DataNode defaults.
       * When using DataNode defaults, we use a heuristic where we only
       * drop the cache for large reads.
       */
      if (cachingStrategy.getDropBehind() == null) {

        this.dropCacheBehindAllReads = false;

        this.dropCacheBehindLargeReads =
            datanode.getDnConf().dropCacheBehindReads;

      } else {
        this.dropCacheBehindAllReads =
            this.dropCacheBehindLargeReads =
                 cachingStrategy.getDropBehind().booleanValue();
      }
      /* 預設開啟預讀取, 除非 請求頭 指定 "不開啟預讀取
       * Similarly, if readahead was explicitly requested, we always do it.
       * Otherwise, we read ahead based on the DataNode settings, and only
       * when the reads are large.
       */
      if (cachingStrategy.getReadahead() == null) {
        this.alwaysReadahead = false;
        ///  readaheadLength : 4194304 = 4M    預設 預讀取大小: 4M
        this.readaheadLength = datanode.getDnConf().readaheadLength;

      } else {

        this.alwaysReadahead = true;

        this.readaheadLength = cachingStrategy.getReadahead().longValue();
      }
      this.datanode = datanode;
      
      if (verifyChecksum) {

        // To simplify implementation, callers may not specify verification without sending.
        Preconditions.checkArgument(sendChecksum,
            "If verifying checksum, currently must also send it.");
      }
      // 如果在構造BlockSender之後有一個追加寫操作,那麼最後一個部分校驗和可能被append覆蓋,
      // BlockSender需要在append write之前使用部分校驗和。
      // if there is a append write happening right after the BlockSender
      // is constructed, the last partial checksum maybe overwritten by the
      // append, the BlockSender need to use the partial checksum before
      // the append write.
      ChunkChecksum chunkChecksum = null;

      final long replicaVisibleLength;
      try(AutoCloseableLock lock = datanode.data.acquireDatasetLock()) {
        // 獲取datanode上的副本資訊
        // FinalizedReplica, blk_1073746921_6097, FINALIZED
        //  getNumBytes()     = 42648690
        //  getBytesOnDisk()  = 42648690
        //  getVisibleLength()= 42648690
        //  getVolume()       = /opt/tools/hadoop-3.2.1/data/hdfs/data
        //  getBlockURI()     = file:/opt/tools/hadoop-3.2.1/data/hdfs/data/current/BP-451827885-192.168.8.156-1584099133244/current/finalized/subdir0/subdir19/blk_1073746921
        replica = getReplica(block, datanode);
        // 42648690
        replicaVisibleLength = replica.getVisibleLength();
      }
      if (replica.getState() == ReplicaState.RBW) {
        // 副本正在被寫入 , 等待寫入足夠的內容
        final ReplicaInPipeline rbw = (ReplicaInPipeline) replica;

        waitForMinLength(rbw, startOffset + length);

        chunkChecksum = rbw.getLastChecksumAndDataLen();
      }
      if (replica instanceof FinalizedReplica) {
        // 副本已經被寫入完成 , 獲取該副本的 ChunkChecksum :dataLength = 42648690 checksum = {byte[4]@5845} 四位 : [-11,-56,-5,28]
        chunkChecksum = getPartialChunkChecksumForFinalized(  (FinalizedReplica)replica);
      }

      if (replica.getGenerationStamp() < block.getGenerationStamp()) {
        throw new IOException("Replica gen stamp < block genstamp, block="
            + block + ", replica=" + replica);
      } else if (replica.getGenerationStamp() > block.getGenerationStamp()) {
        if (DataNode.LOG.isDebugEnabled()) {
          DataNode.LOG.debug("Bumping up the client provided"
              + " block's genstamp to latest " + replica.getGenerationStamp()
              + " for block " + block);
        }

        block.setGenerationStamp(replica.getGenerationStamp());
      }
      /// 副本可見長度小於0 , 不可見
      if (replicaVisibleLength < 0) {
        throw new IOException("Replica is not readable, block="+ block + ", replica=" + replica);
      }
      if (DataNode.LOG.isDebugEnabled()) {
        DataNode.LOG.debug("block=" + block + ", replica=" + replica);
      }
      // 是否開啟零拷貝   dfs.datanode.transferTo.allowed : true
      // transferToFully() fails on 32 bit platforms for block sizes >= 2GB,
      // use normal transfer in those cases
      this.transferToAllowed = datanode.getDnConf().transferToAllowed &&
        (!is32Bit || length <= Integer.MAX_VALUE);
      // 在讀取資料之前獲取引用  FsVolumeImple$FsVolumeReferenceImpl@5928
      // Obtain a reference before reading data
      volumeRef = datanode.data.getVolume(block).obtainReference();

      /* 判斷是否要驗證 DataChecksum
       * (corruptChecksumOK, meta_file_exist): operation
       * True,   True: will verify checksum  
       * True,  False: No verify, e.g., need to read data from a corrupted file 
       * False,  True: will verify checksum
       * False, False: throws IOException file not found
       */
      DataChecksum csum = null;
      if (verifyChecksum || sendChecksum) {
        LengthInputStream metaIn = null;
        boolean keepMetaInOpen = false;
        try {


          DataNodeFaultInjector.get().throwTooManyOpenFiles();


          metaIn = datanode.data.getMetaDataInputStream(block);
          if (!corruptChecksumOk || metaIn != null) {
            if (metaIn == null) {
              //need checksum but meta-data not found
              throw new FileNotFoundException("Meta-data not found for " +
                  block);
            }

            // The meta file will contain only the header if the NULL checksum
            // type was used, or if the replica was written to transient storage.
            // Also, when only header portion of a data packet was transferred
            // and then pipeline breaks, the meta file can contain only the
            // header and 0 byte in the block data file.
            // Checksum verification is not performed for replicas on transient
            // storage.  The header is important for determining the checksum
            // type later when lazy persistence copies the block to non-transient
            // storage and computes the checksum.
            int expectedHeaderSize = BlockMetadataHeader.getHeaderSize();  // 7
            if (!replica.isOnTransientStorage() &&
                metaIn.getLength() >= expectedHeaderSize) {
              checksumIn = new DataInputStream(new BufferedInputStream(
                  metaIn, IO_FILE_BUFFER_SIZE));
              // DataChecksum(type=CRC32C, chunkSize=512)
              csum = BlockMetadataHeader.readDataChecksum(checksumIn, block);
              keepMetaInOpen = true;
            } else if (!replica.isOnTransientStorage() &&
                metaIn.getLength() < expectedHeaderSize) {
              LOG.warn("The meta file length {} is less than the expected " +
                  "header length {}, indicating the meta file is corrupt",
                  metaIn.getLength(), expectedHeaderSize);
              throw new CorruptMetaHeaderException("The meta file length "+
                  metaIn.getLength()+" is less than the expected length "+
                  expectedHeaderSize);
            }
          } else {
            LOG.warn("Could not find metadata file for " + block);
          }
        } catch (FileNotFoundException e) {
          if ((e.getMessage() != null) && !(e.getMessage()
              .contains("Too many open files"))) {
            // The replica is on its volume map but not on disk
            datanode
                .notifyNamenodeDeletedBlock(block, replica.getStorageUuid());
            datanode.data.invalidate(block.getBlockPoolId(),
                new Block[] {block.getLocalBlock()});
          }
          throw e;
        } finally {
          if (!keepMetaInOpen) {  // keepMetaInOpen : true
            IOUtils.closeStream(metaIn);
          }
        }
      }
      if (csum == null) {
        csum = DataChecksum.newDataChecksum(DataChecksum.Type.NULL,
            (int)CHUNK_SIZE);
      }

      /*
       * If chunkSize is very large, then the metadata file is mostly
       * corrupted. For now just truncate bytesPerchecksum to blockLength.
       */       
      int size = csum.getBytesPerChecksum();
      // 如果chunkSize非常大,則後設資料檔案大部分已損壞。現在只需將bytesPerchecksum截斷為blockLength。
      if (size > 10*1024*1024 && size > replicaVisibleLength) {
        //後設資料檔案損壞了, 重新構建 DataChecksum
        csum = DataChecksum.newDataChecksum(csum.getChecksumType(),
            Math.max((int)replicaVisibleLength, 10*1024*1024));
        size = csum.getBytesPerChecksum();

      }

      //校驗塊大小 512
      chunkSize = size;
      //校驗演算法 DataChecksum(type=CRC32C, chunkSize=512)
      checksum = csum;
      //校驗和長度  4
      checksumSize = checksum.getChecksumSize();

      // 檔案大小 42648690
      length = length < 0 ? replicaVisibleLength : length;

      // end is either last byte on disk or the length for which we have a  checksum
      // end要麼是磁碟上的最後一個位元組,要麼是校驗和的長度 :   這裡是檔案長度.
      long end = chunkChecksum != null ? chunkChecksum.getDataLength()  : replica.getBytesOnDisk();

      if (startOffset < 0 || startOffset > end
          || (length + startOffset) > end) {
        String msg = " Offset " + startOffset + " and length " + length
        + " don't match block " + block + " ( blockLen " + end + " )";
        LOG.warn(datanode.getDNRegistrationForBP(block.getBlockPoolId()) +
            ":sendBlock() : " + msg);
        throw new IOException(msg);
      }

      // 將offset位置設定在校驗塊的邊界上, 也就是校驗塊的起始位置
      // Ensure read offset is position at the beginning of chunk
      offset = startOffset - (startOffset % chunkSize);
      if (length >= 0) {

        //計算endOffset的位置, 確保endOffset在校驗塊的結束位置
        // Ensure endOffset points to end of chunk.
        long tmpLen = startOffset + length;
        if (tmpLen % chunkSize != 0) {
          tmpLen += (chunkSize - tmpLen % chunkSize); //補齊資料, 使資料正好是512的整倍數
        }
        if (tmpLen < end) {
          //結束位置還在資料塊內, 則可以使用磁碟上的校驗值 , 理論上應該不走這裡.
          // will use on-disk checksum here since the end is a stable chunk
          end = tmpLen;
        } else if (chunkChecksum != null) {
          //目前有寫執行緒[當前執行緒]正在處理這個校驗塊, 則使用記憶體中的校驗值
          // last chunk is changing. flag that we need to use in-memory checksum 
          this.lastChunkChecksum = chunkChecksum;
        }

      }
      endOffset = end; // 設定最後的偏移量為檔案的偏移量
      // 將校驗檔案的座標移動到offset對應的位置
      // seek to the right offsets
      if (offset > 0 && checksumIn != null) {

        long checksumSkip = (offset / chunkSize) * checksumSize;
        // note blockInStream is seeked when created below

        if (checksumSkip > 0) {
          // Should we use seek() for checksum file as well?
          IOUtils.skipFully(checksumIn, checksumSkip);
        }

      }

      //packet序列號設定為0
      seqno = 0;

      if (DataNode.LOG.isDebugEnabled()) {

        DataNode.LOG.debug("replica=" + replica);

      }
      //將資料塊檔案的座標移動到offset位置 準備開始讀寫
      blockIn = datanode.data.getBlockInputStream(block, offset); // seek to offset
      // 構建block資料的讀取流 ReplicaInputStreams
      ris = new ReplicaInputStreams(  blockIn, checksumIn, volumeRef, fileIoProvider);

    } catch (IOException ioe) {
      IOUtils.closeStream(this);
      org.apache.commons.io.IOUtils.closeQuietly(blockIn);
      org.apache.commons.io.IOUtils.closeQuietly(checksumIn);
      throw ioe;
    }
  }

5.2.預讀取&丟棄——manageOsCache()

BlockSender在讀取資料塊之前, 會先呼叫manageOsCache()方法執行預讀取(readahead) 操作以提高讀取效率。 預讀取操作就是將資料塊檔案提前讀取到作業系統的快取中, 這樣當BlockSender到檔案系統中讀取資料塊檔案時, 可以直接從作業系統的快取中讀取資料, 比直接從磁碟上讀取快很多。 但是作業系統的快取空間是有限的, 所以需要呼叫manageOsCache()方法將不再使用的資料從快取中丟棄(drop-behind) , 為新的資料挪出空間。 BlockSender在讀取資料時, 使用了預讀取以及丟棄這兩個特性.

manageOsCache()方法在HDFS管理員設定了預讀取的長度(預設是4MB) 並且設定了所有的操作都使用預讀取時, 或者當前讀取是一個長讀取(超過256KB的讀取) 時, 會呼叫ReadaheadPool.readaheadStream()方法觸發一個預讀取操作, 這個預讀取操作會從磁碟
檔案上預讀取部分資料塊檔案的資料到作業系統的快取中。
同時managerOsCache()還會處理丟棄操作, 如果dropCacheBehindAllReads(所有讀操作後都丟棄) 為true或者當前讀取是一個大讀取時, 則觸發丟棄操作。 manageOsCache()方法會判斷如果下一次讀取資料的座標offset大於下一次丟棄操作的開始座標, 則將lastCacheDropOffset(上一次丟棄操作的結束位置) 和offset之間的資料全部從快取中丟棄, 因為這些資料Datanode已經讀取了,不需要放在快取中了.

manageOsCache()的開啟需要readaheadPool物件例項, readaheadPoold 建立是DataNode#startDataNode的方法進行初始化的.
但是需要本地庫的支援ReadaheadPool.getInstance(); 如果不支援是開啟不了的,需要hadoop的定製依賴 : native hadoop library .

由BlockSender#doSendBlock()方法呼叫


  /**
   *
   * Manage the OS buffer cache by performing read-ahead and drop-behind.
   */
  private void manageOsCache() throws IOException {
    // We can't manage the cache for this block if we don't have a file
    // descriptor to work with.
    if (ris.getDataInFd() == null) {
      return;
    }

    //按條件觸發預讀取操作
    // Perform readahead if necessary
    if ((readaheadLength > 0) && (datanode.readaheadPool != null) &&
          (alwaysReadahead || isLongRead())) {

      //滿足預讀取條件, 則呼叫ReadaheadPool.readaheadStream()方法觸發預讀取
      curReadahead = datanode.readaheadPool.readaheadStream(
          clientTraceFmt, ris.getDataInFd(), offset, readaheadLength,
          Long.MAX_VALUE, curReadahead);
    }

    //丟棄剛才從快取中讀取的資料, 因為不再需要使用這些資料了
    // Drop what we've just read from cache, since we aren't likely to need it again
    if (dropCacheBehindAllReads ||
        (dropCacheBehindLargeReads && isLongRead())) {
      //丟棄資料的位置
      long nextCacheDropOffset = lastCacheDropOffset + CACHE_DROP_INTERVAL_BYTES;
      if (offset >= nextCacheDropOffset) {
        //如果下一次讀取資料的位置大於丟棄資料的位置, 則將讀取資料位置前的資料全部丟棄
        long dropLength = offset - lastCacheDropOffset;
        ris.dropCacheBehindReads(block.getBlockName(), lastCacheDropOffset,
            dropLength, POSIX_FADV_DONTNEED);
        lastCacheDropOffset = offset;
      }
    }
  }

ReadaheadPool.readaheadStream()方法執行了一個預讀取操作, 只有在上一次預讀取的資料已經使用了一半時, 才會觸發一次新的預讀取。 新的預讀取操作是通過在Datanode.readaheadPool執行緒池中建立一個ReadaheadRequestImpl任務來執行的。
ReadaheadRequestImpl.run()方法的程式碼如下, 它通過呼叫fadvise()系統呼叫, 完成OS層面的預讀取, 將資料放入作業系統的快取中。

@Override
    public void run() {
      if (canceled) return;
      // There's a very narrow race here that the file will close right at
      // this instant. But if that happens, we'll likely receive an EBADF
      // error below, and see that it's canceled, ignoring the error.
      // It's also possible that we'll end up requesting readahead on some
      // other FD, which may be wasted work, but won't cause a problem.
      try {
        if (fd.valid()) {

          //呼叫fadvise()系統呼叫完成預讀取
          NativeIO.POSIX.getCacheManipulator().posixFadviseIfPossible(
              identifier, fd, off, len, POSIX_FADV_WILLNEED);
        }
      } catch (IOException ioe) {
        if (canceled) {
          // no big deal - the reader canceled the request and closed
          // the file.
          return;
        }
        LOG.warn("Failed readahead on " + identifier,
            ioe);
      }
    }

5.3.傳送資料塊——sendBlock()

sendBlock()方法, 這個方法用於讀取資料以及校驗和, 並將它們傳送到接收方。

整個傳送的流程可以分為如下幾步:
■ 在剛開始讀取檔案時, 觸發一次預讀取, 預讀取部分資料到作業系統的緩衝區中。
■ 構造pktBuf緩衝區, 也就是能容納一個資料包的緩衝區。 這裡首先要確定的就是pktBuf緩衝區的大小, 最好是一個資料包的大小。
對於兩種不同的傳送資料包的模式transferToioStream, 緩衝區的大小是不同的。 在transfertTo模式中 , 資料塊檔案是通過零拷貝方式直接傳輸給客戶端的,並不需要將資料塊檔案寫入緩衝區中, 所以pktBuf緩衝區只需要緩衝校驗資料即可; 而ioStream模式則需要將實際資料以及校驗資料都緩衝下來, 所以pktBuf大小是完全不同的。
■ 接下來就是迴圈呼叫sendPacket()方法傳送資料包序列, 直到offset>=endOffset,也就是整個資料塊都傳送完成了。 這裡首先呼叫manageOsCache()進行預讀取,然後迴圈呼叫sendPacket()依次將所有資料包傳送到客戶端。 最後更新offset——也就是資料遊標, 更新seqno——記錄已經傳送了幾個資料包。
■ 傳送一個空的資料包用以標識資料塊的結束。
■ 完成資料塊傳送操作之後, 呼叫close()方法關閉資料塊以及校驗檔案, 並從作業系統的快取中刪除已讀取的資料。


  private long doSendBlock(DataOutputStream out, OutputStream baseStream,
        DataTransferThrottler throttler) throws IOException {
    if (out == null) {
      throw new IOException( "out stream is null" );
    }
    initialOffset = offset;

    long totalRead = 0;

    OutputStream streamForSendChunks = out;
    
    lastCacheDropOffset = initialOffset;

    if (isLongRead() && ris.getDataInFd() != null) {
      // Advise that this file descriptor will be accessed sequentially.
      ris.dropCacheBehindReads(block.getBlockName(), 0, 0,
          POSIX_FADV_SEQUENTIAL);
    }
    //1. 將資料預讀取至作業系統的快取中
    // Trigger readahead of beginning of file if configured.
    manageOsCache();

    final long startTime = ClientTraceLog.isDebugEnabled() ? System.nanoTime() : 0;

    //2. 構造存放資料包(packet) 的緩衝區
    try {
      int maxChunksPerPacket;

      // pktBufSize : 33
      int pktBufSize = PacketHeader.PKT_MAX_HEADER_LEN;
      boolean transferTo = transferToAllowed && !verifyChecksum
          && baseStream instanceof SocketOutputStream
          && ris.getDataIn() instanceof FileInputStream;


      if (transferTo) {
        FileChannel fileChannel =  ((FileInputStream)ris.getDataIn()).getChannel();
        blockInPosition = fileChannel.position();
        streamForSendChunks = baseStream;

        // 這裡的TRANSFERTO_BUFFER_SIZE大小預設是64KB
        // maxChunksPerPacket變數表明一個資料包中最多包含多少個校驗塊 : 128 個
        maxChunksPerPacket = numberOfChunks(TRANSFERTO_BUFFER_SIZE);
        //緩沖區中只存放校驗資料 pktBufSize : 545
        // Smaller packet size to only hold checksum when doing transferTo
        pktBufSize += checksumSize * maxChunksPerPacket;
      } else {


        //這裡的IO—FILE_BUFFER_SIZE大小預設是4KB
        maxChunksPerPacket = Math.max(1, numberOfChunks(IO_FILE_BUFFER_SIZE));
        // Packet size includes both checksum and data
        //緩衝區存放校驗資料以及實際資料
        pktBufSize += (chunkSize + checksumSize) * maxChunksPerPacket;
      }
      //構造緩沖區pktBuf  : 545
      ByteBuffer pktBuf = ByteBuffer.allocate(pktBufSize);

      //迴圈呼叫sendPacket()傳送packet
      while (endOffset > offset && !Thread.currentThread().isInterrupted()) {

        manageOsCache();
        long len = sendPacket(pktBuf, maxChunksPerPacket, streamForSendChunks,
            transferTo, throttler);
        offset += len;
        totalRead += len + (numberOfChunks(len) * checksumSize);
        seqno++;
      }
      //如果當前執行緒被中斷, 則不再傳送完整的資料塊
      // If this thread was interrupted, then it did not send the full block.
      if (!Thread.currentThread().isInterrupted()) {
        try {
          // 傳送一個空的資料包用以標識資料塊的結束
          // send an empty packet to mark the end of the block
          sendPacket(pktBuf, maxChunksPerPacket, streamForSendChunks, transferTo,
              throttler);
          out.flush();
        } catch (IOException e) { //socket error
          throw ioeToSocketException(e);
        }

        sentEntireByteRange = true;
      }
    } finally {
      if ((clientTraceFmt != null) && ClientTraceLog.isDebugEnabled()) {
        final long endTime = System.nanoTime();
        ClientTraceLog.debug(String.format(clientTraceFmt, totalRead,
            initialOffset, endTime - startTime));
      }
      // 呼叫close()檔案關閉資料塊檔案、 校驗檔案以及回收作業系統緩衝區
      close();
    }
    return totalRead;
  }

5.4.傳送資料包——sendPacket()

sendPacket()也可以分為三個部分

■ 首先計算資料包頭域在pkt快取中的位置headerOff, 再計算checksum在pkt中的位置checksumOff, 以及實際資料在pkt中的位置dataOff。 然後將資料包頭域、 校驗資料以及實際資料寫入pkt快取中。 如果verifyChecksum屬性被設定為true, 則呼叫verifyChecksum()方法確認校驗和資料正確。
■ 接下來就是傳送資料塊了, 將pkt快取中的資料寫入IO流中。 這裡要注意, 如果是transferT()方式, pkt中只有資料包頭域以及校驗資料, 實際資料則直接通過transferTo方式從檔案通道(FileChannel) 直接寫入IO流中。
■ 使用節流器控制寫入的速度


  /**
   * Sends a packet with up to maxChunks chunks of data.
   * 
   * @param pkt buffer used for writing packet data
   * @param maxChunks maximum number of chunks to send
   * @param out stream to send data to
   * @param transferTo use transferTo to send data
   * @param throttler used for throttling data transfer bandwidth
   */
  private int sendPacket(ByteBuffer pkt, int maxChunks, OutputStream out,
      boolean transferTo, DataTransferThrottler throttler) throws IOException {


    int dataLen = (int) Math.min(endOffset - offset,
                             (chunkSize * (long) maxChunks));


    //資料包中包含多少個校驗塊
    int numChunks = numberOfChunks(dataLen); // Number of chunks be sent in the packet

    //校驗資料長度
    int checksumDataLen = numChunks * checksumSize;

    //資料包長度
    int packetLen = dataLen + checksumDataLen + 4;

    boolean lastDataPacket = offset + dataLen == endOffset && dataLen > 0;

    // The packet buffer is organized as follows:
    // _______HHHHCCCCD?D?D?D?
    //        ^   ^
    //        |   \ checksumOff
    //        \ headerOff
    // _ padding, since the header is variable-length
    // H = header and length prefixes
    // C = checksums
    // D? = data, if transferTo is false.

    //將資料包頭域寫入快取中
    int headerLen = writePacketHeader(pkt, dataLen, packetLen);

    //資料包頭域在快取中的位置
    // Per above, the header doesn't start at the beginning of the buffer
    int headerOff = pkt.position() - headerLen;

    //校驗資料在快取中的位置
    int checksumOff = pkt.position();

    byte[] buf = pkt.array();
    
    if (checksumSize > 0 && ris.getChecksumIn() != null) {

      //將校驗資料寫入快取中
      readChecksum(buf, checksumOff, checksumDataLen);

      // write in progress that we need to use to get last checksum
      if (lastDataPacket && lastChunkChecksum != null) {


        int start = checksumOff + checksumDataLen - checksumSize;


        byte[] updatedChecksum = lastChunkChecksum.getChecksum();


        if (updatedChecksum != null) {
          System.arraycopy(updatedChecksum, 0, buf, start, checksumSize);
        }
      }
    }
    
    int dataOff = checksumOff + checksumDataLen;


    if (!transferTo) { // normal transfer
      try {

        //在普通模式下, 將實際資料寫入快取中
        ris.readDataFully(buf, dataOff, dataLen);
      } catch (IOException ioe) {
        if (ioe.getMessage().startsWith(EIO_ERROR)) {
          throw new DiskFileCorruptException("A disk IO error occurred", ioe);
        }
        throw ioe;
      }

      if (verifyChecksum) {

        //確認校驗和正確
        verifyChecksum(buf, dataOff, dataLen, numChunks, checksumOff);
      }
    }
    
    try {
      //transferTo模式
      if (transferTo) {

        //將頭域和校驗和寫入輸出流中
        SocketOutputStream sockOut = (SocketOutputStream)out;

        //使用transfer方式, 將資料從資料塊檔案直接零拷貝到IO流中
        // First write header and checksums
        sockOut.write(buf, headerOff, dataOff - headerOff);

        // no need to flush since we know out is not a buffered stream
        FileChannel fileCh = ((FileInputStream)ris.getDataIn()).getChannel();


        LongWritable waitTime = new LongWritable();

        LongWritable transferTime = new LongWritable();


        fileIoProvider.transferToSocketFully(
            ris.getVolumeRef().getVolume(), sockOut, fileCh, blockInPosition,
            dataLen, waitTime, transferTime);


        datanode.metrics.addSendDataPacketBlockedOnNetworkNanos(waitTime.get());
        datanode.metrics.addSendDataPacketTransferNanos(transferTime.get());


        blockInPosition += dataLen;
      } else {

        //在正常模式下
        //將快取中的所有資料(包括頭域、 校驗和以及實際資料) 寫入輸出流中
        // normal transfer
        out.write(buf, headerOff, dataOff + dataLen - headerOff);
      }
    } catch (IOException e) {
      if (e instanceof SocketTimeoutException) {
        /*
         * writing to client timed out.  This happens if the client reads
         * part of a block and then decides not to read the rest (but leaves
         * the socket open).
         * 
         * Reporting of this case is done in DataXceiver#run
         */
      } else {
        /* Exception while writing to the client. Connection closure from
         * the other end is mostly the case and we do not care much about
         * it. But other things can go wrong, especially in transferTo(),
         * which we do not want to ignore.
         *
         * The message parsing below should not be considered as a good
         * coding example. NEVER do it to drive a program logic. NEVER.
         * It was done here because the NIO throws an IOException for EPIPE.
         */
        String ioem = e.getMessage();
        /*
         * If we got an EIO when reading files or transferTo the client socket,
         * it's very likely caused by bad disk track or other file corruptions.
         */
        if (ioem.startsWith(EIO_ERROR)) {
          throw new DiskFileCorruptException("A disk IO error occurred", e);
        }
        if (!ioem.startsWith("Broken pipe") && !ioem.startsWith("Connection reset")) {
          LOG.error("BlockSender.sendChunks() exception: ", e);
          datanode.getBlockScanner().markSuspectBlock(
              ris.getVolumeRef().getVolume().getStorageID(),
              block);
        }
      }
      throw ioeToSocketException(e);
    }

    if (throttler != null) {

      // rebalancing so throttle
      //調整節流器
      throttler.throttle(packetLen);
    }

    return dataLen;
  }
  

相關文章