这篇来分析一下HDFS写文件的流程.

首先还是客户端调用DistributedFileSystem类中的方法,写文件调用的是create().

public FSDataOutputStream create(......) throws IOException {
    statistics.incrementWriteOps(1);
    Path absF = fixRelativePart(f);
    return new FileSystemLinkResolver<FSDataOutputStream>() {
      @Override
      public FSDataOutputStream doCall(final Path p)
          throws IOException, UnresolvedLinkException {
        final DFSOutputStream dfsos = dfs.create(getPathName(p), permission,
                cflags, replication, blockSize, progress, bufferSize,
                checksumOpt);
        return dfs.createWrappedOutputStream(dfsos, statistics);
      }
      @Override
      public FSDataOutputStream next(final FileSystem fs, final Path p)
          throws IOException {
        return fs.create(p, permission, cflags, bufferSize,
            replication, blockSize, progress, checksumOpt);
      }
    }.resolve(this, absF);
  }

可以看到还是利用匿名内部类创建一个FileSystemLinkResolver对象,只不过泛型变成了FSDataOutputStream.在doCall()和next()中分别返回一个FSDataOutputStream对象.最后调用FileSystemLinkResolver的resolve().

doCall()最后用"dfs"调用了createWrappedOutputStream(),返回的是HdfsDataOutputStream对象,是FSDataOutputStream的子类.这个"dfs"在读流程的时候我们已经见过了,本质是个DFSClient对象,所以最终的create()还是由DFSClient完成的,所以来看一下DFSClient.create()的一些关键代码:

public DFSOutputStream create(......) throws IOException {
    checkOpen();
     ......
    final DFSOutputStream result = DFSOutputStream.newStreamForCreate(......);
    beginFileLease(result.getFileId(), result);
    return result;
  }

在读流程中也有这个checkOpen(),它的具体作用是看看HDFS是否已经正在运行,如果没有运行,就抛出一个IOException.

void checkOpen() throws IOException {
    if (!clientRunning) {//如果没有在运行
      IOException result = new IOException("Filesystem closed");//创建一个IOException对象
      throw result;//抛出
    }
  }

然后DFSOutputStream调用了一个静态方法newStreamForCreate(),此处就是创建了一个DFSOutputStream对象,也就是实际负责写操作的对象.

FSDataOutputStream封装一个DFSOutputStream对象,该对象负责处理datanode与namenode之间的通信.

beginFileLease()的作用是获得文件的租约,通俗地讲就是对文件的操作权,有了租约才可以对文件进行操作.

DFSClient.create()也是通过RPC对namenode进行调用,最后转化为NamenNodeRpcServer.create().NameNodeRpcServer是提供RPC服务的一个桥梁,最终都是对FSNamesystem中某个方法的调用.此处调用的是FSNamesystem.startFile().而在startFile()内部,又调用了startFileInt(),这个方法内部又调用了startFileInternal(),这就是最终的操作,<权威指南>中提到的

namenode执行各种不同的检查以确保这个文件不存在以及客户端有新建该文件的权利.如果这些检查均通过,namenode就会为创建新文件记录一条记录,否则文件创建失败并向客户端抛出一个IOException.

就由这个方法来全部完成.

其实还有一种情况是追加写入文件,也就是在一个已有数据的块上继续写文件.其调用的是DistributedFileSystem.append(),容易想到,最终也是DFSClient.append()执行的操作.经过RPC调用,最终由FSNamesystem.appendFile()完成操作.

各种准备操作完成后,就该进行写入操作了,从上面我们知道,写入操作是由DFSOutputStream对象完成的.不过写入跟读取不太一样,其中还包含了一个DataStreamer对象.这个对象负责管理<权威指南>中提到的dataQueue(数据队列)与ackQueue(确认队列),其本质是一个以DFSPacket对象为元素的LinkedList.

private final LinkedList<DFSPacket> dataQueue = new LinkedList<DFSPacket>();
private final LinkedList<DFSPacket> ackQueue = new LinkedList<DFSPacket>();

DFSOutputStream并没有提供write(),但是它是FSOutputSummer的子类,而后者提供了write().

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();
    }
    for (int n=0;n<len;n+=write1(b, off+n, len-n)) {}
  }

其for循环中调用了write1(),其中又调用了writeChecksumChunks().主要逻辑是,先判断缓冲区中是否是无数据且写入的数据量达到了缓冲区的大小,全部符合就把缓冲区中的数据作为若干个Chunk发送,然后退出write1(),进入下一轮for循环.若上述条件不符合,就在缓冲区中写数据,写满之后就冲刷出去(flushBuffer()).缓冲区写满后,就调用writeChecksumChunks().该方法内部会把数据写入Packet对象,然后挂载在dataQueue的末尾.如果到了数据块的末尾,就要发一个空的Packet以示结束.每次都会创建一个DataStreamer线程并start(),也就是说,DataStreamer每次只发送一个Packet.

DataStreamer将数据包流式传输到管线中的第一个datanode,该datanode存储数据并发送给下一个datanode,如此循环,最后一个datanode写入完成后,会沿着数据流逆序返回确认数据包(ack),当收到管线中全部datanode的确认数据包后,数据包才会从确认队列中删除.

当前数据块写满之后,就向namanode申请新的数据块,即addBlock().

最终写入完毕后,就调用DFSOutputStream.close().其内部通过RPC向namenode发出了关闭文件的通知,以及放弃租约.然后FSNamesystem调用complete()关闭文件,文件写入最终完成.

同读取文件一样,不可避免的会发生datanode损坏的情况,下面来看看Hadoop如何处理这种情况.

首先关闭管线,确认把队列中的所有数据包添加回数据队列的最前端,以保证故障节点下游的datanode不会漏掉任何一个数据包.为存储在另一个正常datanode的当前数据块指定一个新的标识,并将该标识传递给namenode,以便故障datanode在恢复后可以删除存储的部分数据块.从管线中删除故障datanode,基于其他正常datanode再次构建数据管线.余下的数据继续写入管线中的datanode.之后namenode会注意到这个数据块的复本数不足,会在另一个节点创建一个新的复本.

那么如何判断写入已经成功呢,只要写入了满足属性dfs.namenode.replication.min中设置的数量就认为本次写入成功,不,namenode注意到复本数不足之后,数据块会在集群中异步复制,直到满足最小复本数要求.