在客户端写hdfs文件的过程中,其会将数据以packet包的形式向DataNode发送,DataNode在接收到这个packet包时,会进行将该packet写入本地磁盘,之后便向数据流管道中的下游数据节点继续发送该数据包;并会接收来自下游数据节点的数据包确认消息。这个确认消息会逆向的通过数据流管道送回到客户端client端。接下来详细分析一下在客户端写过程中,DataNode上所进行的操作流程。
- 基本的客户端写流程已经在上篇博文中给出:Hdfs 客户端写过程 源码解析
- DataNode上的用于响应流式接口请求的服务也在博文中给出:DataNode DataXceiverServer readBlock详解
在客户端写流程中,在其通过nextBlockOutputStream()获取到NameNode所分配用于存储该block的DataNode信息后,其会建立到数据流管道中第一个DataNode的输出流;并向其发送数据块写入的操作指令:new Sender(out).writeBlock();接着DataNode上的DataXceiverServer将会构造一个DataXceiver线程对象用于响应该写操作请求;DataXceiver会解析其流式请求的操作符(此处对应WRITE_BLOCK),并调用DataXceiver.writeBlock()方法响应这个请求。接下来逐步分析DataXceiver.writeBlock()响应方法。
1、DataXceiver.writeBlock()的基本流程:
DataXceiver.writeBlock()方法的具体分析如下:
1、检查、设置对应的参数变量
// 是否是datannode发起的请求
final boolean isDatanode = clientname.length() == 0;
// 是否是客户端发起的请求
final boolean isClient = !isDatanode;
// 是否是数据块的复制操作
final boolean isTransfer = stage == BlockConstructionStage.TRANSFER_RBW
|| stage == BlockConstructionStage.TRANSFER_FINALIZED;
// reply to upstream datanode or client
// 和上游的数据节点或者客户端交互的输出流,用于发送响应数据
final DataOutputStream replyOut = getBufferedOutputStream();
2、定义用于上下游节点的输入和输出流,其会在后续中进行对应的构造初始化
DataOutputStream mirrorOut = null; // stream to next target 下游节点的输出流
DataInputStream mirrorIn = null; // reply from next target 下游节点的输入流
Socket mirrorSock = null; // socket to next target 下一个节点的Socket
String mirrorNode = null; // the name:port of next target 下一个节点的名称:端口
String firstBadLink = ""; // first datanode that failed in connection setup 管道中第一恶坏的节点
DataNode与数据流管道中的上游节点通信包括输入流in和输出流replyOut,与数据流管道中的下游节点通信包括输入流mirrorIn和输出流mirrorOut,其共同构成当前节点的输入输出流如下:
3、构造BlockReceiver对象,并将其用来接收数据块信息
// open a block receiver
blockReceiver = new BlockReceiver(block, storageType, in,
peer.getRemoteAddressString(),
peer.getLocalAddressString(),
stage, latestGenerationStamp, minBytesRcvd, maxBytesRcvd,
clientname, srcDataNode, datanode, requestedChecksum,
cachingStrategy, allowLazyPersist, pinning);
4、建立到下游节点的socket连接,创建对应的输入流mirrorIn和输出流mirrorOut,并向下游发送数据块写入操作请求new Sender(mirrorOut).writeBlock();并从mirrorIn中解析来自下游节点的响应确认并记录相应的确认状态;并通过replyOut流向上游节点返回请求响应确认信息。
// 如果下一个节点不为空
if (targets.length > 0) {
InetSocketAddress mirrorTarget = null;
// Connect to backup machine
// 从target节点中获取下一个节点。
mirrorNode = targets[0].getXferAddr(connectToDnViaHostname);
mirrorTarget = NetUtils.createSocketAddr(mirrorNode);
mirrorSock = datanode.newSocket();
try {
// .........
// 连接到下一个datanode
NetUtils.connect(mirrorSock, mirrorTarget, timeoutValue);
mirrorSock.setSoTimeout(timeoutValue);
mirrorSock.setSendBufferSize(HdfsConstants.DEFAULT_DATA_SOCKET_SIZE);
// .........
// 构造下游节点的输出流mirrorOut,用于往下游节点写数据流
// 构造下游节点的输入流mirrorIn,用于接收来自下游节点的相应信息
mirrorOut = new DataOutputStream(new BufferedOutputStream(unbufMirrorOut,
HdfsConstants.SMALL_BUFFER_SIZE));
mirrorIn = new DataInputStream(unbufMirrorIn);
// Do not propagate allowLazyPersist to downstream DataNodes.
// 构造了Sender对象,向下游节点发送writeBlock操作指令
new Sender(mirrorOut).writeBlock(originalBlock, targetStorageTypes[0],
blockToken, clientname, targets, targetStorageTypes, srcDataNode,
stage, pipelineSize, minBytesRcvd, maxBytesRcvd,
latestGenerationStamp, requestedChecksum, cachingStrategy, false);
// flush数据流
mirrorOut.flush();
// read connect ack (only for clients, not for replication req)
// 从mirrorIn解析来自下游节点的确认请求信息
if (isClient) {
BlockOpResponseProto connectAck =
BlockOpResponseProto.parseFrom(PBHelper.vintPrefixed(mirrorIn));
mirrorInStatus = connectAck.getStatus();
firstBadLink = connectAck.getFirstBadLink();
if (LOG.isDebugEnabled() || mirrorInStatus != SUCCESS) {
LOG.info("Datanode " + targets.length +
" got response for connect ack " +
" from downstream datanode with firstbadlink as " +
firstBadLink);
}
}
} catch (IOException e) {
// 异常, 关闭输入输出流
// .........
IOUtils.closeStream(mirrorOut);
mirrorOut = null;
IOUtils.closeStream(mirrorIn);
mirrorIn = null;
IOUtils.closeSocket(mirrorSock);
mirrorSock = null;
// .........
}
}
// send connect-ack to source for clients and not transfer-RBW/Finalized
// 向上游节点发送请求响应确认信息
if (isClient && !isTransfer) {
BlockOpResponseProto.newBuilder()
.setStatus(mirrorInStatus)
.setFirstBadLink(firstBadLink)
.build()
.writeDelimitedTo(replyOut);
replyOut.flush();
}
5、在成功的建立了上下游节点的输入/输出流后;writeBlock()方法会调用blockReceiver.receiveBlock()方法从数据流管道中的上游接收数据块,然后保存数据块到当前数据节点的存储中,再将数据块转发到数据流管道中的下游数据节点。同时Blockreceiver还会接收来自下游节点的响应,并将这个响应发送给数据流管道中的上游节点。
if (blockReceiver != null) {
String mirrorAddr = (mirrorSock == null) ? null : mirrorNode;
blockReceiver.receiveBlock(mirrorOut, mirrorIn, replyOut,
mirrorAddr, null, targets, false);
// send close-ack for transfer-RBW/Finalized
// 数据块复制操作,直接把状态置成SUCCESS,返回上游节点相关信息
if (isTransfer) {
if (LOG.isTraceEnabled()) {
LOG.trace("TRANSFER: send close-ack");
}
writeResponse(SUCCESS, null, replyOut);
}
}
6、更新记录当前新写入的数据块副本的时间戳、副本大小等信息,并根据是否是数据流管道的恢复操作或者数据块的复制操作,调用datanode.closeBlock()向NameNode汇报当前DataNode接收到新的数据块notifyNamenodeReceivedBlock。
// update its generation stamp
if (isClient &&
stage == BlockConstructionStage.PIPELINE_CLOSE_RECOVERY) {
block.setGenerationStamp(latestGenerationStamp);
block.setNumBytes(minBytesRcvd);
}
if (isDatanode ||
stage == BlockConstructionStage.PIPELINE_CLOSE_RECOVERY) {
datanode.closeBlock(block, DataNode.EMPTY_DEL_HINT, storageUuid);
LOG.info("Received " + block + " src: " + remoteAddress + " dest: "
+ localAddress + " of size " + block.getNumBytes());
}
从上面的DataXceiver.writeBlock()方法的具体分析可以知道,其真正处理接收上游数据块,写入本地磁盘,并转发到数据流管道中的下游节点的处理类及过程是:blockReceiver.receiveBlock(),接下来对blockReceiver类进行详细的分析:
BlockReceiver类:
blockReceiver.receiveBlock()方法会先启动packetResponder线程负责接收并转发下游数据节点发送的确认数据包的ACK消息。之后receiverBlock方法循环调用receiverpacket()方法接收上游写入的数据包并发送这个数据包到下游节点,成功完成整个数据块的写入操作后,receiverBlock方法关闭Packetresponder线程。其接收数据块流程如下:
void receiveBlock(
DataOutputStream mirrOut, // output to next datanode
DataInputStream mirrIn, // input from next datanode
DataOutputStream replyOut, // output to previous datanode
String mirrAddr, DataTransferThrottler throttlerArg,
DatanodeInfo[] downstreams,
boolean isReplaceBlock) throws IOException {
//...... 参数设置
try {
// 如果是客户端发起的写请求(此处即为数据块create),
// 则启动PacketResponder发送ack
if (isClient && !isTransfer) {
responder = new Daemon(datanode.threadGroup,
new PacketResponder(replyOut, mirrIn, downstreams));
responder.start(); // start thread to processes responses
}
// 循环同步接收packet,写block文件和meta文件
while (receivePacket() >= 0) {}
// 此时节点已接收了所有packet,可以等待发送完所有ack后关闭responder
if (responder != null) {
((PacketResponder)responder.getRunnable()).close();
responderClosed = true;
}
//...... 数据块复制相关
} catch (IOException ioe) {
//...... 异常处理
} finally {
//...... 清理
}
}
在blockReceiver.receiveBlock()方法中会同步循环调用receivePacket()方法来完整的接收数据块所切分的所有数据包;receivePacket()方法首先会从输入流中取出一个数据包,并将这个数据包放在缓冲区中,receivePacket()成功接收数据包之后,会判断当前节点是否是数据流管道中的最后一个节点,或者输入流是否启动了同步数据块标识,要求Datanode立即将数据包同步到磁盘。在这两种情况下,datanode会先将数据写入磁盘,然后再通知packetResponder处理确认消息,否则,receivePacket()方法接收完数据包后会立即通知packetResponder处理确认消息。接下来receivePacket()会将数据包发送给数据流管道中的下游节点,然后就可以将数据块文件和校验文件写入数据节点的磁盘,如果当前节点时数据流管道中的最后一个节点,则在写入磁盘前,需要对数据包进行校验。
private int receivePacket() throws IOException {
// read the next packet
packetReceiver.receiveNextPacket(in);
// ...... 检查packet头
PacketHeader header = packetReceiver.getHeader();
long offsetInBlock = header.getOffsetInBlock();
long seqno = header.getSeqno();
boolean lastPacketInBlock = header.isLastPacketInBlock();
final int len = header.getDataLen();
boolean syncBlock = header.getSyncBlock();
// ......
// 如果不需要立即持久化也不需要校验收到的数据,
// 则可以将当前packet响应信息ack加入ackQueue中,委托PacketResponder线程返回SUCCESS的ack,然后再进行校验和持久化
if (responder != null && !syncBlock && !shouldVerifyChecksum()) {
((PacketResponder) responder.getRunnable()).enqueue(seqno,
lastPacketInBlock, offsetInBlock, Status.SUCCESS);
}
// First write the packet to the mirror:
// 向下游节点发送数据包
if (mirrorOut != null && !mirrorError) {
try {
long begin = Time.monotonicNow();
// For testing. Normally no-op.
DataNodeFaultInjector.get().stopSendingPacketDownstream();
packetReceiver.mirrorPacketTo(mirrorOut);
mirrorOut.flush();
} catch (IOException e) {
handleMirrorOutError(e);
}
}
ByteBuffer dataBuf = packetReceiver.getDataSlice();
ByteBuffer checksumBuf = packetReceiver.getChecksumSlice();
if (lastPacketInBlock || len == 0) {
// 收到空packet可能是表示心跳或数据块发送
// 这两种情况都可以尝试把之前的数据刷到磁盘
if (syncBlock) {
flushOrSync(true);
}
} else {
// 持久化packet
// 如果是管道中的最后一个节点,则持久化之前,要先对收到的packet做一次校验
// 如果校验错误,则将packet响应信息ack加入ackQueue中,委托PacketResponder线程返回 ERROR_CHECKSUM 的ack
final boolean shouldNotWriteChecksum = checksumReceivedLen == 0
&& streams.isTransientStorage();
try {
long onDiskLen = replicaInfo.getBytesOnDisk();
if (onDiskLen<offsetInBlock) {
// 如果校验块不完整,需要加载并调整旧的meta文件内容,供后续重新计算crc
// 写block文件
int startByteToDisk = (int)(onDiskLen-firstByteInBlock)
+ dataBuf.arrayOffset() + dataBuf.position();
int numBytesToDisk = (int)(offsetInBlock-onDiskLen);
out.write(dataBuf.array(), startByteToDisk, numBytesToDisk);
// 写meta文件
final byte[] lastCrc;
if (shouldNotWriteChecksum) {
lastCrc = null;
} else if (partialCrc != null) { // 如果是校验块不完整(之前收到过一部分)
// 重新计算crc 更新lastCrc
checksumOut.write(buf);
partialCrc = null;
} else { // 如果校验块完整
// 更新lastCrc
checksumOut.write(checksumBuf.array(), offset, checksumLen);
}
// ......
}
} catch (IOException iex) {
datanode.checkDiskErrorAsync();
throw iex;
}
}
// if sync was requested, put in queue for pending acks here
// (after the fsync finished)
// 如果需要立即持久化或需要校验收到的数据,则现在已经完成了持久化和校验
// 将当前packet响应信息ack加入ackQueue中,委托PacketResponder线程返回SUCCESS的ack
if (responder != null && (syncBlock || shouldVerifyChecksum())) {
((PacketResponder) responder.getRunnable()).enqueue(seqno,
lastPacketInBlock, offsetInBlock, Status.SUCCESS);
}
// ......
return lastPacketInBlock?-1:len;
}
BlockReceiver.PacketResponder类:
PacketResponder是一个线程类,它和BlockReceiver共同完成数据块的写操作流程。BlockReceiver完成对指定数据包的处理之后,会触发PacketResponder类处理当前数据包的响应消息,PacketResponder监听下游的输入流,接收到这个数据包的确认消息之后,在确认信息中,添加当前数据节点的确认消息,然后将这个消息发送给上游数据节点。
BlockReceiver在完成对指定的数据包处理之后,会调用委托PacketResponder类来处理这个数据包的响应。其首先会调用PacketResponder.enqueue()方法,将当前节点对当前数据包的响应信息ack加入到ackQueue队列(存储当前节点对当前packet包的响应信息)中。然后调用notify()方法通知PacketResponder.run()方法处理该数据包的响应信息。可以看到ackQueue是一个典型的生产者-消费者队列。
完成上述操作之后,packetResponder会判断当前接收的数据包的响应是否为数据块中最后一个数据包的响应,如果是,则调用finalizeBlock()方法向namenode提交这个数据块,并在完成数据包的响应处理之后,从ackQueue队列中移除这个数据包。
void enqueue(final long seqno, final boolean lastPacketInBlock,
final long offsetInBlock, final Status ackStatus) {
final Packet p = new Packet(seqno, lastPacketInBlock, offsetInBlock,
System.nanoTime(), ackStatus);
if(LOG.isDebugEnabled()) {
LOG.debug(myString + ": enqueue " + p);
}
synchronized(ackQueue) {
if (running) {
// 将当前节点对当前packet包的响应信息加入ackQueue中
ackQueue.addLast(p);
ackQueue.notifyAll();
}
}
}
public void run() {
while (isRunning() && !lastPacketInBlock) {
try {
Packet pkt = null
PipelineAck ack = new PipelineAck();
try {
// 如果当前节点不是管道的最后一个节点,且下游节点正常,则从下游读取ack
if (type != PacketResponderType.LAST_IN_PIPELINE && !mirrorError) {
ack.readFields(downstreamIn);
seqno = ack.getSeqno();
}
// 如果从下游节点收到了正常的ack,或当前节点是管道的最后一个节点,
// 则需要从队列中取出当前节点对pkt的响应信息(即BlockReceiver#receivePacket()放入的ack)
if (seqno != PipelineAck.UNKOWN_SEQNO
|| type == PacketResponderType.LAST_IN_PIPELINE) {
pkt = waitForAckHead(seqno);
if (!isRunning()) {
break;
}
// 判断下游接收序号与当前节处理序号是否相等
// 可知该序号的packet是否已经正确接收
expected = pkt.seqno;
if (type == PacketResponderType.HAS_DOWNSTREAM_IN_PIPELINE
&& seqno != expected) {
throw new IOException(myString + "seqno: expected=" + expected
+ ", received=" + seqno);
}
lastPacketInBlock = pkt.lastPacketInBlock;
}
} catch (InterruptedException ine) {
// ......异常处理
}
// ......
// 如果是最后一个packet,将block的状态转换为FINALIZED,并关闭BlockReceiver
if (lastPacketInBlock) {
finalizeBlock(startTime);
}
// 此时ack.seqno==pkt.seqno,将 下游节点的响应和当前节点的响应 构造新ack发送给上游
sendAckUpstream(ack, expected, totalAckTimeNanos,
(pkt != null ? pkt.offsetInBlock : 0),
(pkt != null ? pkt.ackStatus : Status.SUCCESS));
if (pkt != null) {
removeAckHead();
}
} catch (IOException e) {
// ......异常处理
}
}
}
最后在PacketResponder处理完所有数据包的响应信息后,其会调用PacketResponder#finalizeBlock()方法来告知NameNode当前DataNode已经成功的接受了当前数据块;以便NameNode更新对应的命名空间。PacketResponder#finalizeBlock()方法最终会调用BPOfferService.notifyNamenodeReceivedBlock()来通知NameNode。该处的源码较为简单就不做过多的赘述了。
最后,总结一下PacketResponder响应线程的整体流程如下:
- 从下游的datanode中读取响应数据ack
- 调用waitForAckHead方法从ackQueue队列中获取数据包响应信息。
- 对从下游获取的数据包和从队列中的数据包的seqno进行比较,如果不一致的话,直接抛出异常。
- 如果是最后一个数据包,调用finalizeBlock()方法完成数据块
- 把当前的packet ack确认信息和下游的ack确认信息合并,然后发到上游节点
- 从ackQueue里删除响应的数据包信息