Hadoop3.2.1 【 HDFS 】原始碼分析 : DataXceiver: 讀取資料塊 解析 [二]
一. 前言
Receiver.processOp()方法用於處理流式介面的請求, 它首先從資料流中讀取序列化後的引數, 對引數反序列化, 然後根據操作碼呼叫DataTransferProtocol中定義的方法, 這些方法都是在DataXceiver中具體實現的。
流式介面中最重要的一個部分就是客戶端從資料節點上讀取資料塊, DataTransferProtocol.readBlock()給出了讀取操作的介面定義, 操作碼是81。 DataXceiver.readBlock()則實現了DataTransferProtocol.readBlock()方法。
客戶端通過呼叫Sender.readBlock()方法從指定資料節點上讀取資料塊, 請求通過IO流到達資料節點後, 資料節點的DataXceiverServer會建立一個DataXceiver物件響應流式介面請求。 DataXceiver.processOp()方法解析操作碼為81(讀請求) , 則呼叫DataXceiver.readBlock()響應這個讀請求。
DataXceiver.readBlock()首先向客戶端回覆一個BlockOpResponseProto響應, 指明請求已經成功接收, 並通過BlockOpResponseProto響應給出Datanode當前使用的校驗方式。 接下來DataXceiver.readBlock()方法會將資料節點上的資料塊(block) 切分成若干個資料包(packet) , 然後依次將資料包傳送給客戶端。 客戶端會在收到每個資料包時進行校驗,如果校驗和錯誤, 客戶端會切斷與當前資料節點的連線, 選擇新的資料節點讀取資料; 如果資料塊內的所有資料包都校驗成功, 客戶端會給資料節點傳送一個Status.CHECKSUM_OK響應, 表明讀取成功。
二. DataXceiver.readBlock()
先看一下DataTransferProtocol.readBlock()方法的定義。
/**
* Read a block.
*
* @param blk the block being read.
* @param blockToken security token for accessing the block.
* @param clientName client's name.
* @param blockOffset offset of the block.
* @param length maximum number of bytes for this read.
* @param sendChecksum if false, the DN should skip reading and sending
* checksums
* @param cachingStrategy The caching strategy to use.
*
* 從當前Datanode讀取指定的資料塊。
*/
void readBlock(final ExtendedBlock blk,
final Token<BlockTokenIdentifier> blockToken,
final String clientName,
final long blockOffset,
final long length,
final boolean sendChecksum,
final CachingStrategy cachingStrategy) throws IOException;
readBlock()需要傳入的引數主要有如下幾個。
■ ExtendedBlock blk: 要讀取的資料塊。
■ TokenblockToken: 資料塊的訪問令牌。
■ String clientName: 客戶端的名稱。
■ long blockOffset: 要讀取資料在資料塊中的位置。
■ long length: 讀取資料的長度。
■ sendChecksum: Datanode是否傳送校驗資料, 如果為false, 則Datanode不傳送校驗資料。 這裡要特別注意, 資料塊的讀取校驗工作是在客戶端完成的, 客戶端會將校驗結果返回給Datanode。
■ CachingStrategy cachingStrategy: 快取策略, 這裡主要包括兩個重要的欄位——readahead, 預讀取操作, Datanode會在讀取資料塊檔案時預讀取部分資料至作業系統快取中, 以提高讀取檔案效率 ; dropBehind, 如果快取中存放的檔案比較多, 那麼在讀取完資料之後, 就馬上從快取中將資料刪除。
三.readBlock()方法的執行流程
■ 建立BlockSender物件: 首先呼叫getOutputStream()方法獲取Datanode連線到客戶端的IO流, 然後構造BlockSender物件。
■ 成功建立BlockSender物件後, 呼叫writeSuccessWithChecksumInfo()方法傳送BlockOpResponseProto響應給客戶端, 通知客戶端讀請求已經成功接收, 並且告知客戶端當前資料節點的校驗資訊。
■ 呼叫BlockSender.sendBlock()方法將資料塊傳送給客戶端。
■ 當BlockSender完成傳送資料塊的所有內容後, 客戶端會響應一個狀態碼,Datanode需要解析這個狀態碼。
readBlock()的異常處理邏輯也比較簡單, 當客戶端關閉了當前Socket(可能是出現了校驗錯誤) , 或者無法從IO流中成功獲取客戶端發回的響應時, 則直接關閉Datanode到客戶端的底層輸出流。 在readBlock()方法的最後, 會關閉BlockSender類以執行清理操作。
@Override
public void readBlock(final ExtendedBlock block, // BP-451827885-192.168.8.156-1584099133244:blk_1073746920_6096
final Token<BlockTokenIdentifier> blockToken, // Kind: , Service: , Ident:
final String clientName, //DFSClient_NONMAPREDUCE_368352401_1
final long blockOffset, // 0
final long length, // 1361
final boolean sendChecksum, // true
final CachingStrategy cachingStrategy) throws IOException { // CachingStrategy(dropBehind=null, readahead=null)
// 客戶端名稱 DFSClient_NONMAPREDUCE_368352401_1
previousOpClientName = clientName;
long read = 0;
updateCurrentThreadName("Sending block " + block);
OutputStream baseStream = getOutputStream();
DataOutputStream out = getBufferedOutputStream();
checkAccess(out, true, block, blockToken, Op.READ_BLOCK, BlockTokenIdentifier.AccessMode.READ);
// send the block
BlockSender blockSender = null;
DatanodeRegistration dnR = datanode.getDNRegistrationForBP(block.getBlockPoolId());
// src: /127.0.0.1:9866, dest: /127.0.0.1:51764, bytes: %d, op: HDFS_READ, cliID: DFSClient_NONMAPREDUCE_368352401_1, offset: %d, srvID: 9efa402a-df6b-48cf-9273-5468f68cc42f, blockid: BP-451827885-192.168.8.156-1584099133244:blk_1073746920_6096, duration(ns): %d
final String clientTraceFmt =
clientName.length() > 0 && ClientTraceLog.isInfoEnabled()
? String.format(DN_CLIENTTRACE_FORMAT, localAddress, remoteAddress,
"%d", "HDFS_READ", clientName, "%d",
dnR.getDatanodeUuid(), block, "%d")
: dnR + " Served block " + block + " to " +
remoteAddress;
try {
try {
blockSender = new BlockSender(block, blockOffset, length,
true, false, sendChecksum, datanode, clientTraceFmt,
cachingStrategy);
} catch(IOException e) {
String msg = "opReadBlock " + block + " received exception " + e;
LOG.info(msg);
sendResponse(ERROR, msg);
throw e;
}
// send op status
writeSuccessWithChecksumInfo(blockSender, new DataOutputStream(getOutputStream()));
long beginRead = Time.monotonicNow();
// 傳送資料
read = blockSender.sendBlock(out, baseStream, null); // send data
long duration = Time.monotonicNow() - beginRead;
if (blockSender.didSendEntireByteRange()) {
// If we sent the entire range, then we should expect the client
// to respond with a Status enum.
try {
ClientReadStatusProto stat = ClientReadStatusProto.parseFrom(
PBHelperClient.vintPrefixed(in));
if (!stat.hasStatus()) {
LOG.warn("Client {} did not send a valid status code " +
"after reading. Will close connection.",
peer.getRemoteAddressString());
IOUtils.closeStream(out);
}
} catch (IOException ioe) {
LOG.debug("Error reading client status response. Will close connection.", ioe);
IOUtils.closeStream(out);
incrDatanodeNetworkErrors();
}
} else {
IOUtils.closeStream(out);
}
datanode.metrics.incrBytesRead((int) read);
datanode.metrics.incrBlocksRead();
datanode.metrics.incrTotalReadTime(duration);
} catch ( SocketException ignored ) {
LOG.trace("{}:Ignoring exception while serving {} to {}",
dnR, block, remoteAddress, ignored);
// Its ok for remote side to close the connection anytime.
datanode.metrics.incrBlocksRead();
IOUtils.closeStream(out);
} catch ( IOException ioe ) {
/* What exactly should we do here?
* Earlier version shutdown() datanode if there is disk error.
*/
if (!(ioe instanceof SocketTimeoutException)) {
LOG.warn("{}:Got exception while serving {} to {}",
dnR, block, remoteAddress, ioe);
incrDatanodeNetworkErrors();
}
// Normally the client reports a bad block to the NN. However if the
// meta file is corrupt or an disk error occurs (EIO), then the client
// never gets a chance to do validation, and hence will never report
// the block as bad. For some classes of IO exception, the DN should
// report the block as bad, via the handleBadBlock() method
datanode.handleBadBlock(block, ioe, false);
throw ioe;
} finally {
IOUtils.closeStream(blockSender);
}
//update metrics
datanode.metrics.addReadBlockOp(elapsed());
datanode.metrics.incrReadsFromClient(peer.isLocal(), read);
}
四. 資料塊的傳輸格式
BlockSender類主要負責從資料節點的磁碟讀取資料塊, 然後傳送資料塊到接收方。需要注意的是, BlockSender傳送的資料是以一定結構組織的。
PacketLength大小為: 4 + CHECKSUMS(校驗資料的大小)+ DATA(真實資料的大小)
BlockSender傳送資料的格式包括兩個部分: 校驗資訊頭(ChecksumHeader) 和資料包序列(packets) 。
|校驗資訊頭(ChecksumHeader) |資料包序列(packets) |
4.1.校驗資訊頭(ChecksumHeader)
ChecksumHeader是一個校驗資訊頭, 用於描述當前Datanode使用的校驗方式等資訊。
| 1 byte校驗型別(CHECKSUM—TYPE) | 4 byte校驗塊大小(BYTES_PER_CHECKSUM)|
■ 資料校驗型別: 資料校驗型別定義在org.apache.hadoop.util.DataChecksum中, 目前包括三種方式——空校驗(不進行校驗) 、 CRC32以及CRC32C。 這裡使用1byte描述資料校驗型別, 空校驗、 CRC32、 CRC32C、分別對應於值0、 1、 2、3、4。
另外兩種型別 : CHECKSUM_DEFAULT、CHECKSUM_MIXED 對應於值3、4 不能使用者建立 DataChecksum .
■ 校驗塊大小: 校驗資訊頭中的第二個部分是校驗塊的大小, 也就是多少位元組的資料產生一個校驗值。 這裡以CRC32為例, 一般情況下是512位元組的資料產生一個4位元組的校驗和, 我們把這512位元組的資料稱為一個校驗塊(chunk) 。 這個校驗塊的概念非常重要, 它是HDFS中讀取和寫入資料塊操作的最小單元.
4.2.資料包序列
BlockSender會將資料塊切分成若干資料包(packet) 對外傳送, 當資料傳送完成後,會以一個空的資料包作為結束。
每個資料包都包括一個變長的包頭、 校驗資料以及若干位元組的實際資料。
|變長的資料包頭(packetHeader) | |校驗資料 | |實際資料…… |
■ 資料包頭——資料包頭用於描述當前資料包的資訊, 是通過ProtoBuf序列化的,包括4位元組的全包長度, 以及2位元組的包頭長度, 之後緊跟如下資料包資訊。
- 當前資料包在整個資料塊中的位置。
- 資料包在管道中的序列號。
- 當前資料包是不是資料塊中的最後一個資料包。
- 當前資料包中資料部分的長度。
- 是否需要DN同步。
■ 校驗資料——校驗資料是對實際資料做校驗操作產生的, 它將實際資料以校驗塊為單位, 每個校驗塊產生一個檢驗和, 校驗資料中包含了所有校驗塊的校驗和。校驗資料的大小為: (實際資料長度+校驗塊大小 - 1) / 校驗塊大小×校驗和長度。
■ 實際資料——資料包中的實際資料就是資料塊檔案中儲存的資料, 實際資料的傳輸是以校驗塊為單位的, 一個校驗塊對應產生一個校驗和的實際資料。 在資料包中會將校驗塊與校驗資料分開傳送, 首先將所有校驗塊的校驗資料傳送出去, 然後再傳送所有的校驗塊。
五. BlockSender實現
資料塊的傳送主要是由BlockSender類執行
BlockSender中資料塊的傳送過程包括: 傳送準備、 傳送資料塊以及清理工作。
5.1.傳送準備——構造方法
BlockSender中傳送資料的準備工作主要是在BlockSender的構造方法中執行的,BlockSender的構造方法執行了以下操作。
■ readahead & dropBehind的處理: 如果使用者通過cachingStrategy設定了這兩個欄位, 則按照這兩個欄位初始化讀取操作。 如果cachingStrategy為Null, 則按照配置檔案設定dropCacheBehindLargeReads為dfs.datanode.drop.cache.behind.reads,設定readaheadLength為dfs.datanode.readahead.bytes, 預設為4MB。
■ 賦值與校驗: 檢查當前Datanode上被讀取資料塊的時間戳、 資料塊檔案的長度等狀態是否正常。
■ 是否開啟transferTo模式: 預設為true, transferTo機制請參考零拷貝資料傳輸小節內容。
■ 獲取checksum資訊: 從Meta檔案中獲取當前資料塊的校驗演算法、 校驗和長度, 以及多少位元組產生一個校驗值, 也就是校驗塊的大小。
■ 計算offset以及endOffset: offset變數用於標識要讀取的資料在資料塊的起始位置,endOffset則用於標識結束的位置。 由於讀取位置往往不會落在某個校驗塊的起始位置, 所以在準備工作中需要確保offset在校驗塊的起始位置, endOffset在校驗塊的結束位置。 這樣讀取時就可以以校驗塊為單位讀取, 方便校驗和的操作。
■ 將資料塊檔案與校驗和檔案的offset都移動到指定位置。
/**
* Constructor
*
* @param block Block that is being read
* @param startOffset starting offset to read from
* @param length length of data to read
* @param corruptChecksumOk if true, corrupt checksum is okay
* @param verifyChecksum verify checksum while reading the data
* @param sendChecksum send checksum to client.
* @param datanode datanode from which the block is being read
* @param clientTraceFmt format string used to print client trace logs
* @throws IOException
*/
BlockSender(ExtendedBlock block, long startOffset, long length,
boolean corruptChecksumOk, boolean verifyChecksum,
boolean sendChecksum, DataNode datanode, String clientTraceFmt,
CachingStrategy cachingStrategy)
throws IOException {
InputStream blockIn = null;
DataInputStream checksumIn = null;
FsVolumeReference volumeRef = null;
// FileIoprovider@5795 DataNode{data=FSDataset{dirpath='[/opt/tools/hadoop-3.2.1/data/hdfs/data, /opt/tools/hadoop-3.2.1/data/hdfs/data01]'}, localName='192.168.8.188:9866', datanodeUuid='9efa402a-df6b-48cf-9273-5468f68cc42f', xmitsInProgress=0}
this.fileIoProvider = datanode.getFileIoProvider();
try {
this.block = block;
this.corruptChecksumOk = corruptChecksumOk;
this.verifyChecksum = verifyChecksum;
this.clientTraceFmt = clientTraceFmt;
/*
* If the client asked for the cache to be dropped behind all reads,
* we honor that. Otherwise, we use the DataNode defaults.
* When using DataNode defaults, we use a heuristic where we only
* drop the cache for large reads.
*/
if (cachingStrategy.getDropBehind() == null) {
this.dropCacheBehindAllReads = false;
this.dropCacheBehindLargeReads =
datanode.getDnConf().dropCacheBehindReads;
} else {
this.dropCacheBehindAllReads =
this.dropCacheBehindLargeReads =
cachingStrategy.getDropBehind().booleanValue();
}
/* 預設開啟預讀取, 除非 請求頭 指定 "不開啟預讀取
* Similarly, if readahead was explicitly requested, we always do it.
* Otherwise, we read ahead based on the DataNode settings, and only
* when the reads are large.
*/
if (cachingStrategy.getReadahead() == null) {
this.alwaysReadahead = false;
/// readaheadLength : 4194304 = 4M 預設 預讀取大小: 4M
this.readaheadLength = datanode.getDnConf().readaheadLength;
} else {
this.alwaysReadahead = true;
this.readaheadLength = cachingStrategy.getReadahead().longValue();
}
this.datanode = datanode;
if (verifyChecksum) {
// To simplify implementation, callers may not specify verification without sending.
Preconditions.checkArgument(sendChecksum,
"If verifying checksum, currently must also send it.");
}
// 如果在構造BlockSender之後有一個追加寫操作,那麼最後一個部分校驗和可能被append覆蓋,
// BlockSender需要在append write之前使用部分校驗和。
// if there is a append write happening right after the BlockSender
// is constructed, the last partial checksum maybe overwritten by the
// append, the BlockSender need to use the partial checksum before
// the append write.
ChunkChecksum chunkChecksum = null;
final long replicaVisibleLength;
try(AutoCloseableLock lock = datanode.data.acquireDatasetLock()) {
// 獲取datanode上的副本資訊
// FinalizedReplica, blk_1073746921_6097, FINALIZED
// getNumBytes() = 42648690
// getBytesOnDisk() = 42648690
// getVisibleLength()= 42648690
// getVolume() = /opt/tools/hadoop-3.2.1/data/hdfs/data
// getBlockURI() = file:/opt/tools/hadoop-3.2.1/data/hdfs/data/current/BP-451827885-192.168.8.156-1584099133244/current/finalized/subdir0/subdir19/blk_1073746921
replica = getReplica(block, datanode);
// 42648690
replicaVisibleLength = replica.getVisibleLength();
}
if (replica.getState() == ReplicaState.RBW) {
// 副本正在被寫入 , 等待寫入足夠的內容
final ReplicaInPipeline rbw = (ReplicaInPipeline) replica;
waitForMinLength(rbw, startOffset + length);
chunkChecksum = rbw.getLastChecksumAndDataLen();
}
if (replica instanceof FinalizedReplica) {
// 副本已經被寫入完成 , 獲取該副本的 ChunkChecksum :dataLength = 42648690 checksum = {byte[4]@5845} 四位 : [-11,-56,-5,28]
chunkChecksum = getPartialChunkChecksumForFinalized( (FinalizedReplica)replica);
}
if (replica.getGenerationStamp() < block.getGenerationStamp()) {
throw new IOException("Replica gen stamp < block genstamp, block="
+ block + ", replica=" + replica);
} else if (replica.getGenerationStamp() > block.getGenerationStamp()) {
if (DataNode.LOG.isDebugEnabled()) {
DataNode.LOG.debug("Bumping up the client provided"
+ " block's genstamp to latest " + replica.getGenerationStamp()
+ " for block " + block);
}
block.setGenerationStamp(replica.getGenerationStamp());
}
/// 副本可見長度小於0 , 不可見
if (replicaVisibleLength < 0) {
throw new IOException("Replica is not readable, block="+ block + ", replica=" + replica);
}
if (DataNode.LOG.isDebugEnabled()) {
DataNode.LOG.debug("block=" + block + ", replica=" + replica);
}
// 是否開啟零拷貝 dfs.datanode.transferTo.allowed : true
// transferToFully() fails on 32 bit platforms for block sizes >= 2GB,
// use normal transfer in those cases
this.transferToAllowed = datanode.getDnConf().transferToAllowed &&
(!is32Bit || length <= Integer.MAX_VALUE);
// 在讀取資料之前獲取引用 FsVolumeImple$FsVolumeReferenceImpl@5928
// Obtain a reference before reading data
volumeRef = datanode.data.getVolume(block).obtainReference();
/* 判斷是否要驗證 DataChecksum
* (corruptChecksumOK, meta_file_exist): operation
* True, True: will verify checksum
* True, False: No verify, e.g., need to read data from a corrupted file
* False, True: will verify checksum
* False, False: throws IOException file not found
*/
DataChecksum csum = null;
if (verifyChecksum || sendChecksum) {
LengthInputStream metaIn = null;
boolean keepMetaInOpen = false;
try {
DataNodeFaultInjector.get().throwTooManyOpenFiles();
metaIn = datanode.data.getMetaDataInputStream(block);
if (!corruptChecksumOk || metaIn != null) {
if (metaIn == null) {
//need checksum but meta-data not found
throw new FileNotFoundException("Meta-data not found for " +
block);
}
// The meta file will contain only the header if the NULL checksum
// type was used, or if the replica was written to transient storage.
// Also, when only header portion of a data packet was transferred
// and then pipeline breaks, the meta file can contain only the
// header and 0 byte in the block data file.
// Checksum verification is not performed for replicas on transient
// storage. The header is important for determining the checksum
// type later when lazy persistence copies the block to non-transient
// storage and computes the checksum.
int expectedHeaderSize = BlockMetadataHeader.getHeaderSize(); // 7
if (!replica.isOnTransientStorage() &&
metaIn.getLength() >= expectedHeaderSize) {
checksumIn = new DataInputStream(new BufferedInputStream(
metaIn, IO_FILE_BUFFER_SIZE));
// DataChecksum(type=CRC32C, chunkSize=512)
csum = BlockMetadataHeader.readDataChecksum(checksumIn, block);
keepMetaInOpen = true;
} else if (!replica.isOnTransientStorage() &&
metaIn.getLength() < expectedHeaderSize) {
LOG.warn("The meta file length {} is less than the expected " +
"header length {}, indicating the meta file is corrupt",
metaIn.getLength(), expectedHeaderSize);
throw new CorruptMetaHeaderException("The meta file length "+
metaIn.getLength()+" is less than the expected length "+
expectedHeaderSize);
}
} else {
LOG.warn("Could not find metadata file for " + block);
}
} catch (FileNotFoundException e) {
if ((e.getMessage() != null) && !(e.getMessage()
.contains("Too many open files"))) {
// The replica is on its volume map but not on disk
datanode
.notifyNamenodeDeletedBlock(block, replica.getStorageUuid());
datanode.data.invalidate(block.getBlockPoolId(),
new Block[] {block.getLocalBlock()});
}
throw e;
} finally {
if (!keepMetaInOpen) { // keepMetaInOpen : true
IOUtils.closeStream(metaIn);
}
}
}
if (csum == null) {
csum = DataChecksum.newDataChecksum(DataChecksum.Type.NULL,
(int)CHUNK_SIZE);
}
/*
* If chunkSize is very large, then the metadata file is mostly
* corrupted. For now just truncate bytesPerchecksum to blockLength.
*/
int size = csum.getBytesPerChecksum();
// 如果chunkSize非常大,則後設資料檔案大部分已損壞。現在只需將bytesPerchecksum截斷為blockLength。
if (size > 10*1024*1024 && size > replicaVisibleLength) {
//後設資料檔案損壞了, 重新構建 DataChecksum
csum = DataChecksum.newDataChecksum(csum.getChecksumType(),
Math.max((int)replicaVisibleLength, 10*1024*1024));
size = csum.getBytesPerChecksum();
}
//校驗塊大小 512
chunkSize = size;
//校驗演算法 DataChecksum(type=CRC32C, chunkSize=512)
checksum = csum;
//校驗和長度 4
checksumSize = checksum.getChecksumSize();
// 檔案大小 42648690
length = length < 0 ? replicaVisibleLength : length;
// end is either last byte on disk or the length for which we have a checksum
// end要麼是磁碟上的最後一個位元組,要麼是校驗和的長度 : 這裡是檔案長度.
long end = chunkChecksum != null ? chunkChecksum.getDataLength() : replica.getBytesOnDisk();
if (startOffset < 0 || startOffset > end
|| (length + startOffset) > end) {
String msg = " Offset " + startOffset + " and length " + length
+ " don't match block " + block + " ( blockLen " + end + " )";
LOG.warn(datanode.getDNRegistrationForBP(block.getBlockPoolId()) +
":sendBlock() : " + msg);
throw new IOException(msg);
}
// 將offset位置設定在校驗塊的邊界上, 也就是校驗塊的起始位置
// Ensure read offset is position at the beginning of chunk
offset = startOffset - (startOffset % chunkSize);
if (length >= 0) {
//計算endOffset的位置, 確保endOffset在校驗塊的結束位置
// Ensure endOffset points to end of chunk.
long tmpLen = startOffset + length;
if (tmpLen % chunkSize != 0) {
tmpLen += (chunkSize - tmpLen % chunkSize); //補齊資料, 使資料正好是512的整倍數
}
if (tmpLen < end) {
//結束位置還在資料塊內, 則可以使用磁碟上的校驗值 , 理論上應該不走這裡.
// will use on-disk checksum here since the end is a stable chunk
end = tmpLen;
} else if (chunkChecksum != null) {
//目前有寫執行緒[當前執行緒]正在處理這個校驗塊, 則使用記憶體中的校驗值
// last chunk is changing. flag that we need to use in-memory checksum
this.lastChunkChecksum = chunkChecksum;
}
}
endOffset = end; // 設定最後的偏移量為檔案的偏移量
// 將校驗檔案的座標移動到offset對應的位置
// seek to the right offsets
if (offset > 0 && checksumIn != null) {
long checksumSkip = (offset / chunkSize) * checksumSize;
// note blockInStream is seeked when created below
if (checksumSkip > 0) {
// Should we use seek() for checksum file as well?
IOUtils.skipFully(checksumIn, checksumSkip);
}
}
//packet序列號設定為0
seqno = 0;
if (DataNode.LOG.isDebugEnabled()) {
DataNode.LOG.debug("replica=" + replica);
}
//將資料塊檔案的座標移動到offset位置 準備開始讀寫
blockIn = datanode.data.getBlockInputStream(block, offset); // seek to offset
// 構建block資料的讀取流 ReplicaInputStreams
ris = new ReplicaInputStreams( blockIn, checksumIn, volumeRef, fileIoProvider);
} catch (IOException ioe) {
IOUtils.closeStream(this);
org.apache.commons.io.IOUtils.closeQuietly(blockIn);
org.apache.commons.io.IOUtils.closeQuietly(checksumIn);
throw ioe;
}
}
5.2.預讀取&丟棄——manageOsCache()
BlockSender在讀取資料塊之前, 會先呼叫manageOsCache()方法執行預讀取(readahead) 操作以提高讀取效率。 預讀取操作就是將資料塊檔案提前讀取到作業系統的快取中, 這樣當BlockSender到檔案系統中讀取資料塊檔案時, 可以直接從作業系統的快取中讀取資料, 比直接從磁碟上讀取快很多。 但是作業系統的快取空間是有限的, 所以需要呼叫manageOsCache()方法將不再使用的資料從快取中丟棄(drop-behind) , 為新的資料挪出空間。 BlockSender在讀取資料時, 使用了預讀取以及丟棄這兩個特性.
manageOsCache()方法在HDFS管理員設定了預讀取的長度(預設是4MB) 並且設定了所有的操作都使用預讀取時, 或者當前讀取是一個長讀取(超過256KB的讀取) 時, 會呼叫ReadaheadPool.readaheadStream()方法觸發一個預讀取操作, 這個預讀取操作會從磁碟
檔案上預讀取部分資料塊檔案的資料到作業系統的快取中。
同時managerOsCache()還會處理丟棄操作, 如果dropCacheBehindAllReads(所有讀操作後都丟棄) 為true或者當前讀取是一個大讀取時, 則觸發丟棄操作。 manageOsCache()方法會判斷如果下一次讀取資料的座標offset大於下一次丟棄操作的開始座標, 則將lastCacheDropOffset(上一次丟棄操作的結束位置) 和offset之間的資料全部從快取中丟棄, 因為這些資料Datanode已經讀取了,不需要放在快取中了.
manageOsCache()的開啟需要readaheadPool物件例項, readaheadPoold 建立是DataNode#startDataNode的方法進行初始化的.
但是需要本地庫的支援ReadaheadPool.getInstance(); 如果不支援是開啟不了的,需要hadoop的定製依賴 : native hadoop library .
由BlockSender#doSendBlock()方法呼叫
/**
*
* Manage the OS buffer cache by performing read-ahead and drop-behind.
*/
private void manageOsCache() throws IOException {
// We can't manage the cache for this block if we don't have a file
// descriptor to work with.
if (ris.getDataInFd() == null) {
return;
}
//按條件觸發預讀取操作
// Perform readahead if necessary
if ((readaheadLength > 0) && (datanode.readaheadPool != null) &&
(alwaysReadahead || isLongRead())) {
//滿足預讀取條件, 則呼叫ReadaheadPool.readaheadStream()方法觸發預讀取
curReadahead = datanode.readaheadPool.readaheadStream(
clientTraceFmt, ris.getDataInFd(), offset, readaheadLength,
Long.MAX_VALUE, curReadahead);
}
//丟棄剛才從快取中讀取的資料, 因為不再需要使用這些資料了
// Drop what we've just read from cache, since we aren't likely to need it again
if (dropCacheBehindAllReads ||
(dropCacheBehindLargeReads && isLongRead())) {
//丟棄資料的位置
long nextCacheDropOffset = lastCacheDropOffset + CACHE_DROP_INTERVAL_BYTES;
if (offset >= nextCacheDropOffset) {
//如果下一次讀取資料的位置大於丟棄資料的位置, 則將讀取資料位置前的資料全部丟棄
long dropLength = offset - lastCacheDropOffset;
ris.dropCacheBehindReads(block.getBlockName(), lastCacheDropOffset,
dropLength, POSIX_FADV_DONTNEED);
lastCacheDropOffset = offset;
}
}
}
ReadaheadPool.readaheadStream()方法執行了一個預讀取操作, 只有在上一次預讀取的資料已經使用了一半時, 才會觸發一次新的預讀取。 新的預讀取操作是通過在Datanode.readaheadPool執行緒池中建立一個ReadaheadRequestImpl任務來執行的。
ReadaheadRequestImpl.run()方法的程式碼如下, 它通過呼叫fadvise()系統呼叫, 完成OS層面的預讀取, 將資料放入作業系統的快取中。
@Override
public void run() {
if (canceled) return;
// There's a very narrow race here that the file will close right at
// this instant. But if that happens, we'll likely receive an EBADF
// error below, and see that it's canceled, ignoring the error.
// It's also possible that we'll end up requesting readahead on some
// other FD, which may be wasted work, but won't cause a problem.
try {
if (fd.valid()) {
//呼叫fadvise()系統呼叫完成預讀取
NativeIO.POSIX.getCacheManipulator().posixFadviseIfPossible(
identifier, fd, off, len, POSIX_FADV_WILLNEED);
}
} catch (IOException ioe) {
if (canceled) {
// no big deal - the reader canceled the request and closed
// the file.
return;
}
LOG.warn("Failed readahead on " + identifier,
ioe);
}
}
5.3.傳送資料塊——sendBlock()
sendBlock()方法, 這個方法用於讀取資料以及校驗和, 並將它們傳送到接收方。
整個傳送的流程可以分為如下幾步:
■ 在剛開始讀取檔案時, 觸發一次預讀取, 預讀取部分資料到作業系統的緩衝區中。
■ 構造pktBuf緩衝區, 也就是能容納一個資料包的緩衝區。 這裡首先要確定的就是pktBuf緩衝區的大小, 最好是一個資料包的大小。
對於兩種不同的傳送資料包的模式transferTo和ioStream, 緩衝區的大小是不同的。 在transfertTo模式中 , 資料塊檔案是通過零拷貝方式直接傳輸給客戶端的,並不需要將資料塊檔案寫入緩衝區中, 所以pktBuf緩衝區只需要緩衝校驗資料即可; 而ioStream模式則需要將實際資料以及校驗資料都緩衝下來, 所以pktBuf大小是完全不同的。
■ 接下來就是迴圈呼叫sendPacket()方法傳送資料包序列, 直到offset>=endOffset,也就是整個資料塊都傳送完成了。 這裡首先呼叫manageOsCache()進行預讀取,然後迴圈呼叫sendPacket()依次將所有資料包傳送到客戶端。 最後更新offset——也就是資料遊標, 更新seqno——記錄已經傳送了幾個資料包。
■ 傳送一個空的資料包用以標識資料塊的結束。
■ 完成資料塊傳送操作之後, 呼叫close()方法關閉資料塊以及校驗檔案, 並從作業系統的快取中刪除已讀取的資料。
private long doSendBlock(DataOutputStream out, OutputStream baseStream,
DataTransferThrottler throttler) throws IOException {
if (out == null) {
throw new IOException( "out stream is null" );
}
initialOffset = offset;
long totalRead = 0;
OutputStream streamForSendChunks = out;
lastCacheDropOffset = initialOffset;
if (isLongRead() && ris.getDataInFd() != null) {
// Advise that this file descriptor will be accessed sequentially.
ris.dropCacheBehindReads(block.getBlockName(), 0, 0,
POSIX_FADV_SEQUENTIAL);
}
//1. 將資料預讀取至作業系統的快取中
// Trigger readahead of beginning of file if configured.
manageOsCache();
final long startTime = ClientTraceLog.isDebugEnabled() ? System.nanoTime() : 0;
//2. 構造存放資料包(packet) 的緩衝區
try {
int maxChunksPerPacket;
// pktBufSize : 33
int pktBufSize = PacketHeader.PKT_MAX_HEADER_LEN;
boolean transferTo = transferToAllowed && !verifyChecksum
&& baseStream instanceof SocketOutputStream
&& ris.getDataIn() instanceof FileInputStream;
if (transferTo) {
FileChannel fileChannel = ((FileInputStream)ris.getDataIn()).getChannel();
blockInPosition = fileChannel.position();
streamForSendChunks = baseStream;
// 這裡的TRANSFERTO_BUFFER_SIZE大小預設是64KB
// maxChunksPerPacket變數表明一個資料包中最多包含多少個校驗塊 : 128 個
maxChunksPerPacket = numberOfChunks(TRANSFERTO_BUFFER_SIZE);
//緩沖區中只存放校驗資料 pktBufSize : 545
// Smaller packet size to only hold checksum when doing transferTo
pktBufSize += checksumSize * maxChunksPerPacket;
} else {
//這裡的IO—FILE_BUFFER_SIZE大小預設是4KB
maxChunksPerPacket = Math.max(1, numberOfChunks(IO_FILE_BUFFER_SIZE));
// Packet size includes both checksum and data
//緩衝區存放校驗資料以及實際資料
pktBufSize += (chunkSize + checksumSize) * maxChunksPerPacket;
}
//構造緩沖區pktBuf : 545
ByteBuffer pktBuf = ByteBuffer.allocate(pktBufSize);
//迴圈呼叫sendPacket()傳送packet
while (endOffset > offset && !Thread.currentThread().isInterrupted()) {
manageOsCache();
long len = sendPacket(pktBuf, maxChunksPerPacket, streamForSendChunks,
transferTo, throttler);
offset += len;
totalRead += len + (numberOfChunks(len) * checksumSize);
seqno++;
}
//如果當前執行緒被中斷, 則不再傳送完整的資料塊
// If this thread was interrupted, then it did not send the full block.
if (!Thread.currentThread().isInterrupted()) {
try {
// 傳送一個空的資料包用以標識資料塊的結束
// send an empty packet to mark the end of the block
sendPacket(pktBuf, maxChunksPerPacket, streamForSendChunks, transferTo,
throttler);
out.flush();
} catch (IOException e) { //socket error
throw ioeToSocketException(e);
}
sentEntireByteRange = true;
}
} finally {
if ((clientTraceFmt != null) && ClientTraceLog.isDebugEnabled()) {
final long endTime = System.nanoTime();
ClientTraceLog.debug(String.format(clientTraceFmt, totalRead,
initialOffset, endTime - startTime));
}
// 呼叫close()檔案關閉資料塊檔案、 校驗檔案以及回收作業系統緩衝區
close();
}
return totalRead;
}
5.4.傳送資料包——sendPacket()
sendPacket()也可以分為三個部分
■ 首先計算資料包頭域在pkt快取中的位置headerOff, 再計算checksum在pkt中的位置checksumOff, 以及實際資料在pkt中的位置dataOff。 然後將資料包頭域、 校驗資料以及實際資料寫入pkt快取中。 如果verifyChecksum屬性被設定為true, 則呼叫verifyChecksum()方法確認校驗和資料正確。
■ 接下來就是傳送資料塊了, 將pkt快取中的資料寫入IO流中。 這裡要注意, 如果是transferT()方式, pkt中只有資料包頭域以及校驗資料, 實際資料則直接通過transferTo方式從檔案通道(FileChannel) 直接寫入IO流中。
■ 使用節流器控制寫入的速度
/**
* Sends a packet with up to maxChunks chunks of data.
*
* @param pkt buffer used for writing packet data
* @param maxChunks maximum number of chunks to send
* @param out stream to send data to
* @param transferTo use transferTo to send data
* @param throttler used for throttling data transfer bandwidth
*/
private int sendPacket(ByteBuffer pkt, int maxChunks, OutputStream out,
boolean transferTo, DataTransferThrottler throttler) throws IOException {
int dataLen = (int) Math.min(endOffset - offset,
(chunkSize * (long) maxChunks));
//資料包中包含多少個校驗塊
int numChunks = numberOfChunks(dataLen); // Number of chunks be sent in the packet
//校驗資料長度
int checksumDataLen = numChunks * checksumSize;
//資料包長度
int packetLen = dataLen + checksumDataLen + 4;
boolean lastDataPacket = offset + dataLen == endOffset && dataLen > 0;
// The packet buffer is organized as follows:
// _______HHHHCCCCD?D?D?D?
// ^ ^
// | \ checksumOff
// \ headerOff
// _ padding, since the header is variable-length
// H = header and length prefixes
// C = checksums
// D? = data, if transferTo is false.
//將資料包頭域寫入快取中
int headerLen = writePacketHeader(pkt, dataLen, packetLen);
//資料包頭域在快取中的位置
// Per above, the header doesn't start at the beginning of the buffer
int headerOff = pkt.position() - headerLen;
//校驗資料在快取中的位置
int checksumOff = pkt.position();
byte[] buf = pkt.array();
if (checksumSize > 0 && ris.getChecksumIn() != null) {
//將校驗資料寫入快取中
readChecksum(buf, checksumOff, checksumDataLen);
// write in progress that we need to use to get last checksum
if (lastDataPacket && lastChunkChecksum != null) {
int start = checksumOff + checksumDataLen - checksumSize;
byte[] updatedChecksum = lastChunkChecksum.getChecksum();
if (updatedChecksum != null) {
System.arraycopy(updatedChecksum, 0, buf, start, checksumSize);
}
}
}
int dataOff = checksumOff + checksumDataLen;
if (!transferTo) { // normal transfer
try {
//在普通模式下, 將實際資料寫入快取中
ris.readDataFully(buf, dataOff, dataLen);
} catch (IOException ioe) {
if (ioe.getMessage().startsWith(EIO_ERROR)) {
throw new DiskFileCorruptException("A disk IO error occurred", ioe);
}
throw ioe;
}
if (verifyChecksum) {
//確認校驗和正確
verifyChecksum(buf, dataOff, dataLen, numChunks, checksumOff);
}
}
try {
//transferTo模式
if (transferTo) {
//將頭域和校驗和寫入輸出流中
SocketOutputStream sockOut = (SocketOutputStream)out;
//使用transfer方式, 將資料從資料塊檔案直接零拷貝到IO流中
// First write header and checksums
sockOut.write(buf, headerOff, dataOff - headerOff);
// no need to flush since we know out is not a buffered stream
FileChannel fileCh = ((FileInputStream)ris.getDataIn()).getChannel();
LongWritable waitTime = new LongWritable();
LongWritable transferTime = new LongWritable();
fileIoProvider.transferToSocketFully(
ris.getVolumeRef().getVolume(), sockOut, fileCh, blockInPosition,
dataLen, waitTime, transferTime);
datanode.metrics.addSendDataPacketBlockedOnNetworkNanos(waitTime.get());
datanode.metrics.addSendDataPacketTransferNanos(transferTime.get());
blockInPosition += dataLen;
} else {
//在正常模式下
//將快取中的所有資料(包括頭域、 校驗和以及實際資料) 寫入輸出流中
// normal transfer
out.write(buf, headerOff, dataOff + dataLen - headerOff);
}
} catch (IOException e) {
if (e instanceof SocketTimeoutException) {
/*
* writing to client timed out. This happens if the client reads
* part of a block and then decides not to read the rest (but leaves
* the socket open).
*
* Reporting of this case is done in DataXceiver#run
*/
} else {
/* Exception while writing to the client. Connection closure from
* the other end is mostly the case and we do not care much about
* it. But other things can go wrong, especially in transferTo(),
* which we do not want to ignore.
*
* The message parsing below should not be considered as a good
* coding example. NEVER do it to drive a program logic. NEVER.
* It was done here because the NIO throws an IOException for EPIPE.
*/
String ioem = e.getMessage();
/*
* If we got an EIO when reading files or transferTo the client socket,
* it's very likely caused by bad disk track or other file corruptions.
*/
if (ioem.startsWith(EIO_ERROR)) {
throw new DiskFileCorruptException("A disk IO error occurred", e);
}
if (!ioem.startsWith("Broken pipe") && !ioem.startsWith("Connection reset")) {
LOG.error("BlockSender.sendChunks() exception: ", e);
datanode.getBlockScanner().markSuspectBlock(
ris.getVolumeRef().getVolume().getStorageID(),
block);
}
}
throw ioeToSocketException(e);
}
if (throttler != null) {
// rebalancing so throttle
//調整節流器
throttler.throttle(packetLen);
}
return dataLen;
}
相關文章
- Hadoop3.2.1 【 HDFS 】原始碼分析 : Standby Namenode解析Hadoop原始碼
- Hadoop3.2.1 【 HDFS 】原始碼分析 : Secondary Namenode解析Hadoop原始碼
- Hadoop3.2.1 【 HDFS 】原始碼分析 : 檔案系統資料集 [一]Hadoop原始碼
- HDFS原始碼分析(二)-----後設資料備份機制原始碼
- 原始碼|HDFS之DataNode:寫資料塊(2)原始碼
- 原始碼|HDFS之DataNode:寫資料塊(3)原始碼
- 原始碼|HDFS之DataNode:寫資料塊(1)原始碼
- Hadoop3.2.1 【 YARN 】原始碼分析 :RPC通訊解析HadoopYarn原始碼RPC
- HDFS原始碼解析:教你用HDFS客戶端寫資料原始碼客戶端
- TiKV 原始碼解析系列文章(十三)MVCC 資料讀取原始碼MVC
- HDFS讀檔案過程分析:讀取檔案的Block資料BloC
- HDFS原始碼解析系列一——HDFS通訊協議原始碼協議
- RocketMq 拉取資料流程原始碼分析MQ原始碼
- 【Spring原始碼分析】.properties檔案讀取及佔位符${...}替換原始碼解析Spring原始碼
- spark讀取hdfs資料本地性異常Spark
- Hadoop3.2.1 【 YARN 】原始碼分析 :AdminService 淺析HadoopYarn原始碼
- slate原始碼解析(二)- 基本框架與資料模型原始碼框架模型
- Logstash讀取Kafka資料寫入HDFS詳解Kafka
- OkHttp 原始碼分析(二)—— 快取機制HTTP原始碼快取
- RecyclerView 原始碼分析(二) —— 快取機制View原始碼快取
- TMCache原始碼分析(二)---TMDiskCache磁碟快取原始碼快取
- 解析Pyspark如何讀取parquet資料Spark
- lodash原始碼分析之獲取資料型別原始碼資料型別
- BufferedOutputStream的快取功能解析(原始碼閱讀)快取原始碼
- 如何解析 Ethereum 資料:讀取 LevelDB 資料
- Hadoop系列之HDFS 資料塊Hadoop
- Hadoop2原始碼分析-HDFS核心模組分析Hadoop原始碼
- jQuery1.9.1原始碼分析--資料快取Data模組jQuery原始碼快取
- 【Spring原始碼分析】配置檔案讀取流程Spring原始碼
- myBatis原始碼解析-二級快取的實現方式MyBatis原始碼快取
- vue原始碼分析系列之響應式資料(二)Vue原始碼
- hadoop 原始碼分析HDFS架構演進Hadoop原始碼架構
- buffer cache實驗9-從buffer caceh中讀取資料塊解析-從邏輯讀到物理讀
- 比特幣原始碼研讀(2)資料結構-區塊Block比特幣原始碼資料結構BloC
- Element原始碼分析系列6-Checkbox(核取方塊)原始碼
- Laravel 原始碼閱讀指南 -- Cookie 原始碼解析Laravel原始碼Cookie
- Mybatis原始碼分析(二)XML的解析和Annotation的支援MyBatis原始碼XML
- Picasso-原始碼解析(二)原始碼