原始碼|HDFS之DataNode:寫資料塊(3)

monkeysayhi發表於2018-02-22

原始碼|HDFS之DataNode:寫資料塊(1)原始碼|HDFS之DataNode:寫資料塊(2)分別分析了無管道無異常、管道寫無異常的情況下,datanode上的寫資料塊過程。本文分析管道寫有異常的情況,假設副本系數3(即寫資料塊涉及1個客戶端+3個datanode),討論datanode對不同異常種類、不同異常時機的處理。

原始碼版本:Apache Hadoop 2.6.0

結論與實現都相對簡單。可僅閱讀總覽。

開始之前

總覽

datanode對寫資料塊過程中的異常處理比較簡單,通常採用兩種策略:

  1. 當前節點拋異常,關閉上下游的IO流、socket等,以關閉管道。
  2. 向上遊節點傳送攜帶故障資訊的ack。

只有少部分情況採用方案2;大部分情況採用方案1,簡單關閉管道了事;部分情況二者結合。

雖然異常處理策略簡單,但涉及異常處理的程式碼卻不少,整體思路參照原始碼|HDFS之DataNode:寫資料塊(1)主流程中的DataXceiver#writeBlock()方法,部分融合了原始碼|HDFS之DataNode:寫資料塊(2)中管道寫的內容 。本文從主流程DataXceiver#writeBlock()入手,部分涉及DataXceiver#writeBlock()的外層方法。

更值得關注的是寫資料塊的故障恢復流程,該工作由客戶端主導,猴子將在對客戶端的分析中討論。

文章的組織結構

  1. 如果只涉及單個分支的分析,則放在同一節。
  2. 如果涉及多個分支的分析,則在下一級分多個節,每節討論一個分支。
  3. 多執行緒的分析同多分支。
  4. 每一個分支和執行緒的組織結構遵循規則1-3。

主流程:DataXceiver#writeBlock()

DataXceiver#writeBlock():

  public void writeBlock(final ExtendedBlock block,
      final StorageType storageType, 
      final Token<BlockTokenIdentifier> blockToken,
      final String clientname,
      final DatanodeInfo[] targets,
      final StorageType[] targetStorageTypes, 
      final DatanodeInfo srcDataNode,
      final BlockConstructionStage stage,
      final int pipelineSize,
      final long minBytesRcvd,
      final long maxBytesRcvd,
      final long latestGenerationStamp,
      DataChecksum requestedChecksum,
      CachingStrategy cachingStrategy,
      final boolean allowLazyPersist) throws IOException {
    ...// 檢查,設定引數等

    ...// 構建向上遊節點或客戶端回覆的輸出流(此處即為客戶端)

    ...// 略
    
    try {
      if (isDatanode || 
          stage != BlockConstructionStage.PIPELINE_CLOSE_RECOVERY) {
        // 建立BlockReceiver,準備接收資料塊
        blockReceiver = new BlockReceiver(block, storageType, in,
            peer.getRemoteAddressString(),
            peer.getLocalAddressString(),
            stage, latestGenerationStamp, minBytesRcvd, maxBytesRcvd,
            clientname, srcDataNode, datanode, requestedChecksum,
            cachingStrategy, allowLazyPersist);

        storageUuid = blockReceiver.getStorageUuid();
      } else {
        ...// 管道錯誤恢復相關
      }

      // 下游節點的處理:以當前節點為“客戶端”,繼續觸發下游管道的建立
      if (targets.length > 0) {
        // 連線下游節點
        InetSocketAddress mirrorTarget = null;
        mirrorNode = targets[0].getXferAddr(connectToDnViaHostname);
        if (LOG.isDebugEnabled()) {
          LOG.debug("Connecting to datanode " + mirrorNode);
        }
        mirrorTarget = NetUtils.createSocketAddr(mirrorNode);
        mirrorSock = datanode.newSocket();
        // 嘗試建立管道(下面展開)
        try {
          // 設定建立socket的超時時間、寫packet的超時時間、寫buf大小等
          int timeoutValue = dnConf.socketTimeout
              + (HdfsServerConstants.READ_TIMEOUT_EXTENSION * targets.length);
          int writeTimeout = dnConf.socketWriteTimeout + 
                      (HdfsServerConstants.WRITE_TIMEOUT_EXTENSION * targets.length);
          NetUtils.connect(mirrorSock, mirrorTarget, timeoutValue);
          mirrorSock.setSoTimeout(timeoutValue);
          mirrorSock.setSendBufferSize(HdfsConstants.DEFAULT_DATA_SOCKET_SIZE);
          
          // 設定當前節點到下游的輸出流mirrorOut、下游到當前節點的輸入流mirrorIn等
          OutputStream unbufMirrorOut = NetUtils.getOutputStream(mirrorSock,
              writeTimeout);
          InputStream unbufMirrorIn = NetUtils.getInputStream(mirrorSock);
          DataEncryptionKeyFactory keyFactory =
            datanode.getDataEncryptionKeyFactoryForBlock(block);
          IOStreamPair saslStreams = datanode.saslClient.socketSend(mirrorSock,
            unbufMirrorOut, unbufMirrorIn, keyFactory, blockToken, targets[0]);
          unbufMirrorOut = saslStreams.out;
          unbufMirrorIn = saslStreams.in;
          mirrorOut = new DataOutputStream(new BufferedOutputStream(unbufMirrorOut,
              HdfsConstants.SMALL_BUFFER_SIZE));
          mirrorIn = new DataInputStream(unbufMirrorIn);

          // 向下遊節點傳送建立管道的請求,未來將繼續使用mirrorOut作為寫packet的輸出流
          new Sender(mirrorOut).writeBlock(originalBlock, targetStorageTypes[0],
              blockToken, clientname, targets, targetStorageTypes, srcDataNode,
              stage, pipelineSize, minBytesRcvd, maxBytesRcvd,
              latestGenerationStamp, requestedChecksum, cachingStrategy, false);
          mirrorOut.flush();

          // 如果是客戶端發起的寫資料塊請求(滿足),則存在管道,需要從下游節點讀取建立管道的ack
          if (isClient) {
            BlockOpResponseProto connectAck =
              BlockOpResponseProto.parseFrom(PBHelper.vintPrefixed(mirrorIn));
            // 將下游節點的管道建立結果作為整個管道的建立結果(要麼從尾節點到頭結點都是成功的,要麼都是失敗的)
            mirrorInStatus = connectAck.getStatus();
            firstBadLink = connectAck.getFirstBadLink();
            if (LOG.isDebugEnabled() || mirrorInStatus != SUCCESS) {
              LOG.info("Datanode " + targets.length +
                       " got response for connect ack " +
                       " from downstream datanode with firstbadlink as " +
                       firstBadLink);
            }
          }

        } catch (IOException e) {
          // 如果是客戶端發起的寫資料塊請求(滿足),則立即向上遊傳送狀態ERROR的ack(儘管無法保證上游已收到)
          if (isClient) {
            BlockOpResponseProto.newBuilder()
              .setStatus(ERROR)
               // NB: Unconditionally using the xfer addr w/o hostname
              .setFirstBadLink(targets[0].getXferAddr())
              .build()
              .writeDelimitedTo(replyOut);
            replyOut.flush();
          }
          // 關閉下游的IO流,socket
          IOUtils.closeStream(mirrorOut);
          mirrorOut = null;
          IOUtils.closeStream(mirrorIn);
          mirrorIn = null;
          IOUtils.closeSocket(mirrorSock);
          mirrorSock = null;
          // 如果是客戶端發起的寫資料塊請求(滿足),則重新丟擲該異常
          // 然後,將跳到外層的catch塊
          if (isClient) {
            LOG.error(datanode + ":Exception transfering block " +
                      block + " to mirror " + mirrorNode + ": " + e);
            throw e;
          } else {
            LOG.info(datanode + ":Exception transfering " +
                     block + " to mirror " + mirrorNode +
                     "- continuing without the mirror", e);
          }
        }
      }
      
      // 傳送的第一個packet是空的,只用於建立管道。這裡立即返回ack表示管道是否建立成功
      // 由於該datanode沒有下游節點,則執行到此處,表示管道已經建立成功
      if (isClient && !isTransfer) {
        if (LOG.isDebugEnabled() || mirrorInStatus != SUCCESS) {
          LOG.info("Datanode " + targets.length +
                   " forwarding connect ack to upstream firstbadlink is " +
                   firstBadLink);
        }
        BlockOpResponseProto.newBuilder()
          .setStatus(mirrorInStatus)
          .setFirstBadLink(firstBadLink)
          .build()
          .writeDelimitedTo(replyOut);
        replyOut.flush();
      }

      // 接收資料塊(也負責傳送到下游,不過此處沒有下游節點)
      if (blockReceiver != null) {
        String mirrorAddr = (mirrorSock == null) ? null : mirrorNode;
        blockReceiver.receiveBlock(mirrorOut, mirrorIn, replyOut,
            mirrorAddr, null, targets, false);

        ...// 資料塊複製相關
      }

      ...// 資料塊恢復相關
      
      ...// 資料塊複製相關
      
    } catch (IOException ioe) {
      // 如果捕獲到IOC,則直接丟擲
      LOG.info("opWriteBlock " + block + " received exception " + ioe);
      throw ioe;
    } finally {
      // 不管正常還是異常,都直接關閉IO流、socket
      IOUtils.closeStream(mirrorOut);
      IOUtils.closeStream(mirrorIn);
      IOUtils.closeStream(replyOut);
      IOUtils.closeSocket(mirrorSock);
      IOUtils.closeStream(blockReceiver);
      blockReceiver = null;
    }

    ...// 更新metrics
  }
複製程式碼

最後的finally塊對異常處理至關重要:

正常情況不表。對於異常情況,關閉所有到下游的IO流(mirrorOut、mirrorIn)、socket(mirrorSock),關閉到上游的輸出流(replyOut),關閉blockReceiver內部封裝的大部分資源(通過BlockReceiver#close()完成),剩餘資源如到上游的輸入流(in)由外層的DataXceiver#run()中的finally塊關閉。

replyOut只是一個過濾器流,其包裝的底層輸出流也可以由DataXceiver#run()中的finally塊關閉。限於篇幅,本文不展開。

記住此處finally塊的作用,後面將多次重複該處程式碼,構成總覽中的方案1。

下面以三個關鍵過程為例,分析這三個關鍵過程中的異常處理,及其與外層異常處理邏輯的互動。

本地準備:BlockReceiver.<init>()

根據前文的分析,BlockReceiver.<init>()的主要工作比較簡單:在rbw目錄下建立block檔案和meta檔案:

  BlockReceiver(final ExtendedBlock block, final StorageType storageType,
      final DataInputStream in,
      final String inAddr, final String myAddr,
      final BlockConstructionStage stage, 
      final long newGs, final long minBytesRcvd, final long maxBytesRcvd, 
      final String clientname, final DatanodeInfo srcDataNode,
      final DataNode datanode, DataChecksum requestedChecksum,
      CachingStrategy cachingStrategy,
      final boolean allowLazyPersist) throws IOException {
    try{
      ...// 檢查,設定引數等

      // 開啟檔案,準備接收資料塊
      if (isDatanode) { // 資料塊複製和資料塊移動是由資料節點發起的,這是在tmp目錄下建立block檔案
        replicaInfo = datanode.data.createTemporary(storageType, block);
      } else {
        switch (stage) {
        // 對於客戶端發起的寫資料請求(只考慮create,不考慮append),在rbw目錄下建立資料塊(block檔案、meta檔案,資料塊處於RBW狀態)
        case PIPELINE_SETUP_CREATE:
          replicaInfo = datanode.data.createRbw(storageType, block, allowLazyPersist);
          datanode.notifyNamenodeReceivingBlock(
              block, replicaInfo.getStorageUuid());
          break;
        ...// 其他case
        default: throw new IOException("Unsupported stage " + stage + 
              " while receiving block " + block + " from " + inAddr);
        }
      }
      ...// 略
      
      // 對於資料塊複製、資料塊移動、客戶端建立資料塊,本質上都在建立新的block檔案。對於這些情況,isCreate為true
      final boolean isCreate = isDatanode || isTransfer 
          || stage == BlockConstructionStage.PIPELINE_SETUP_CREATE;
      streams = replicaInfo.createStreams(isCreate, requestedChecksum);
      assert streams != null : "null streams!";

      ...// 計算meta檔案的檔案頭
      // 如果需要建立新的block檔案,也就需要同時建立新的meta檔案,並寫入檔案頭
      if (isCreate) {
        BlockMetadataHeader.writeHeader(checksumOut, diskChecksum);
      } 
    } catch (ReplicaAlreadyExistsException bae) {
      throw bae;
    } catch (ReplicaNotFoundException bne) {
      throw bne;
    } catch(IOException ioe) {
      // IOE通常涉及檔案等資源,因此要額外清理資源
      IOUtils.closeStream(this);
      cleanupBlock();
      
      // check if there is a disk error
      IOException cause = DatanodeUtil.getCauseIfDiskError(ioe);
      DataNode.LOG.warn("IOException in BlockReceiver constructor. Cause is ",
          cause);
      
      if (cause != null) { // possible disk error
        ioe = cause;
        datanode.checkDiskErrorAsync();
      }
      
      // 重新丟擲IOE
      throw ioe;
    }
  }
複製程式碼

特別提一下DataNode#checkDiskErrorAsync(),該方法非同步檢查是否有磁碟錯誤,如果錯誤磁碟超過閾值,就關閉datanode。但閾值的計算猴子還沒有看懂,看起來是對DataStorage的理解有問題。

BlockReceiver#close()的工作已經介紹過了。需要關注的是_對ReplicaAlreadyExistsException與其他IOException的處理:重新丟擲_。

ReplicaAlreadyExistsException是IOException的子類,由FsDatasetImpl#createRbw()丟擲。

至於丟擲IOException的情況就太多了,無許可權、磁碟錯誤等非常原因。

重新丟擲這些異常塊會怎樣呢?觸發外層DataXceiver#writeBlock()中的catch塊與finally塊。

由於至今還沒有建立下游管道,先讓我們看看由於異常執行finally塊,對上游節點產生的惡果:

  • 在DataXceiver執行緒啟動後,DataXceiver#peer中封裝了當前節點到上游節點的輸出流(out)與上游節點到當前節點的輸入流(in)。
  • 這些IO流的本質是socket,關閉當前節點端的socket後,上游節點端的socket也會在一段時間後觸發超時關閉,並丟擲SocketException(IOException的子類)。
  • 上游節點由於socket關閉捕獲到了IOException,於是也執行finally塊,重複一遍當前節點的流程。

如此,逐級關閉上游節點的管道,直到客戶端對管道關閉的異常作出處理。

如果在建立block檔案或meta檔案時丟擲了異常,目前沒有策略及時清理rbw目錄下的“無主”資料塊。讀者可嘗試debug執行BlockReceiver.<init>(),在rbw目錄下建立資料塊後長時間不讓執行緒繼續執行,最終管道超時關閉,但rbw目錄下的檔案依然存在。

不過資料塊恢復過程可完成清理工作,此處不展開。

建立管道:if (targets.length > 0) {程式碼塊

如果本地準備沒有發生異常,則開始建立管道:

      // 下游節點的處理:以當前節點為“客戶端”,繼續觸發下游管道的建立
      if (targets.length > 0) {
        // 連線下游節點
        InetSocketAddress mirrorTarget = null;
        mirrorNode = targets[0].getXferAddr(connectToDnViaHostname);
        if (LOG.isDebugEnabled()) {
          LOG.debug("Connecting to datanode " + mirrorNode);
        }
        mirrorTarget = NetUtils.createSocketAddr(mirrorNode);
        mirrorSock = datanode.newSocket();
        // 嘗試建立管道(下面展開)
        try {
          // 設定建立socket的超時時間、寫packet的超時時間、寫buf大小等
          int timeoutValue = dnConf.socketTimeout
              + (HdfsServerConstants.READ_TIMEOUT_EXTENSION * targets.length);
          int writeTimeout = dnConf.socketWriteTimeout + 
                      (HdfsServerConstants.WRITE_TIMEOUT_EXTENSION * targets.length);
          NetUtils.connect(mirrorSock, mirrorTarget, timeoutValue);
          mirrorSock.setSoTimeout(timeoutValue);
          mirrorSock.setSendBufferSize(HdfsConstants.DEFAULT_DATA_SOCKET_SIZE);
          
          // 設定當前節點到下游的輸出流mirrorOut、下游到當前節點的輸入流mirrorIn等
          OutputStream unbufMirrorOut = NetUtils.getOutputStream(mirrorSock,
              writeTimeout);
          InputStream unbufMirrorIn = NetUtils.getInputStream(mirrorSock);
          DataEncryptionKeyFactory keyFactory =
            datanode.getDataEncryptionKeyFactoryForBlock(block);
          IOStreamPair saslStreams = datanode.saslClient.socketSend(mirrorSock,
            unbufMirrorOut, unbufMirrorIn, keyFactory, blockToken, targets[0]);
          unbufMirrorOut = saslStreams.out;
          unbufMirrorIn = saslStreams.in;
          mirrorOut = new DataOutputStream(new BufferedOutputStream(unbufMirrorOut,
              HdfsConstants.SMALL_BUFFER_SIZE));
          mirrorIn = new DataInputStream(unbufMirrorIn);

          // 向下遊節點傳送建立管道的請求,未來將繼續使用mirrorOut作為寫packet的輸出流
          new Sender(mirrorOut).writeBlock(originalBlock, targetStorageTypes[0],
              blockToken, clientname, targets, targetStorageTypes, srcDataNode,
              stage, pipelineSize, minBytesRcvd, maxBytesRcvd,
              latestGenerationStamp, requestedChecksum, cachingStrategy, false);
          mirrorOut.flush();

          // 如果是客戶端發起的寫資料塊請求(滿足),則存在管道,需要從下游節點讀取建立管道的ack
          if (isClient) {
            BlockOpResponseProto connectAck =
              BlockOpResponseProto.parseFrom(PBHelper.vintPrefixed(mirrorIn));
            // 將下游節點的管道建立結果作為整個管道的建立結果(要麼從尾節點到頭結點都是成功的,要麼都是失敗的)
            mirrorInStatus = connectAck.getStatus();
            firstBadLink = connectAck.getFirstBadLink();
            if (LOG.isDebugEnabled() || mirrorInStatus != SUCCESS) {
              LOG.info("Datanode " + targets.length +
                       " got response for connect ack " +
                       " from downstream datanode with firstbadlink as " +
                       firstBadLink);
            }
          }

        } catch (IOException e) {
          // 如果是客戶端發起的寫資料塊請求(滿足),則立即向上遊傳送狀態ERROR的ack(儘管無法保證上游已收到)
          if (isClient) {
            BlockOpResponseProto.newBuilder()
              .setStatus(ERROR)
               // NB: Unconditionally using the xfer addr w/o hostname
              .setFirstBadLink(targets[0].getXferAddr())
              .build()
              .writeDelimitedTo(replyOut);
            replyOut.flush();
          }
          // 關閉下游的IO流,socket
          IOUtils.closeStream(mirrorOut);
          mirrorOut = null;
          IOUtils.closeStream(mirrorIn);
          mirrorIn = null;
          IOUtils.closeSocket(mirrorSock);
          mirrorSock = null;
          // 如果是客戶端發起的寫資料塊請求(滿足),則重新丟擲該異常
          // 然後,將跳到外層的catch塊
          if (isClient) {
            LOG.error(datanode + ":Exception transfering block " +
                      block + " to mirror " + mirrorNode + ": " + e);
            throw e;
          } else {
            LOG.info(datanode + ":Exception transfering " +
                     block + " to mirror " + mirrorNode +
                     "- continuing without the mirror", e);
          }
        }
      }
複製程式碼

根據前文對管道建立過程的分析,此處要建立到與下游節點間的部分IO流、socket。

建立資源、傳送管道建立請求的過程中都有可能發生故障,丟擲IOException及其子類。catch塊處理這些IOException的邏輯採用了方案2:先向上游節點傳送ack告知ERROR,然後關閉到下游節點的IO流(mirrorOut、mirrorIn)、關閉到下游的socket(mirrorSock)。最後,重新丟擲異常,以觸發外層的finally塊。

此處執行的清理是外層finally塊的子集,重點是多傳送了一個ack,對該ack的處理留到PacketResponder執行緒的分析中。

不過,此時已經開始建立下游管道,再來看看由於異常執行catch塊(外層finally塊的分析見上),對下游節點產生的惡果:

  • 初始化mirrorOut、mirrorIn、mirrorSock後,下游節點也通過DataXceiverServer建立了配套的IO流、socket等。
  • 這些IO流的本質是socket,關閉當前節點端的socket後,下游節點端的socket也會在一段時間後觸發超時關閉,並丟擲SocketException(IOException的子類)。
  • 下游節點由於socket關閉捕獲到了IOException,於是也執行此處的catch塊或外層的finally塊,重複一遍當前節點的流程。

如此,逐級關閉下游節點的管道,直到客戶端對管道關閉的異常作出處理。同時,由於最終會執行外層finally塊,則也會逐級關閉上游節點的管道

IO流mirrorOut、mirrorIn實際上共享TCP套接字mirrorSock;in、out同理。但管子IO流時,除了底層socket,還要清理緩衝區等資源,因此,將它們分別列出是合理的。

管道寫:BlockReceiver#receiveBlock()

根據前文的分析,如果管道成功建立,則BlockReceiver#receiveBlock()開始接收packet並響應ack:

  void receiveBlock(
      DataOutputStream mirrOut, // output to next datanode
      DataInputStream mirrIn,   // input from next datanode
      DataOutputStream replyOut,  // output to previous datanode
      String mirrAddr, DataTransferThrottler throttlerArg,
      DatanodeInfo[] downstreams,
      boolean isReplaceBlock) throws IOException {
    ...// 引數設定

    try {
      // 如果是客戶端發起的寫請求(此處即為資料塊create),則啟動PacketResponder傳送ack
      if (isClient && !isTransfer) {
        responder = new Daemon(datanode.threadGroup, 
            new PacketResponder(replyOut, mirrIn, downstreams));
        responder.start(); // start thread to processes responses
      }

      // 同步接收packet,寫block檔案和meta檔案
      while (receivePacket() >= 0) {}

      // 此時,節點已接收了所有packet,可以等待傳送完所有ack後關閉responder
      if (responder != null) {
        ((PacketResponder)responder.getRunnable()).close();
        responderClosed = true;
      }

      ...// 資料塊複製相關

    } catch (IOException ioe) {
      if (datanode.isRestarting()) {
        LOG.info("Shutting down for restart (" + block + ").");
      } else {
        LOG.info("Exception for " + block, ioe);
        throw ioe;
      }
    } finally {
      ...// 清理
    }
  }
複製程式碼

仍舊分接收packet與響應ack兩部分討論。

同步接收packet:BlockReceiver#receivePacket()

根據前文的分析,BlockReceiver#receivePacket()負責接收上游的packet,並繼續向下遊節點管道寫:

  private int receivePacket() throws IOException {
    // read the next packet
    packetReceiver.receiveNextPacket(in);

    PacketHeader header = packetReceiver.getHeader();
    ...// 略

    // 檢查packet頭
    if (header.getOffsetInBlock() > replicaInfo.getNumBytes()) {
      throw new IOException("Received an out-of-sequence packet for " + block + 
          "from " + inAddr + " at offset " + header.getOffsetInBlock() +
          ". Expecting packet starting at " + replicaInfo.getNumBytes());
    }
    if (header.getDataLen() < 0) {
      throw new IOException("Got wrong length during writeBlock(" + block + 
                            ") from " + inAddr + " at offset " + 
                            header.getOffsetInBlock() + ": " +
                            header.getDataLen()); 
    }

    long offsetInBlock = header.getOffsetInBlock();
    long seqno = header.getSeqno();
    boolean lastPacketInBlock = header.isLastPacketInBlock();
    final int len = header.getDataLen();
    boolean syncBlock = header.getSyncBlock();

    ...// 略
    
    // 如果不需要立即持久化也不需要校驗收到的資料,則可以立即委託PacketResponder執行緒返回 SUCCESS 的ack,然後再進行校驗和持久化
    if (responder != null && !syncBlock && !shouldVerifyChecksum()) {
      ((PacketResponder) responder.getRunnable()).enqueue(seqno,
          lastPacketInBlock, offsetInBlock, Status.SUCCESS);
    }

    // 管道寫相關:將in中收到的packet映象寫入mirrorOut
    if (mirrorOut != null && !mirrorError) {
      try {
        long begin = Time.monotonicNow();
        packetReceiver.mirrorPacketTo(mirrorOut);
        mirrorOut.flush();
        long duration = Time.monotonicNow() - begin;
        if (duration > datanodeSlowLogThresholdMs) {
          LOG.warn("Slow BlockReceiver write packet to mirror took " + duration
              + "ms (threshold=" + datanodeSlowLogThresholdMs + "ms)");
        }
      } catch (IOException e) {
        // 假設沒有發生中斷,則此處僅僅標記mirrorError = true
        handleMirrorOutError(e);
      }
    }
    
    ByteBuffer dataBuf = packetReceiver.getDataSlice();
    ByteBuffer checksumBuf = packetReceiver.getChecksumSlice();
    
    if (lastPacketInBlock || len == 0) {    // 收到空packet可能是表示心跳或資料塊傳送
      // 這兩種情況都可以嘗試把之前的資料刷到磁碟
      if (syncBlock) {
        flushOrSync(true);
      }
    } else {    // 否則,需要持久化packet
      final int checksumLen = diskChecksum.getChecksumSize(len);
      final int checksumReceivedLen = checksumBuf.capacity();

      // packet頭有錯誤,直接丟擲IOE
      if (checksumReceivedLen > 0 && checksumReceivedLen != checksumLen) {
        throw new IOException("Invalid checksum length: received length is "
            + checksumReceivedLen + " but expected length is " + checksumLen);
      }

      // 如果是管道中的最後一個節點,則持久化之前,要先對收到的packet做一次校驗(使用packet本身的校驗機制)
      if (checksumReceivedLen > 0 && shouldVerifyChecksum()) {
        try {
          // 如果校驗失敗,丟擲IOE
          verifyChunks(dataBuf, checksumBuf);
        } catch (IOException ioe) {
          // 如果校驗錯誤,則委託PacketResponder執行緒返回 ERROR_CHECKSUM 的ack
          if (responder != null) {
            try {
              ((PacketResponder) responder.getRunnable()).enqueue(seqno,
                  lastPacketInBlock, offsetInBlock,
                  Status.ERROR_CHECKSUM);
              // 等3s,期望PacketResponder執行緒能把所有ack都傳送完(這樣就不需要重新傳送那麼多packet了)
              Thread.sleep(3000);
            } catch (InterruptedException e) {
              // 不做處理,也不清理中斷標誌,僅僅停止sleep
            }
          }
          // 如果校驗錯誤,則認為上游節點收到的packet也是錯誤的,直接丟擲IOE
          throw new IOException("Terminating due to a checksum error." + ioe);
        }
 
        ...// checksum 翻譯相關
      }

      if (checksumReceivedLen == 0 && !streams.isTransientStorage()) {
        // checksum is missing, need to calculate it
        checksumBuf = ByteBuffer.allocate(checksumLen);
        diskChecksum.calculateChunkedSums(dataBuf, checksumBuf);
      }

      final boolean shouldNotWriteChecksum = checksumReceivedLen == 0
          && streams.isTransientStorage();
      try {
        long onDiskLen = replicaInfo.getBytesOnDisk();
        if (onDiskLen<offsetInBlock) {
          ...// 如果校驗塊不完整,需要載入並調整舊的meta檔案內容,供後續重新計算crc

          // 寫block檔案
          int startByteToDisk = (int)(onDiskLen-firstByteInBlock) 
              + dataBuf.arrayOffset() + dataBuf.position();
          int numBytesToDisk = (int)(offsetInBlock-onDiskLen);
          out.write(dataBuf.array(), startByteToDisk, numBytesToDisk);
          
          // 寫meta檔案
          final byte[] lastCrc;
          if (shouldNotWriteChecksum) {
            lastCrc = null;
          } else if (partialCrc != null) {  // 如果是校驗塊不完整(之前收到過一部分)
            ...// 重新計算crc
            ...// 更新lastCrc
            checksumOut.write(buf);
            partialCrc = null;
          } else { // 如果校驗塊完整
            ...// 更新lastCrc
            checksumOut.write(checksumBuf.array(), offset, checksumLen);
          }

          ...//略
        }
      } catch (IOException iex) {
        // 非同步檢查磁碟
        datanode.checkDiskErrorAsync();
        // 重新丟擲IOE
        throw iex;
      }
    }

    // 相反的,如果需要立即持久化或需要校驗收到的資料,則現在已經完成了持久化和校驗,可以委託PacketResponder執行緒返回 SUCCESS 的ack
    // if sync was requested, put in queue for pending acks here
    // (after the fsync finished)
    if (responder != null && (syncBlock || shouldVerifyChecksum())) {
      ((PacketResponder) responder.getRunnable()).enqueue(seqno,
          lastPacketInBlock, offsetInBlock, Status.SUCCESS);
    }

    ...// 如果超過了響應時間,還要主動傳送一個IN_PROGRESS的ack,防止超時

    ...// 節流器相關
    
    // 當整個資料塊都傳送完成之前,客戶端會可能會傳送有資料的packet,也因為維持心跳或表示結束寫資料塊傳送空packet
    // 因此,當標誌位lastPacketInBlock為true時,不能返回0,要返回一個負值,以區分未到達最後一個packet之前的情況
    return lastPacketInBlock?-1:len;
  }
  
  ...
  
  private boolean shouldVerifyChecksum() {
    // 對於客戶端寫,只有管道中的最後一個節點滿足`mirrorOut == null`
    return (mirrorOut == null || isDatanode || needsChecksumTranslation);
  }
  
  ...
  
  private void handleMirrorOutError(IOException ioe) throws IOException {
    String bpid = block.getBlockPoolId();
    LOG.info(datanode.getDNRegistrationForBP(bpid)
        + ":Exception writing " + block + " to mirror " + mirrorAddr, ioe);
    if (Thread.interrupted()) { // 如果BlockReceiver執行緒被中斷了,則重新丟擲IOE
      throw ioe;
    } else {    // 否則,僅僅標記下游節點錯誤,交給外層處理
      mirrorError = true;
    }
  }
複製程式碼

對管道寫過程的分析要分尾節點與中間節點兩種情況展開:

  • 如果是尾節點,則持久化之前,要先對收到的packet做一次校驗(使用packet本身的校驗機制)。如果校驗失敗,則委託PacketResponder執行緒傳送ERROR_CHECKSUM狀態的ack,並再次丟擲IOE。
  • 如果是中間節點,則只需要向下遊映象寫packet。假設在非中斷的情況下發生異常,則僅僅標記mirrorError = true。這造成兩個影響:
    1. 後續包都不會再寫往下游節點,最終socket超時關閉,並逐級關閉上下游管道。
    2. 上游將通過ack得知下游發生了錯誤(見後)。

尾節點異常的處理還是走方案1,中間節點同時走方案1與方案2。

非同步傳送ack:PacketResponder執行緒

根據前文的分析,PacketResponder執行緒負責接收下游節點的ack,並繼續向上遊管道響應:

    public void run() {
      boolean lastPacketInBlock = false;
      final long startTime = ClientTraceLog.isInfoEnabled() ? System.nanoTime() : 0;
      while (isRunning() && !lastPacketInBlock) {
        long totalAckTimeNanos = 0;
        boolean isInterrupted = false;
        try {
          Packet pkt = null;
          long expected = -2;
          PipelineAck ack = new PipelineAck();
          long seqno = PipelineAck.UNKOWN_SEQNO;
          long ackRecvNanoTime = 0;
          try {
            // 如果當前節點不是管道的最後一個節點,且下游節點正常,則從下游讀取ack
            if (type != PacketResponderType.LAST_IN_PIPELINE && !mirrorError) {
              ack.readFields(downstreamIn);
              ...// 統計相關
              // 如果下游狀態為OOB,則繼續向上遊傳送OOB
              Status oobStatus = ack.getOOBStatus();
              if (oobStatus != null) {
                LOG.info("Relaying an out of band ack of type " + oobStatus);
                sendAckUpstream(ack, PipelineAck.UNKOWN_SEQNO, 0L, 0L,
                    Status.SUCCESS);
                continue;
              }
              seqno = ack.getSeqno();
            }
            // 如果從下游節點收到了正常的 ack,或當前節點是管道的最後一個節點,則需要從佇列中消費pkt(即BlockReceiver#receivePacket()放入的ack)
            if (seqno != PipelineAck.UNKOWN_SEQNO
                || type == PacketResponderType.LAST_IN_PIPELINE) {
              pkt = waitForAckHead(seqno);
              if (!isRunning()) {
                break;
              }
              // 管道寫用seqno控制packet的順序:當且僅當下游正確接收的序號與當前節點正確處理完的序號相等時,當前節點才認為該序號的packet已正確接收;上游同理
              expected = pkt.seqno;
              if (type == PacketResponderType.HAS_DOWNSTREAM_IN_PIPELINE
                  && seqno != expected) {
                throw new IOException(myString + "seqno: expected=" + expected
                    + ", received=" + seqno);
              }
              ...// 統計相關
              lastPacketInBlock = pkt.lastPacketInBlock;
            }
          } catch (InterruptedException ine) {
            // 記錄異常標記,標誌當前InterruptedException
            isInterrupted = true;
          } catch (IOException ioe) {
            ...// 異常處理
            if (Thread.interrupted()) { // 如果發生了中斷(與本地變數isInterrupted區分),則記錄中斷標記
              isInterrupted = true;
            } else {
              // 這裡將所有異常都標記mirrorError = true不太合理,但影響不大
              mirrorError = true;
              LOG.info(myString, ioe);
            }
          }

          // 中斷退出
          if (Thread.interrupted() || isInterrupted) {
            LOG.info(myString + ": Thread is interrupted.");
            running = false;
            continue;
          }

          // 如果是最後一個packet,將block的狀態轉換為FINALIZED,並關閉BlockReceiver
          if (lastPacketInBlock) {
            finalizeBlock(startTime);
          }

          // 此時,必然滿足 ack.seqno == pkt.seqno,構造新的 ack 傳送給上游
          sendAckUpstream(ack, expected, totalAckTimeNanos,
              (pkt != null ? pkt.offsetInBlock : 0), 
              (pkt != null ? pkt.ackStatus : Status.SUCCESS));
          // 已經處理完隊頭元素,出隊
          // 只有一種情況下滿足pkt == null:PacketResponder#isRunning()返回false,即PacketResponder執行緒正在關閉。此時無論佇列中是否有元素,都不需要出隊了
          if (pkt != null) {
            removeAckHead();
          }
        } catch (IOException e) {
          // 一旦發現IOE,如果不是因為中斷引起的,就中斷執行緒
          LOG.warn("IOException in BlockReceiver.run(): ", e);
          if (running) {
            datanode.checkDiskErrorAsync();
            LOG.info(myString, e);
            running = false;
            if (!Thread.interrupted()) { // failure not caused by interruption
              receiverThread.interrupt();
            }
          }
        } catch (Throwable e) {
          // 其他異常則直接中斷
          if (running) {
            LOG.info(myString, e);
            running = false;
            receiverThread.interrupt();
          }
        }
      }
      LOG.info(myString + " terminating");
    }
    
    ...
    
    // PacketResponder#sendAckUpstream()封裝了PacketResponder#sendAckUpstreamUnprotected()
    private void sendAckUpstreamUnprotected(PipelineAck ack, long seqno,
        long totalAckTimeNanos, long offsetInBlock, Status myStatus)
        throws IOException {
      Status[] replies = null;
      if (ack == null) { // 傳送OOB ack時,要求ack為null,myStatus為OOB。什麼破設計。。。
        replies = new Status[1];
        replies[0] = myStatus;
      } else if (mirrorError) { // 前面置為true的mirrorError,在此處派上用場
        replies = MIRROR_ERROR_STATUS;
      } else {  // 否則,正常構造replies
        short ackLen = type == PacketResponderType.LAST_IN_PIPELINE ? 0 : ack
            .getNumOfReplies();
        replies = new Status[1 + ackLen];
        replies[0] = myStatus;
        for (int i = 0; i < ackLen; i++) {
          replies[i + 1] = ack.getReply(i);
        }
        // 如果下游有ERROR_CHECKSUM,則丟擲IOE,中斷當前節點的PacketResponder執行緒(結合後面的程式碼,能保證從第一個ERROR_CHECKSUM節點開始,上游的所有節點都是ERROR_CHECKSUM的)
        if (ackLen > 0 && replies[1] == Status.ERROR_CHECKSUM) {
          throw new IOException("Shutting down writer and responder "
              + "since the down streams reported the data sent by this "
              + "thread is corrupt");
        }
      }
      
      // 構造replyAck,傳送到上游
      PipelineAck replyAck = new PipelineAck(seqno, replies,
          totalAckTimeNanos);
      if (replyAck.isSuccess()
          && offsetInBlock > replicaInfo.getBytesAcked()) {
        replicaInfo.setBytesAcked(offsetInBlock);
      }
      long begin = Time.monotonicNow();
      replyAck.write(upstreamOut);
      upstreamOut.flush();
      long duration = Time.monotonicNow() - begin;
      if (duration > datanodeSlowLogThresholdMs) {
        LOG.warn("Slow PacketResponder send ack to upstream took " + duration
            + "ms (threshold=" + datanodeSlowLogThresholdMs + "ms), " + myString
            + ", replyAck=" + replyAck);
      } else if (LOG.isDebugEnabled()) {
        LOG.debug(myString + ", replyAck=" + replyAck);
      }

      // 如果當前節點是ERROR_CHECKSUM狀態,則傳送ack後,丟擲IOE
      if (myStatus == Status.ERROR_CHECKSUM) {
        throw new IOException("Shutting down writer and responder "
            + "due to a checksum error in received data. The error "
            + "response has been sent upstream.");
      }
    }
複製程式碼

對於OOB,還要關注PipelineAck#getOOBStatus():

  public Status getOOBStatus() {
    // seqno不等於UNKOWN_SEQNO的話,就一定不是OOB狀態
    if (getSeqno() != UNKOWN_SEQNO) {
      return null;
    }
    // 有任何一個下游節點是OOB,則認為下游管道是OOB狀態(當然,該機制保證從第一個OOB節點開始,在每個節點檢視ack時,都能發現下游有節點OOB)
    for (Status reply : proto.getStatusList()) {
      // The following check is valid because protobuf guarantees to
      // preserve the ordering of enum elements.
      if (reply.getNumber() >= OOB_START && reply.getNumber() <= OOB_END) {
        return reply;
      }
    }
    return null;
  }
複製程式碼

與之前的分支相比,PacketResponder執行緒大量使用中斷來代替拋異常使執行緒終止。除此之外,關於OOB狀態與ERROR_CHECKSUM狀態的處理有些特殊:

  • OOB狀態:將第一個OOB節點的狀態,傳遞到客戶端。OOB是由datanode重啟引起的,因此,第一個OOB節點在傳送OOB的ack後,就不會再傳送其他ack,最終由於引起socket超時引起整個管道的關閉。
  • ERROR_CHECKSUM狀態:只有尾節點可能發出ERROR_CHECKSUM狀態的ack,傳送後丟擲IOE主動關閉PacketResponder執行緒然後上游節點收到ERROR_CHECKSUM狀態的ack後,也將丟擲IOE關閉PacketResponder執行緒,但不再傳送ack;如果還有上游節點,將因為長期收不到ack,socket超時關閉。最終關閉整個管道。

需要注意的,OOB通常能保證傳遞到客戶端;但尾節點傳送的ERROR_CHECKSUM無法保證被上游節點發現(先發ack再拋IOE只是一種努力,不過通常能保證),如果多於兩個備份,則一定不會被客戶端發現。

猴子沒明白為什麼此處要使用中斷使執行緒終止。

總結

儘管總覽中列出了兩種方案,但可以看到,作為異常處理的主要方式,主要還是依靠方案1:拋異常關socket,然後逐級導致管道關閉。

關閉管道後,由客戶端決定後續處理,如資料塊恢復等。


本文連結:原始碼|HDFS之DataNode:寫資料塊(3)
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章