hadoop 4.1.0 cdh4读文件源码分析

本文详细解析了Hadoop中客户端读取文件的流程,包括客户端如何与NameNode交互获取Block信息,选择合适的DataNode进行读取,以及通过BlockReader逐个Packet读取数据并进行校验的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上篇文章分析了hadoop写文件的流程,既然明白了文件是怎么写入的,再来理解读就顺畅一些了。

 

同样的,本文主要探讨客户端的实现,同样的,我依然推荐读一下http://www.cnblogs.com/duguguiyu/archive/2009/02/22/1396034.html,读文件的大致流程如下:


不论是文件读取,还是文件的写入,主控服务器扮演的都是中介的角色。客户端把自己的需求提交给主控服务器,主控服务器挑选合适的数据服务器,介绍给客户端,让客户端和数据服务器单聊,要读要写随你们便。这种策略类似于DMA,降低了主控服务器的负载,提高了效率。。。
因此,在文件读写操作中,最主要的通信,发生在客户端与数据服务器之间。它们之间跑的协议是ClientDatanodeProtocol。从这个协议中间,你无法看到和读写相关的接口,因为,在Hadoop中,读写操作是不走RPC机制的,而是另立门户,独立搭了一套通信框架。在数据服务器一端,DataNode类中有一个DataXceiverServer类的实例,它在一个单独的线程等待请求,一旦接到,就启动一个DataXceiver的线程,处理此次请求。一个请求一个线程,对于数据服务器来说,逻辑上很简单。当下,DataXceiver支持的请求类型有六种,具体的请求包和回复包格式,请参见这里,这里,这里。在Hadoop的实现中,并没有用类来封装这些请求,而是按流的次序写下来,这给代码阅读带来挺多的麻烦,也对代码的维护带来一定的困难,不知道是出于何种考虑。。。
相比于写,文件的读取实在是一个简单的过程。在客户端DFSClient中,有一个DFSClient.DFSInputStream类。当需要读取一个文件的时候,会生成一个DFSInputStream的实例。它会先调用ClientProtocol定义getBlockLocations接口,提供给NameNode文件路径、读取位置、读取长度信息,从中取得一个LocatedBlocks类的对象,这个对象包含一组LocatedBlock,那里面有所规定位置中包含的所有数据块信息,以及数据块对应的所有数据服务器的位置信息。当读取开始后,DFSInputStream会先尝试从某个数据块对应的一组数据服务器中选出一个,进行连接。这个选取算法,在当下的实现中,非常简单,就是选出第一个未挂的数据服务器,并没有加入客户端与数据服务器相对位置的考量。读取的请求,发送到数据服务器后,自然会有DataXceiver来处理,数据被一个包一个包发送回客户端,等到整个数据块的数据都被读取完了,就会断开此链接,尝试连接下一个数据块对应的数据服务器,整个流程,依次如此反复,直到所有想读的都读取完了为止。。。

 

跟写文件类似,读文件的主要逻辑在DFSInputStream类中。先看下构造函数:

[java]  view plain copy
  1. DFSInputStream(DFSClient dfsClient, String src, int buffersize, boolean verifyChecksum  
  2.                  ) throws IOException, UnresolvedLinkException {  
  3.     this.dfsClient = dfsClient;  
  4.     this.verifyChecksum = verifyChecksum;  
  5.     this.buffersize = buffersize;  
  6.     this.src = src;  
  7.     this.socketCache = dfsClient.socketCache;  
  8.     prefetchSize = dfsClient.getConf().prefetchSize;  
  9.     timeWindow = dfsClient.getConf().timeWindow;  
  10.     nCachedConnRetry = dfsClient.getConf().nCachedConnRetry;  
  11.     openInfo();  
  12.   }  

在写文件的准备工作方法openInfo中会向namenode读取被打开文件所需的所有BlockId。

[java]  view plain copy
  1. LocatedBlocks newInfo = dfsClient.getLocatedBlocks(src, 0, prefetchSize);  
  2.     if (DFSClient.LOG.isDebugEnabled()) {  
  3.       DFSClient.LOG.debug("newInfo = " + newInfo);  
  4.     }  
  5.     if (newInfo == null) {  
  6.       throw new IOException("Cannot open filename " + src);  
  7.     }  
  8.   
  9.     if (locatedBlocks != null) {  
  10.       Iterator<LocatedBlock> oldIter = locatedBlocks.getLocatedBlocks().iterator();  
  11.       Iterator<LocatedBlock> newIter = newInfo.getLocatedBlocks().iterator();  
  12.       while (oldIter.hasNext() && newIter.hasNext()) {  
  13.         if (! oldIter.next().getBlock().equals(newIter.next().getBlock())) {  
  14.           throw new IOException("Blocklist for " + src + " has changed!");  
  15.         }  
  16.       }  
  17.     }  
  18.     locatedBlocks = newInfo;  
  19.     long lastBlockBeingWrittenLength = 0;  
  20.     if (!locatedBlocks.isLastBlockComplete()) {  
  21.       final LocatedBlock last = locatedBlocks.getLastLocatedBlock();  
  22.       if (last != null) {  
  23.         if (last.getLocations().length == 0) {  
  24.           return -1;  
  25.         }  
  26.         final long len = readBlockLength(last);  
  27.         last.getBlock().setNumBytes(len);  
  28.         lastBlockBeingWrittenLength = len;   
  29.       }  
  30.     }  
  31.   
  32.     currentNode = null;  
  33.     return lastBlockBeingWrittenLength;  

1.dfsClient.getLocatedBlocks方法实际调用了namenode.getBlockLocations返回所有的blockId。

2.查看blockId信息是否已被cache,如没有则将cache赋值。

3.判断该文件是否isLastBlockComplete,在hadoop中写文件实际是把block写入到datanode中,而namenode是通过datanode定期的汇报得知该文件到底由哪几个block组成的。因此,在读某个文件时可能存在datanode还未汇报给namenode的情况,因此,我们在读文件时只能读到最后一个汇报的block块。

 

下面看下read方法。

[java]  view plain copy
  1. public synchronized int read(final byte buf[], int off, int len) throws IOException {  
  2.     ReaderStrategy byteArrayReader = new ByteArrayStrategy(buf);  
  3.   
  4.     return readWithStrategy(byteArrayReader, off, len);  
  5.   }  

首先会创建一个ByteArrayStrategy的reader,这种reader会将block依次读到buf数组中,hadoop还提供一个ByteBufferStrategy用来支持NIO模式的读。

然后执行readWithStrategy。

[java]  view plain copy
  1. try {  
  2.           // currentNode can be left as null if previous read had a checksum  
  3.           // error on the same block. See HDFS-3067  
  4.           if (pos > blockEnd || currentNode == null) {  
  5.             currentNode = blockSeekTo(pos);  
  6.           }  
  7.           int realLen = (int) Math.min(len, (blockEnd - pos + 1L));  
  8.           int result = readBuffer(strategy, off, realLen, corruptedBlockMap);  
  9.             
  10.           if (result >= 0) {  
  11.             pos += result;  
  12.           } else {  
  13.             // got a EOS from reader though we expect more data on it.  
  14.             throw new IOException("Unexpected EOS from the reader");  
  15.           }  
  16.           if (dfsClient.stats != null && result != -1) {  
  17.             dfsClient.stats.incrementBytesRead(result);  
  18.           }  
  19.           return result;  

1.pos指当前读文件的偏移量,首先根据pos获取当前应该读的block对象

2.根据block对象向namenode询问有哪些datanode拥有该block,选择需要读取的datanode

3.建立client-datanode的链接,创建BlockReader。

4.readBuffer方法调用不同的readStrategy的doRead方法从block中读取想要的数据。

 

下面对上面三步分别解释下:

1.获取block对象

[java]  view plain copy
  1. public int findBlock(long offset) {  
  2.     // create fake block of size 0 as a key  
  3.     LocatedBlock key = new LocatedBlock(  
  4.         new ExtendedBlock(), new DatanodeInfo[0], 0L, false);  
  5.     key.setStartOffset(offset);  
  6.     key.getBlock().setNumBytes(1);  
  7.     Comparator<LocatedBlock> comp =   
  8.       new Comparator<LocatedBlock>() {  
  9.         // Returns 0 iff a is inside b or b is inside a  
  10.         @Override  
  11.         public int compare(LocatedBlock a, LocatedBlock b) {  
  12.           long aBeg = a.getStartOffset();  
  13.           long bBeg = b.getStartOffset();  
  14.           long aEnd = aBeg + a.getBlockSize();  
  15.           long bEnd = bBeg + b.getBlockSize();  
  16.           if(aBeg <= bBeg && bEnd <= aEnd   
  17.               || bBeg <= aBeg && aEnd <= bEnd)  
  18.             return 0// one of the blocks is inside the other  
  19.           if(aBeg < bBeg)  
  20.             return -1// a's left bound is to the left of the b's  
  21.           return 1;  
  22.         }  
  23.       };  
  24.     return Collections.binarySearch(blocks, key, comp);  
  25.   }  

获得Block对象的核心方法是findBlock方法。通过比较各个block在整个文件中的位移来确定当前位移在哪个block中

 

2.获得datanode

[java]  view plain copy
  1. static DatanodeInfo bestNode(DatanodeInfo nodes[],   
  2.                                AbstractMap<DatanodeInfo, DatanodeInfo> deadNodes)  
  3.                                throws IOException {  
  4.     if (nodes != null) {   
  5.       for (int i = 0; i < nodes.length; i++) {  
  6.         if (!deadNodes.containsKey(nodes[i])) {  
  7.           return nodes[i];  
  8.         }  
  9.       }  
  10.     }  
  11.     throw new IOException("No live nodes contain current block");  
  12.   }  

获得best的datanode很简单。就是遍历该block所有的datanode,按顺序取最前面的。不过,其实在namenode端返回datanodeList时就是按照优先级顺序返回的。

 

[java]  view plain copy
  1. private DNAddrPair chooseDataNode(LocatedBlock block)  
  2.     throws IOException {  
  3.     while (true) {  
  4.       DatanodeInfo[] nodes = block.getLocations();  
  5.       try {  
  6.         DatanodeInfo chosenNode = bestNode(nodes, deadNodes);  
  7.         final String dnAddr =  
  8.             chosenNode.getXferAddr(dfsClient.connectToDnViaHostname());  
  9.         if (DFSClient.LOG.isDebugEnabled()) {  
  10.           DFSClient.LOG.debug("Connecting to datanode " + dnAddr);  
  11.         }  
  12.         InetSocketAddress targetAddr = NetUtils.createSocketAddr(dnAddr);  
  13.         return new DNAddrPair(chosenNode, targetAddr);  
  14.       } catch (IOException ie) {  
  15.         String blockInfo = block.getBlock() + " file=" + src;  
  16.         if (failures >= dfsClient.getMaxBlockAcquireFailures()) {  
  17.           throw new BlockMissingException(src, "Could not obtain block: " + blockInfo,  
  18.                                           block.getStartOffset());  
  19.         }  
  20.           
  21.         if (nodes == null || nodes.length == 0) {  
  22.           DFSClient.LOG.info("No node available for block: " + blockInfo);  
  23.         }  
  24.         DFSClient.LOG.info("Could not obtain block " + block.getBlock()  
  25.             + " from any node: " + ie  
  26.             + ". Will get new block locations from namenode and retry...");  
  27.         try {  
  28.           // Introducing a random factor to the wait time before another retry.  
  29.           // The wait time is dependent on # of failures and a random factor.  
  30.           // At the first time of getting a BlockMissingException, the wait time  
  31.           // is a random number between 0..3000 ms. If the first retry  
  32.           // still fails, we will wait 3000 ms grace period before the 2nd retry.  
  33.           // Also at the second retry, the waiting window is expanded to 6000 ms  
  34.           // alleviating the request rate from the server. Similarly the 3rd retry  
  35.           // will wait 6000ms grace period before retry and the waiting window is  
  36.           // expanded to 9000ms.   
  37.           double waitTime = timeWindow * failures +       // grace period for the last round of attempt  
  38.             timeWindow * (failures + 1) * DFSUtil.getRandom().nextDouble(); // expanding time window for each failure  
  39.           DFSClient.LOG.warn("DFS chooseDataNode: got # " + (failures + 1) + " IOException, will wait for " + waitTime + " msec.");  
  40.           Thread.sleep((long)waitTime);  
  41.         } catch (InterruptedException iex) {  
  42.         }  
  43.         deadNodes.clear(); //2nd option is to remove only nodes[blockId]  
  44.         openInfo();  
  45.         block = getBlockAt(block.getStartOffset(), false);  
  46.         failures++;  
  47.         continue;  
  48.     }  
  49.     }  
  50.   }   

这边主要是创建datanode的socket失败的重试,采取了graceful的sleep方式,可以学习一下,不过好像实际sleep的方式跟注释中描述的不一样。

 

3.创建BlockReader

[java]  view plain copy
  1. // Can't local read a block under construction, see HDFS-2757  
  2.     if (dfsClient.shouldTryShortCircuitRead(dnAddr) &&  
  3.         !blockUnderConstruction()) {  
  4.       return DFSClient.getLocalBlockReader(dfsClient.conf, src, block,  
  5.           blockToken, chosenNode, dfsClient.hdfsTimeout, startOffset,  
  6.           dfsClient.connectToDnViaHostname());  
  7.     }  

ShortCircuitRead是hadoop的一个优化。在client与datanode在同一台机器时会直接读本地文件而不是通过socket向datanode读取block。

[java]  view plain copy
  1. // Allow retry since there is no way of knowing whether the cached socket  
  2.     // is good until we actually use it.  
  3.     for (int retries = 0; retries <= nCachedConnRetry && fromCache; ++retries) {  
  4.       SocketAndStreams sockAndStreams = null;  
  5.       // Don't use the cache on the last attempt - it's possible that there  
  6.       // are arbitrarily many unusable sockets in the cache, but we don't  
  7.       // want to fail the read.  
  8.       if (retries < nCachedConnRetry) {  
  9.         sockAndStreams = socketCache.get(dnAddr);  
  10.       }  
  11.       Socket sock;  
  12.       if (sockAndStreams == null) {  
  13.         fromCache = false;  
  14.   
  15.         sock = dfsClient.socketFactory.createSocket();  
  16.           
  17.         // TCP_NODELAY is crucial here because of bad interactions between  
  18.         // Nagle's Algorithm and Delayed ACKs. With connection keepalive  
  19.         // between the client and DN, the conversation looks like:  
  20.         //   1. Client -> DN: Read block X  
  21.         //   2. DN -> Client: data for block X  
  22.         //   3. Client -> DN: Status OK (successful read)  
  23.         //   4. Client -> DN: Read block Y  
  24.         // The fact that step #3 and #4 are both in the client->DN direction  
  25.         // triggers Nagling. If the DN is using delayed ACKs, this results  
  26.         // in a delay of 40ms or more.  
  27.         //  
  28.         // TCP_NODELAY disables nagling and thus avoids this performance  
  29.         // disaster.  
  30.         sock.setTcpNoDelay(true);  
  31.   
  32.         NetUtils.connect(sock, dnAddr,  
  33.             dfsClient.getRandomLocalInterfaceAddr(),  
  34.             dfsClient.getConf().socketTimeout);  
  35.         sock.setSoTimeout(dfsClient.getConf().socketTimeout);  
  36.       } else {  
  37.         sock = sockAndStreams.sock;  
  38.       }  
  39.   
  40.       try {  
  41.         // The OP_READ_BLOCK request is sent as we make the BlockReader  
  42.         BlockReader reader =  
  43.             BlockReaderFactory.newBlockReader(dfsClient.getConf(),  
  44.                                        sock, file, block,  
  45.                                        blockToken,  
  46.                                        startOffset, len,  
  47.                                        bufferSize, verifyChecksum,  
  48.                                        clientName,  
  49.                                        dfsClient.getDataEncryptionKey(),  
  50.                                        sockAndStreams == null ? null : sockAndStreams.ioStreams);  
  51.         return reader;  
  52.       } catch (IOException ex) {  
  53.         // Our socket is no good.  
  54.         DFSClient.LOG.debug("Error making BlockReader. Closing stale " + sock, ex);  
  55.         if (sockAndStreams != null) {  
  56.           sockAndStreams.close();  
  57.         } else {  
  58.           sock.close();  
  59.         }  
  60.         err = ex;  
  61.       }  

首先对socket进行了初始化。这边设置了TCPNODELAY。因为client-datanode的交互是严格时序的。如果不设置client会非常慢。

BlockReaderFactory.newBlockReader创建BlockReader对象。后面就通过这个reader来依次读出block的内容。

 4.BlockReader读block

[java]  view plain copy
  1. public synchronized int read(byte[] buf, int off, int len)   
  2.                               throws IOException {  
  3.   
  4.    if (curDataSlice == null || curDataSlice.remaining() == 0 && bytesNeededToFinish > 0) {  
  5.      readNextPacket();  
  6.    }  
  7.    if (curDataSlice.remaining() == 0) {  
  8.      // we're at EOF now  
  9.      return -1;  
  10.    }  
  11.      
  12.    int nRead = Math.min(curDataSlice.remaining(), len);  
  13.    curDataSlice.get(buf, off, nRead);  
  14.      
  15.    return nRead;  

 

[java]  view plain copy
  1. //Read packet headers.  
  2.     packetReceiver.receiveNextPacket(in);  
  3.   
  4.     PacketHeader curHeader = packetReceiver.getHeader();  
  5.     curDataSlice = packetReceiver.getDataSlice();  
  6.     assert curDataSlice.capacity() == curHeader.getDataLen();  
  7.       
  8.     if (LOG.isTraceEnabled()) {  
  9.       LOG.trace("DFSClient readNextPacket got header " + curHeader);  
  10.     }  
  11.   
  12.     // Sanity check the lengths  
  13.     if (!curHeader.sanityCheck(lastSeqNo)) {  
  14.          throw new IOException("BlockReader: error in packet header " +  
  15.                                curHeader);  
  16.     }  
  17.       
  18.     if (curHeader.getDataLen() > 0) {  
  19.       int chunks = 1 + (curHeader.getDataLen() - 1) / bytesPerChecksum;  
  20.       int checksumsLen = chunks * checksumSize;  
  21.   
  22.       assert packetReceiver.getChecksumSlice().capacity() == checksumsLen :  
  23.         "checksum slice capacity=" + packetReceiver.getChecksumSlice().capacity() +   
  24.           " checksumsLen=" + checksumsLen;  
  25.         
  26.       lastSeqNo = curHeader.getSeqno();  
  27.       if (verifyChecksum && curDataSlice.remaining() > 0) {  
  28.         // N.B.: the checksum error offset reported here is actually  
  29.         // relative to the start of the block, not the start of the file.  
  30.         // This is slightly misleading, but preserves the behavior from  
  31.         // the older BlockReader.  
  32.         checksum.verifyChunkedSums(curDataSlice,  
  33.             packetReceiver.getChecksumSlice(),  
  34.             filename, curHeader.getOffsetInBlock());  
  35.       }  
  36.       bytesNeededToFinish -= curHeader.getDataLen();  
  37.     }      
  38.       
  39.     // First packet will include some data prior to the first byte  
  40.     // the user requested. Skip it.  
  41.     if (curHeader.getOffsetInBlock() < startOffset) {  
  42.       int newPos = (int) (startOffset - curHeader.getOffsetInBlock());  
  43.       curDataSlice.position(newPos);  
  44.     }  
  45.   
  46.     // If we've now satisfied the whole client read, read one last packet  
  47.     // header, which should be empty  
  48.     if (bytesNeededToFinish <= 0) {  
  49.       readTrailingEmptyPacket();  
  50.       if (verifyChecksum) {  
  51.         sendReadResult(Status.CHECKSUM_OK);  
  52.       } else {  
  53.         sendReadResult(Status.SUCCESS);  
  54.       }  
  55.     }  

这边调用了RemoteBlockReader2的read方法读取block。这边的逻辑也不简单,datanode把block按一个个packet发送过来,每发送一个packet都需要checkSum校验数据正确性和检查packet的头数据,把实际data数据放到curDataSlice中,数据正确后会发送response给datanode,只有收到了client的ack信息datanode才会发送下一个packet。

 

刚读这边代码有一个奇怪的地方。如果用户从block中读一段数据到字符数据中时,如果读的长度超过block的大小,超过的部分不会被读到。不过后来仔细看了下,DFSInputStream的readWithStrategy方法每次都只会读一个int型,所以就不会出现此问题了。

 

总结一下:

1.客户端采用了read,ack的强时序模式而且没有用线程来receive数据,保证了读的正确性,但也稍微降低了读的效率。

2.socket采用了NIO提高了效率,这可能是与较早版本最大的改进。

3.虽然说0.92版本shortCircuit没有实现,但看cdh4代码中是有的。不知道如何测试是否能够使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值