HDFS讀檔案過程分析:讀取檔案的Block資料
我們可以從java.io.InputStream類中看到,抽象出一個read方法,用來讀取已經開啟的InputStream例項中的位元組,每次呼叫read方法,會讀取一個位元組資料,該方法抽象定義,如下所示:
public abstract int read() throws IOException;
Hadoop的DFSClient.DFSInputStream類實現了該抽象邏輯,如果我們清楚瞭如何從HDFS中讀取一個檔案的一個block的一個位元組的原理,更加抽象的頂層只需要迭代即可獲取到該檔案的全部資料。
從HDFS讀檔案過程分析:獲取檔案對應的Block列表中,我們已經獲取到一個檔案對應的Block列表資訊,開啟一個檔案,接下來就要讀取實際的物理塊資料,我們從下面的幾個方面來詳細說明讀取資料的過程。
Client從Datanode讀取檔案的一個位元組
下面,我們通過分析DFSClient.DFSInputStream中實現的程式碼,讀取HDFS上檔案的內容。首先從下面的方法開始:
@Override public synchronized int read() throws IOException { int ret = read( oneByteBuf, 0, 1 ); return ( ret <= 0 ) ? -1 : (oneByteBuf[0] & 0xff); }
上面呼叫read(oneByteBuf, 0, 1)讀取一個位元組到單位元組緩衝區oneByteBuf中,具體實現見如下方法:
@Override public synchronized int read(byte buf[], int off, int len) throws IOException { checkOpen(); // 檢查Client是否正在執行 if (closed) { throw new IOException("Stream closed"); } failures = 0; if (pos < getFileLength()) { // getFileLength()獲取檔案所包含的總位元組數,pos表示讀取當前檔案的第(pos+1)個位元組 int retries = 2; while (retries > 0) { try { if (pos > blockEnd) { // blockEnd表示檔案的長度(位元組數) currentNode = blockSeekTo(pos); // 找到第pos個位元組資料所在的Datanode(實際根據該位元組資料所在的block後設資料來定位) } int realLen = (int) Math.min((long) len, (blockEnd - pos + 1L)); int result = readBuffer(buf, off, realLen); // 讀取一個位元組到緩衝區中 if (result >= 0) { pos += result; // 每成功讀取result個位元組,pos增加result } else { // got a EOS from reader though we expect more data on it. throw new IOException("Unexpected EOS from the reader"); } if (stats != null && result != -1) { stats.incrementBytesRead(result); } return result; } catch (ChecksumException ce) { throw ce; } catch (IOException e) { if (retries == 1) { LOG.warn("DFS Read: " + StringUtils.stringifyException(e)); } blockEnd = -1; if (currentNode != null) { addToDeadNodes(currentNode); } if (--retries == 0) { throw e; } } } } return -1; }
讀取檔案資料的一個位元組,具體過程如下:
- 檢查流物件是否處於開啟狀態(前面已經獲取到檔案對應的block列表的後設資料,並開啟一個InputStream物件)
- 從檔案的第一個block開始讀取,首先需要找到第一個block對應的資料塊所在的Datanode,可以從快取的block列表中查詢到(如果查詢不到,則會與Namenode進行一次RPC通訊請求獲取到)
- 開啟一個到該讀取的block所在Datanode節點的流,準備讀取block資料
- 建立了到Datanode的連線後,讀取一個位元組資料到位元組緩衝區中,返回讀取的位元組數(1個位元組)
在讀取的過程中,以位元組為單位,通過判斷某個偏移位置的位元組屬於哪個block(根據block後設資料所限定的位元組偏移範圍),在根據這個block去定位某一個Datanode節點,這樣就可連續地讀取一個檔案的全部資料(組成檔案的、連續的多個block資料塊)。
查詢待讀取的一個位元組所在的Datanode節點
上面public synchronized int read(byte buf[], int off, int len) throws IOException方法,呼叫了blockSeekTo方法來獲取,檔案某個位元組索引位置的資料所在的Datanode節點。其實,很容易就能想到,想要獲取到資料所在的Datanode節點,一定是從block後設資料中計算得到,然後根據Client快取的block對映列表,找到block對應的Datanode列表,我們看一下blockSeekTo方法的程式碼實現:
private synchronized DatanodeInfo blockSeekTo(long target) throws IOException { ... ... DatanodeInfo chosenNode = null; int refetchToken = 1; // only need to get a new access token once while (true) { LocatedBlock targetBlock = getBlockAt(target, true); // 獲取位元組偏移位置為target的位元組資料所在的block後設資料物件 assert (target==this.pos) : "Wrong postion " + pos + " expect " + target; long offsetIntoBlock = target - targetBlock.getStartOffset(); DNAddrPair retval = chooseDataNode(targetBlock); // 選擇一個Datanode去讀取資料 chosenNode = retval.info; InetSocketAddress targetAddr = retval.addr; // 先嚐試從本地讀取資料,如果資料不在本地,則正常去讀取遠端的Datanode節點 Block blk = targetBlock.getBlock(); Token<BlockTokenIdentifier> accessToken = targetBlock.getBlockToken(); if (shouldTryShortCircuitRead(targetAddr)) { try { blockReader = getLocalBlockReader(conf, src, blk, accessToken, chosenNode, DFSClient.this.socketTimeout, offsetIntoBlock); // 建立一個用來讀取本地資料的BlockReader物件 return chosenNode; } catch (AccessControlException ex) { LOG.warn("Short circuit access failed ", ex); //Disable short circuit reads shortCircuitLocalReads = false; } catch (IOException ex) { if (refetchToken > 0 && tokenRefetchNeeded(ex, targetAddr)) { /* Get a new access token and retry. */ refetchToken--; fetchBlockAt(target); continue; } else { LOG.info("Failed to read " + targetBlock.getBlock() + " on local machine" + StringUtils.stringifyException(ex)); LOG.info("Try reading via the datanode on " + targetAddr); } } } // 本地讀取失敗,按照更一般的方式去讀取遠端的Datanode節點來獲取資料 try { s = socketFactory.createSocket(); LOG.debug("Connecting to " + targetAddr); NetUtils.connect(s, targetAddr, getRandomLocalInterfaceAddr(), socketTimeout); s.setSoTimeout(socketTimeout); blockReader = RemoteBlockReader.newBlockReader(s, src, blk.getBlockId(), accessToken, blk.getGenerationStamp(), offsetIntoBlock, blk.getNumBytes() - offsetIntoBlock, buffersize, verifyChecksum, clientName); // 建立一個遠端的BlockReader物件 return chosenNode; } catch (IOException ex) { if (refetchToken > 0 && tokenRefetchNeeded(ex, targetAddr)) { refetchToken--; fetchBlockAt(target); } else { LOG.warn("Failed to connect to " + targetAddr + ", add to deadNodes and continue" + ex); if (LOG.isDebugEnabled()) { LOG.debug("Connection failure", ex); } // Put chosen node into dead list, continue addToDeadNodes(chosenNode); // 讀取失敗,會將選擇的Datanode加入到Client的dead node列表,為下次讀取選擇合適的Datanode讀取檔案資料提供參考後設資料資訊 } if (s != null) { try { s.close(); } catch (IOException iex) { } } s = null; } } }
上面程式碼中,主要包括如下幾個要點:
- 選擇合適的Datanode節點,提高讀取效率
在讀取檔案的時候,首先會從Namenode獲取檔案對應的block列表後設資料,返回的block列表是按照Datanode的網路拓撲結構進行排序過的(本地節點優先,其次是同一機架節點),而且,Client還維護了一個dead node列表,只要此時bock對應的Datanode列表中節點不出現在dead node列表中就會被返回,用來作為讀取資料的Datanode節點。
- 如果Client為叢集Datanode節點,嘗試從本地讀取block
通過呼叫chooseDataNode方法返回一個Datanode結點,通過判斷,如果該節點地址是本地地址,並且該節點上對應的block後設資料資訊的狀態不是正在建立的狀態,則滿足從本地讀取資料塊的條件,然後會建立一個LocalBlockReader物件,直接從本地讀取。在建立LocalBlockReader物件的過程中,會先從快取中查詢一個本地Datanode相關的LocalDatanodeInfo物件,該物件定義了與從本地Datanode讀取資料的重要資訊,以及快取了待讀取block對應的本地路徑資訊,可以從LocalDatanodeInfo類定義的屬性來說明:
private ClientDatanodeProtocol proxy = null; private final Map<Block, BlockLocalPathInfo> cache;
如果快取中存在待讀取的block的相關資訊,可以直接進行讀取;否則,會建立一個proxy物件,以及計算待讀取block的路徑資訊BlockLocalPathInfo,最後再加入到快取,為後續可能的讀取加速。我們看一下如果沒有從快取中找到LocalDatanodeInfo資訊(尤其是BlockLocalPathInfo),則會執行如下邏輯:
// make RPC to local datanode to find local pathnames of blocks pathinfo = proxy.getBlockLocalPathInfo(blk, token);
上面proxy為ClientDatanodeProtocol型別,Client與Datanode進行RPC通訊的協議,RPC呼叫getBlockLocalPathInfo獲取block對應的本地路徑資訊,可以在Datanode類中檢視具體實現,如下所示:
BlockLocalPathInfo info = data.getBlockLocalPathInfo(block);
Datanode呼叫FSDataset(實現介面FSDatasetInterface)的getBlockLocalPathInfo,如下所示:
@Override //FSDatasetInterface public BlockLocalPathInfo getBlockLocalPathInfo(Block block) throws IOException { File datafile = getBlockFile(block); // 獲取本地block在本地Datanode檔案系統中的檔案路徑 File metafile = getMetaFile(datafile, block); // 獲取本地block在本地Datanode檔案系統中的後設資料的檔案路徑 BlockLocalPathInfo info = new BlockLocalPathInfo(block, datafile.getAbsolutePath(), metafile.getAbsolutePath()); return info; }
接著可以直接去讀取該block檔案(如果需要檢查校驗和檔案,會讀取block的後設資料檔案metafile):
... // BlockReaderLocal類的newBlockReader靜態方法 // get a local file system File blkfile = new File(pathinfo.getBlockPath()); dataIn = new FileInputStream(blkfile); if (!skipChecksum) { // 如果檢查block的校驗和 // get the metadata file File metafile = new File(pathinfo.getMetaPath()); checksumIn = new FileInputStream(metafile); // read and handle the common header here. For now just a version BlockMetadataHeader header = BlockMetadataHeader.readHeader(new DataInputStream(checksumIn)); short version = header.getVersion(); if (version != FSDataset.METADATA_VERSION) { LOG.warn("Wrong version (" + version + ") for metadata file for " + blk + " ignoring ..."); } DataChecksum checksum = header.getChecksum(); localBlockReader = new BlockReaderLocal(conf, file, blk, token, startOffset, length, pathinfo, checksum, true, dataIn, checksumIn); } else { localBlockReader = new BlockReaderLocal(conf, file, blk, token, startOffset, length, pathinfo, dataIn); }
在上面程式碼中,返回了BlockLocalPathInfo,但是很可能在這個過程中block被刪除了,在刪除block的時候,Namenode會排程指派該Datanode刪除該block,恰好在這個時間間隔內block對應的BlockLocalPathInfo資訊已經失效(檔案已經被刪除),所以上面這段程式碼再try中會丟擲異常,並在catch中捕獲到IO異常,會從快取中再清除掉失效的block到BlockLocalPathInfo的對映資訊。
- 如果Client非叢集Datanode節點,遠端讀取block
如果Client不是Datanode本地節點,則只能跨網路節點遠端讀取,首先建立Socket連線:
s = socketFactory.createSocket(); LOG.debug("Connecting to " + targetAddr); NetUtils.connect(s, targetAddr, getRandomLocalInterfaceAddr(), socketTimeout); s.setSoTimeout(socketTimeout);
建立Client到目標Datanode(targetAddr)的連線,然後同樣也是建立一個遠端BlockReader物件RemoteBlockReader來輔助讀取block資料。建立RemoteBlockReader過程中,首先向目標Datanode傳送RPC請求:
// in and out will be closed when sock is closed (by the caller) DataOutputStream out = new DataOutputStream(new BufferedOutputStream(NetUtils.getOutputStream(sock,HdfsConstants.WRITE_TIMEOUT))); //write the header. out.writeShort( DataTransferProtocol.DATA_TRANSFER_VERSION ); // Client與Datanode之間傳輸資料的版本號 out.write( DataTransferProtocol.OP_READ_BLOCK ); // 傳輸操作型別:讀取block out.writeLong( blockId ); // block ID out.writeLong( genStamp ); // 時間戳資訊 out.writeLong( startOffset ); // block起始偏移量 out.writeLong( len ); // block長度 Text.writeString(out, clientName); // 客戶端標識 accessToken.write(out); out.flush();
然後獲取到DataInputStream物件來讀取Datanode的響應資訊:
DataInputStream in = new DataInputStream( new BufferedInputStream(NetUtils.getInputStream(sock), bufferSize));
最後,返回一個物件RemoteBlockReader:
return new RemoteBlockReader(file, blockId, in, checksum, verifyChecksum, startOffset, firstChunkOffset, sock);
藉助BlockReader來讀取block位元組
我們再回到blockSeekTo方法中,待讀取block所在的Datanode資訊、BlockReader資訊都已經具備,接著就可以從包含輸入流(InputStream)物件的BlockReader中讀取資料塊中一個位元組資料:
int result = readBuffer(buf, off, realLen);
將block資料中一個位元組讀取到buf中,如下所示:
private synchronized int readBuffer(byte buf[], int off, int len) throws IOException { IOException ioe; boolean retryCurrentNode = true; while (true) { // retry as many times as seekToNewSource allows. try { return blockReader.read(buf, off, len); // 呼叫blockReader的read方法讀取位元組資料到buf中 } catch ( ChecksumException ce ) { LOG.warn("Found Checksum error for " + currentBlock + " from " + currentNode.getName() + " at " + ce.getPos()); reportChecksumFailure(src, currentBlock, currentNode); ioe = ce; retryCurrentNode = false; // 只嘗試讀取當前選擇的Datanode一次,失敗的話就會被加入到Client的dead node列表中 } catch ( IOException e ) { if (!retryCurrentNode) { LOG.warn("Exception while reading from " + currentBlock + " of " + src + " from " + currentNode + ": " + StringUtils.stringifyException(e)); } ioe = e; } boolean sourceFound = false; if (retryCurrentNode) { /* possibly retry the same node so that transient errors don't * result in application level failures (e.g. Datanode could have * closed the connection because the client is idle for too long). */ sourceFound = seekToBlockSource(pos); } else { addToDeadNodes(currentNode); // 加入到Client的dead node列表中 sourceFound = seekToNewSource(pos); // 從當前選擇的Datanode上讀取資料失敗,會再次選擇一個Datanode,這裡seekToNewSource方法內部呼叫了blockSeekTo方法去選擇一個Datanode } if (!sourceFound) { throw ioe; } retryCurrentNode = false; } }
通過BlockReaderLocal或者RemoteBlockReader來讀取block資料,邏輯非常類似,主要是控制讀取位元組的偏移量,記錄偏移量的狀態資訊,詳細可以檢視它們的原始碼。
DataNode節點處理讀檔案Block請求
我們可以在DataNode端看一下,如何處理一個讀取Block的請求。如果Client與DataNode不是同一個節點,則為遠端讀取檔案Block,首先Client需要傳送一個請求頭資訊,程式碼如下所示:
//write the header. out.writeShort( DataTransferProtocol.DATA_TRANSFER_VERSION ); // Client與Datanode之間傳輸資料的版本號 out.write( DataTransferProtocol.OP_READ_BLOCK ); // 傳輸操作型別:讀取block out.writeLong( blockId ); // block ID out.writeLong( genStamp ); // 時間戳資訊 out.writeLong( startOffset ); // block起始偏移量 out.writeLong( len ); // block長度 Text.writeString(out, clientName); // 客戶端標識 accessToken.write(out); out.flush();
DataNode節點端通過驗證資料傳輸版本號(DataTransferProtocol.DATA_TRANSFER_VERSION)一致以後,會判斷傳輸操作型別,如果是讀操作DataTransferProtocol.OP_READ_BLOCK,則會通過Client建立的Socket來建立一個OutputStream物件,然後通過BlockSender向Client傳送Block資料,程式碼如下所示:
try { blockSender = new BlockSender(block, startOffset, length, true, true, false, datanode, clientTraceFmt); // 建立BlockSender物件 } catch(IOException e) { out.writeShort(DataTransferProtocol.OP_STATUS_ERROR); throw e; } out.writeShort(DataTransferProtocol.OP_STATUS_SUCCESS); // 回覆一個響應Header資訊:成功狀態 long read = blockSender.sendBlock(out, baseStream, null); // 傳送請求的Block資料
相關文章
- HDFS讀檔案過程分析:獲取檔案對應的Block列表BloC
- SpringBoot配置檔案讀取過程分析Spring Boot
- Java API 讀取HDFS的單檔案JavaAPI
- 讀取資料夾檔案
- 使用yaml檔案讀取資料YAML
- Java 讀取檔案Java
- tiff檔案讀取
- 任意檔案讀取
- xbbed一鍵讀取ASM block到檔案系統ASMBloC
- 使用openpyxl庫讀取Excel檔案資料Excel
- android直接讀取資料庫檔案Android資料庫
- VB讀取文字檔案的例子:逐行讀取
- python讀取檔案——python讀取和儲存mat檔案Python
- go–讀取檔案的方式Go
- Pandas之EXCEL資料讀取/儲存/檔案分割/檔案合併Excel
- C#讀取資料夾特定檔案的方法C#
- viper 讀取配置檔案
- go配置檔案讀取Go
- iOS讀取.csv檔案iOS
- php 讀取超大檔案PHP
- JAVA 讀取xml檔案JavaXML
- WinForm讀取Excel檔案ORMExcel
- java讀取properties檔案Java
- 前端讀取excel檔案前端Excel
- 用友任意檔案讀取
- IOC - 讀取配置檔案
- 如何讀取HDFS上的csv/tsv檔案的Timestamp列 - Qiita
- Java讀取properties檔案連線資料庫Java資料庫
- spark直接讀取本地檔案系統的檔案Spark
- hdfs小檔案分析
- 透過python讀取ini配置檔案Python
- 讀取檔案流並寫入檔案流
- 【萬里征程——Windows App開發】檔案&資料——讀取檔案/資料夾名WindowsAPP
- kodbox讀取alist檔案失敗,問題解決過程
- XMl 檔案屬性的讀取XML
- Java屬性檔案的讀取Java
- EasyExcel庫來讀取指定Excel檔案中的資料Excel
- 通過讀取properties檔案動態生成對資料庫的連線資料庫