HDFS1.0源代码解析—数据传输和接受的类BlockSender和BlockReceiver

11 篇文章 0 订阅
10 篇文章 0 订阅
本次主要介绍DN端进行数据传输和接受的类BlockSender和BlockReceiver,其中BlockSender是读取DN本地block数据传送回数据请求端,BlockReceiver是接受存储数据,写入到本地的block中。
首选介绍BlockSender的主要函数的作用:

构造函数

 94       this.blockLength = datanode.data.getVisibleLength(block);

 99         checksumIn = new DataInputStream(
100                 new BufferedInputStream(datanode.data.getMetaDataInputStream(block),
101                                         BUFFER_SIZE));

123       bytesPerChecksum = checksum.getBytesPerChecksum();

129       checksumSize = checksum.getChecksumSize();

145       offset = (startOffset - (startOffset % bytesPerChecksum));
146       if (length >= 0) {
147         // Make sure endOffset points to end of a checksumed chunk.
148         long tmpLen = startOffset + length;
149         if (tmpLen % bytesPerChecksum != 0) {
150           tmpLen += (bytesPerChecksum - tmpLen % bytesPerChecksum);
151         }
152         if (tmpLen < endOffset) {
153           endOffset = tmpLen;
154         }
155       }
156
157       // seek to the right offsets
158       if (offset > 0) {
159         long checksumSkip = (offset / bytesPerChecksum) * checksumSize;
160         // note blockInStream is  seeked when created below
161         if (checksumSkip > 0) {
162           // Should we use seek() for checksum file as well?
163           IOUtils.skipFully(checksumIn, checksumSkip);
164         }
165       }
166       seqno = 0;
167
168       blockIn = datanode.data.getBlockInputStream(block, offset); // seek to offset

在开始介绍构造函数之前,先说一下DN上每个block的存放形式,block以blk_3910275445015356807和blk_3910275445015356807_1001.meta这样的形式存在,第一个文件存储内容,第二个存储元信息,所以才读取block的时候需要读取这两个文件。

开始看一下构造函数的主要作用,94行获取block文件的长度,99行打开block对应的元信息文件,123行获取每个chunk的大小,129行获取chunksum的大小。那么chunk和chunksum是什么呢,这里需要介绍下DN传送和接受数据的特点,每次传输和接受一个package,每个package包含若干个chunk,每个chunk对应的元信息就是chunksum。有了这个背景知识就知道145行的含义了,我们要求读取的数据可以从任意位置开始,但是DN传送时却要从一个整的chunk开始,因为元信息就是按照chunk为单位计算和存储的,这个ooffset就是我们要求读取的位置和实际读取位置的一个偏移,这个需要传回客户端,根据这个值用户才不会多介绍数据。149和150行对应读取的末尾做同样的处理,但是这个地方为什么没有把多读取数据的偏移量返回呢,是因为我们既然已经知道了起始位置有知道了读取的长度,所以我们知道实际的末尾,即使传送的数据有多余信息也不会造成影响。163行是使读取的元信息的文件流移动到要读取的位置,168行是打开block文件,并将文件流偏移到读取的位置。

构造函数初始化完毕以后,再来看传送数据的主函数sendBlock

383   long sendBlock(DataOutputStream out, OutputStream baseStream,
384                  BlockTransferThrottler throttler) throws IOException {

396       try {
397         checksum.writeHeader(out);
398         if ( chunkOffsetOK ) {
399           out.writeLong( offset );
400         }

433       ByteBuffer pktBuf = ByteBuffer.allocate(pktSize);
434
435       while (endOffset > offset) {
436         long len = sendChunks(pktBuf, maxChunksPerPacket,
437                               streamForSendChunks);
438         offset += len;
439         totalRead += len + ((len + bytesPerChecksum - 1)/bytesPerChecksum*
440                             checksumSize);
441         seqno++;
442       }
443       try {
444         out.writeInt(0); // mark the end of block
445         out.flush();
397行是首先传送一些元数据信息,399传送读取数据时多余的偏移,433行声明读取数据的缓冲区,436调用另外一个函数真正进行数据的传输,每次传送一个package,444行传送一个0表示传送结束。

下面来看真正负责数据传输的sendChunks方法

245     if (len > bytesPerChecksum && len % bytesPerChecksum != 0) {
246       len -= len % bytesPerChecksum;
247     }

257     // write packet header
258     pkt.putInt(packetLen);
259     pkt.putLong(offset);
260     pkt.putLong(seqno);
261     pkt.put((byte)((offset + len >= endOffset) ? 1 : 0));
262                //why no ByteBuf.putBoolean()?
263     pkt.putInt(len);
264
265     int checksumOff = pkt.position();
266     int checksumLen = numChunks * checksumSize;
267     byte[] buf = pkt.array();

269     if (checksumSize > 0 && checksumIn != null) {
270       try {
271         checksumIn.readFully(buf, checksumOff, checksumLen);
272       } catch (IOException e) {

289     int dataOff = checksumOff + checksumLen;
245-249行主要是控制在传输最后一块数据之前传输的所有都是整块,这点好处注释中有写,但是不是太明白,希望大神留言指点。257-263行是写入一些读取使用的控制信息,比如packet的长度、偏移、序号等等。269到272行读取元数据信息,289以后是对读取block真实数据的方式做判断,最终都是将数据写入到pkt中。最后,输出流out将数据输出。


下面来看BlockReceiver类的分析,首先来看一个该类的构造函数,因为在DataXceiver类中也是先调用构造函数创建该类的对象,

构造函数主要工作还是初始化一些必须的变量的信息,一个不太直观的地方是下面吗这句代码

 99       streams = datanode.data.writeToBlock(block, isRecovery,
100                               clientName == null || clientName.length() == 0);
这个地方调用的是FSDataset的writeToBlock方法,我们来跟进看一下这个方法干了些什么事情,

如果这个block是不存在的,流程如下

1438       if (!isRecovery) {
1439         v = volumes.getNextVolume(blockSize);
1440         // create temporary file to hold block in the designated volume
1441         f = createTmpFile(v, b, replicationRequest);
1442       } else if (f != null) {

1487       if (replicationRequest) {
1488         volumeMap.put(b, new DatanodeBlockInfo(v));
1489       } else {
1490         volumeMap.put(b, new DatanodeBlockInfo(v, f));
1491       }
1492       ongoingCreates.put(b, new ActiveFile(f, threads));
1493     }
1439行返回一个可以容纳要写入block的一个volume(也就是一个配置的存储路径)1441行是创建存储写入数据的一个临时文件,也就是在前面博客中提到的tmp文件目录中,接下来就是将创建的block与对应的存储路径的关系添加到volumeMap中,最后将正在写入的block添加到 ongoingCreates中,说明这是一个正在创建的文件。最后是打开存储block的数据文件和元信息的文件,分别对应两个文件流dataOut和checksumOut。

下面来数据存储的主函数receiveBlock

516     try {
517       // write data chunk header
518       if (!finalized) {
519         BlockMetadataHeader.writeHeader(checksumOut, checksum);
520       }
521       if (clientName.length() > 0) {
522         responder = new Daemon(datanode.threadGroup,
523                                new PacketResponder(this, block, mirrIn,
524                                                    replyOut, numTargets,
525                                                    Thread.currentThread()));
526         responder.start(); // start thread to processes reponses
527       }
528
529       /*
530        * Receive until packet length is zero.
531        */
532       while (receivePacket() > 0) {}
519行首先介绍存储的一些原始信息,在上边blockSender我们也看到发送数据一开始也是传输的元信息,两者是对应的。521-526可以看出是创建一个PacketResponder的线程对象,并启动这个线程,这个线程的作用后便单独介绍。看一个512行这个while循环,receivePacket方法是接受数据的主方法

376     int payloadLen = readNextPacket();

382     buf.mark();
383     //read the header
384     buf.getInt(); // packet length
385     offsetInBlock = buf.getLong(); // get offset of packet in block
386     long seqno = buf.getLong();    // get seqno
387     boolean lastPacketInBlock = (buf.get() != 0);
388
389     int endOfHeader = buf.position();
390     buf.reset();

402     // First write the packet to the mirror:
403     if (mirrorOut != null && !mirrorError) {
404       try {
405         mirrorOut.write(buf.array(), buf.position(), buf.remaining());
406         mirrorOut.flush();

412     buf.position(endOfHeader);
413     int len = buf.getInt();

433       int checksumOff = buf.position();
434       int dataOff = checksumOff + checksumLen;
435       byte pktBuf[] = buf.array();
436
437       buf.position(buf.limit()); // move to the end of the data.

446       if (mirrorOut == null || clientName.length() == 0) {
447         verifyChunks(pktBuf, dataOff, len, pktBuf, checksumOff);
448       }

453           out.write(pktBuf, dataOff, len);

471             checksumOut.write(pktBuf, checksumOff, checksumLen);

488     if (responder != null) {
489       ((PacketResponder)responder.getRunnable()).enqueue(seqno,
490                                       lastPacketInBlock);
491     }
376行是具体执行数据读取的方法,一会介绍(好多层啊^_^)。382mark做一下标记,为后面的reset做准备。384-387行读取传送的vblock的一些信息,389行记录当前读取的位置。390将buf复位到382mark的地方(ByteBuffer是一个操作非常灵活的数据结构)。403-406行将读取的数据传送到流水线下游的机器,412将buf的读取指针移动到刚才读取的位置,接下来就是读取一下信息,将buf的真正的数据内容拷贝到pktBuf中。446-448行是在两种情况下对数据进行CRC校验,一种是流水线末端的机器,一种是数据从非客户端传输而来(比如NN的copy命令)。453和471是真正将数据写入到磁盘中,488-491是将接受到的package加入到PacketResponder的队列中。

再来看刚才提到的readNextPacket方法,

290     if (buf == null) {
291       /* initialize buffer to the best guess size:
292        * 'chunksPerPacket' calculation here should match the same
293        * calculation in DFSClient to make the guess accurate.
294        */
295       int chunkSize = bytesPerChecksum + checksumSize;
296       int chunksPerPacket = (datanode.writePacketSize - DataNode.PKT_HEADER_LEN -
297                              SIZE_OF_INTEGER + chunkSize - 1)/chunkSize;
298       buf = ByteBuffer.allocate(DataNode.PKT_HEADER_LEN + SIZE_OF_INTEGER +
299                                 Math.max(chunksPerPacket, 1) * chunkSize);
300       buf.limit(0);
301     }

317     buf.mark();
318     int payloadLen = buf.getInt();
319     buf.reset();
320
321     if (payloadLen == 0) {
322       //end of stream!
323       buf.limit(buf.position() + SIZE_OF_INTEGER);
324       return 0;
325     }

333     int pktSize = payloadLen + DataNode.PKT_HEADER_LEN;
334
335     if (buf.remaining() < pktSize) {
336       //we need to read more data
337       int toRead = pktSize - buf.remaining();
338
339       // first make sure buf has enough space.
340       int spaceLeft = buf.capacity() - buf.limit();
341       if (toRead > spaceLeft && buf.position() > 0) {
342         shiftBufData();
343         spaceLeft = buf.capacity() - buf.limit();
344       }
345       if (toRead > spaceLeft) {
346         byte oldBuf[] = buf.array();
347         int toCopy = buf.limit();
348         buf = ByteBuffer.allocate(toCopy + toRead);
349         System.arraycopy(oldBuf, 0, buf.array(), 0, toCopy);
350         buf.limit(toCopy);
351       }

354       while (toRead > 0) {
355         toRead -= readToBuf(toRead);
356       }
290-301行是当buf没有初始化的时候,对buf的可能大小进行一个预判,声明一个尽量符合有求的缓冲区。317-325行读取第一个整数,根据它的值判断到达传送的末尾,因为在传输结束时,会发送一个0表示传送结束。335到351这一堆代码是计算需要读取的数据的数量,以及当前的缓冲区是不是够大,如果不够首先进行移位操作(如果缓冲区最左边有无效的数据),再不够就开辟一段更大的缓冲区。最后,调用readToBuf方法调用。

好吧,我们接着来看readToBuf

260     if (toRead < 0) {
261       toRead = (maxPacketReadLen > 0 ? maxPacketReadLen : buf.capacity())
262                - buf.limit();
263     }
264
265     int nRead = in.read(buf.array(), buf.limit(), toRead);
266
267     if (nRead < 0) {
268       throw new EOFException("while trying to read " + toRead + " bytes");
269     }
270     bufRead = buf.limit() + nRead;
271     buf.limit(bufRead);
260-263行当toRead小于0时,通过计算获得一个值,其中maxPacketReadLen这个值记录的是所有读取中最大的package的值,然后就是读取数据。ok这个嵌套多层的读取过程结束。

最后我们来看PacketResponder线程,既然是线程类我们就看一个它的run方法

799                 pkt = ackQueue.removeFirst();
800                 expected = pkt.seqno;
801                 notifyAll();

804               if (numTargets > 0 && !localMirrorError) {
805                 // read an ack from downstream datanode
806                 ack.readFields(mirrorIn);

851             if (lastPacketInBlock && !receiver.finalized) {
852               receiver.close();
853               final long endTime = ClientTraceLog.isInfoEnabled() ? System.nanoTime() : 0;
854               block.setNumBytes(receiver.offsetInBlock);
855               //将写入的文件从tem文件夹转移到current文件夹下
856               datanode.data.finalizeBlock(block);

881                 short ackLen = numTargets == 0 ? 0 : ack.getNumOfReplies();
882                 replies = new short[1+ackLen];
883                 replies[0] = DataTransferProtocol.OP_STATUS_SUCCESS;
884                 for (int i=0; i<ackLen; i++) {
885                     replies[i+1] = ack.getReply(i);

888             PipelineAck replyAck = new PipelineAck(expected, replies);
889
890             // send my ack back to upstream datanode
891             replyAck.write(replyOut);
892             replyOut.flush();
799-801行获取期待返回的pkt,804-806行读取返回的pjt信息,之后会对期待获取的信息与实际获取的信息进行对比。851-856行会对pkt是否是最后一个pkt进行判断如果是将关闭receiver线程,并将写入的文件数据从tmp文件移动到current文件目录下,前面介绍过一开始数据暂时写到tmp目录下。881到892行是构造返回给流水线上一级的确认信息,并将信息写出。




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值