首选介绍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行是构造返回给流水线上一级的确认信息,并将信息写出。