MapReduce源码分析
忙碌中,补充一下之前阅读源码的过程,以及MapReduce如何保证读取文件的逻辑完整性的问题。
提交
waitForCompletion()
=>
submit()->submitJobInternal(){
int maps = writeSplits(job, submitJobDir)
=>
writeNewSplits()
=>
getSplits()//计算切片
}
public void submit()
throws IOException, InterruptedException, ClassNotFoundException {
//确保该job的状态正常
ensureState(JobState.DEFINE);
//新api 略过
setUseNewAPI();
//连接集群
connect();
//提交
final JobSubmitter submitter =
getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
public JobStatus run() throws IOException, InterruptedException,
ClassNotFoundException {
return submitter.submitJobInternal(Job.this, cluster);
}
});
state = JobState.RUNNING;
LOG.info("The url to track the job: " + getTrackingURL());
}
计算切片
- 计算切片的最小长度
- 计算切片的最大长度
- 文件是否是可切片的,否则直接根绝block信息生成切片信息
- 遍历每个文件计算切片信息
- 计算切片长度
- 如果设置的分片的最小值>blockSize 那么分片大小为设置的最小值,这时候分片的值大于块大小
- 如果设置的分片最大值<blockSize,那么分片大小为设置的最大值,这时候分片的值小于块大小
- 否则为block大小,分片大小设置,其实就是设置了一个上下限
- 如果要增加分片大小,就需要设置最小值>blockSize;如果要减小分片大小,就需要设置最大值<blockSize
- 计算切片信息
- 限定了文件切分的最小值,当剩余文件<splitSize*1.1的时候,就不会继续分片了
- 获取block的索引信息,length-bytesRemaining即切片的offset,必须在block上,那么该block的index即blkLocations的索引,就是 blockIndex 。
- 创建切片对象 包含block的路径、split偏移量即block的offset、split长度、block所在的DataNode的地址
- 计算切片长度
private <T extends InputSplit>
int writeNewSplits(JobContext job, Path jobSubmitDir) throws IOException,
InterruptedException, ClassNotFoundException {
Configuration conf = job.getConfiguration();
//默认是TextInputFormat
InputFormat<?, ?> input =
ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
//获取切片
List<InputSplit> splits = input.getSplits(job);
T[] array = (T[]) splits.toArray(new InputSplit[splits.size()]);
// sort the splits into order based on size, so that the biggest
// go first
Arrays.sort(array, new SplitComparator());
JobSplitWriter.createSplitFiles(jobSubmitDir, conf,
jobSubmitDir.getFileSystem(conf), array);
return array.length;
}
/**
* 计算切片
*/
public List<InputSplit> getSplits(JobContext job) throws IOException {
//最小切片大小,getFormatMinSplitSize()返回是1
//getMinSplitSize()返回mapreduce.input.fileinputformat.split.minsize,默认为1
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
//最大切片大小,返回mapreduce.input.fileinputformat.split.maxsize,默认是Int最大值
long maxSize = getMaxSplitSize(job);
//创建切片 splits
List<InputSplit> splits = new ArrayList<InputSplit>();
//对每个文件都分别计算切片,并没有对文件进行处理
List<FileStatus> files = listStatus(job);
for (FileStatus file: files) {
Path path = file.getPath();
//获取文件字节长度
long length = file.getLen();
if (length != 0) {
//block块的位置信息
//1.String[] hosts Datanode hostnames
//2.String[] names Datanode IP:xferPort for accessing the block
//String[] topologyPaths Full path name in network topology
//long offset Offset of the block in the file
//long length
//boolean corrupt
BlockLocation[] blkLocations;
//获取文件的block信息,包括了副本信息
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
FileSystem fs = path.getFileSystem(job.getConfiguration());
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
//如果可以切分,那么就创建切片信息,向压缩的文件就不能进行切分
if (isSplitable(job, path)) {
//块的大小 默认128M
long blockSize = file.getBlockSize();
//1.如果设置的分片的最小值>blockSize 那么分片大小为设置的最小值,这时候分片的值大于块大小
//2.如果设置的分片最大值<blockSize,那么分片大小为设置的最大值,这时候分片的值小于块大小
//否则为block大小
//所以 如果要增加分片大小,就需要设置最小值>blockSize
// 如果要减小分片大小,就需要设置最大值<blockSize
//分片大小设置,其实就是设置了一个上下限
//Math.max(minSize, Math.min(maxSize, blockSize))
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
//剩余字节大小,初始为文件的大小
long bytesRemaining = length;
//这里限定了文件切分的最小值,当剩余文件<splitSize*1.1的时候,就不会继续分片了
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
//这里是获取block索引,length-bytesRemaining即切片的offset,必须在block上,那么该block的index即blkLocations的索引,就是 blockIndex
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
//这里真正在组装split
//block的路径、split偏移量即block的offset、split长度、block所在的DataNode的地址
// new FileSplit(file, start, length, hosts);
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts()));
bytesRemaining -= splitSize;
}
//为最后一个分片
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts()));
}
} else { // not splitable
//这里不需要分片
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts()));
}
} else {
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
// Save the number of input files for metrics/loadgen
job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
LOG.debug("Total # of splits: " + splits.size());
return splits;
}
读取
思考:为什么Map阶段在分片读取文件的时候,不会导致文件乱码?要知道HDFS的文件block存储是严格按照字节进行拆分的。
LineRecordReader
该类负责每一行数据的读取工作,在初始化的时候,修改了split的偏移量,除了第一个分片,其他的整体多读取了一行,那么在真正读取操作时nextKeyValue(),
总会多读取一行,这里就保证前面的问题,文件被分割后,Map任务还是可以做到正常读取数据,保证数据的正确性。
public void initialize(InputSplit genericSplit,
TaskAttemptContext context) throws IOException {
// If this is not the first split, we always throw away first record
// because we always (except the last split) read one extra line in
// next() method.
if (start != 0) {
//这里读取了一行,返回字节数,更改原start的位置,除了第一个split,都是从第二行开始偏移
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
this.pos = start;
}
/**
* 读取下一行
*/
public boolean nextKeyValue() throws IOException {
if (key == null) {
key = new LongWritable();
}
key.set(pos);
if (value == null) {
value = new Text();
}
int newSize = 0;
// We always read one extra line, which lies outside the upper
// split limit i.e. (end - 1)
//这里总是多读一行,<=操作一目了然,这里的循环主要是为了避免读取一行数据过大,如果数据过大,那么这一行将被抛弃,继续读取,直到newSize < maxLineLength,那么一次读取将会结束,新的一行的数据将会覆盖上一行(超大行的数据)
while (getFilePosition() <= end) {
newSize = in.readLine(value, maxLineLength,
Math.max(maxBytesToConsume(pos), maxLineLength));
pos += newSize;
//小于最大值,这里属于正常的一行,就会结束掉
if (newSize < maxLineLength) {
break;
}
// line too long. try again
LOG.info("Skipped line of size " + newSize + " at pos " +
(pos - newSize));
}
if (newSize == 0) {
key = null;
value = null;
return false;
} else {
return true;
}
}
总结
困扰我的一个问题,一个被切分的文件如何做到业务层面的完整性的,那么现在就可以看出,LineRecordReader该读取器,首先在初始化的时候修改了切面的偏移量,在读取时,又补偿式的多读取了一行数据,这样看来数据数据完整性就被很好的规避掉了。