hadoop的体系十分庞大,这里对一些主要功能的细节做分析。
map阶段
input阶段
主要涉及的难点有 :
- 切片的划分
- 切片数据的读取(多余数据的剔除,和有效数据的拉取)
//MyWordCount.java
job.waitForCompletion(true);
该方法会跳转到Job.class类中,
...
public boolean waitForCompletion(boolean verbose
) throws IOException, InterruptedException,
ClassNotFoundException {
if (state == JobState.DEFINE) {
//如果该Job 已经定义,则提交Job
submit();// 从这里开始跳转到下一个方法
}
if (verbose) {
// 如果开启 开启监视器
monitorAndPrintJob();
} else {
// get the completion poll interval from the client.
int completionPollIntervalMillis =
Job.getCompletionPollInterval(cluster.getConf());
while (!isComplete()) {
try {
Thread.sleep(completionPollIntervalMillis);
} catch (InterruptedException ie) {
}
}
}//返回job执行成功!
return isSuccessful();
}
...
上面的submit()方法属于 Job.class 类
...
public void submit()
throws IOException, InterruptedException, ClassNotFoundException {
ensureState(JobState.DEFINE);
setUseNewAPI();//使用新的API
connect();//做一些链接
// 重点步骤 !!!定义一个submitter 对象 用于提交任务
final JobSubmitter submitter =
getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
public JobStatus run() throws IOException, InterruptedException,
ClassNotFoundException {
//getJobSubmitter 只是一个简单的构造方法,这里使用submitJobInternal()方法 提交一个任务。,下点submitJobInternal 方法。
return submitter.submitJobInternal(Job.this, cluster);
}
});
state = JobState.RUNNING;
LOG.info("The url to track the job: " + getTrackingURL());
}
...
下面查看 submitJobInternal()方法,此方法定义在submitJobInternal.class类
JobStatus submitJobInternal(Job job, Cluster cluster)
throws ClassNotFoundException, InterruptedException, IOException {
//validate the jobs output specs
checkSpecs(job);
Configuration conf = job.getConfiguration();
//拿到配置信息,并将其添加到框架。
addMRFrameworkToDistributedCache(conf);
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
//configure the command line options correctly on the submitting dfs
InetAddress ip = InetAddress.getLocalHost();
if (ip != null) {
submitHostAddress = ip.getHostAddress();
submitHostName = ip.getHostName();
conf.set(MRJobConfig.JOB_SUBMITHOST,submitHostName);
conf.set(MRJobConfig.JOB_SUBMITHOSTADDR,submitHostAddress);
}
JobID jobId = submitClient.getNewJobID();
job.setJobID(jobId);
Path submitJobDir = new Path(jobStagingArea, jobId.toString());
JobStatus status = null;
try {
conf.set(MRJobConfig.USER_NAME,
UserGroupInformation.getCurrentUser().getShortUserName());
conf.set("hadoop.http.filter.initializers",
"org.apache.hadoop.yarn.server.webproxy.amfilter.AmFilterInitializer");
conf.set(MRJobConfig.MAPREDUCE_JOB_DIR, submitJobDir.toString());
LOG.debug("Configuring job " + jobId + " with " + submitJobDir
+ " as the submit dir");
// get delegation token for the dir
TokenCache.obtainTokensForNamenodes(job.getCredentials(),
new Path[] { submitJobDir }, conf);
populateTokenCache(conf, job.getCredentials());
// generate a secret to authenticate shuffle transfers
if (TokenCache.getShuffleSecretKey(job.getCredentials()) == null) {
KeyGenerator keyGen;
try {
keyGen = KeyGenerator.getInstance(SHUFFLE_KEYGEN_ALGORITHM);
keyGen.init(SHUFFLE_KEY_LENGTH);
} catch (NoSuchAlgorithmException e) {
throw new IOException("Error generating shuffle secret key", e);
}
SecretKey shuffleKey = keyGen.generateKey();
TokenCache.setShuffleSecretKey(shuffleKey.getEncoded(),
job.getCredentials());
}
if (CryptoUtils.isEncryptedSpillEnabled(conf)) {
conf.setInt(MRJobConfig.MR_AM_MAX_ATTEMPTS, 1);
LOG.warn("Max job attempts set to 1 since encrypted intermediate" +
"data spill is enabled");
}
copyAndConfigureFiles(job, submitJobDir);
Path submitJobFile = JobSubmissionFiles.getJobConfPath(submitJobDir);
// 以上内容都是一些配置 信息 ,下面开始 重点的切片操作怕
/*
*
* 下面是数据的切片操作
*
*/
// Create the splits for the job
LOG.debug("Creating splits at " + jtFs.makeQualified(submitJobDir));
// 主要是查看 writeSplits()这个函数的处理内容.
int maps = writeSplits(job, submitJobDir);
conf.setInt(MRJobConfig.NUM_MAPS, maps);
LOG.info("number of splits:" + maps);
// write "queue admins of the queue to which job is being submitted"
// to job file.
String queue = conf.get(MRJobConfig.QUEUE_NAME,
JobConf.DEFAULT_QUEUE_NAME);
AccessControlList acl = submitClient.getQueueAdmins(queue);
conf.set(toFullPropertyName(queue,
QueueACL.ADMINISTER_JOBS.getAclName()), acl.getAclString());
// removing jobtoken referrals before copying the jobconf to HDFS
// as the tasks don't need this setting, actually they may break
// because of it if present as the referral will point to a
// different job.
TokenCache.cleanUpTokenReferral(conf);
if (conf.getBoolean(
MRJobConfig.JOB_TOKEN_TRACKING_IDS_ENABLED,
MRJobConfig.DEFAULT_JOB_TOKEN_TRACKING_IDS_ENABLED)) {
// Add HDFS tracking ids
ArrayList<String> trackingIds = new ArrayList<String>();
for (Token<? extends TokenIdentifier> t :
job.getCredentials().getAllTokens()) {
trackingIds.add(t.decodeIdentifier().getTrackingId());
}
conf.setStrings(MRJobConfig.JOB_TOKEN_TRACKING_IDS,
trackingIds.toArray(new String[trackingIds.size()]));
}
// Set reservation info if it exists
ReservationId reservationId = job.getReservationId();
if (reservationId != null) {
conf.set(MRJobConfig.RESERVATION_ID, reservationId.toString());
}
// Write job file to submit dir
writeConf(conf, submitJobFile);
//
// Now, actually submit the job (using the submit name)
//
printTokens(jobId, job.getCredentials());
status = submitClient.submitJob(
jobId, submitJobDir.toString(), job.getCredentials());
if (status != null) {
return status;
} else {
throw new IOException("Could not launch job");
}
} finally {
if (status == null) {
LOG.info("Cleaning up the staging area " + submitJobDir);
if (jtFs != null && submitJobDir != null)
jtFs.delete(submitJobDir, true);
}
}
}
查看 writeSplits()这个函数的处理内容. 在JobSubmitter.class类中.
//这个类是一个套娃操作,就是判断下使用新旧API 并进行了一个重定向.主要查看writeNewSplits()方法
private int writeSplits(org.apache.hadoop.mapreduce.JobContext job,
Path jobSubmitDir) throws IOException,
InterruptedException, ClassNotFoundException {
JobConf jConf = (JobConf)job.getConfiguration();
int maps;
if (jConf.getUseNewMapper()) {
maps = writeNewSplits(job, jobSubmitDir);
} else {
maps = writeOldSplits(jConf, jobSubmitDir);
}
return maps;
}
这个类是一个套娃操作,就是判断下使用新旧API 并进行了一个重定向.主要查看writeNewSplits()方法.
private <T extends InputSplit>
int writeNewSplits(JobContext job, Path jobSubmitDir) throws IOException,
InterruptedException, ClassNotFoundException {
//同样的,拿到一个配置文件
Configuration conf = job.getConfiguration();
//然后使用反射工具,将配置文件反射成类,得到input对象
InputFormat<?, ?> input =
ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
/*
*
* 下面单说一下,job.getXXXXX()方法 这是一个在 JobContext类中定义的一个抽象类 ,其具体内容如下
* public Class<? extends InputFormat<?,?>> getInputFormatClass() throws ClassNotFoundException;
* //Get the {@link Mapper} class for the job.
* //@return the {@link Mapper} class for the job.
* 这个方法在 JobContextImpl类中实现 ,内容如下
* public Class<? extends InputFormat<?,?>> getInputFormatClass() throws ClassNotFoundException {
* //都是相同的操作,首先看用户是否配置了,如果没有配置就使用默认的 TextInputFormat.class
* return (Class<? extends InputFormat<?,?>>) conf.getClass(INPUT_FORMAT_CLASS_ATTR, TextInputFormat.class);}
*
* job.getxx() 类 都是 在jobcontext中抽象,在jobcontextimpl中实现.
*
*/
//下面进入核心方法 也就是 input 的getSplits(job)方法
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;
}
这个切片方法 getSplits() 在抽象在InputFormat.class中 ,内容如下
public abstract
List<InputSplit> getSplits(JobContext context
) throws IOException, InterruptedException;
但是它的实例化对象在 FileInputFormat.class 类中 .
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = new StopWatch().start();//此处是一个计时器对象,用于记录程序执行了多久
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
// 设置最大切片长度和最小切片长度
/*
* protected long getFormatMinSplitSize() { return 1;}
*
* getMinSplitSize(job)是获用户设置的切片大小 ,默认为1
*
* public static long getMaxSplitSize(JobContext context) {
* 默认的最大值 是long 类型吧的最大值
* return context.getConfiguration().getLong(SPLIT_MAXSIZE, Long.MAX_VALUE); }
*
*/
// generate splits 下面开始生成切片 .
List<InputSplit> splits = new ArrayList<InputSplit>();//创建一个inputsplit类型的array
List<FileStatus> files = listStatus(job);//listStatus 是获得job 的输入目录下的一级文件的清单
for (FileStatus file: files) {//下面开始对每一个文件进行操作.
//获得文件的地址和长度
Path path = file.getPath();
long length = file.getLen();
if (length != 0) {//如果文件大小不为0 ,对文件进行操作
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) { //使用instanceof 判断一个类是不是另一个类的实现类
blkLocations = ((LocatedFileStatus) file).getBlockLocations();//如果是本地文件 则blocklocation 的数组 使用本地类的方式获得
} else {// 否则使用分布式文件系统的方式获得 .分区信息
FileSystem fs = path.getFileSystem(job.getConfiguration());//使用job 的配置文件获得FileSystem对象
blkLocations = fs.getFileBlockLocations(file, 0, length);//获得该文件的分区信息
}
if (isSplitable(job, path)) {//判断下该文件是否可以分片, 如果可分执行如下代码 .
long blockSize = file.getBlockSize();//得到文件的block 块的大小 ,可能是默认的128M 也可能使用户自己设置的
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
// 知识点又来了 ,如何计算分片的大小呢 ,下面我们看一下
/*
* protected long computeSplitSize(long blockSize, long minSize,long maxSize) {
* return Math.max(minSize, Math.min(maxSize, blockSize));
}
* 默认情况下 minsize 是1 maxsize 是一个超级大的数9223372036854775807 ,假设这里的blocksize 是128M
* 那么 min(maxsize,blocksize) 就是 blocksize 并且 max(minsize ,blocksize) 就是 blocksize
* 这就是所说的默认情况下 ,切片大小等于块大小 .
*
* 那么如何调整切片的大小呢-----(调大splitsize ,改大minsize) (调小splitsize ,改小 maxsize)
*
*/
long bytesRemaining = length;//定义字节剩余长度变量,用于记录当前还有多少字节没有被分片.
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
// 循环的退出条件是 bytesRemaining /splitSize > 1.1 注意这里SPLIT_SLOP 是静态值 1.1
// 也就是说 当剩余的字节不足一个分片时 退出循环
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
/*
*
*
*叮咚 知识点又来了 ,如何找到blocklocation的索引呢 ? 还是去看看get函中怎么定义的吧
*
* protected int getBlockIndex(BlockLocation[] blkLocations, long offset) {
* for (int i = 0 ; i < blkLocations.length; i++) {//遍历整个bolck块
* // is the offset inside this block?
* // 如果offset 正好在一个分区内 ,则返回这个分区的索引,getoffset()得到一个块的起始位置,而 getoffset+length 整好是一个块的结束位置.
* if ((blkLocations[i].getOffset() <= offset) &&
* (offset < blkLocations[i].getOffset() + blkLocations[i].getLength())){
* return i;
* }
* }
* // 如果for 循环结束,表示offset 已经越界了 ,下面我们求一下 最后一个块的结束位置.
* BlockLocation last = blkLocations[blkLocations.length -1];
* long fileLength = last.getOffset() + last.getLength() -1;//得到最后一个块的位置.并抛出数组越界异常.
* throw new IllegalArgumentException("Offset " + offset + " is outside of file (0.." +
* fileLength + ")");
* }
*
*
*
*/
// makeSplit( 文件的路径,文件的开始位置,和结束位置,block块所在的主机,block块已经缓存的主机)
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
bytesRemaining -= splitSize;//将剩余文件,减去已经分片的字节数据
}
if (bytesRemaining != 0) {//判断 剩余自己是否为0?
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
//同样操作 ,将剩余的文件字节加入到一个分片中
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
}
} else { // not splitable 如果文件不可切片,则将整个文件 加入到切片中
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
blkLocations[0].getCachedHosts()));
}
} else { //length的长度为零说明 这是一个空文件 ,则添加一条空字符记录到切片
//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());
sw.stop();//计时器结束 在LOG中打印执行的时间
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size()
+ ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
}
return splits;
}
到这里分片就结束了,返回到writeNewSplits()方法,将spilts 转化为array 并返回array的length .
也就是说返回了分片的个数.也就是map 的数量 代码如下
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;
返回到submitJobInternal方法 将map的数量加入配置,启动相应的MapTask
int maps = writeSplits(job, submitJobDir);
conf.setInt(MRJobConfig.NUM_MAPS, maps);
LOG.info("number of splits:" + maps);
其调用关系图,如下图所示:
[tupian.jpg]
MapTask 类解析
打开框架先找run()方法,找了run 在看try()。
private <INKEY, INVALUE, OUTKEY, OUTVALUE> void runNewMapper(JobConf job, TaskSplitIndex splitIndex, TaskUmbilicalProtocol umbilical, TaskReporter reporter) throws IOException, ClassNotFoundException, InterruptedException {
TaskAttemptContext taskContext = new TaskAttemptContextImpl(job, this.getTaskID(), reporter);
Mapper<INKEY, INVALUE, OUTKEY, OUTVALUE> mapper = (Mapper)ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
//拿到mapper类 ,如果用户定义了,使用用户定义的,否则使用默认的Mapper类.
InputFormat<INKEY, INVALUE> inputFormat = (InputFormat)ReflectionUtils.newInstance(taskContext.getInputFormatClass(), job);
org.apache.hadoop.mapreduce.InputSplit split = null;
split = (org.apache.hadoop.mapreduce.InputSplit)this.getSplitDetails(new Path(splitIndex.getSplitLocation()), splitIndex.getStartOffset());
LOG.info("Processing split: " + split);
org.apache.hadoop.mapreduce.RecordReader<INKEY, INVALUE> input = new MapTask.NewTrackingRecordReader(split, inputFormat, reporter, taskContext);
job.setBoolean("mapreduce.job.skiprecords", this.isSkipping());
RecordWriter output = null;
if (job.getNumReduceTasks() == 0) {
output = new MapTask.NewDirectOutputCollector(taskContext, job, umbilical, reporter);
} else {
output = new MapTask.NewOutputCollector(taskContext, job, umbilical, reporter);
}
MapContext<INKEY, INVALUE, OUTKEY, OUTVALUE> mapContext = new MapContextImpl(job, this.getTaskID(), input, (RecordWriter)output, this.committer, reporter, split);
org.apache.hadoop.mapreduce.Mapper.Context mapperContext = (new WrappedMapper()).getMapContext(mapContext);
/*
* 重头戏来了
*
*
*/
try {
input.initialize(split, mapperContext);
//根据initialize()可知这是一个初始化
/*
*定义在RecodeReader.class 中的一个抽象方法。
* public abstract void initialize(InputSplit split TaskAttemptContext context) throws IOException, InterruptedException;
* 在LineRecodeReader.class类中实现 。
*/
mapper.run(mapperContext);
//这是 Mapper 类中的run方法,我们在下面的代码分析中,进行了Mapper类的分析.
this.mapPhase.complete();//将map阶段设置未完成 .
this.setPhase(Phase.SORT);//设置排序阶段开始
this.statusUpdate(umbilical);//更新排序阶段
input.close();
input = null;
((RecordWriter)output).close(mapperContext);
output = null;
} finally {
this.closeQuietly((org.apache.hadoop.mapreduce.RecordReader)input);
this.closeQuietly((RecordWriter)output, mapperContext);
}
}
initialize()方法,在LineRecordReader.class 类中实现
public void initialize(InputSplit genericSplit,
TaskAttemptContext context) throws IOException {
FileSplit split = (FileSplit) genericSplit;//拿到一个切片文件对象
Configuration job = context.getConfiguration();// 通过context 获得配置文件。
this.maxLineLength = job.getInt(MAX_LINE_LENGTH, Integer.MAX_VALUE);
// 获得最大的行长度,用于判断一个文件是不是一行。★
start = split.getStart();
end = start + split.getLength();
// 获得切片的开始位置和结束位置。
final Path file = split.getPath();//拿到切片的路径
// open the file and seek to the start of the split
final FileSystem fs = file.getFileSystem(job);
fileIn = fs.open(file);//并在分布式文件系统中,拿到这个文件的内容。
CompressionCodec codec = new CompressionCodecFactory(job).getCodec(file);
//当在读取一个压缩文件的时候,可能并不知道压缩文件用的是哪种压缩算法,那么无法完成解压任务。在Hadoop中,CompressionCodecFactory通过使用其getCodec()方法,可以通过文件扩展名映射到一个与其对应的CompressionCodec类
if (null!=codec) {//返回的类不为空 ,则表明读取的是一个压缩文件
isCompressedInput = true; //是一个压缩输入流
decompressor = CodecPool.getDecompressor(codec);//拿到一个解压器文件
if (codec instanceof SplittableCompressionCodec) {// 如果得到的是一个可分片的压缩文件 ,执行以下操作
/*以下是读取压缩文件流 的方法 现在先不做具体讨论 */
final SplitCompressionInputStream cIn =
((SplittableCompressionCodec)codec).createInputStream(
fileIn, decompressor, start, end,
SplittableCompressionCodec.READ_MODE.BYBLOCK);
in = new CompressedSplitLineReader(cIn, job,
this.recordDelimiterBytes);
start = cIn.getAdjustedStart();
end = cIn.getAdjustedEnd();
filePosition = cIn;
} else { // 如果是一个不可分片的压缩数据文件 ,
in = new SplitLineReader(codec.createInputStream(fileIn,
decompressor), job, this.recordDelimiterBytes);
filePosition = fileIn;
}
} else {//如果不是一个压缩文件 ,直接进行寻址读取。
fileIn.seek(start);//将文件指针 移动到数据开始位置
//创建一个未压缩的切片,行读取器
in = new UncompressedSplitLineReader(
fileIn, job, this.recordDelimiterBytes, split.getLength());
filePosition = fileIn;
}
// 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 += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
this.pos = start;//将当前位置的定义到开始位置。
}
Mapper.class 类的分析
打开类先看run()方法
public void run(Context context) throws IOException, InterruptedException {
setup(context);//这是一个初始化的函数
try {
while (context.nextKeyValue()) {
// 我们只要关心map函数就可以了
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
cleanup(context);//这是一个结束清理的函数
}
}
........
// mappr中的原始函数
protected void map(KEYIN key, VALUEIN value,
Context context) throws IOException, InterruptedException {
context.write((KEYOUT) key, (VALUEOUT) value);
}
// my mapper 中重写的map方法
// 创建一个intWritable 对象,并将其初始化为1 ,用于记录单词出现的次数
private final static IntWritable one = new IntWritable(1);
// 创建一个Text 对象用于记录单词的内容 ,所以我们Map输出的键值对是 <Text ,IntWritable >
private Text word = new Text();
// 实现父类的方法
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
// 使用 StringTokenizer 进行分词操作,返回一个迭代器对象
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens()) {// 当迭代器不为空时,继续循环
word.set(itr.nextToken());//将单词放入到word 实际上是放入一个数组中。
context.write(word, one);
map中的两个参数分别时通过context.getCurrentKey()
和context.getCurrentValue()
得到的,但是这个两个方法主要受 context.nextKeyValue()
方法控制,我们依次查看下这三个方法。
context.nextKeyValue() 方法
抽象在TaskInputOutputContext.class
中 ,实现在
//TaskInputOutputContext.class
public boolean nextKeyValue() throws IOException, InterruptedException;
// MapContexti=Impl.class
public boolean nextKeyValue() throws IOException, InterruptedException {
return reader.nextKeyValue();
}
而reader.nextKeyValue()
方法抽象在 RecordReader
中 实现在LineRecordReader.class
中
public boolean nextKeyValue() throws IOException {
if (key == null) {//首次调用 key==null 创建一个长整形数 用来存放key的值
key = new LongWritable();
}
key.set(pos);
/*
* 设置key的值,其中pos 在initialize()方法中已经初始化为
* this.pos = start ;
* 首次 start = 0 ;
*
*/
if (value == null) { // 首次 value == null 所以会创建一个空的文本对象
value = new Text();
}
int newSize = 0;//我们总是多向后读一行 从而保证数据的完整性。现在还不知道new size 的意思
// We always read one extra line, which lies outside the upper
// split limit i.e. (end - 1)
while (getFilePosition() <= end || in.needAdditionalRecordAfterSplit()) {
/*
* getFilePosition() 拿到pos 的当前位置 ,对压缩文件 和 非压缩文件有两种不同的策略
*
* needAdditionalRecordAfterSplit() 默认值返回false ,判断是不是有多余的数据要读取
*
*
*/
if (pos == 0) {// 如果是首次调用 则 pos == 0 ,这时候newsize = 跳过utf前置符号的位置
// utf-8编码的TXT文件在第一行会自动添加一个编码标识符
newSize = skipUtfByteOrderMark();//跳过 Utf 字节顺序标记 ,在附录的时候做具体分析
} else {// 不是首次调用 就没有多余字符的风险了 ,读取一行 ,并返回 行的长度 。
newSize = in.readLine(value, maxLineLength, maxBytesToConsume(pos));//maxBytesToConsume()也颇为复杂 ,附录中解释
pos += newSize;
/*
* LineRecorder.class
* public int readLine(Text str, int maxLineLength, int maxBytesToConsume) throws IOException {
* if (this.recordDelimiterBytes != null) {
* return readCustomLine(str, maxLineLength, maxBytesToConsume);
* } else {
* return readDefaultLine(str, maxLineLength, maxBytesToConsume);
* }
* }
*
*/
}
if ((newSize == 0) || (newSize < maxLineLength)) {// 如果读到了空文件 或者 已经读完了所有文件,直接跳出循环
break;
}
// line too long. try again
LOG.info("Skipped line of size " + newSize + " at pos " +
(pos - newSize));
}
if (newSize == 0) {// 如果 newsize == 0 ,表示读取的是空文件 读取失败 重置 key value ,否则 返回true 此时这个切片的数据存储在 value 中
key = null;
value = null;
return false;
} else {
/*
* key是数据的起始地址
* value 存放的是数据的内容 ,在这里将这两个数据设置好,然后 通过两个get方法读取。
*
*/
return true;
}
}
在map()处理完的数据会调用 context.write(key,values) 函数将数据写入到缓存 。
点开该方法会发现,他是一个接口;
// TaskInputOutputContext.class
public void write(KEYOUT key, VALUEOUT value)
throws IOException, InterruptedException;
我们找一下他的子类实现,在这里他又调用了output对象的write方法
// TaskInputOutputContextimpl.class
public void write(KEYOUT key, VALUEOUT value
) throws IOException, InterruptedException {
output.write(key, value);
}
我们继续打开看一下,又是一个抽象方法,下面继续点开
//Recordwriter.class
public abstract void write(K key, V value
) throws IOException, InterruptedException;
// 在MapTask类中,是NewOutputCollector 类中的方法。也就是将数据选择一个分区 partions 进行存入,下面开始看看output 功能。
@Override
public void write(K key, V value) throws IOException, InterruptedException {
collector.collect(key, value,
partitioner.getPartition(key, value, partitions));
}
附录
关于needAdditionalRecordAfterSplit()
方法的解析
//看名字就知道 ,填充缓冲区
protected int fillBuffer(InputStream in, byte[] buffer, boolean inDelimiter)throws IOException {
int maxBytesToRead = buffer.length;//获得完整缓冲区长度。
if (totalBytesRead < splitLength) {
long leftBytesForSplit = splitLength - totalBytesRead;
// check if leftBytesForSplit exceed Integer.MAX_VALUE
if (leftBytesForSplit <= Integer.MAX_VALUE) {
maxBytesToRead = Math.min(maxBytesToRead, (int)leftBytesForSplit);
}
}
int bytesRead = in.read(buffer, 0, maxBytesToRead);
// If the split ended in the middle of a record delimiter then we need
// to read one additional record, as the consumer of the next split will
// not recognize the partial delimiter as a record.
// However if using the default delimiter and the next character is a
// linefeed then next split will treat it as a delimiter all by itself
// and the additional record read should not be performed.
if (totalBytesRead == splitLength && inDelimiter && bytesRead > 0) {
if (usingCRLF) {//判断是否使用默认的换行符分割 ; usingCRLF 表示 使用换行符 分割 。
needAdditionalRecord = (buffer[0] != '\n');
} else {
needAdditionalRecord = true;
}
}
if (bytesRead > 0) {
totalBytesRead += bytesRead;
}
return bytesRead;
}