HDFS讀檔案過程分析:獲取檔案對應的Block列表
在使用Java讀取一個檔案系統中的一個檔案時,我們會首先構造一個DataInputStream物件,然後就能夠從檔案中讀取資料。對於儲存在HDFS上的檔案,也對應著類似的工具類,但是底層的實現邏輯卻是非常不同的。我們先從使用DFSClient.DFSDataInputStream類來讀取HDFS上一個檔案的一段程式碼來看,如下所示:
package org.shirdrn.hadoop.hdfs; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; public class HdfsFileReader { public static void main(String[] args) { String file = "hdfs://hadoop-cluster-m:8020/data/logs/basis_user_behavior/201405071237_10_10_1_73.log"; Path path = new Path(file); Configuration conf = new Configuration(); FileSystem fs; FSDataInputStream in; BufferedReader reader = null; try { fs = FileSystem.get(conf); in = fs.open(path); // 開啟檔案path,返回一個FSDataInputStream流物件 reader = new BufferedReader(new InputStreamReader(in)); String line = null; while((line = reader.readLine()) != null) { // 讀取檔案行內容 System.out.println("Record: " + line); } } catch (IOException e) { e.printStackTrace(); } finally { try { if(reader != null) reader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
基於上面程式碼,我們可以看到,通過一個FileSystem物件可以開啟一個Path檔案,返回一個FSDataInputStream檔案輸入流物件,然後從該FSDataInputStream物件就能夠讀取出檔案的內容。所以,我們從FSDataInputStream入手,詳細分析從HDFS讀取檔案內容的過程,在實際地讀取物理資料塊之前,首先要獲取到檔案對應的Block列表後設資料資訊,整體流程如下圖所示:
下面,詳細說明整個流程:
建立FSDataInputStream流物件
從一個Path路徑物件,能夠獲取到一個FileSystem物件,然後通過呼叫FileSystem的open方法開啟一個檔案流:
public FSDataInputStream open(Path f) throws IOException { return open(f, getConf().getInt("io.file.buffer.size", 4096)); }
由於FileSystem是抽象類,將具體的開啟操作留給具體子類實現,例如FTPFileSystem、HarFileSystem、WebHdfsFileSystem等,不同的檔案系統具有不同開啟檔案的行為,我們以DistributedFileSystem為例,open方法實現,程式碼如下所示:
public FSDataInputStream open(Path f, int bufferSize) throws IOException { statistics.incrementReadOps(1); return new DFSClient.DFSDataInputStream( dfs.open(getPathName(f), bufferSize, verifyChecksum, statistics)); }
statistics物件用來收集檔案系統操作的統計資料,這裡使讀取檔案操作的計數器加1。然後建立了一個DFSClient.DFSDataInputStream物件,該物件的引數是通過DFSClient dfs客戶端物件開啟一個這個檔案從而返回一個DFSInputStream物件,下面,我們看DFSClient的open方法實現,程式碼如下所示:
public DFSInputStream open(String src, int buffersize, boolean verifyChecksum, FileSystem.Statistics stats) throws IOException { checkOpen(); // Get block info from namenode return new DFSInputStream(src, buffersize, verifyChecksum); }
checkOpen方法就是檢查一個標誌位clientRunning,表示當前的dfs客戶端物件是否已經建立並初始化,在dfs客戶端建立的時候該標誌就為true,表示客戶端正在執行狀態。我們知道,當客戶端DFSClient連線到Namenode的時候,實際上是建立了一個到Namenode的RPC連線,Namenode作為Server角色,DFSClient作為Client角色,它們之間建立起Socket連線。只有顯式呼叫DFSClient的close方法時,才會修改clientRunning的值為false,實際上真正地關閉了已經建立的RPC連線。
我們看一下建立DFSInputStream的構造方法實現:
DFSInputStream(String src, int buffersize, boolean verifyChecksum) throws IOException { this.verifyChecksum = verifyChecksum; this.buffersize = buffersize; this.src = src; prefetchSize = conf.getLong("dfs.read.prefetch.size", prefetchSize); openInfo(); }
先設定了幾個與讀取檔案相關的引數值,這裡有一個預先讀取檔案的Block位元組數的引數prefetchSize,它的值設定如下:
public static final long DEFAULT_BLOCK_SIZE = DFSConfigKeys.DFS_BLOCK_SIZE_DEFAULT; public static final long DFS_BLOCK_SIZE_DEFAULT = 64*1024*1024; defaultBlockSize = conf.getLong("dfs.block.size", DEFAULT_BLOCK_SIZE); private long prefetchSize = 10 * defaultBlockSize;
這個prefetchSize的值預設為10*64*1024*1024=671088640,也就是說,預設預讀取一個檔案的10個塊,即671088640B=640M,如果想要修改這個值,設定dfs.block.size即可覆蓋預設值。
然後呼叫了openInfo方法,從Namenode獲取到該開啟檔案的資訊,在openInfo方法中,具體實現如下所示:
synchronized void openInfo() throws IOException { for (int retries = 3; retries > 0; retries--) { if (fetchLocatedBlocks()) { // fetch block success. 如果成功獲取到待讀取檔案對應的Block列表,則直接返回 return; } else { // Last block location unavailable. When a cluster restarts, // DNs may not report immediately. At this time partial block // locations will not be available with NN for getting the length. // Lets retry a few times to get the length. DFSClient.LOG.warn("Last block locations unavailable. " + "Datanodes might not have reported blocks completely." + " Will retry for " + retries + " times"); waitFor(4000); } } throw new IOException("Could not obtain the last block locations."); }
上述程式碼中,有一個for迴圈用來獲取Block列表。如果成功獲取到待讀取檔案的Block列表,則直接返回,否則,最多執行3次等待重試操作(最多花費時間大於12秒)。未能成功讀取檔案的Block列表資訊,是因為Namenode無法獲取到檔案對應的塊列表的資訊,當整個叢集啟動的時候,Datanode會主動向NNamenode上報對應的Block資訊,只有Block Report完成之後,Namenode就能夠知道組成檔案的Block及其所在Datanode列表的資訊。openInfo方法方法中呼叫了fetchLocatedBlocks方法,用來與Namenode進行RPC通訊呼叫,實際獲取對應的Block列表,實現程式碼如下所示:
private boolean fetchLocatedBlocks() throws IOException, FileNotFoundException { LocatedBlocks newInfo = callGetBlockLocations(namenode, src, 0, prefetchSize); if (newInfo == null) { throw new FileNotFoundException("File does not exist: " + src); } if (locatedBlocks != null && !locatedBlocks.isUnderConstruction() && !newInfo.isUnderConstruction()) { Iterator<LocatedBlock> oldIter = locatedBlocks.getLocatedBlocks().iterator(); Iterator<LocatedBlock> newIter = newInfo.getLocatedBlocks().iterator(); while (oldIter.hasNext() && newIter.hasNext()) { if (!oldIter.next().getBlock().equals(newIter.next().getBlock())) { throw new IOException("Blocklist for " + src + " has changed!"); } } } boolean isBlkInfoUpdated = updateBlockInfo(newInfo); this.locatedBlocks = newInfo; this.currentNode = null; return isBlkInfoUpdated; }
呼叫callGetBlockLocations方法,實際上是根據建立RPC連線以後得到的Namenode的代理物件,呼叫Namenode來獲取到指定檔案的Block的位置資訊(位於哪些Datanode節點上):namenode.getBlockLocations(src, start, length)。呼叫callGetBlockLocations方法返回一個LocatedBlocks物件,該物件包含了檔案長度資訊、List blocks列表物件,其中LocatedBlock包含了一個Block的基本資訊:
private Block b; private long offset; // offset of the first byte of the block in the file private DatanodeInfo[] locs; private boolean corrupt;
有了這些檔案的資訊(檔案長度、檔案包含的Block的位置等資訊),DFSClient就能夠執行後續讀取檔案資料的操作了,詳細過程我們在後面分析說明。
通過Namenode獲取檔案資訊
上面,我們提到獲取一個檔案的基本資訊,是通過Namenode來得到的,這裡詳細分析Namenode是如何獲取到這些檔案資訊的,實現方法getBlockLocations的程式碼,如下所示:
public LocatedBlocks getBlockLocations(String src, long offset, long length) throws IOException { myMetrics.incrNumGetBlockLocations(); return namesystem.getBlockLocations(getClientMachine(), src, offset, length); }
可以看到,Namenode又委託管理HDFS name後設資料的FSNamesystem的getBlockLocations方法實現:
LocatedBlocks getBlockLocations(String clientMachine, String src, long offset, long length) throws IOException { LocatedBlocks blocks = getBlockLocations(src, offset, length, true, true, true); if (blocks != null) { //sort the blocks // In some deployment cases, cluster is with separation of task tracker // and datanode which means client machines will not always be recognized // as known data nodes, so here we should try to get node (but not // datanode only) for locality based sort. Node client = host2DataNodeMap.getDatanodeByHost(clientMachine); if (client == null) { List<String> hosts = new ArrayList<String> (1); hosts.add(clientMachine); String rName = dnsToSwitchMapping.resolve(hosts).get(0); if (rName != null) client = new NodeBase(clientMachine, rName); } DFSUtil.StaleComparator comparator = null; if (avoidStaleDataNodesForRead) { comparator = new DFSUtil.StaleComparator(staleInterval); } // Note: the last block is also included and sorted for (LocatedBlock b : blocks.getLocatedBlocks()) { clusterMap.pseudoSortByDistance(client, b.getLocations()); if (avoidStaleDataNodesForRead) { Arrays.sort(b.getLocations(), comparator); } } } return blocks; }
跟蹤程式碼,最終會在下面的方法中實現了,如何獲取到待讀取檔案的Block的後設資料列表,以及如何取出該檔案的各個Block的資料,方法實現程式碼,這裡我做了詳細的註釋,可以參考,如下所示:
private synchronized LocatedBlocks getBlockLocationsInternal(String src, long offset, long length, int nrBlocksToReturn, boolean doAccessTime, boolean needBlockToken) throws IOException { INodeFile inode = dir.getFileINode(src); // 獲取到與待讀取檔案相關的inode資料 if (inode == null) { return null; } if (doAccessTime && isAccessTimeSupported()) { dir.setTimes(src, inode, -1, now(), false); } Block[] blocks = inode.getBlocks(); // 獲取到檔案src所包含的Block的後設資料列表資訊 if (blocks == null) { return null; } if (blocks.length == 0) { // 獲取到檔案src的Block數,這裡=0,該檔案的Block資料還沒建立,可能正在建立 return inode.createLocatedBlocks(new ArrayList<LocatedBlock>(blocks.length)); } List<LocatedBlock> results; results = new ArrayList<LocatedBlock>(blocks.length); int curBlk = 0; // 當前Block在Block[] blocks陣列中的索引位置 long curPos = 0, blkSize = 0; // curPos表示某個block在檔案中的位元組偏移量,blkSize為Block的大小(位元組數) int nrBlocks = (blocks[0].getNumBytes() == 0) ? 0 : blocks.length; // 獲取到檔案src的Block數,實際上一定>0,但是第一個block大小可能為0,這種情況認為nrBlocks=0 for (curBlk = 0; curBlk < nrBlocks; curBlk++) { // 根據前面程式碼,我們知道offset=0,所以這個迴圈第一次進來肯定就break出去了(正常的話,blkSize>0,所以我覺得這段程式碼寫的稍微有點晦澀) blkSize = blocks[curBlk].getNumBytes(); assert blkSize > 0 : "Block of size 0"; if (curPos + blkSize > offset) { break; } curPos += blkSize; } if (nrBlocks > 0 && curBlk == nrBlocks) // offset >= end of file, 到這裡curBlk=0,如果從檔案src的第一個Block的位元組數累加計算,知道所有的Block的位元組數都累加上了,總位元組數仍然<=請求的offset,說明即使到了檔案尾部,仍然沒有達到offset的值。從前面fetchLocatedBlocks()方法中呼叫我們知道,offset=0,所以執行該分支表示檔案src沒有可用的Block資料塊可讀 return null; long endOff = offset + length; // do { // 獲取Block所在位置(Datanode節點) int numNodes = blocksMap.numNodes(blocks[curBlk]); // 計算檔案src中第curBlk個Block儲存在哪些Datanode節點上 int numCorruptNodes = countNodes(blocks[curBlk]).corruptReplicas(); // 計算儲存檔案src中第curBlk個Block但無法讀取該Block的Datanode節點數 int numCorruptReplicas = corruptReplicas.numCorruptReplicas(blocks[curBlk]); // 計算FSNamesystem在記憶體中維護的Block=>Datanode對映的列表中,無法讀取該Block的Datanode節點數 if (numCorruptNodes != numCorruptReplicas) { LOG.warn("Inconsistent number of corrupt replicas for " + blocks[curBlk] + "blockMap has " + numCorruptNodes + " but corrupt replicas map has " + numCorruptReplicas); } DatanodeDescriptor[] machineSet = null; // 下面的if...else用來獲取一個Block所在的Datanode節點 boolean blockCorrupt = false; if (inode.isUnderConstruction() && curBlk == blocks.length - 1 && blocksMap.numNodes(blocks[curBlk]) == 0) { // 如果檔案正在建立,當前blocks[curBlk]還沒有建立成功(即沒有可用的Datanode可以提供該Block的服務),仍然返回待建立Block所在的Datanode節點列表。資料塊是在Datanode上儲存的,只要Datanode完成資料塊的儲存後,通過heartbeat將資料塊的資訊上報給Namenode後,這些資訊才會儲存到blocksMap中 // get unfinished block locations INodeFileUnderConstruction cons = (INodeFileUnderConstruction) inode; machineSet = cons.getTargets(); blockCorrupt = false; } else { // 檔案已經建立完成 blockCorrupt = (numCorruptNodes == numNodes); // 是否當前的Block在所有Datanode節點上的副本都壞掉,無法提供服務 int numMachineSet = blockCorrupt ? numNodes : (numNodes - numCorruptNodes); // 如果是,則返回所有Datanode節點,否則,只返回可用的Block副本所在的Datanode節點 machineSet = new DatanodeDescriptor[numMachineSet]; if (numMachineSet > 0) { // 獲取到當前Block所有副本所在的Datanode節點列表 numNodes = 0; for (Iterator<DatanodeDescriptor> it = blocksMap.nodeIterator(blocks[curBlk]); it.hasNext();) { DatanodeDescriptor dn = it.next(); boolean replicaCorrupt = corruptReplicas.isReplicaCorrupt(blocks[curBlk], dn); if (blockCorrupt || (!blockCorrupt && !replicaCorrupt)) machineSet[numNodes++] = dn; } } } LocatedBlock b = new LocatedBlock(blocks[curBlk], machineSet, curPos, blockCorrupt); // 建立一個包含Block的後設資料物件、所在Datanode節點列表、起始索引位置(位元組數)、健康狀況的LocatedBlock物件 if (isAccessTokenEnabled && needBlockToken) { // 如果啟用Block級的令牌(Token)訪問,則為當前使用者生成讀模式的令牌資訊,一同封裝到返回的LocatedBlock物件中 b.setBlockToken(accessTokenHandler.generateToken(b.getBlock(), EnumSet.of(BlockTokenSecretManager.AccessMode.READ))); } results.add(b); // 收集待返回給讀取檔案的客戶端需要的LocatedBlock列表 curPos += blocks[curBlk].getNumBytes(); curBlk++; } while (curPos < endOff && curBlk < blocks.length && results.size() < nrBlocksToReturn); return inode.createLocatedBlocks(results); // 將收集的LocatedBlock列表資料封裝到一個LocatedBlocks物件中返回 }
我們可以看一下,最後的呼叫inode.createLocatedBlocks(results)生成LocatedBlocks物件的實現,程式碼如下所示:
LocatedBlocks createLocatedBlocks(List<LocatedBlock> blocks) { return new LocatedBlocks(computeContentSummary().getLength(), blocks, isUnderConstruction()); // 通過ContentSummary物件獲取到檔案的長度 }
客戶端通過RPC呼叫,獲取到了檔案對應的Block以及所在Datanode列表的資訊,然後就可以根據LocatedBlocks來進一步獲取到對應的Block對應的物理資料塊。
對Block列表進行排序
我們再回到FSNamesystem類,呼叫getBlockLocationsInternal方法的getBlockLocations方法中,在返回檔案block列表LocatedBlocks之後,會對每一個Block所在的Datanode進行的一個排序,排序的基本規則有如下2點:
- Client到Block所在的Datanode的距離最近,這個是通過網路拓撲關係來進行計算,例如Client的網路路徑為/dc1/r1/c1,那麼路徑為/dc1/r1/dn1的Datanode就比路徑為/dc1/r2/dn2的距離小,/dc1/r1/dn1對應的Block就會排在前面
- 從上面一點可以推出,如果Client就是某個Datanode,恰好某個Block的Datanode列表中包括該Datanode,則該Datanode對應的Block排在前面
- Block所在的Datanode列表中,如果其中某個Datanode在指定的時間內沒有向Namenode傳送heartbeat(預設由常量DFSConfigKeys.DFS_NAMENODE_STALE_DATANODE_INTERVAL_DEFAULT定義,預設值為30s),則該Datanode的狀態即為STALE,具有該狀態的Datanode對應的Block排在後面
基於上述規則排序後,Block列表返回到Client。
Client與Datanode互動更新檔案Block列表
我們要回到前面分析的DFSClient.DFSInputStream.fetchLocatedBlocks()方法中,檢視在呼叫該方法之後,是如何執行實際處理邏輯的:
private boolean fetchLocatedBlocks() throws IOException, FileNotFoundException { LocatedBlocks newInfo = callGetBlockLocations(namenode, src, 0, prefetchSize); // RPC呼叫向Namenode獲取待讀取檔案對應的Block及其位置資訊LocatedBlocks物件 if (newInfo == null) { throw new FileNotFoundException("File does not exist: " + src); } if (locatedBlocks != null && !locatedBlocks.isUnderConstruction() && !newInfo.isUnderConstruction()) { // 這裡面locatedBlocks!=null是和後面呼叫updateBlockInfo方法返回的狀態有關的 Iterator<LocatedBlock> oldIter = locatedBlocks.getLocatedBlocks().iterator(); Iterator<LocatedBlock> newIter = newInfo.getLocatedBlocks().iterator(); while (oldIter.hasNext() && newIter.hasNext()) { // 檢查2次獲取到的LocatedBlock列表:第2次得到newInfo包含的Block列表,在第2次得到的locatedBlocks中是否發生變化,如果發生了變化,則不允許讀取,丟擲異常 if (!oldIter.next().getBlock().equals(newIter.next().getBlock())) { throw new IOException("Blocklist for " + src + " has changed!"); } } } boolean isBlkInfoUpdated = updateBlockInfo(newInfo); this.locatedBlocks = newInfo; this.currentNode = null; return isBlkInfoUpdated; }
如果第一次讀取該檔案時,已經獲取到了對應的block列表,快取在客戶端;如果客戶端第二次又讀取了該檔案,仍然獲取到一個block列表物件。在兩次讀取之間,可能存在原檔案完全被重寫的情況,所以新得到的block列表與原列表完全不同了,存在這種情況,客戶端直接丟擲IO異常,如果原檔案對應的block列表沒有變化,則更新客戶端快取的對應block列表資訊。
當叢集重啟的時候(如果允許安全模式下讀檔案),或者當一個檔案正在建立的時候,Datanode向Namenode進行Block Report,這個過程中可能Namenode還沒有完全重建好Block到Datanode的對映關係資訊,所以即使在這種情況下,仍然會返回對應的正在建立的Block所在的Datanode列表資訊,可以從前面getBlockLocationsInternal方法中看到,INode的對應UnderConstruction狀態為true。這時,一個Block對應的所有副本中的某些可能還在建立過程中。
上面方法中,呼叫updateBlockInfo來更新檔案的Block後設資料列表資訊,對於檔案的某些Block可能沒有建立完成,所以Namenode所儲存的關於檔案的Block的的後設資料資訊可能沒有及時更新(Datanode可能還沒有完成Block的報告),程式碼實現如下所示:
private boolean updateBlockInfo(LocatedBlocks newInfo) throws IOException { if (!serverSupportsHdfs200 || !newInfo.isUnderConstruction() || !(newInfo.locatedBlockCount() > 0)) { // 如果獲取到的newInfo可以讀取檔案對應的Block資訊,則返回true return true; } LocatedBlock last = newInfo.get(newInfo.locatedBlockCount() - 1); // 從Namenode獲取檔案的最後一個Block的後設資料物件LocatedBlock boolean lastBlockInFile = (last.getStartOffset() + last.getBlockSize() == newInfo.getFileLength()); if (!lastBlockInFile) { // 如果“檔案長度 != 最後一個塊起始偏移量 + 最後一個塊長度”,說明檔案對應Block的後設資料資訊還沒有更新,但是仍然返回給讀取檔案的該客戶端 return true; } // 這時,已經確定last是該檔案的最後一個bolck,檢查最後個block的儲存位置資訊 if (last.getLocations().length == 0) { return false; } ClientDatanodeProtocol primary = null; Block newBlock = null; for (int i = 0; i < last.getLocations().length && newBlock == null; i++) { // 根據從Namenode獲取到的LocatedBlock last中對應的Datanode列表資訊,Client與Datanode建立RPC連線,獲取最後一個Block的後設資料 DatanodeInfo datanode = last.getLocations()[i]; try { primary = createClientDatanodeProtocolProxy(datanode, conf, last .getBlock(), last.getBlockToken(), socketTimeout, connectToDnViaHostname); newBlock = primary.getBlockInfo(last.getBlock()); } catch (IOException e) { if (e.getMessage().startsWith( "java.io.IOException: java.lang.NoSuchMethodException: " + "org.apache.hadoop.hdfs.protocol" + ".ClientDatanodeProtocol.getBlockInfo")) { // We're talking to a server that doesn't implement HDFS-200. serverSupportsHdfs200 = false; } else { LOG.info("Failed to get block info from " + datanode.getHostName() + " probably does not have " + last.getBlock(), e); } } finally { if (primary != null) { RPC.stopProxy(primary); } } } if (newBlock == null) { // Datanode上不存在最後一個Block對應的後設資料資訊,直接返回 if (!serverSupportsHdfs200) { return true; } throw new IOException("Failed to get block info from any of the DN in pipeline: " + Arrays.toString(last.getLocations())); } long newBlockSize = newBlock.getNumBytes(); long delta = newBlockSize - last.getBlockSize(); // 對於檔案的最後一個Block,如果從Namenode獲取到的後設資料,與從Datanode實際獲取到的後設資料不同,則以Datanode獲取的為準,因為可能Datanode還沒有及時將Block的變化資訊向Namenode彙報 last.getBlock().setNumBytes(newBlockSize); long newlength = newInfo.getFileLength() + delta; newInfo.setFileLength(newlength); // 修改檔案Block和位置後設資料列表資訊 LOG.debug("DFSClient setting last block " + last + " to length " + newBlockSize + " filesize is now " + newInfo.getFileLength()); return true; }
我們看一下,在updateBlockInfo方法中,返回false的情況:Client向Namenode發起的RPC請求,已經獲取到了組成該檔案的資料塊的後設資料資訊列表,但是,檔案的最後一個資料塊的儲存位置資訊無法獲取到,說明Datanode還沒有及時通過block report將資料塊的儲存位置資訊報告給Namenode。通過在openInfo()方法中可以看到,獲取檔案的block列表資訊有3次重試機會,也就是呼叫updateBlockInfo方法返回false,可以有12秒的時間,等待Datanode向Namenode彙報檔案的最後一個塊的位置資訊,以及Namenode更新記憶體中儲存的檔案對應的資料塊列表後設資料資訊。
我們再看一下,在updateBlockInfo方法中,返回true的情況:
- 檔案已經建立完成,檔案對應的block列表後設資料資訊可用
- 檔案正在建立中,但是當前能夠讀取到的已經完成的最後一個塊(非組成檔案的最後一個block)的後設資料資訊可用
- 檔案正在建立中,檔案的最後一個block的後設資料部分可讀:從Namenode無法獲取到該block對應的位置資訊,這時Client會與Datanode直接進行RPC通訊,獲取到該檔案最後一個block的位置資訊
上面Client會與Datanode直接進行RPC通訊,獲取檔案最後一個block的後設資料,這時可能由於網路問題等等,無法得到檔案最後一個block的後設資料,所以也會返回true,也就是說,Client仍然可以讀取該檔案,只是無法讀取到最後一個block的資料。
這樣,在Client從Namenode/Datanode獲取到的檔案的Block列表後設資料已經是可用的資訊,可以根據這些資訊讀取到各個Block的物理資料塊內容了,準確地說,應該是檔案處於開啟狀態了,已經準備好後續進行的讀操作了。
相關文章
- HDFS讀檔案過程分析:讀取檔案的Block資料BloC
- 獲取檔案列表 .net
- SpringBoot配置檔案讀取過程分析Spring Boot
- Java API 讀取HDFS的單檔案JavaAPI
- 遞迴獲取檔案列表遞迴
- Java中的獲取檔案的物理絕對路徑,和讀取檔案Java
- 如何獲取HDFS上檔案的儲存位置
- Oracle引數檔案解析——引數檔案分析獲取Oracle
- xbbed一鍵讀取ASM block到檔案系統ASMBloC
- 通過web url獲取檔案資訊Web
- 如何讀取HDFS上的csv/tsv檔案的Timestamp列 - Qiita
- Java 讀取檔案Java
- tiff檔案讀取
- 驗證控制檔案、歸檔檔案、不同BLOCK大小的資料檔案對應的RMAN備份集不在同一PIECEBloC
- Java 最佳化:讀取配置檔案 "萬能方式" 跨平臺,動態獲取檔案的絕對路徑Java
- Python3 - 獲取資料夾中的檔案列表Python
- kodbox讀取alist檔案失敗,問題解決過程
- go–讀取檔案的方式Go
- 透過python讀取ini配置檔案Python
- 讀取檔案流並寫入檔案流
- python讀取檔案——python讀取和儲存mat檔案Python
- struts檔案上傳,獲取檔名和檔案型別型別
- viper 讀取配置檔案
- go配置檔案讀取Go
- iOS讀取.csv檔案iOS
- php 讀取超大檔案PHP
- JAVA 讀取xml檔案JavaXML
- WinForm讀取Excel檔案ORMExcel
- java讀取properties檔案Java
- 用友任意檔案讀取
- 獲取上傳檔案的大小
- VB讀取文字檔案的例子:逐行讀取
- 獲取絕對路徑下的檔名和檔案字尾方法
- 通過反射獲取上傳檔案方法引數中的檔名反射
- C#讀取文字檔案和寫文字檔案C#
- Java讀取以.xlsx結尾的excel檔案,並寫出每張表對應的c#類、java類、儲存資料的xml檔案、讀取xml檔案的工具類JavaExcelC#XML
- 獲取跟蹤檔案位置
- 獲取跟蹤檔案_eygle