Hadoop源码分析之客户端向HDFS写数据
Hadoop源码分析之客户端向HDFS写数据
在上一篇博文中分析了客户端从HDFS读取数据的过程,下面来看看客户端是怎么样向HDFS写数据的,下面的代码将本地文件系统中/home/hadoop/input目录下的文件写入到本地搭建的HDFS的/test文件中,代码如下:import java.io.IOException; import java.net.URI; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; /** * Hadoop版本1.2.1 * 系统ubuntu 12.04 * JDK 1.7 */ public class PutMerge { public static void main(String[] args) throws IOException { Configuration conf = new Configuration(); Path inputDir = new Path("/home/hadoop/input"); String serverPath = "hdfs://localhost:9000/test"; Path hdfsfile = new Path(serverPath); FileSystem hdfs = FileSystem.get(URI.create(serverPath), conf); //根据上面的serverPath,获取到的是一个org.apache.hadoop.hdfs.DistributedFileSystem对象 FileSystem local = FileSystem.getLocal(conf); FileStatus[] status = local.listStatus(inputDir); FSDataOutputStream out = hdfs.create(hdfsfile); for(int i = 0; i < status.length; i++) { FSDataInputStream in = local.open(status[i].getPath()); byte buffer[] = new byte[256]; int byteread = 0; while((byteread = in.read(buffer)) > 0) { out.write(buffer); } in.close(); } out.close(); } }
在上面的代码中,与从HDFS中读取数据的代码类似,首先得到代表HDFS的FileSystem类的一个对象(即org.apache.hadoop.hdfs.DistributedFileSystem类的对象),与从HDFS中读取数据不同的是,这里是调用FileSystem.create()方法创建一个输出流FSDataOutputStream类对象,这个对象将数据流管道中的数据写入到HDFS中的hdfsfile的文件中。
HDFS客户端的输出流的类继承结构
HDFS客户端处理输出流的类是org.apache.hadoop.hdfs.DFSClient.DFSOutputStream,这个类用于向HDFS文件写入数据,继承自类org.apache.hadoop.fs.FSOutputSummer,这个类提供在流上计算校验和并输出的能力,可以看到FSOutputSummer是java.io.OutputStream的子类。
在上面的PutMerge类的代码中,调用FileSystem.create()方法返回的是一个FSDataOutputStream对象,所以这里与HDFS客户端输入流类继承结构类似,也是使用的装饰器模式,在FSDataOutputStream中有一个OutputStream对象的引用,FSDataOutputStream的与数据流输出相关的处理由OutputStream对象完成。
与HDFS客户端输入流类对比,发现客户端的类中多了一个DFSDataInputStream类,那么为什么输出流中没有DFSDataOutputStream?从DFSDataInputStream的源代码中可以看到,这个类中定义了四个方法,四个方法的作用分别是获取当前正在读的数据节点(DataNode),获取当前读的数据块,获取已定位到的所有数据块和获取文件长度,这些信息在读HDFS中的数据时可能会用到,但是向HDFS写数据时,数据是按照数据流管道写的,这个过程中不需要知道除数据流管道第一个数据节点之外的其他数据节点的信息,客户端只需要将数据写入到第一个数据节点中,剩下的事情由数据节点完成,对比DFSDataInputStream,没有DFSDataOutputStream类的原因可能是这样的。
HDFS客户端的输出流类继承结构相对输入流,少了DFSDataInputStream,但是在写数据的过程中,涉及多个线程,所以还是比较复杂的。
HDFS客户端写数据的流程
向HDFS中写入数据,首先需要创建一个数据流输出管道,这个数据流输出管道将数据输出到HDFS集群中。在上面的代码中,hdfs.create(hdfsfile)创建了一个FSDataOutputStream对象,这个对象就是一个输出管道,通过这个管道可以将数据写入HDFS集群。
对上面的代码中,调用FileSystem.create()会通过调用DistributedFileSystem.create()方法来实现,该方法代码如下:
public FSDataOutputStream create(Path f, FsPermission permission, boolean overwrite, int bufferSize, short replication, long blockSize, Progressable progress) throws IOException { statistics.incrementWriteOps(1); //创建一个FSDataOutputStream对象返回,FSDataOutputStream的第一个参数是OutputStream引用 return new FSDataOutputStream (dfs.create(getPathName(f), permission, overwrite, true, replication, blockSize, progress, bufferSize), statistics); }
参数f表示将要在HDFS中建立的文件路径,permission表示客户端的权限信息,overwrite如果为true,则同名文件将被覆盖,如果为false,则有同名文件会抛出错误,bufferSize表示写文件过程中的缓存大小,replication表示文件写入到HDFS中后拥有的副本数量,blockSize表示数据块大小,progress用于客户端向HDFS报告进度。
该方法首先通过DFSClient.create()方法创建一个DFSOutputStream对象,然后使用FSDataOutputStream的构造方法创建目标文件的输出流管道对象返回给客户端,这样客户端就可以通过这个管道向HDFS写入数据了。DFSClient.create()共有7个重载方法,对任何一个方法的调用都会调用下面这个方法:
public OutputStream create(String src,FsPermission permission,boolean overwrite, boolean createParent, short replication,long blockSize, Progressable progress, int buffersize) throws IOException { checkOpen();//检查DFSClient是否关闭 if (permission == null) { permission = FsPermission.getDefault(); } FsPermission masked = permission.applyUMask(FsPermission.getUMask(conf)); LOG.debug(src + ": masked=" + masked); final DFSOutputStream result = new DFSOutputStream(src, masked, overwrite, createParent, replication, blockSize, progress, buffersize, conf.getInt("io.bytes.per.checksum", 512)); beginFileLease(src, result); return result; }
参数src是目标文件的路径的字符串表示,因为HDFS中的文件的路径在NameNode中是使用/分隔的字符串表示的,参数createParent如果为true,则会创建不存在的父目录。在这个方法中首先检查DFSClient是否关闭,然后通过DFSOutputStream的构造方法创建了一个DFSOutputStream对象,最后,通过beginFileLease()方法开启客户端向HDFS写文件的租约。
在DFSOutputStream类中定义了3个内部类,分别是Packet,DataStreamer和ResponseProcessor,其中Packet是在客户端发往HDFS集群过程中的数据包的抽象,同时也用于构造数据包的特殊形式-心跳包,DataStreamer用于在数据流管道中向DataNode发送数据包。它从NameNode上检索新的blockid和block的位置,然后开始向DataNode组成的数据流管道中发送数据包。每个数据包都有一个序列号。当一个数据块的所有的数据包,都发送完毕并且每个数据包的确认消息都被接收,那么DataStreamer就关闭当前的数据块。ResponseProcessor类用于发送客户端向DataNode发送数据包后的响应处理,即DataNode接收到数据包后,会向客户端发送一个确认,ResponseProcessor就是用来处理这些确认信息的。DataStreamer和ResponseProcessor分别代表单独的线程。
DFSOutputStream中定义了一些成员变量,用于与HDFS中的DataNode进行通信,这些变量如下:
class DFSOutputStream extends FSOutputSummer implements Syncable { /**与HDFS中的DataNode节点进行网络通信**/ private Socket s; /**标识数据输出流是否关闭**/ boolean closed = false; /**文件名**/ private String src; /**用于写出数据流**/ private DataOutputStream blockStream; /**响应流**/ private DataInputStream blockReplyStream; private Block block; private Token<BlockTokenIdentifier> accessToken; /**数据块大小**/ final private long blockSize; private DataChecksum checksum; /**保存了输出流中要发送的数据包**/ private LinkedList<Packet> dataQueue = new LinkedList<Packet>(); /**保存已经发送的,但是还没有收到确认的数据包**/ private LinkedList<Packet> ackQueue = new LinkedList<Packet>(); /**当前数据包**/ private Packet currentPacket = null; private int maxPackets = 80; // each packet 64K, total 5MB // private int maxPackets = 1000; // each packet 64K, total 64MB private DataStreamer streamer = new DataStreamer();; private ResponseProcessor response = null; private long currentSeqno = 0; private long lastQueuedSeqno = -1; /**当前收到应答的最后一个数据包**/ private long lastAckedSeqno = -1; /**在当前数据块中的位置**/ private long bytesCurBlock = 0; // bytes writen in current block /**数据包的大小**/ private int packetSize = 0; // write packet size, including the header. private int chunksPerPacket = 0; /**当前数据块要输出到的数据节点**/ private DatanodeInfo[] nodes = null; // list of targets for current block /**存放创建数据流管道时失败的DataNode节点,将这个DataNode排除在外,以免再次访问到故障节点**/ private ArrayList<DatanodeInfo> excludedNodes = new ArrayList<DatanodeInfo>(); /**是否出现错误或者异常**/ private volatile boolean hasError = false; /**创建数据流管道时标识哪个DataNode出错**/ private volatile int errorIndex = 0; /**保存调用过程中最后出现的一个异常**/ private volatile IOException lastException = null; private long artificialSlowdown = 0; private long lastFlushOffset = 0; // offset when flush was invoked private boolean persistBlocks = false; // persist blocks on namenode private int recoveryErrorCount = 0; // number of times block recovery failed private int maxRecoveryErrorCount = 5; // try block recovery 5 times private volatile boolean appendChunk = false; // appending to existing partial block private long initialFileSize = 0; // at time of file open /**用于向HDFS报告进度**/ private Progressable progress; /**文件的数据块副本数**/ private short blockReplication; // replication factor of file
其中部分变量会在构造DFSOutputStream对象是初始化,而有些则是在与DataNode通信的过程中会不断的改变,具体会在下面详细分析,在DFSClient.create()方法中调用的DFSOutputStream构造方法的代码如下:
DFSOutputStream(String src, FsPermission masked, boolean overwrite, boolean createParent, short replication, long blockSize, Progressable progress, int buffersize, int bytesPerChecksum) throws IOException { //初始化成员变量 this(src, blockSize, progress, bytesPerChecksum, replication); computePacketChunkSize(writePacketSize, bytesPerChecksum); try { //旧版本的HDFS集群的create()方法不含createParent参数,使用createParent参数来兼容就版本 //create()方法在NameNode上创建一个新的处于构建状态的文件 if (createParent) { namenode.create( src, masked, clientName, overwrite, replication, blockSize); } else { namenode.create( src, masked, clientName, overwrite, false, replication, blockSize); } } catch(RemoteException re) { throw re.unwrapRemoteException(AccessControlException.class,FileAlreadyExistsException.class, FileNotFoundException.class,NSQuotaExceededException.class,DSQuotaExceededException.class); } streamer.start(); }
这个DFSOutputStream构造方法的最后一个参数代表多少字节计算一次校验和,默认是512字节的数据计算一次校验和。在方法中首先调用另一个构造方法初始化成员变量,然后调用方法computePacketChunkSize来计算发往数据节点的数据包能包含多少校验块,以及包的长度,一般来说数据包最大能达到64K字节。HDFS传输数据时使用的校验方式有两种选择,一种是空校验,即不对数据进行校验,一种是CRC32校验,关于CRC校验方法参考http://zh.wikipedia.org/wiki/%E5%BE%AA%E7%8E%AF%E5%86%97%E4%BD%99%E6%A0%A1%E9%AA%8C。一般选择使用CRC32校验方式。
然后调用NameNode节点上的IPC方法create()来创建文件,即按照给定的src路径,在NameNode的目录树结构中创建一个文件,同时传入这个文件的数据块大小,副本数等参数信息,这个远程方法执行完之后,在NameNode的目录树中就创建好了一个条目,在NameNode上创建文件成功后,就启动DataStreamer线程,向HDFS集群中写入数据。
在DFSOutputStream的构造方法的执行完之后,返回到DFSClient.create()方法,执行beginFileLease(src, result);这行语句,获取写文件的租约。对于HDFS来说,租约是NameNode节点给予租约持有者在规定时间内一定权限(写文件)的合同。NameNode节点通过租约管理器管理管理租约,客户端在写文件过程中,需要定期更新租约,否则,租约过期后,NameNode节点会通过租约恢复机制关闭文件。
至此,就完成了HDFS集群中文件的创建,接下来就是获得保存文件的数据块的DataNode节点的信息,然后建立数据流管道向DataNode中写入文件数据。
在HDFS写文件的过程中为了提高效率,会在写数据前创建一个数据流管道,在这个管道中有replication(副本数)个DataNode节点,逻辑上这些DataNode节点线性排列,组成一条管道。按照这个线性排列给各个DataNode编号分别为DataNode-1,DataNode-2,DataNode-3,假设副本数为3,那么客户就在数据包中保存这3个DataNode节点的信息,并将数据包发送给DataNode-1,DataNode-1接收到数据包之后就在本地保存数据,同时将数据推送到DataNode-2,随后照这样进行,直到数据到达最后一个DataNode(这里的最后一个DataNode是DataNode-3),最后一个DataNode保存了数据后,就向前一个DataNode发送确认包,收到确认包的节点再向它上一个DataNode节点发送确认包,这样逆流而上,最后DataNode-1将确认包发送给客户端。
HDFS客户端写文件的具体过程是由3个线程配合完成的,这三个线程分别是DataStreamer线程,ResponseProcessor线程和DFSOutputStream线程。在上面的PutMerge类的代码中调用了out.write(buffer);这行代码来向HDFS写数据,其中out对象是FSDataOutputStream类的一个对象,FSDataoutputStream.write()方法的调用最终会执行其内部out变量(java.io.FilterOutputStream类的成员变量)的write()方法,由于在构造FSDataOutputStream类时传入的是一个DFSOutputStream对象,所以会调用DFSOutputStream.write()方法(其实是DFSOutputStream父类FSOutputSummer的write方法),根据调用关系,最后会进入到DFSOutputStream.writeChunk()方法,在这个方法中进行具体的写数据逻辑。
先来看看DFSOutputStream.writeChunk()方法,这个方法负责将数据准备好发送数据,代码如下:
protected synchronized void writeChunk(byte[] b, int offset, int len, byte[] checksum) throws IOException { checkOpen();//检查DFSClient对象的状态 isClosed();//检查DFSOutputStream对象的状态 int cklen = checksum.length; int bytesPerChecksum = this.checksum.getBytesPerChecksum(); if (len > bytesPerChecksum) {//输出的数据比一个校验块还大 throw new IOException("writeChunk() buffer size is " + len + " is larger than supported bytesPerChecksum " + bytesPerChecksum); } if (checksum.length != this.checksum.getChecksumSize()) {//校验数据的大小不正确 throw new IOException("writeChunk() checksum size is supposed to be " + this.checksum.getChecksumSize() + " but found to be " + checksum.length); } synchronized (dataQueue) { // If queue is full, then wait till we can create enough space while (!closed && dataQueue.size() + ackQueue.size() > maxPackets) { try { dataQueue.wait(); } catch (InterruptedException e) { } } isClosed(); if (currentPacket == null) { currentPacket = new Packet(packetSize, chunksPerPacket, bytesCurBlock); } currentPacket.writeChecksum(checksum, 0, cklen);//向Packet中写入校验数据 currentPacket.writeData(b, offset, len);//向Packet中写入要发送的数据 currentPacket.numChunks++; bytesCurBlock += len; // If packet is full, enqueue it for transmission //当前数据包已经满了,将其放入待发送对列中 if (currentPacket.numChunks == currentPacket.maxChunks || bytesCurBlock == blockSize) { //当前数据块写入完成了,那么重置bytesCurBlock为0 if (bytesCurBlock == blockSize) { currentPacket.lastPacketInBlock = true; bytesCurBlock = 0; lastFlushOffset = 0; } enqueueCurrentPacket();//将数据包放入待发送队列 if (appendChunk) {//处理追加数据的情况 appendChunk = false; resetChecksumChunk(bytesPerChecksum); } int psize = Math.min((int)(blockSize-bytesCurBlock), writePacketSize); computePacketChunkSize(psize, bytesPerChecksum); } } //LOG.debug("DFSClient writeChunk done length " + len + // " checksum length " + cklen); }
这个方法提供带校验数据输出流和具体存储系统的适配,对于DFSOutputStream,这里的具体存储系统就是数据节点,即HDFS集群中的DataNode节点,方法的四个形参作用分别是,b表示要写入HDFS集群中的原始数据,offset表示要输出的原始数据在数组b中的起始位置,len表示要输出数据的长度,checksum为要输出的数据的CRC32校验和,为什么会有offset和len这两个形参?因为数组b中并不是所有数据都是有效数据。
在方法中先检查了参数的合法性之后,就对dataQueue对象进行同步,dataQueue队列中保存的是输出流中要发送的数据包,而ackQueue对象中保存的则是已经发送的,但是还没有收到确认的数据包,在writeChunk方法中只需要将要发送的数据包放入队列dataQueue中,然后由DataStreamer线程发送数据包。使用在同步块内,先判断dataQueue队列是否已经满了,如果满了那么就调用dataQueue.wait()方法让当前线程阻塞,进入等待,否则就继续向下执行(线程被唤醒以后,该线程是从wait()方法那里开始执行)。在继续向下执行之前,要再次调用isClosed()方法判断这个输出流是否被关闭了,然后判断当前数据包对象currentPacket是否为空,如果为空,则表示需要新创建一个数据包来保存要发送的数据,数据包满了之后就将其放入待发送队列dataQueue中,此外变量appendChunk表示当前是否是追加数据的情况,这中情况稍微复杂一些,先不考虑。将数据包入队之后,再调用computePacketChunkSize方法计算下一次要发送的数据包大小。
在writeChunk()方法中执行enqueueCurrentPacket()方法之后,就会将待发送数据包放入队列dataQueue中,在DataStreamer线程中则会从dataQueue队列中取出队列中的第一个待发送数据包,发送给HDFS集群,DataStreamer线程的run()方法代码如下:
public void run() { long lastPacket = 0; while (!closed && clientRunning) { //如果出现错误,关闭应答,处理数据节点故障 if (hasError && response != null) { try { response.close(); response.join(); response = null; } catch (InterruptedException e) { } } Packet one = null; synchronized (dataQueue) { // process IO errors if any boolean doSleep = processDatanodeError(hasError, false); // wait for a packet to be sent. long now = System.currentTimeMillis(); while ((!closed && !hasError && clientRunning && dataQueue.size() == 0 && (blockStream == null || ( blockStream != null && now - lastPacket < timeoutValue/2))) || doSleep) { long timeout = timeoutValue/2 - (now-lastPacket); timeout = timeout <= 0 ? 1000 : timeout; try { dataQueue.wait(timeout); now = System.currentTimeMillis(); } catch (InterruptedException e) { } doSleep = false; } if (closed || hasError || !clientRunning) { //如果有错误或者DataStreamer/DFSClient已经关闭,通过continue //重新执行循环或退出循环(DataStreamer/DFSClient已经关闭时) continue; } try { // get packet to be sent. if (dataQueue.isEmpty()) {//队列为空,发送心跳包 one = new Packet(); // heartbeat packet } else { one = dataQueue.getFirst(); // regular data packet,队列不为空,发送正常包 } long offsetInBlock = one.offsetInBlock;//在数据节点中的偏移 // get new block from namenode. if (blockStream == null) {//blockStream为null,表明还没有和数据节点建立联系,或者需要与数据节点重新建立连接 LOG.debug("Allocating new block"); nodes = nextBlockOutputStream();//建立数据流管道 this.setName("DataStreamer for file " + src + " block " + block); response = new ResponseProcessor(nodes);//应答处理器 response.start(); } if (offsetInBlock >= blockSize) { throw new IOException("BlockSize " + blockSize + " is smaller than data size. " + " Offset of packet in block " + offsetInBlock + " Aborting file " + src); } ByteBuffer buf = one.getBuffer(); // move packet from dataQueue to ackQueue //如果不是一个心跳包,则要发送这个包,所以要将这个包从dataQueue移到ackQueue中 if (!one.isHeartbeatPacket()) { dataQueue.removeFirst(); dataQueue.notifyAll(); synchronized (ackQueue) { ackQueue.addLast(one); ackQueue.notifyAll(); } } // write out data to remote datanode //将这个数据包写入到远程数据节点中 blockStream.write(buf.array(), buf.position(), buf.remaining()); if (one.lastPacketInBlock) {//如果是数据块中的最后一个数据包,则写一个0到数据节点,表示这个数据块结束 blockStream.writeInt(0); // indicate end-of-block } blockStream.flush(); lastPacket = System.currentTimeMillis(); if (LOG.isDebugEnabled()) { LOG.debug("DataStreamer block " + block + " wrote packet seqno:" + one.seqno + " size:" + buf.remaining() + " offsetInBlock:" + one.offsetInBlock + " lastPacketInBlock:" + one.lastPacketInBlock); } } catch (Throwable e) { LOG.warn("DataStreamer Exception: " + StringUtils.stringifyException(e)); if (e instanceof IOException) { setLastException((IOException)e); } hasError = true; } } if (closed || hasError || !clientRunning) { continue; } // Is this block full? if (one.lastPacketInBlock) {//数据块已经写完了 synchronized (ackQueue) { while (!hasError && ackQueue.size() != 0 && clientRunning) { try { ackQueue.wait(); // wait for acks to arrive from datanodes } catch (InterruptedException e) { } } } LOG.debug("Closing old block " + block); this.setName("DataStreamer for file " + src); response.close();// ignore all errors in Response,关闭应答处理器 try { response.join(); response = null; } catch (InterruptedException e) { } if (closed || hasError || !clientRunning) { continue;//前面的代码处理出错 } synchronized (dataQueue) { //关闭流并释放资源 IOUtils.cleanup(LOG, blockStream, blockReplyStream); nodes = null; response = null; blockStream = null; blockReplyStream = null; } } if (progress != null) { progress.progress(); } // This is used by unit test to trigger race conditions. if (artificialSlowdown != 0 && clientRunning) { LOG.debug("Sleeping for artificial slowdown of " + artificialSlowdown + "ms"); try { Thread.sleep(artificialSlowdown); } catch (InterruptedException e) {} } } }
这个方法比较长,包含了发送数据,处理错误等一系列处理过程,首先在进入循环发送数据包之前,将lastPacket变量置0,这个变量表示上一次发送数据包的时间。然后执行while循环,while循环退出的条件是客户端在执行(clientRunning==true)并且当前输出流没有关闭(closed==false)。在while循环体的最前面处理可能出现的错误,再对dataQueue对象进行同步,同步块中先调用processDatanodeError方法处理可能的数据节点错误,这个方法处理数据流管道上出现错误的处理过程。下面一个while循环比较复杂,用于执行等待一段时间,首先是!closed && !hasError && clientRunning这三个条件,比较好理解,即当前输出流没有关闭,没有错误且客户端正处于执行状态,那么可以执行等待,否则跳出循环要么退出外层循环终止该线程,要么在外层循环中处理错误,dataQueue.size() == 0这个条件则表示待发送队列为空,需要等待其他线程准备好要发送的数据包,(blockStream == null || (blockStream != null && now - lastPacket < timeoutValue/2))),这个条件中blockStream是一个输出流,用于将数据写到HDFS集群中,如果blockStream为null,那么就表明客户端还没有和数据节点建立连接,或者需要与数据节点重新建立连接,如果blockStream不为null,但是当前时间与上一次发送数倨包或心跳的时间间隔小于timeoutValue/2,这种情况下,线程可以阻塞一段时间,doSleep是processDatanodeError方法返回的结果,如果为true,则就表明线程要阻塞一段时间,实际上如果doSleep为true,那么就一定执行了processDatanodeError方法,这种情况下相当于等待一段时间后再执行processDatanodeError方法。
下面就是判断dataQueue是否为空,如果为空,就表示当前没有数据包要发送,但是客户端要与HDFS集群保持连接状态,那么就创建一个数据包,当作心跳发送到HDFS集群,告诉集群,客户端还活着,如果dataQueue中包含数据包,就获取第一个数据包对象,检查这个数据包是否属于当前待发送数据块(即检查数据包在数据块中的偏移)。再检查blockStream是否为null,如果为null,就要建立与HDFS集群的连接,首先调用DFSOutputStream.nextBlockOutputStream()方法建立数据流管道,用于输出这个数据块,DFSOutputStream.nextBlockOutputStream()方法的代码如下:
private DatanodeInfo[] nextBlockOutputStream() throws IOException { LocatedBlock lb = null; boolean retry = false; DatanodeInfo[] nodes; int count = conf.getInt("dfs.client.block.write.retries", 3); boolean success; do { hasError = false; lastException = null; errorIndex = 0; retry = false; nodes = null; success = false; long startTime = System.currentTimeMillis(); DatanodeInfo[] excluded = excludedNodes.toArray(new DatanodeInfo[0]); //分配一个新的数据块 lb = locateFollowingBlock(startTime, excluded.length > 0 ? excluded : null); block = lb.getBlock();//数据块信息 accessToken = lb.getBlockToken(); nodes = lb.getLocations();//数据节点信息 // // Connect to first DataNode in the list. //创建数据流管道 success = createBlockOutputStream(nodes, clientName, false); if (!success) {//创建数据流管道失败,调用anandonBlock()方法放弃已经申请的数据块 LOG.info("Abandoning " + block); namenode.abandonBlock(block, src, clientName); if (errorIndex < nodes.length) { LOG.info("Excluding datanode " + nodes[errorIndex]); //将这个节点放入excludeNodes中 excludedNodes.add(nodes[errorIndex]); } // Connection failed. Let's wait a little bit and retry retry = true;//进行重试,即向NameNode申请一个新的数据块 } } while (retry && --count >= 0); if (!success) { throw new IOException("Unable to create new block."); } return nodes; }
这个方法主要在循环内完成,循环的条件retry表示进行重试,即上次执行失败,count表示重试的次数,这个值是从配置中取出的。lastException表示上一次抛出的异常,errorIndex表示第一个在创建数据流管道中第一个出错的节点,excludedNodes列表存放创建数据流管道时失败的DataNode节点,将这个DataNode排除在外,以免再次访问到故障节点。
在循环中,先调用方法DFSOutputStream.locateFollowingBlock()来向NameNode申请一个数据块,即在NameNode节点中建立一个与当前写入数据的文件对应的数据块,这个方法返回将这个数据块保存在哪些DataNode中,在给定的实参中如果excludedNodes中有元素,则传递这个列表,那么在NameNode执行对应的方法时会将这些节点排除在外,如果没有元素,那么传递一个null值。locateFollowingBlock方法返回了下一个数据块保存在哪些DataNode节点中,那么就需要建立这些节点的数据流管道,DFSOutputStream.createBlockOutputStream()方法就是完成这个工作,它创建客户端到这些节点的数据流管道,客户端的创建数据流管道只需要向第一个DataNode节点写入请求,即建立到第一个DataNode的TCP连接,写入DataNode节点流式接口的80请求包,并等待应答。在这个过程中会初始化blockStream对象与blockReplyStream对象,其中blockReplyStream对象用于读入数据流管道中的DataNode节点的响应。如果createBlockOutputStream方法执行出错,那么errorIndex变量会记录第一个出错的节点,在nextBlockOutputStream方法中会将这个节点加入到excludedNodes列表中。
建立数据流管道成功后,返回到DataStreamer.run()方法中,设置线程名,并且创建处理数据包响应的应答处理器,即ResponseProcessor线程对象response,并且启动这个线程,等待数据流管道的应答。
然后就可以向数据流管道中发送数据了,if (!one.isHeartbeatPacket()) 这个if语句判断从这个数据包是否是心跳包,如果不是心跳包,则需要将这个数据包从dataQueue队列中移除,如果writeChunk方法所在的线程因为dataQueue队列满而进入阻塞,那么就使用dataQueue.notifyAll();这行代码将这个阻塞的线程唤醒,再对ackQueue队列进行同步,将数据包one,加入到ackQueue队列中,同样如果ResponseProcessor线程因为ackQueue为空而阻塞,使用ackQueue.notifyAll();将其唤醒。
下面就将数据包通过blockStream发送给数据流管道的第一个DataNode节点,如果当前发送的数据包是数据块中的最后一个数据包,则写一个0到数据节点,表示这个数据块结束。
从对dataQueue进行同步的代码块退出后,就判断刚刚发送的数据包是否是这个数据块的最后一个数据包,如果是并且ackQueue队列中还有元素,那么DataStreamer线程就进入等待,否则就关闭关闭应答处理器线程response(调用response.close()方法),并且释放其他资源。
客户端将数据包发送给数据流管道中的数据节点之后,数据流管道中的节点收到数据包还需要向客户端发送确认数据包,在客户端ResponseProcessor线程就是用于处理这个过程,ResponseProcessor.run()方法代码如下:
public void run() { this.setName("ResponseProcessor for block " + block); PipelineAck ack = new PipelineAck(); while (!closed && clientRunning && !lastPacketInBlock) { // process responses from datanodes. try { // read an ack from the pipeline ack.readFields(blockReplyStream);//从应答数据流中读入ack对象 if (LOG.isDebugEnabled()) { LOG.debug("DFSClient for block " + block + " " + ack); } // processes response status from all datanodes. //处理应答 for (int i = ack.getNumOfReplies()-1; i >=0 && clientRunning; i--) { short reply = ack.getReply(i); if (reply != DataTransferProtocol.OP_STATUS_SUCCESS) { errorIndex = i; // first bad datanode,有故障数据节点 throw new IOException("Bad response " + reply + " for block " + block + " from datanode " + targets[i].getName()); } } long seqno = ack.getSeqno(); assert seqno != PipelineAck.UNKOWN_SEQNO : "Ack for unkown seqno should be a failed ack: " + ack; if (seqno == Packet.HEART_BEAT_SEQNO) { // a heartbeat ack continue; } Packet one = null; synchronized (ackQueue) { one = ackQueue.getFirst(); } if (one.seqno != seqno) {//应答包中包序号与ackQueue中第一个包的序号不等 throw new IOException("Responseprocessor: Expecting seqno " + " for block " + block + " " + one.seqno + " but received " + seqno); } lastPacketInBlock = one.lastPacketInBlock; synchronized (ackQueue) { assert ack.getSeqno() == lastAckedSeqno + 1; lastAckedSeqno = ack.getSeqno(); ackQueue.removeFirst(); ackQueue.notifyAll(); } } catch (Exception e) { if (!closed) { hasError = true;//设置错误标识 if (e instanceof IOException) { setLastException((IOException)e); } LOG.warn("DFSOutputStream ResponseProcessor exception " + " for block " + block + StringUtils.stringifyException(e)); closed = true; } } synchronized (dataQueue) { dataQueue.notifyAll(); } synchronized (ackQueue) { ackQueue.notifyAll(); } } }
这个线程的开始处首先创建一个PipelineAck对象,然后从数据流管道中的应答数据流blockReplyStream中读入一个ack数据包。PipelineAck类有两个成员变量,分别是seqno和replies数组,其中seqno表示该确认数据包对应的写请求数据包的序列号,replies数据包含了管道上的各个节点对写请求数据包的处理结果,如果DataNode顺利将数据写入磁盘,则结果为DataTransferProtocol.OP_STATUS_SUCCESS,否则为DataTransferProtocol.OP_STATUS_ERROR。所以在ResponseProcessor线程中,通过一个for循环来处理应答数据包。这个for循环遍历replies数组,对每个元素,判断是不是DataTransferProtocol.OP_STATUS_SUCCESS,如果不是就通过errorIndex记录这个有故障的节点,并抛出异常。如果replies数组中的所有元素都是DataTransferProtocol.OP_STATUS_SUCCESS,那么就取出这个应答数据包的序号(seqno),判断是否是一个心跳应答(Packet.HEART_BEAT_SEQNO),如果不是心跳应答才能继续向下进行处理。
处理写数据的响应数据包时,首先从ackQueue队列中取出第一个元素,判断这个元素的序号是否与seqno相等,如果不等那么应答包中包序号与ackQueue中第一个包的序号不等,抛出异常(为什么没有考虑网络延迟带来的应答包没有按序到达?),否则设置ResponseProcessor.lastAckedSeqno为这个这个响应包的序号,并从ackQueue中移除第一个元素,再通过ackQueue.notifyAll();唤醒因为ackQueue满了而阻塞的线程。
最后还有两个同步语句分别对dataQueue,和ackQueue来同步,然后唤醒其它线程,不知这是何故?
Reference
《Hadoop技术内幕:深入理解Hadoop Common和HDFS架构设计与实现原理》
评论暂时关闭