2021SC@SDUSC
Hadoop源码分析(三)——DFSClient HDFS客户端
文章目录
1.DFSClient HDFS客户端
DFSClient 所在的包为 org.apache.hadoop.hdfs ,它是分布式文件系统 HDFS 的客户端。 DFSClient 可以连接到 Hadoop 的文件系统,然后执行基本的文件操作。它使用 ClientProtocol 协议通过 RPC 机制和 NameNode 进行通信并获得文件的元数据信息,然后连接到 DataNode 并通过 DFSOutputStream 和 DFSInputStream 来进行数据块的真正读写操作。
2.内部类
2.1 DFSOutputStream
DFSOutputStream 集成自 FSOutputSummer 父类,并实现了 Syncable 接口。 FSOutputSummer 为数据在写入 HDFS 之前提供了校验和功能,具体代码如下:
//数据块所对应的校验和
private Checksum sum;
//存储原生数据的缓冲区
private byte buf[];
//存储校验和的缓冲区
private byte checksum[];
//buf缓冲区中有效数据的长度
private int count;
写入单个字节b,首先更新字节b对应的校验和,之后将字节b添加到数据缓冲区buf中。当buf已经被填满时,调用flushBuffer 方法来讲缓冲区中的数据强制写入到文件中。
public synchronized void write(int b) throws lOException {
sum.update (b);
buf[count++] = (byte)b;
if(count == buf.length) {
flushBuffer();
}
}
在flushBuffer方法内部是通过调用writeChecksumChunk方法来真正完成数据块的写入操作的。
protected synchronized void flushBuffer(boolean keep) throws lOException {
if (count != 0) (
int chunkLen = count;
count = 0;
writeChecksumChunk(buf, 0, chunkLen, keep);
if (keep) {
count = chunkLen;
}
}
writeChecksumChunk方法内部首先会生成数据块对应的校验和,然后通过writeChunk方法来将数据 块和校验和写入到输出流中。
private void writeChecksumChunk(byte b[], int off, int len, boolean keep) throws lOException {
int ten^>Checksum = (int) sum.getValue ();
if (!keep) {
sum.reset ();
}
int2byte(tempchecksum, checksum);
writeChunk(b, off, len, checksum);
}
writeChunk是个抽象方法。该方法会将b中开始于offset位置处而且长度为len的数据块写入到数据 文件中,同时将该数据块所对应的校验和checksum写入到校验和文件中。
protected abstract void writeChunk(byte[] b, int offset, int len, byte[] checksum)throws lOException;
将数据块缓冲区和校验和进行重置。
protected synchronized void resetChecksumChunk(int size) {
sum. reset ();
this.buf = new byte[size];
this.count = 0;
}
将一个整型的校验和转换为对应的字节流即字节数组。
static public byte[] convertToByteStream(Checksum sum, int checksumSize) {
return int2byte((int)sum.getValue()f new byte[checksumSize]);
}
将一个整型数转换为字节数组的工具方法。
static byte[] int2byte(int integer, byte[] bytes) {
bytes[0] = (byte)((integer >>> 24) & 0xFF);
bytes[1] = (byte)((integer >>> 16) & 0xFF);
bytes[2] = (byte)((integer >>> 8) & 0xFF);
bytes[3] = (byte)((integer >>> 0) & 0xFF);
return bytes;
}
write方法用于将b中开始于offset位置处而且长度为len的数据块写入到文件中。如果要写入的数据块 的长度len大于数据缓冲区buf的大小,则直接调用writeChecksumChunk方法来将长度为buf缓冲区长 度的数据块及校验和写入到对应的文件中。否则,首先调用System的arraycopy方法来将b中的数据复 制到buf中,当缓冲区buf满了之后通过调用flushBuffer方法来将数据写入到文件中。
private int writel(byte b[], int off, int len) throws lOException {
if (count—0 && len>==buf. length) {
final int length = buf.length;
sum.update(b, off, length);
writeChecksumChunk(br off, length, false);
return length;
}
int bytesToCopy = buf.length-count;
bytesToCopy = (len<bytesToCopy) ? len : bytesToCopy;
sum.update(b, off, bytesToCopy);
System.arraycopy(br off, buf, count, bytesToCopy);
count += bytesToCopy;
if (count — buf.length) (
flushBuffer();
}
return bytesToCopy;
}
分析完抽象的FSOutputSummer父类之后,接下来开始分析DFSOutputStream中的源代码。首先分 析 DFSOutputStream 中的内部类 Packet、DataStreamer 和 ResponseProcessor。
2.2Packet
DFSClient是通过一个个Packet来向DataNode写入数据的。一个Packet由多个数据chunk组成,每 个chunk对应着一个校验和。当写入足够多的chunk之后,Packet会被添加到dataQueue中。
//字节缓冲区。它用于保存Packet的头信息和不包含校验和的实际数据的长度信息。
ByteBuffer buffer;
//数据缓冲区。
byte[] buf;
//缓冲区在Block中的序列号。
long seqno;
//该Packet在Block中的偏移量。
long offsetlnBlock;
//该Packet是否为Block中的最后一个Packeto
boolean lastPacketInBlock;
//该Packet中当前所包含的Chunk的数量。
int numChunks;
//—个Packet中最多可以包含的Chunk的数量。
int maxChunks;
//数据的开始位置。
int dataStart;
//数据的当前位置。
int dataPos;
//校验和的开始位置。
int checksumStart;
//校验和的当前位置。
int checksumPos;
//心跳序列号即心跳Packet所对应的序列号。
private static final long HEART_BEAT_SEQNO = -IL;
Packet中包含两个构造方法,一个用于创建心跳Packet, 一个用于创建数据Packet.
Packet () {
this.lastPacketlnBlock = false;
this.numChunks = 0;
this.offsetlnBlock = 0;
this.seqno = HEART_BEAT_SEQNO;
buffer = null;
int packetsize = DataNode.PKT_HEADER_LEN + SIZE_OF_INTEGER;
buf = new byte[packetsize];
checksumStart = dataStart = packetSize;
checksvunPos = checksumStar t;
dataPos = dataStart;
maxChunks = 0;
}
该构造方法用于创建一个心跳Packet =心跳Packet中不包含任何的数据Chunk,它在数据Block中的 偏移量为 0,即在 Block 的头部,而 Packet 的大小为 DataNode.PKT_HEADER_LEN + SlZE_OF_INTEGERo 其中 DataNode.PKT_HEADER_LEN=(4 +8 + 8 + 1 ), 4 个字节存储 Packet 的长度,8 个字节存储该 Packet 在Block中的偏移量,8个字节存储该Packet在Block中的序列号,一个字节用于标识该Packet是否为 Block 中的最后一个 Packet。S1ZE_OF_INTEGER = Integer.SIZE / Byte.SIZE。
Packet(int pktSize, int chunksPerPkt, long offsetlnBlock) {
this.lastPacketlnBlock = false;
this.numChunks = 0;
this.offsetlnBlock = offsetlnBlock;
this.seqno = currentSeqno;
currentSeqno++;
buffer = null;
buf = new byte[pktsize];
checksumstart = DataNode.PKT_HEADER_LEN + SIZE_OF_INTEGER;
checksumPos = checksumstart;
dataStart = checksumstart + chunksPerPkt * checksum.getChecksumSize();
dataPos = dataStart;
maxChunks = chunksPerPkt;
}
该构造方法用于创建一个数据Packet。数据Packet在Block中的序列号从0开始依次增加。数据Packet 的大小和Packet中最多能够包含的Chunk的数量分别是通过构造参数pktSize和chunksPerPkt来指定的。 校验和在数据包中的开始位置 checksumStart 为 DataNode.PKT_HEADER_LEN + SIZE_OF INTEGER, 而数据的开始位置为校验和的开始位置加上所有数据Chunk所对应的校验和的大小。
void writeData(byte[] inarray, int off, int len) {
if ( dataPos + len > buf.length) (
throw new BufferOverflowException();
)
System.arraycopy(inarray, off, buf, dataPos, len);
dataPos += len;
}
该方法用于向数据Packet中写入数据信息。如果要写入的数据的长度大于Packet中buf的长度,那 么会抛出对应的异常;否则调用System的arraycopy方法来将数据复制到buf中。
void writeChecksum(byte[] inarray, int off, int len) {
if (checksumPos + len > dataStart) (
throw new BufferOverflowException();
}
System.arraycopy(inarrayr off, buf, checksumPos, len);
checksumPos += len;
}
该方法用于向数据Packet中写入数据的校验和信息。如果要写入的校验和的长度过长,那么会抛出 对应的异常;否则调用System的arraycopy方法来将校验和复制到buf中。
ByteBuffer getBuffer()方法用于返回Packet中封装的所有数据包括原生数据信息、校验和信息和头 信息。当需要将Packet发送到数据结点时,该方法将会被调用,而且当该方法被调用时,就不能继续向 Packet中写入数据了。
if (buffer != null) {
return buffer;
}
如果Packet中的buffer不为空,则直接返回buffer。
int dataLen = dataPos - dataStart;
计算出Packet中数据的实际长度。
int checksumLen = checksumPos - checksumStart;
计算出Packet中校验和的实际长度。
if (checksumPos !- dataStart) {
System.arraycopy(buf, checksumStart, buf,dataStart - checksumLen , checksumLen);
}
如果校验和和数据段不连续,则使用校验和来填充校验和与数据段之间的间隔。
int pktLen = SIZE_OF_INTEGER + dataLen + checksumLen;
计算出Packet的总长度。
buffer = ByteBuffer.wrap(buf, dataStart - checksumPos,DataNode.PKT_HEADER_LEN + pktLen);
将该Packet中封装的所有数据写入到buffer中。
buf = null;
buffer.mark();
将Packet中的buf置空。
buffer.putlnt(pktLen);
将该Packet的长度信息写入到buffer中。
buffer.putLong(offsetlnBlock);
将该Packet在Block中的偏移量信息写入到buffer中。
buffer.putLong(segno);
将该Packet在Block中的序列号信息写入到buffer中。
buffer.put((byte) ((lastPacketlnBlock) ? 1 : 0));
将该Packet是否为Block中的最后一个Packet的标识信息写入到buffer中。
buffer.putlnt(dataLen);
将该Packet中的实际的数据信息(不包括校验和)的长度写入到buffer中。
buffer.reset();
最后将buffer进行重置。
2.3 DATAStreamer
DataStreamer是真正写入数据的进程。在发送Packet之前,它会首先从NameNode中获得一个新的 blockid和Block的位置信息。然后它会循环地从dataQueue中取得一个Packet,然后将该Packet真正写 入到与DataNode所建立的socket中。当将属于一个Block的所有Packet都发送给DataNode,并且返回 了与每个Packet所对应的响应信息之后,DataStreamer会关闭当前的数据Block。
private volatile boolean closed = false;
DataStreamer进程是否被关闭的标识。
run方法的处理逻辑如下:
long lastPacket = 0;
该线程发送最后一个数据Packet的时间。
while (!closed && clientRunning)
当该线程没有被关闭,而且客户端正在运行着,则开始循环发送Packet。
if (hasError && response != null) (
try {
response.close();
response. join ();
response = null;
} catch (InterrvptedException e) (
}
如果在发送Packet的过程中,处理DataNode返回的响应的response线程发生了错误,那么需要将 response 关闭掉。
boolean doSleep = processDatanodeError(hasError, false);
调用processDatanodeError方法来处理任何可能的IO错误。
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;
}
然后DataStreamer会在dataQueue上进行等待,一直到dataQueue上出现需要发送的Packet为止。
if(closed || hasError || !clientRunning) {
continue;
}
如果在等待数据包的过程中,DataStreamer线程被关闭了,或者发生了 IO错误,或者客户端停止了 运行,那么将直接跳过此次发送Packet的循环。
if (dataQueue.isEmpty()) {
one = new Packet();
} else (
one = dataQueue.getFirst();
}
如果dataQueue是空的,则创建一个心跳Packet;否则从dataQueue中获取第一个数据Packet。
long offsetInBlock = one.offsetlnBlock;
取得要发送的Packet在数据Block中的偏移量。
if (blockStream == null) {
LOG. debug (n Al locating new block*1);
nodes = nextBlockOutputStream(src);
this.setName(^DataStreamer for file " + src +" block " + block);
response = new Responseprocessor(nodes);
response.start();
}
如果到DataNode的blockStream输出流还没有被打开,那么首先需要调用nextBlockOutputStream方 法来连接起与DataNode的连接。然后启动ResponseProcessor线程。
if (offsetInBlock >= blocksize) {
throw new lOException("Blocksize n + blocksize +” is smaller than data size. ” +
” Offset of packet in block ” +offsetInBlock +" Aborting file ” + src);
}
//如果Packet在数据Block中的偏移量大于Block的大小,那么会抛出对应的异常。
ByteBuffer buf = one.getBuffer();
//取得Packet中的所有信息'
if (!one.isHeartbeatPacket())(
dataQueue.removeFirst();
dataQueue.notifyAll();
synchronized (ackQueue) {
ackQueue.addLast(one); ackQueue.notifyAll();
}
}
如果要发送的Packet不是心跳Packet,那么需要将该Packet从dataQueue移动到ackQueue中。
blockstream.write(buf.array(), buf.position()r buf.remaining());
通过blockStream来将Packet写入到远程的DataNode中。
if (one.lastPacketlnBlock) {
blockstream.writelnt(0);
}
如果该Packet是数据Block中的最后一个Packet,那么需要向blockStream写入0从而通知DataNode 数据传输完成。
blockStream.flush();
刷新blockStream。
lastPacket = System.currentTimeMillis();
将lastPacket更新为系统当前的时间。
if (one.lastPacketlnBlock) {
synchronized (ackQueue) (
while (!hasError && ackQueue.size() != 0 && clientRunning) (
try {
ackQueue.wait();
} catch (InterruptedException e) (
}}}
LOG.debug ("Closing old block 11 + block);
this.setName("DataStreamer for file ” + src);
response.close();
try (
response.join();
response = null;
} catch (InterruptedException e) {
}
if (closed I I hasError | | !clientRunning) (
continue;
} synchronized (dataQueue) { lOUtils.cleanup(LOG, blockStream, blockReplyStream); nodes - null;
response = null;
blockstream = null;
blockReplyStream = null;
}
}
当Block的最后一个Packet发送出去后,DataStreamer会一直等待ackQueue队列为空,即与所有
Packet对应的响应都已经被接受了。然后执行清理工作,首先关闭response线程,然后关闭socket连接。
if (progress != null) ( progress.progress()😉
汇报数据的写入进度信息。
至此,run方法就分析完毕了。
void close () {
closed = true;
synchronized (dataQueue) ( dataQueue.notifyAll();
)
synchronized (ackQueue) ( ackQueue.notifyAl1();
}
this,interrupt();
}
close方法会关闭DataStreamer线程。首先将线程的关闭标识closed设置为true,然后分别通知 dataQueue和ackQueue队列上的所有等待者。
2.4 ResponseProcessor
ResponseProcessor 继承自 Thread, DataStreamer 会为写入的每个 Block 启动一个 ResponseProcessor 线程。该线程主要用于等待来自DataNode管道中的DataNode的响应。如果是成功的响应,则将对应的 Packet从ackQueue删除;如果是失败的响应,则需要记录下出错的DataNode,并设置对应的标志位。
private volatile boolean closed = false;
ResponseProcessor线程是否被关闭的标识。
private Datanodelnfo[] targets = null;
等待发送响应的DataNode集合。
private boolean lastPacketlnBlock = false;
该变量用于标识某个Packet是否为Block中的最后一个Packet。
ResponseProcessor (Datanodelnfo[] targets) {
this.targets = targets;
}
在构造方法中完成对目标DataNode集合的初始化。
接下来看一下run方法的处理逻辑。
PipelineAck ack = new PipelineAck();
初始化管道响应对象。它是DataTransferProtocol接口中的一个内部类。
while (!closed && clientRunning && !LastPacketlnBlock)
只要该ResponseProcessor线程没有被关闭,而且客户端也正在运行着,同时要处理的Packet也不是 Block中的最后一个Packet,那么就循环处理来自DataNode的响应。
ack.readFields(blockReplyStream);
从管道流中读取一个响应。
for (int i = ack.getNumOfReplies()-1; i >=0 && clientRunning; i--) {
short reply = ack.getReply(i);
if (reply != DataTransferProtocol.OP_STATUS_SUCCESS) {
errorindex = i;
throw new lOException("Bad response ” + reply +
” for block ” + block +" from datanode " ^targets[i].getName());
}
}
循环处理DataNode管道中所有DataNode返回的响应。如果某个DataNode返回失败的响应,则用 errorindex记录下出错的DataNode的索引,并抛出对应的异常。
long seqno = ack.getSeqno();
取得返回的响应所对应的Packet的序列号。
if (seqno == Packet.HEART_BEAT_SEQNO) { // a heartbeat
ack continue;
}
如果返回的序列号为心跳Packet对应的序列号,则终止此次循环,进入下一次循环处理。
Packet one = null;
synchronized (ackQueue) {
one = ackQueue.getFirst();
}
//从ackQueue队歹U中取出第一个Packet
if (one.seqno != seqno) {
throw new lOException("Responseprocessor: Expecting seqno " +" for block " + block + "" +one.seqno + " but received ” + seqno);
}
如果DataNode返回的序列号和从ackQueue队列中取出的Packet的序列号不一致,那么会抛出对应 的异常。
synchronized (ackQueue) {
assert ack.getSeqno() == lastAckedSeqno + 1; lastAckedSeqno = ack.getSeqno();
ackQueue.removeFirst();
ackQueue.notifyAll();
}
将成功返回响应的Packet从ackQueue队列中删除。
synchronized (dataQueue) {
dataQueue.notifyAll();
)
synchronized (ackQueue) {
ac kQueue.noti fyAl1();
}
最后通知dataQueue队列和ackQueue队列的所有监听者。