1. 背景

在Hadoop Yarn中,App、AppAttempt、Container、Node都有自己的生命周期,因此Yarn实现了一套状态机进行管理。通过状态机的管理后,用户可以直观看到App、AppAttempt、Container、Node的状态,其状态切换也更规范。但是状态机也导致Yarn的代码可能性很差,无法很好调试。

在HDFS中就不需要维护状态机,对于HDFS的操作,只有成功和失败。因此,在代码分析上,更容易对代码进行阅读。

通过https://blog.51cto.com/u_15327484/7995505文章了解到HDFS是GFS之上进行了一些简化,本文在此基础上,分析客户端向DataNode写数据的详细过程,文章最后会总结客户端发送数据的流程。

同时,本文通过代码分析,会解释以下难点:

  1. Datanode响应客户端的数据粒度。是按Block响应?还是按packet响应?还是按chunk响应?
  2. 客户端如何切分block?block在哪里切分成packet?packet哪里切分成chunk?
  3. 一个packet64KB,实际每个chunk写入packet的大小为516Byte,它们不是倍数关系,是不是一个packet没办法装满了?(来自强迫症患者的疑问)

2. HDFS客户端写操作流程概览

  1. 客户端创建DistributedFileSystem,请求Namenode创建文件。
  2. 向FSDataOutputStream写入要发送给DataNode的文件数据。
  3. FSDataOutputStream中,DataStream线程先向Namenode申请创建Block,并根据返回的block位置信息,与datanode建立连接。
  4. DataStream向DataNode发送packet。如果Block写入数据量达到128MB,就跳转到步骤3。

Untitled.png

3. 写流程源码分析

3.1 写操作典型业务代码

进行写操作时,一般业务代码包含以下几步骤:

  1. 设置配置文件。
  2. 根据配置文件创建HDFS客户端对象。
  3. 基于HDFS客户端对象执行write写操作。

如下所示:

public void create() throws URISyntaxException, IOException, InterruptedException {
        // 配置文件
        Configuration conf = new Configuration();
        // 获取文件系统
        FileSystem fs = FileSystem.get(new URI("hdfs://{namenode IP}:9000"), conf, 访问的用户);
        // 创建文件并写入数据
        FSDataOutputStream out = fs.create(new Path("/root/test3.txt"));
        out.write("Hello, HDFS".getBytes());
        out.flush();
        // 关闭流
        fs.close();
    }

3.2 根据URL创建HDFS客户端

FileSystem是一个抽象类,它提供get方法获取文件系统客户端实现类对象。之所以没有直接new一个HDFS客户端,是因为Hadoop提供多种文件系统的访问,例如alluxio、S3a。FileSystem读取url和Configuration即可创建对应的文件系统客户端。Hadoop自带的FileSystem实现类有如下:

Untitled 1.png

本文文档只用于研究HDFS,因此这里FileSystem.get方法访问的是HDFS文件系统的客户端:

public abstract class FileSystem extends Configured
    implements Closeable, DelegationTokenIssuer {

public static FileSystem get(final URI uri, final Configuration conf,
        final String user) throws IOException, InterruptedException {
    String ticketCachePath =
      conf.get(CommonConfigurationKeys.KERBEROS_TICKET_CACHE_PATH);
    UserGroupInformation ugi =
        UserGroupInformation.getBestUGI(ticketCachePath, user);
    return ugi.doAs(new PrivilegedExceptionAction<FileSystem>() {
      @Override
      public FileSystem run() throws IOException {
        return get(uri, conf);
      }
    });
  }
}

由于FileSystem是一个抽象类,返回的对象必须是其子类。get方法负责创建FileSystem子类,如下,调用getFileSystemClass方法,它会获取core-site.xml中fs.hdfs.impl的配置,默认为org.apache.hadoop.hdfs.DistributedFileSystem,后续通过反射创建DistributedFileSystem对象:

private static FileSystem createFileSystem(URI uri, Configuration conf)
      throws IOException {
    Tracer tracer = FsTracer.get(conf);
    try(TraceScope scope = tracer.newScope("FileSystem#createFileSystem")) {
      scope.addKVAnnotation("scheme", uri.getScheme());
      //查询core-site.xml中fs.hdfs.impl的配置,默认是org.apache.hadoop.hdfs.DistributedFileSystem
      Class<?> clazz = getFileSystemClass(uri.getScheme(), conf);
      //通过反射创建FileSystem子类DistributedFileSystem对象
      FileSystem fs = (FileSystem)ReflectionUtils.newInstance(clazz, conf);
      //DistributedFileSystem初始化操作
      fs.initialize(uri, conf);
      return fs;
    }
  }

查看DistributedFileSystem类,可以发现,它包含最核心的成员是DFSClient,DFSClient负责执行真正的请求namenode和datanode读写操作:

public class DistributedFileSystem extends FileSystem
    implements KeyProviderTokenIssuer {
  //工作目录,一般为/user/{USER},类似linux中,hadoop用户的工作目录是/home/hadoop。默认情况下,使用hdfs dfs -ls . 即可访问/user/{User}家目录
  private Path workingDir;
  //集群地址,一般为hdfs://{集群名}
  private URI uri;
  //用来连接namenode和datanode最核心的客户端
  DFSClient dfs;
  //检验数据是否正确写入或读取
  private boolean verifyChecksum = true;
  //客户端审计信息
  private DFSOpsCountStatistics storageStatistics;
}

3.3 客户端请求namenode创建文件

创建好HDFS客户端对象后,业务方此时会调用.create方法新建一个文件以为后续写数据作准备。

在执行create方法创建文件时,有几个非常重要的参数可以留意。

  1. permission:默认情况下,为默认权限与默认umask异或运算。即666-022=644
  2. overwrite:是否覆盖原有有文件,默认为true,即进行覆盖。
  3. bufferSize:写入数据时使用的缓冲区大小。默认情况下读取io.file.buffer.size配置,单位byte,默认4KB。
  4. replication:默认情况下,读取dfs.replication获取文件副本数,默认3。获取默认值的方法由DistributedFileSystem#getDefaultReplication提供。
  5. blockSize:默认情况下:读取file.blocksize获取每个block大小,默认为64MB,可以设置为128MB。获取默认值的方法由DistributedFileSystem#getDefaultBlockSize提供。
  6. favoredNodes:数据块首选节点,默认为空。即不指定首选节点。
public HdfsDataOutputStream create(final Path f,
      final FsPermission permission, final boolean overwrite,
      final int bufferSize, final short replication, final long blockSize,
      final Progressable progress, final InetSocketAddress[] favoredNodes)
      throws IOException {
    statistics.incrementWriteOps(1);
    storageStatistics.incrementOpCounter(OpType.CREATE);
    Path absF = fixRelativePart(f);
    return new FileSystemLinkResolver<HdfsDataOutputStream>() {
      @Override
      public HdfsDataOutputStream doCall(final Path p) throws IOException {
        //创建文件
        final DFSOutputStream out = dfs.create(getPathName(f), permission,
            overwrite ? EnumSet.of(CreateFlag.CREATE, CreateFlag.OVERWRITE)
                : EnumSet.of(CreateFlag.CREATE),
            true, replication, blockSize, progress, bufferSize, null,
            favoredNodes);
        //准备开始写数据
        return dfs.createWrappedOutputStream(out, statistics);
      }
      //省略
    }.resolve(this, absF);
  }

create方法最终调用DFSOutputStream#newStreamForCreate方法,创建文件并启动写入流程:

  1. 通过执行dfsClient.namenode.create()方法向namenode服务端发起请求创建文件。
  2. 创建DFSOutputStream对象,该对象内部有一个DataStreamer线程。向DFSOutputStream写入的数据会发送给DataStreamer线程。
static DFSOutputStream newStreamForCreate(DFSClient dfsClient, String src,
      FsPermission masked, EnumSet<CreateFlag> flag, boolean createParent,
      short replication, long blockSize, Progressable progress,
      DataChecksum checksum, String[] favoredNodes, String ecPolicyName)
      throws IOException {
    try (TraceScope ignored =
             dfsClient.newPathTraceScope("newStreamForCreate", src)) {
      HdfsFileStatus stat = null;

      // Retry the create if we get a RetryStartFileException up to a maximum
      // number of times
      boolean shouldRetry = true;
      int retryCount = CREATE_RETRY_COUNT;
      while (shouldRetry) {
        shouldRetry = false;
        try {
          //创建文件
          stat = dfsClient.namenode.create(src, masked, dfsClient.clientName,
              new EnumSetWritable<>(flag), createParent, replication,
              blockSize, SUPPORTED_CRYPTO_VERSIONS, ecPolicyName);
          break;
      //省略
      }
      final DFSOutputStream out;
      if(stat.getErasureCodingPolicy() != null) {
        out = new DFSStripedOutputStream(dfsClient, src, stat,
            flag, progress, checksum, favoredNodes);
      } else {
        out = new DFSOutputStream(dfsClient, src, stat,
            flag, progress, checksum, favoredNodes, true);
      }
      //启动发送数据的线程
      out.start();
      return out;
    }
  }

首先,查看创建文件的请求流程。dfsClient.namenode成员类型实际上是客户端与Namenode的协议ClientProtocol,通过Haddoop RPC可以知道,要想访问namenode,必须创建ClientProtocol的代理对象。

在DFSClient的对象创建过程汇中,可以看到,代理对象的创建是在NameNodeProxiesClient#createProxyWithLossyRetryHandler方法中,它指定创建ClientProtocol.class的代理对象:

public class DFSClient implements java.io.Closeable, RemotePeerFactory,
    DataEncryptionKeyFactory, KeyProviderTokenIssuer {
final ClientProtocol namenode;
ProxyAndInfo<ClientProtocol> proxyInfo = null;
proxyInfo = NameNodeProxiesClient.createProxyWithLossyRetryHandler(conf,
          nameNodeUri, ClientProtocol.class, numResponseToDrop,
          nnFallbackToSimpleAuth);
this.namenode = proxyInfo.getProxy();
}

进入NameNodeProxiesClient,它先创建ProxyProvider,再由ProxyProvider创建代理对象:


AbstractNNFailoverProxyProvider<T> failoverProxyProvider =
        createFailoverProxyProvider(config, nameNodeUri, xface, true,
            fallbackToSimpleAuth);
T proxy = (T) Proxy.newProxyInstance(
          failoverProxyProvider.getInterface().getClassLoader(),
          new Class[]{xface}, dummyHandler);

ProxyProvider的实现类由hdfs-site.xml中的dfs.client.failover.proxy.provider.{集群名}配置指定,默认是org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider。

在ConfiguredFailoverProxyProvider中,创建了active namenode和standby namenode两个proxy对象。当通过RetryProxy创建的代理对象执行rpc失败后,会通过performFailover方法切换到另外一个proxy对象,这涉及到Java Proxy机制,不详细研究。

protected final List<NNProxyInfo<T>> proxies;

ConfiguredFailoverProxyProvider最终调用NameNodeProxiesClient#createProxyWithAlignmentContext创建proxy对象,如下,最终的基于ClientNamenodeProtocolPB创建proxy,后续就是protobuf生成代码,不详细研究:

public static ClientProtocol createProxyWithAlignmentContext(
      InetSocketAddress address, Configuration conf, UserGroupInformation ugi,
      boolean withRetries, AtomicBoolean fallbackToSimpleAuth,
      AlignmentContext alignmentContext)
      throws IOException {
    RPC.setProtocolEngine(conf, ClientNamenodeProtocolPB.class,
        ProtobufRpcEngine.class);

    final RetryPolicy defaultPolicy =
        RetryUtils.getDefaultRetryPolicy(
            conf,
            HdfsClientConfigKeys.Retry.POLICY_ENABLED_KEY,
            HdfsClientConfigKeys.Retry.POLICY_ENABLED_DEFAULT,
            HdfsClientConfigKeys.Retry.POLICY_SPEC_KEY,
            HdfsClientConfigKeys.Retry.POLICY_SPEC_DEFAULT,
            SafeModeException.class.getName());

    final long version = RPC.getProtocolVersion(ClientNamenodeProtocolPB.class);
    ClientNamenodeProtocolPB proxy = RPC.getProtocolProxy(
        ClientNamenodeProtocolPB.class, version, address, ugi, conf,
        NetUtils.getDefaultSocketFactory(conf),
        org.apache.hadoop.ipc.Client.getTimeout(conf), defaultPolicy,
        fallbackToSimpleAuth, alignmentContext).getProxy();

    if (withRetries) { // create the proxy with retries
      Map<String, RetryPolicy> methodNameToPolicyMap = new HashMap<>();
      ClientProtocol translatorProxy =
          new ClientNamenodeProtocolTranslatorPB(proxy);
      return (ClientProtocol) RetryProxy.create(
          ClientProtocol.class,
          new DefaultFailoverProxyProvider<>(ClientProtocol.class,
              translatorProxy),
          methodNameToPolicyMap,
          defaultPolicy);
    } else {
      return new ClientNamenodeProtocolTranslatorPB(proxy);
    }
  }

基于协议,namenode会返回文件的元数据给client。比如:路径、权限、拥有者、组、大小、修改时间,block位置信息。详细元数据如下所示:

    private long length                    = 0L;
    private boolean isdir                  = false;
    private int replication                = 0;
    private long blocksize                 = 0L;
    private long mtime                     = 0L;
    private long atime                     = 0L;
    private FsPermission permission        = null;
    private EnumSet<Flags> flags           = EnumSet.noneOf(Flags.class);
    private String owner                   = null;
    private String group                   = null;
    private byte[] symlink                 = null;
    private byte[] path                    = EMPTY_NAME;
    private long fileId                    = -1L;
    private int childrenNum                = 0;
    private FileEncryptionInfo feInfo      = null;
    private byte storagePolicy             =
        HdfsConstants.BLOCK_STORAGE_POLICY_ID_UNSPECIFIED;
    private ErasureCodingPolicy ecPolicy   = null;
    private LocatedBlocks locations        = null;

在客户端创建完Namenode代理对象后,会启动DFSOutputStream中DataStreamer线程。客户端会将数据通过该线程向DataNode中进行写入。将在下一节中详细介绍DataStreamer线程:

     out = new DFSOutputStream(dfsClient, src, stat,
            flag, progress, checksum, favoredNodes, true);
      //启动发送数据的线程
      out.start();

3.4 客户端向datanode发送数据

💡 在了解写入流程前,需要知道数据的三个单位:

  1. block:对于一个逻辑上的大文件首先划分为block,大文件按block为单位存储到datanode中。block大小在客户端创建文件时指定,它读取dfs.block.size配置,默认为64MB,建议128MB。
  2. packet:对于一个block的读写,会讲block划分成为packet进行传输,它读取dfs.client-write-packet-size配置,默认64KB。划分packet好处是,当传输异常时,重新传输只需要重传64KB的packet,而不是128MB的block,效率更高。
  3. chunk:对于64KB的packet,为了保证传输过程中不出现数据传输错误的问题,每512字节的业务数据会生成4字节的checkSum校验数据,它们一起传输到DataNode中。数据与检验值的比值为128:1,所以对于一个128M的block会有一个1M的校验文件与之对应。备注:在datanode中,checkSum校验数据会保存在*.meta文件中。

创建好客户端代理对象后,业务代码就开始向datanode发送数据了。如下所示:

FSDataOutputStream out = fs.create(new Path("/root/test3.txt"));
//开始写入数据
out.write("Hello, HDFS".getBytes());
out.flush();

FSDataOutputStream.write最终会调用其内部对象DFSOutputStream.write写入数据,它会调用FSOutputSummer#write1方法开始写入业务数据到输出流。其中:

  1. 输入参数byte b[]是要写到datanode中的业务数据。
  2. 业务数据通过write1方法是要写到chunk中的,chunk实际上就是一个byte类型的数组的成员buf,它的大小设置为512*9。即一次写入9个chunk,避免多次系统调用。
  3. 每个chunk有一个4字节的checksum,由成员变量checksum表示,每次写入5129字节的业务数据,都会生成49字节的checksum写入到packet中。
  4. 写完9个chunk后如果业务数据还没写完,继续调用write1方法写入,知道业务数据全部写完。

public synchronized void write(byte b[], int off, int len)
      throws IOException {
    
    checkClosed();
    
    if (off < 0 || len < 0 || off > b.length - len) {
      throw new ArrayIndexOutOfBoundsException();
    }
    //如果write1还没有写完业务数据,则继续写
    for (int n=0;n<len;n+=write1(b, off+n, len-n)) {
    }
  }

this.buf = new byte[sum.getBytesPerChecksum() * BUFFER_NUM_CHUNKS];
this.checksum = new byte[getChecksumSize() * BUFFER_NUM_CHUNKS];

private int write1(byte b[], int off, int len) throws IOException {
    //如果业务数据大于512 * 9,就直接写到输出流程
    if(count==0 && len>=buf.length) {
      // local buffer is empty and user buffer size >= local buffer size, so
      // simply checksum the user buffer and send it directly to the underlying
      // stream
      final int length = buf.length;
      writeChecksumChunks(b, off, length);
      return length;
    }
    //如果业务数据小于512 * 9,将业务数据拷贝到buffer中,再flushBuffer,避免数组越界
    // copy user data to local buffer
    int bytesToCopy = buf.length-count;
    bytesToCopy = (len<bytesToCopy) ? len : bytesToCopy;
    System.arraycopy(b, off, buf, count, bytesToCopy);
    count += bytesToCopy;
    if (count == buf.length) {
      // local buffer is full
      flushBuffer();
    } 
    return bytesToCopy;
  }

如下,开始调用writeChunk方法循环写入chunk。注意,此时指定的len参数为buf的大小,即512*9,后续的业务数据本次调用不会处理。

  1. 它先生成9个chunk对应的9个checkSum,写入到checksum数组中。
  2. 将9个chunk和9个checksum作为参数,准备向packet中写数据。
private void writeChecksumChunks(byte b[], int off, int len)
  throws IOException {
    //生成9个chunk对应的9个checkSum,写入到checksum数组中
    sum.calculateChunkedSums(b, off, len, checksum, 0);
    TraceScope scope = createWriteTraceScope();
    try {
      for (int i = 0; i < len; i += sum.getBytesPerChecksum()) {
        int chunkLen = Math.min(sum.getBytesPerChecksum(), len - i);
        int ckOffset = i / sum.getBytesPerChecksum() * getChecksumSize();
        //将9个chunk和9个checksum作为参数,准备向packet中写数据
        writeChunk(b, off + i, chunkLen, checksum, ckOffset,
            getChecksumSize());
      }
    } finally {
      if (scope != null) {
        scope.close();
      }
    }
  }

调用FSDataOutputStream#writeChunk向packet中写chunk。首先会通过createPacket方法创建一个packect。同时,指定packet参数,有几个非常重要:

  1. packetSize:读dfs.client-write-packet-size配置,默认64KB。
  2. chunksPerPacket:每个packet能够写入的chunk数量。通过computePacketChunkSize方法计算。首先packet要给header预留PKT_MAX_HEADER_LEN大小,其次,每个chunk实际上是chunksize+checksum=516。因此每个packet能够写入的chunk数为(64KB-PKT_MAX_HEADER_LEN)/516
  3. BytesCurBlock:block大小,配置中设置为128MB。
  4. Seqno:当前packet编号。

currentPacket = createPacket(packetSize, chunksPerPacket, getStreamer()
          .getBytesCurBlock(), getStreamer().getAndIncCurrentSeqno(), false);

protected void computePacketChunkSize(int psize, int csize) {
    final int bodySize = psize - PacketHeader.PKT_MAX_HEADER_LEN;
    final int chunkSize = csize + getChecksumSize();
    chunksPerPacket = Math.max(bodySize/chunkSize, 1);
    packetSize = chunkSize*chunksPerPacket;
    DFSClient.LOG.debug("computePacketChunkSize: src={}, chunkSize={}, "
            + "chunksPerPacket={}, packetSize={}",
        src, chunkSize, chunksPerPacket, packetSize);
  }

writeChunk写入chunk到packet中。注意,每次写完checksum和data后,incBytesCurBlock只在DataStream中记录增加512字节而不是516字节。这说明checksum不会和业务数据保存在datanode的同一个文件中:

protected synchronized void writeChunk(byte[] b, int offset, int len,
      byte[] checksum, int ckoff, int cklen) throws IOException {
    //创建packet
    writeChunkPrepare(len, ckoff, cklen);
    //packet写入4字节checksum
    currentPacket.writeChecksum(checksum, ckoff, cklen);
    //packet写入512字节业务数据
    currentPacket.writeData(b, offset, len);
    //packet中的chunk数+1
    currentPacket.incNumChunks();
    //当前block写入的大小增加512
    getStreamer().incBytesCurBlock(len);
    //如果写入的chunk数量达到每个packet的上限,将packet加入到DataStreamer队列dataQueue中
    // If packet is full, enqueue it for transmission
    if (currentPacket.getNumChunks() == currentPacket.getMaxChunks() ||
        getStreamer().getBytesCurBlock() == blockSize) {
      enqueueCurrentPacketFull();
    }
  }

在创建HDFS客户端代理对象后,我们看到了它启动了DFSOutputStream中的一个线程:

     out = new DFSOutputStream(dfsClient, src, stat,
            flag, progress, checksum, favoredNodes, true);
      //启动发送数据的线程
      out.start();

该线程其实就是DataStreamer,它有以下几个重要步骤:

  1. 向Namenode申请创建HDFS文件的block。
  2. 构建DataNode数据管道。
  3. 从DFSOutputStream的dataQueue中获取一个packet。
  4. 将packet发送给DataNode。
  5. 将packet加入到ackQueue队列中。
public void run() {
    while (!closed && clientRunning) {
      Packet one = null;
     //省略
	  
	  // get new block from namenode.
	  // 从namenode处申请获得新的数据块
	  if (stage == BlockConstructionStage.PIPELINE_SETUP_CREATE) { // create新建文件写入模式
		setPipeline(nextBlockOutputStream());
		initDataStreaming();
	  } else if (stage == BlockConstructionStage.PIPELINE_SETUP_APPEND) { // append追加写模式
		setupPipelineForAppendOrRecovery();
		initDataStreaming();
	  }
      
	  // send the packet
	  // 将packet从dataQueue移至ackQueue,等待确认;数据包发送前准备
	  synchronized (dataQueue) {
		// move packet from dataQueue to ackQueue
		if (!one.isHeartbeatPacket()) {
		  dataQueue.removeFirst();
      //将packet加入到ackQueue中
		  ackQueue.addLast(one);
		  dataQueue.notifyAll();
		}
	  }
	  // write out data to remote datanode
	  try {
		// 利用生成的写入流将数据写入DataNode中的block
		one.writeTo(blockStream);
		blockStream.flush();   
	  } catch (IOException e) {
	  }
     
    //省略
	closeInternal();
  }

上述流程中,可以仔细看一下申请block并构建数据管道的流程,写入流程直接调用write方法,比较简单。在DataStreamer#nextBlockOutputStream中申请block并建立datanode的连接:

protected LocatedBlock nextBlockOutputStream() throws IOException {
    LocatedBlock lb;
    //省略
      lb = locateFollowingBlock(
          excluded.length > 0 ? excluded : null, oldBlock);
      //省略
      success = createBlockOutputStream(nodes, nextStorageTypes, nextStorageIDs,
          0L, false);
     //省略
    return lb;
  }

通过方法,最终调用DFSOutputStream#addBlock请求创建block:

return dfsClient.namenode.addBlock(src, dfsClient.clientName, prevBlock,
            excludedNodes, fileId, favoredNodes, allocFlags);

在createBlockOutputStream方法中,根据block位置,向datanode建立连接,发送socket请求:

boolean createBlockOutputStream(DatanodeInfo[] nodes,
      StorageType[] nodeStorageTypes, String[] nodeStorageIDs,
      long newGS, boolean recoveryFlag) {
        //省略
        //与datanode创建socket连接
        s = createSocketForPipeline(nodes[0], nodes.length, dfsClient);
        //省略
        // send the request
        //向datanode发送writeBlock的请求
        new Sender(out).writeBlock(blockCopy, nodeStorageTypes[0], accessToken,
            dfsClient.clientName, nodes, nodeStorageTypes, null, bcs,
            nodes.length, block.getNumBytes(), bytesSent, newGS,
            checksum4WriteBlock, cachingStrategy.get(), isLazyPersistFile,
            (targetPinnings != null && targetPinnings[0]), targetPinnings,
            nodeStorageIDs[0], nodeStorageIDs);
        //
        // receive ack for connect
        BlockOpResponseProto resp = BlockOpResponseProto.parseFrom(
            PBHelperClient.vintPrefixed(blockReplyStream));
        //省略

  }

创建完连接后,启动ResponseProcessor线程用于处理datanode对于packet的响应:

private void initDataStreaming() {
    response = new ResponseProcessor(nodes);
    response.start();
    stage = BlockConstructionStage.DATA_STREAMING;
  }

最后,ResponseProcessor线程获取获取Datanode响应,如果Datanode返回失败信息,直接在catch中修改errorState状态,会导致DataStreamer直接线程终止;否则DataStreamer正常执行:

public void run() {
  ...
  PipelineAck ack = new PipelineAck();
  while (!closed && clientRunning && !lastPacketInBlock) {
    try {
      // read an ack from the pipeline
      ack.readFields(blockReplyStream);
      ...
      // 处理所有DataNode响应的状态
      for (int i = ack.getNumOfReplies()-1; i >=0 && clientRunning; i--) {
          short reply = ack.getReply(i);  
        // ack验证,如果DataNode写入packet失败,则出错    
        if (reply != DataTransferProtocol.OP_STATUS_SUCCESS) {
          // 记录损坏的DataNode,会在processDatanodeError方法移除该失败的DataNode
          errorIndex = i;
          throw new IOException("Bad response " + reply + " for block " + block +  " from datanode " + targets[i].getName());    
        }   
      }
 
      //省略
        one = ackQueue.getFirst();
      //省略
      synchronized (ackQueue) {
        // 验证ack
        assert ack.getSeqno() == lastAckedSeqno + 1;
        lastAckedSeqno = ack.getSeqno();
        // 移除确认写入成功的packet
        ackQueue.removeFirst();
        ackQueue.notifyAll();
      }
    } catch (Exception e) {
            lastException.set(e);
            errorState.setInternalError();
            errorState.markFirstNodeIfNotMarked();
            synchronized (dataQueue) {
              dataQueue.notifyAll();
            }
    }
  }
}

DataStreamer在循环时,会判断errorState状态,如果有一场,直接终止DataStreamer线程:

private boolean shouldStop() {
    return streamerClosed || errorState.hasError() || !dfsClient.clientRunning;
  }

packet写入DataNode线程模型如下所示:

Untitled 2.png

4. 写流程总结

  1. 业务方调用FileSystem.get方法获取URL对应的文件系统客户端对象。它获取hdfs://{集群名}的URL前缀hdfs,拼接成配置项fs.hdfs.impl,从core-site.xml中获取该配置,默认是org.apache.hadoop.hdfs.DistributedFileSystem,通过反射创建DistributedFileSystem对象并返回。
  2. 业务方调用DistributedFileSystem.create方法创建HDFS文件。它携带HDFS的副本数、权限、block大小等参数对文件规格进行设置。实际上,它是读取hdfs-site.xml中的dfs.client.failover.proxy.provider.{集群名}配置,默认为org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider。由ConfiguredFailoverProxyProvider创建每个Namenode客户端代理对象,这样一个Namenode挂掉,还可以访问另外一个。通过该代理对象访问Namenode,并创建文件。创建完HDFS文件后,启动DataStreamer线程,准备接受数据并发送给DataNode。
  3. 业务方调用FSDataOutputStream.write开始写数据。FSDataOutputStream创建一个5129大小的数组和49大小的数组,前者表示有9个chunk,每个chunk大小为512字节,每个chunk会生成4字节的校验数据checkSum,后者数组中存储的是每个chunk对应的checkSum。FSDataOutputStream循环读取512*9大小的业务数组,并声成对应的checkSum数据,分别写入到上述两个数组中。
  4. FSDataOutputStream创建一个packet,默认packet大小是64KB。指定单个packet可以容纳chunk数量上限是(64KB-PKT_MAX_HEADER_LEN)/516,持续写入chunk和checkSum到packet中,知道达到可容纳chunk数量上限。然后将packet加入到DataStreamer的dataQueue队列中。
  5. DataStreamer线程持续获取dataQueue队列中的packet,准备发送给datanode。在发送datanode之前,按需向Namenode申请创建block,成功后与datanode建立socket连接并发送writeBlock请求,后续就写入block了。同时,启动ResponseProcessor建成检查datanode是否正确存储packet,如果失败,就会导致datastreamr写入流程终止,即客户端写数据流程终止。
  6. 当一个packet处理完后,跳转到第三步继续写入chunk数据。

5. 疑问解答

  1. Datanode响应客户端的数据粒度。是按Block响应?还是按packet响应?还是按chunk响应?
    • 答:DataNode按packet向客户端进行响应。
  2. 客户端如何切分block?block在哪里切分成packet?packet哪里切分成chunk?
    • 答:对于业务写入的数据,每次FSDataOutputStream只会获取512*9的数据,复制到空字节数组中,这个过程可以看作是切分chunk过程。将chunk字节数组内容复制到packet的字节数组中,这个过程可以看作是切分packet过程。当客户端传输的packet大小总共超过block大小,就会新建一个block,这个过程可以看作是切分block过程。
  3. 一个packet64KB,实际每个chunk写入packet的大小为516Byte,它们不是倍数关系,是不是一个packet没办法装满了?(来自强迫症患者的疑问)
    • 答:packet按照chunk数量来接收chunk数据,chunk上限为(64KB-PKT_MAX_HEADER_LEN)/516