本部分主要分析EC策略下的条带化读取、写入过程,包括StripeReader、StripeWriter、StripedBlockReader、StripedBlockWriter、StripedBlockUtil部分源码。另外,整个过程还涉及EC策略下的数据重构,包括StripedReconstruction、StripedBlockReconstruction、BlockRecoveryWorker,还涉及HDFS的数据读写流DFSStripedOutputStream、DFSStripedInputStream。本文旨在回答以下问题:
1、hdfs中,file, block, datanode, stripe, cell, chunk这些概念之间的关系;
2、一个文件是如何被以条带化的形式存储在不同datanode的不同block上的,又是如何读出的;
3、EC策略下,数据如何被encode, decode, reconstruction, recovery。
block group:通俗的理解就是组成“一次”编码的若干块;
Internal blocks:即block group中的一个块;
striping cell:块中的一个条带化的单元;
cell size:一个条带单元的尺寸。
striping chunk:是读取buffer的最小单位,index与cell相同但是范围可以不同
aligned stripe:读取的基本单位,可以理解为stripe但是范围可以不同
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cjMAJAOU-1592839623320)(C:\Users\hitzzk\AppData\Roaming\Typora\typora-user-images\image-20200622000215161.png)]
条带化读取和写入的过程,就是将文件按照规则写入到不同datanode中的不同block中去,也就是,将文件的逻辑字节映射到物理存储器上,这样就可以理解,为什么在上述基本概念中,读取写入过程的chunk和stripe和逻辑上的cell和stripe并不完全一致。另外,在hdfs中另有定义,没两个cell之间间隔1byte:
the end offset of the previous range should be 1 byte lower than the start offset of the next one
官方源码中给出的示意图:
* | <---- Block Group ----> | <- Block Group: logical unit composing
* | | striped HDFS files.
* blk_0 blk_1 blk_2 <- Internal Blocks: each internal block
* | | | represents a physically stored local
* v v v block file
* +------+ +------+ +------+
* |cell_0| |cell_1| |cell_2| <- {@link StripingCell} represents the
* +------+ +------+ +------+ logical order that a Block Group should
* |cell_3| |cell_4| |cell_5| be accessed: cell_0, cell_1, ...
* +------+ +------+ +------+
* |cell_6| |cell_7| |cell_8|
* +------+ +------+ +------+
* |cell_9|
* +------+ <- A cell contains cellSize bytes of data
StripedBlockUtil类的第一个方法就是parseStripedBlockGroup这个方法(或称函数)的功能是
This method parses a striped block group into individual blocks.
public static LocatedBlock[] parseStripedBlockGroup(LocatedStripedBlock bg,
int cellSize, int dataBlkNum, int parityBlkNum, int localParityBlkNum) {
int locatedBGSize = bg.getBlockIndices().length;
LocatedBlock[] lbs = new LocatedBlock[dataBlkNum + parityBlkNum + localParityBlkNum];
for (short i = 0; i < locatedBGSize; i++) {
final int idx = bg.getBlockIndices()[i];
// for now we do not use redundant replica of an internal block
if (idx < (dataBlkNum + parityBlkNum + localParityBlkNum) && lbs[idx] == null) {
lbs[idx] = constructInternalBlock(bg, i, cellSize,
dataBlkNum, idx);
}
}
return lbs;
}
该函数的输入参数分别为一个block group,一个cell的大小,数据块数量、校验快数量、局部校验快数量(后增加),返回值为该block group中各个block的参数,LocatedBlock类在LocatedBlock.java文件中给出定义,在这里不再详细描述,只需知道该类给出了block的基本信息,如存储位置、存储id、安全令牌等,也就是我们所说的metadata(元数据)。parseStripedBlockGroup方法的关键是调用了constructInternalBlock方法,显然,从名字可以看出该方法具体实现了构建内部块的方法
public static LocatedBlock constructInternalBlock(LocatedStripedBlock bg,
int idxInReturnedLocs, int cellSize, int dataBlkNum,
int idxInBlockGroup) {
final ExtendedBlock blk = constructInternalBlock(
bg.getBlock(), cellSize, dataBlkNum, idxInBlockGroup);
final LocatedBlock locatedBlock;
if (idxInReturnedLocs < bg.getLocations().length) {
locatedBlock = new LocatedBlock(blk,
new DatanodeInfo[]{bg.getLocations()[idxInReturnedLocs]},
new String[]{bg.getStorageIDs()[idxInReturnedLocs]},
new StorageType[]{bg.getStorageTypes()[idxInReturnedLocs]},
bg.getStartOffset(), bg.isCorrupt(), null);
} else {
locatedBlock = new LocatedBlock(blk, null, null, null,
bg.getStartOffset(), bg.isCorrupt(), null);
}
Token<BlockTokenIdentifier>[] blockTokens = bg.getBlockTokens();
if (idxInReturnedLocs < blockTokens.length) {
locatedBlock.setBlockToken(blockTokens[idxInReturnedLocs]);
}
return locatedBlock;
}
此方法的输入参数为一个blockgroup,返回的block的位置标号(即实际存储它的datanode),cell的大小,数据块数量,block在blockgroup中的位置,返回值是该block在datanode上的位置(此处也包含存储类型、副本信息等其他metadata)。在以上代码块第4行可以看到这里调用了本函数的一个多态函数
public static ExtendedBlock constructInternalBlock(ExtendedBlock blockGroup,
int cellSize, int dataBlkNum, int idxInBlockGroup) {
ExtendedBlock block = new ExtendedBlock(blockGroup);
block.setBlockId(blockGroup.getBlockId() + idxInBlockGroup);
block.setNumBytes(getInternalBlockLength(blockGroup.getNumBytes(),
cellSize, dataBlkNum, idxInBlockGroup));
return block;
}
本函数构造了一个实际的块,本方法的关键显然在第5行,即设置本块的大小(字节数),而本块的大小又是由getInternalBlockLength方法所决定。在hdfs中,block的最大尺寸是确定的,一般设置为64M,128M,256M等,但是实际块的大小是由具体存储需求决定的。实际文件的大小,不一定能整除stripe的大小,也即会存在不完整的stripe,也不一定能整除cell的大小,也即会存在不完整的cell。
public static long getInternalBlockLength(long dataSize,
int cellSize, int numDataBlocks, int idxInBlockGroup) {
Preconditions.checkArgument(dataSize >= 0);
Preconditions.checkArgument(cellSize > 0);
Preconditions.checkArgument(numDataBlocks > 0);
Preconditions.checkArgument(idxInBlockGroup >= 0);
// Size of each stripe (only counting data blocks)
final int stripeSize = cellSize * numDataBlocks;
// If block group ends at stripe boundary, each internal block has an equal
// share of the group
final int lastStripeDataLen = (int)(dataSize % stripeSize);
if (lastStripeDataLen == 0) {
return dataSize / numDataBlocks;
}
final int numStripes = (int) ((dataSize - 1) / stripeSize + 1);
return (numStripes - 1L)*cellSize
+ lastCellSize(lastStripeDataLen, cellSize,
numDataBlocks, idxInBlockGroup);
}
可以看到getInternalBlockLength方法输入参数为数据大小,cell大小,数据块数量,该块在blockgroup中的标号。注意在这里我们都是在block group的范围内讨论,这里的datasize也是指一个blockgroup的数据大小。从以上代码可以看出,stripe的大小是cell的大小乘上数据块的数量,要注意,这里只计算了数据块,实际上stripe是包含校验块的。第11行可以看到,显然数据大小不一定是stripe大小的整数倍,若能整除,那么块的大小都相同,一个块的大小就是总的数据量除以块的数量(13行),若不能整除余数就是最后一个stripe的大小,此时各个块的大小显然就不再相同,而且最后一个块未必能存满一个cell。
return (numStripes - 1L)*cellSize
+ lastCellSize(lastStripeDataLen, cellSize,
numDataBlocks, idxInBlockGroup);
从返回值上可以看到,此种条件下块的大小为条带的数量-1再乘上cell的大小,再加上本块最后一个cell的大小,下面再来看lasCellsize是如何计算最后一个cell大小的:
private static int lastCellSize(int size, int cellSize, int numDataBlocks,
int i) {
if (i < numDataBlocks) {
// parity block size (i.e. i >= numDataBlocks) is the same as
// the first data block size (i.e. i = 0).
size -= i*cellSize;
if (size < 0) {
size = 0;
}
}
return size > cellSize? cellSize: size;
}
本函数非常简单,若本块是数据块(i 继续看stripedBlockUtil的下一个方法 第11,12行可以看出这里通过整除后强制取整的方式得到最后一个完整的stripe,但是在这里为什么要取cpy[cpy.length - dataBlkNum]令我百思不得其解,这里按常理应该是取最后一个block的大小除以cellsize,即可得到完整的stripe的数量,也就是 dataBlkNum - 1而不是cpy.length - dataBlkNum。 spaceConsumedByStripedBlock方法功能较为简单不做赘述。 offsetInBlkToOffsetInBG方法功能较为简单不做赘述。 divideOneStripe方法: 此方法是较为关键的一个方法,功能为将字节数据分成条带,步骤已经在方法内的注释中写明,第一步将要条带化存储的数据,即一堆字节,分成若干cell,第二步,计算每个block需要存多少cell,也就是垂直范围(VerticalRange),第三步将数据形成条带,第四步将数据读到缓冲区(buffer)中 下一个方法getStripingCellsOfByteRange作用是得到blockgroup中的各个cell,不做赘述 getRangesForInternalBlocks与getStripingCellsOfByteRange类似,不做赘述, 下一个方法mergeRangesForInternalBlocks功能也类似,不做赘述。在这里阐明hdfs对于“长度”和"宽度"的定义。一个alignedstripe占用的block数量称为width,在一个块中占的byte数就是length。例如采用RS-3-2-1024k的EC策略,则alignedstripe的width是3+2=5,length是1024k。 以上三个方法基本可以搞清楚cell,stripe,block的关系。 未完待续。 sOfByteRange类似,不做赘述, 下一个方法mergeRangesForInternalBlocks功能也类似,不做赘述。在这里阐明hdfs对于“长度”和"宽度"的定义。一个alignedstripe占用的block数量称为width,在一个块中占的byte数就是length。例如采用RS-3-2-1024k的EC策略,则alignedstripe的width是3+2=5,length是1024k。 以上三个方法基本可以搞清楚cell,stripe,block的关系。 未完待续。public static long getSafeLength(ErasureCodingPolicy ecPolicy,
long[] blockLens) {
final int cellSize = ecPolicy.getCellSize();
final int dataBlkNum = ecPolicy.getNumDataUnits();
Preconditions.checkArgument(blockLens.length >= dataBlkNum);
final int stripeSize = dataBlkNum * cellSize;
long[] cpy = Arrays.copyOf(blockLens, blockLens.length);
Arrays.sort(cpy);
// full stripe is a stripe has at least dataBlkNum full cells.
// lastFullStripeIdx is the index of the last full stripe.
int lastFullStripeIdx =
(int) (cpy[cpy.length - dataBlkNum] / cellSize);
return lastFullStripeIdx * stripeSize; // return the safeLength
// TODO: Include lastFullStripeIdx+1 stripe in safeLength, if there exists
// such a stripe (and it must be partial).
}
public static AlignedStripe[] divideOneStripe(ErasureCodingPolicy ecPolicy,
int cellSize, LocatedStripedBlock blockGroup, long rangeStartInBlockGroup,
long rangeEndInBlockGroup, ByteBuffer buf) {
final int dataBlkNum = ecPolicy.getNumDataUnits();
// Step 1: map the byte range to StripingCells
StripingCell[] cells = getStripingCellsOfByteRange(ecPolicy, cellSize,
blockGroup, rangeStartInBlockGroup, rangeEndInBlockGroup);
// Step 2: get the unmerged ranges on each internal block
VerticalRange[] ranges = getRangesForInternalBlocks(ecPolicy, cellSize,
cells);
// Step 3: merge into stripes
AlignedStripe[] stripes = mergeRangesForInternalBlocks(ecPolicy, ranges);
// Step 4: calculate each chunk's position in destination buffer. Since the
// whole read range is within a single stripe, the logic is simpler here.
int bufOffset =
(int) (rangeStartInBlockGroup % ((long) cellSize * dataBlkNum));
for (StripingCell cell : cells) {
long cellStart = cell.idxInInternalBlk * cellSize + cell.offset;
long cellEnd = cellStart + cell.size - 1;
for (AlignedStripe s : stripes) {
long stripeEnd = s.getOffsetInBlock() + s.getSpanInBlock() - 1;
long overlapStart = Math.max(cellStart, s.getOffsetInBlock());
long overlapEnd = Math.min(cellEnd, stripeEnd);
int overLapLen = (int) (overlapEnd - overlapStart + 1);
if (overLapLen > 0) {
Preconditions.checkState(s.chunks[cell.idxInStripe] == null);
final int pos = (int) (bufOffset + overlapStart - cellStart);
buf.position(pos);
buf.limit(pos + overLapLen);
s.chunks[cell.idxInStripe] = new StripingChunk(buf.slice());
}
}
bufOffset += cell.size;
}
// Step 5: prepare ALLZERO blocks
prepareAllZeroChunks(blockGroup, stripes, cellSize, dataBlkNum);
return stripes;
}