读HDFS书笔记---5.3 文件短路读操作

这一节的目录:

5.3 文件短路读操作

        5.3.1 短路读共享内存

        5.3.2 DataTransferProtocol

        5.3.3 DFSClient短路读操作流程

        5.3.4 Datanode短路读操作流程

在HDFS早期版本中,本地读取和远程读取的实现是一样的,客户端通过TCP套接字连接Datanode,并通过DataTransferProtocol传输数据。这种方式很简单,但是有一些不好的地方,例如Datanode需要为每个读取数据块的客户端都维持一个线程和TCP套接字。内核中TCP协议是有开销的,DataTransferProtocol本身也有开销,因此这种实现方式有值得优化的地方,早起版本读操作流程图如下:

网络读操作示意图
标题网络读操作示意图

既然客户端和Datanode在同一台机器上,那么DFSClient可以跳过Datanode,直接读取磁盘上的数据,目前HDFS的实现方案有两种

<1>、FS-2246

Datanode将所有的数据路径权开发给客户端,当执行一个本地读取时,客户端直接从本地磁盘的数据路径读取数据。但这种实现方式带来了安全问题,客户端用户可以直接浏览所有数据,这会存在一定的不安全性,不是一个好选择,下图是相应的方案流程图

扫描二维码关注公众号,回复: 4420495 查看本文章
HDFS-2246短路读操作示意图
HDFS-2246短路读操作示意图

<2> HDFS-347

UNIX提供了一种UNIX Domain Socket进程间通信方式,它使得同一台机器上的两个进程能以Socket的方式通信,并且还可以在进程间传递文件描述符。

HDFS-347使用该机制实现了安全的本地短路读取,如下图所示:

HDFS-347短路读操作示意图
HDFS-347短路读操作示意图

客户端向Datanode请求数据时,Datanode会打开块文件和校验和文件,将这两个文件的文件描述符直接传递给客户端,而不是将路径传递给客户端。客户端接收到这两个文件的文件描述符之后,就可以直接打开文件读取数据了,也就是绕过了Datanode进程的转发,提高了读取效率。因为文件描述符是只读的,所以客户端不能修改该文件,同时,由于客户端自身无法访问数据块文件所在的目录,所以它也就不能访问其他不该访问的数据了,保证了读取的安全性。HDFS 2.x采取的就是HDFS-347的设计实现短路读取功能的。

5.3.1 短路读共享内存

了解了短路读取的概念之后,我们看一下HDFS是如何实现这种模式的,在DFSClient中,使用ShortCircuitReplica类封装可以进行短路读取的副本。ShortCircuitReplica对象中包含了短路读取副本的数据块文件输入流、校验文件输入流、短路读取副本在共享内存中的槽位(slot)以及副本的引用次数等信息。DFSClient会持有一个ShortCircuitCache对象缓存并管理所有的ShortCircuitReplica对象,DFSClient从ShortCircuitCache获得了ShortCircuitReplica的引用之后,就可以构造BlockReaderLocal对象进行本地读取操作了。

如图5-15所示,当DFSClient和Datanode在同一台机器上时,需要一个共享内存段来维护所有短路读取副本的状态,共享内存段中会有很多槽位,每个槽位都记录了一个短路读取副本的信息,例如当前副本是否有效、锚(anchor)的次数等。当Datanode将一个数据块副本缓存到内存中时,会将这个数据块副本设置为可锚(anchorable)状态,也就是在共享内存中该副本对应的槽位上设置可锚状态位。当一个副本被设置为可锚状态之后,DFSClient的BlockReaderLocal对象读取该副本时就不需要校验了(因为缓存中的副本已经执行过校验操作),并且输入流可以通过零拷贝模式读取这个副本。每当客户端进行这两种读取操作时,都需要在副本对应的槽位上添加一个锚计数,只有副本的锚计数为零时,Datanode才可以从缓存中删除这个副本。可以看到共享内存以及槽位机制很好地在Datanode进程和DFSClient进程间同步了副本的状态,保证了Datanode缓存操作以及DFSClient读取副本操作的正确性。

图5-15 共享内存段的结构
图5-15 共享内存段的结构

如图5-16所示,共享内存机制是由DFSClient和Datanode对同一个文件执行内存映射操作实现的。因为MappedByteBuffer对象能让内存与物理文件的数据实时同步,所以DFSClient和Datanode进程会通过中间文件来交换数据,中间文件使得两个进程的内存区域得到及时的同步。DFSClient和Datanode之间可能会有多段共享内存,所以DFSClient定义了DFSClientShm类抽象DFSClient侧的一段共享内存,定义了DFSClientShmManager类管理所有的DFSClientShm对象,而Datanode则定义了RegisteredShm类抽象Datanode侧的一段共享内存,同时定义了ShortCircuitRegistry类管理所有Datanode侧的共享内存。

图5-16 共享内存机制示意图
图5-16 共享内存机制示意图

DFSClient会调用DataTransferProtocol.requestShortCircuitShm接口与Datanode协商创建一段共享内存,共享内存创建成功后,DFSClient和Datanode会分别构造DFSClientShm以及RegisteredShm对象维护这段共享内存。如图5-17所示,共享内存中的文件映射数据是实时同步的,它保存了所有槽位二进制信息。但是映射数据中二进制的槽位信息并不便于操作,所以DFSClientShm和RegisteredShm会构造一个Slot对象操作映射数据中的一个槽位,同时各自定义了集合字段保存所有的Slot对象。这里需要特别注意的是,Slot对象会由DFSClientShm和RegisteredShm分别构造并保存在各自的集合字段中,所以DFSClientShm和RegisteredShm之间需要同步Slot对象的创建和删除操作,以保证DFSClientShm和RegisteredShm保存的Slot对象信息是完全同步的。DataTransferProtocol接口就提供了requestShortCircuitFds()以及releaseShortCircuitFds()方法同步Slot对象的创建和删除操作。

图5-17 Slot状态同步示意图
图5-17 Slot状态同步示意图

5.3.2 DataTransferProtocol

DataTransferProtocol定义了requestShortCircuitShm()、requestShortCircuitFds()以及releaseShortCircuitFds()三个接口方法同步Datanode和DFSClient对共享内存的操作。

需要注意的是DataTransferProtocol底层是基于Socket流的,而当DFSClient和Datanode在同一台物理机器上时,DataTransferProtocol底层的Socket将会是DomainSocket,使用DomainSocket的DataTransferProtocol可以在Socket流中传递文件描述符。

接下来我们依次看一下requestShortCircuitShm()、requestShortCircuitFds()以及releaseShortCircuitFds()方法的实现。

<1>、requestShortCircuitShm()

代码如下:

@Override
  public void requestShortCircuitShm(String clientName) throws IOException {
    NewShmInfo shmInfo = null;
    boolean success = false;
    DomainSocket sock = peer.getDomainSocket();//获取底层DomainSocket对象
    try {
      if (sock == null) {//如果DataTransferProtocol底层不是DomainSocket,则发回异常
        sendShmErrorResponse(ERROR_INVALID, "Bad request from " +
            peer + ": must request a shared " +
            "memory segment over a UNIX domain socket.");
        return;
      }
      try {
        //调用ShortCircuitRegistry.createNewMemorySegment()方法创建共享内存段
        shmInfo = datanode.shortCircuitRegistry.
            createNewMemorySegment(clientName, sock);
        // After calling #{ShortCircuitRegistry#createNewMemorySegment}, the
        // socket is managed by the DomainSocketWatcher, not the DataXceiver.
        releaseSocket();
      } catch (UnsupportedOperationException e) {//抛出异常,则响应异常消息
        sendShmErrorResponse(ERROR_UNSUPPORTED, 
            "This datanode has not been configured to support " +
            "short-circuit shared memory segments.");
        return;
      } catch (IOException e) {//抛出异常,则响应异常消息
        sendShmErrorResponse(ERROR,
            "Failed to create shared file descriptor: " + e.getMessage());
        return;
      }
      //调用sendShmSuccessResponse()方法将共享内存文件的文件描述符发回客户端
      sendShmSuccessResponse(sock, shmInfo);
      success = true;
    } finally {
      if (ClientTraceLog.isInfoEnabled()) {
        if (success) {
          BlockSender.ClientTraceLog.info(String.format(
              "cliID: %s, src: 127.0.0.1, dest: 127.0.0.1, " +
              "op: REQUEST_SHORT_CIRCUIT_SHM," +
              " shmId: %016x%016x, srvID: %s, success: true",
              clientName, shmInfo.shmId.getHi(), shmInfo.shmId.getLo(),
              datanode.getDatanodeUuid()));
        } else {
          BlockSender.ClientTraceLog.info(String.format(
              "cliID: %s, src: 127.0.0.1, dest: 127.0.0.1, " +
              "op: REQUEST_SHORT_CIRCUIT_SHM, " +
              "shmId: n/a, srvID: %s, success: false",
              clientName, datanode.getDatanodeUuid()));
        }
      }
      if ((!success) && (peer == null)) {
        // If we failed to pass the shared memory segment to the client,
        // close the UNIX domain socket now.  This will trigger the 
        // DomainSocketWatcher callback, cleaning up the segment.
        IOUtils.cleanup(null, sock);
      }
      IOUtils.cleanup(null, shmInfo);
    }
  }

DFSClient在执行任何短路读取操作之前,需要先申请一段共享内存保存短路读取副本的状态。DFSClient会调用DataTransferProtocol.requestShortCircuitShm()方法向Datanode发起申请共享内存的请求,Datanode的DataXceiver.requestShortCircuitShm()方法会响应这个请求。

如上面的代码所示,DataXceiver.requestShortCircuitShm()会调用ShortCircuitRegistry.createNewMemorySegment()方法创建共享内存段,createNewMemorySegment()方法会将共享内存文件映射到Datanode的内存中,然后构造RegisteredShm类管理这段共享内存(请参考ShortCircuitRegistry类小节中的分析)。之后DataXceiver.requestShortCircuitShm()方法会调用sendShmSuccessResponse()方法将共享内存文件的文件描述符通过domainSocket发回客户端。

DFSClient的DfsClientShmManager对象从domainSocket接收了共享内存文件的文件描述符后,会打开共享内存文件并将该文件映射到DFSClient的内存中,之后创建DFSClientShm对象管理这段共享内存(DFSClientShmManager类小节中的分析),并将这个DFSClientShm对象保存在DFSClientShmManager的对应字段中。

<2>、requestShortCircuitFds()

代码如下:

@Override
  public void requestShortCircuitFds(final ExtendedBlock blk,
      final Token<BlockTokenIdentifier> token,
      SlotId slotId, int maxVersion) throws IOException {
    updateCurrentThreadName("Passing file descriptors for block " + blk);
    BlockOpResponseProto.Builder bld = BlockOpResponseProto.newBuilder();
    FileInputStream fis[] = null;
    try {
      if (peer.getDomainSocket() == null) {//如果底层不是DomainSocket,则抛出异常
        throw new IOException("You cannot pass file descriptors over " +
            "anything but a UNIX domain socket.");
      }
      if (slotId != null) {
        boolean isCached = datanode.data.
            isCached(blk.getBlockPoolId(), blk.getBlockId());
        //调用ShortCircuitRegistry.registerSlot()方法在Datanode的共享内存中注册这个Slot对象
        datanode.shortCircuitRegistry.registerSlot(
            ExtendedBlockId.fromExtendedBlock(blk), slotId, isCached);
      }
      try {
        //获取数据块文件以及校验和文件的文件描述符
        fis = datanode.requestShortCircuitFdsForRead(blk, token, maxVersion);
      } finally {
        if ((fis == null) && (slotId != null)) {
          datanode.shortCircuitRegistry.unregisterSlot(slotId);
        }
      }
      //构造响应消息
      bld.setStatus(SUCCESS);
      bld.setShortCircuitAccessVersion(DataNode.CURRENT_BLOCK_FORMAT_VERSION);
    } catch (ShortCircuitFdsVersionException e) {
      bld.setStatus(ERROR_UNSUPPORTED);
      bld.setShortCircuitAccessVersion(DataNode.CURRENT_BLOCK_FORMAT_VERSION);
      bld.setMessage(e.getMessage());
    } catch (ShortCircuitFdsUnsupportedException e) {
      bld.setStatus(ERROR_UNSUPPORTED);
      bld.setMessage(e.getMessage());
    } catch (InvalidToken e) {
      bld.setStatus(ERROR_ACCESS_TOKEN);
      bld.setMessage(e.getMessage());
    } catch (IOException e) {
      bld.setStatus(ERROR);
      bld.setMessage(e.getMessage());
    }
    try {
      //发回成功的响应消息
      bld.build().writeDelimitedTo(socketOut);
      if (fis != null) {
        FileDescriptor fds[] = new FileDescriptor[fis.length];
        for (int i = 0; i < fds.length; i++) {
          fds[i] = fis[i].getFD();
        }
        byte buf[] = new byte[] { (byte)0 };
        //通过DomainSocket将数据块文件和校验和文件的文件描述符发送给客户端
        peer.getDomainSocket().
          sendFileDescriptors(fds, buf, 0, buf.length);
      }
    } finally {
      if (ClientTraceLog.isInfoEnabled()) {
        DatanodeRegistration dnR = datanode.getDNRegistrationForBP(blk
            .getBlockPoolId());
        BlockSender.ClientTraceLog.info(String.format(
            "src: 127.0.0.1, dest: 127.0.0.1, op: REQUEST_SHORT_CIRCUIT_FDS," +
            " blockid: %s, srvID: %s, success: %b",
            blk.getBlockId(), dnR.getDatanodeUuid(), (fis != null)
          ));
      }
      if (fis != null) {
        IOUtils.cleanup(LOG, fis);
      }
    }
  }

<3>、releaseShortCircuitFds()

代码如下:

@Override
  public void releaseShortCircuitFds(SlotId slotId) throws IOException {
    boolean success = false;
    try {
      String error;
      Status status;
      try {
        //释放共享内存中的槽位
        datanode.shortCircuitRegistry.unregisterSlot(slotId);
        error = null;
        status = Status.SUCCESS;
      } catch (UnsupportedOperationException e) {
        error = "unsupported operation";
        status = Status.ERROR_UNSUPPORTED;
      } catch (Throwable e) {
        error = e.getMessage();
        status = Status.ERROR_INVALID;
      }
      //构造响应消息
      ReleaseShortCircuitAccessResponseProto.Builder bld =
          ReleaseShortCircuitAccessResponseProto.newBuilder();
      bld.setStatus(status);
      if (error != null) {
        bld.setError(error);
      }
      //发回响应消息
      bld.build().writeDelimitedTo(socketOut);
      success = true;
    } finally {
      if (ClientTraceLog.isInfoEnabled()) {
        BlockSender.ClientTraceLog.info(String.format(
            "src: 127.0.0.1, dest: 127.0.0.1, op: RELEASE_SHORT_CIRCUIT_FDS," +
            " shmId: %016x%016x, slotIdx: %d, srvID: %s, success: %b",
            slotId.getShmId().getHi(), slotId.getShmId().getLo(),
            slotId.getSlotIdx(), datanode.getDatanodeUuid(), success));
      }
    }
  }

猜你喜欢

转载自blog.csdn.net/weixin_39935887/article/details/84476012