2021年山东大学软件工程应用与实践项目——Hadoop源码分析(三)

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队列的所有监听者。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值