Hadoop2 MapReduce框架原理深度解析:从输入到输出的完整指南

在人工智能和大数据时代,MapReduce作为分布式计算的核心框架,其原理深度影响AI系统的数据处理效率。本文将系统解析Hadoop2 MapReduce框架的完整工作流程,从InputFormat到OutputFormat,涵盖shuffle机制、分区、排序等关键环节,帮助开发者构建高性能的数据处理管道。

1. MapReduce工作流程与核心机制

MapReduce框架将复杂的数据处理任务分解为Map和Reduce两个阶段。整个工作流程从数据输入开始,经过切片、Map任务、shuffle、Reduce任务,最终输出结果。Shuffle过程是框架的核心,它负责将Map输出按key排序并传输给Reduce任务。

Shuffle过程从Map任务的输出开始,经过分区、排序、溢写、合并等步骤,最终将数据传递给Reduce任务。缓冲区大小(默认100MB)直接影响磁盘I/O次数和执行效率,可通过io.sort.mb参数调整。

1)MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中

2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件

3)多个溢出文件会被合并成大的溢出文件

4)在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序

5)ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据

6)ReduceTask会取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)

7)合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)

2. InputFormat数据输入机制

InputFormat负责将输入数据切分为独立的记录,供Map任务处理。常见的实现类包括:

  • TextInputFormat:默认实现,每行作为一条记录,key为字节偏移量
  • KeyValueTextInputFormat:按分隔符(默认Tab)将每行拆分为key-value
  • NLineInputFormat:固定行数切片,适合需要均匀分配数据量的场景
  • CombineTextInputFormat:合并小文件,优化小文件处理效率

2.1 TextInputFormat详解

TextInputFormat是默认的InputFormat,每条记录对应一行文本。键是LongWritable类型,存储该行在整个文件中的字节偏移量;值是Text类型,包含该行内容(不含换行符)。

例如:一个分片包含了如下4条文本记录。

Rich learning form

Intelligent learning engine

Learning more convenient

From the real demand for more close to the enterprise

每条记录表示为以下键/值对:

(0,Rich learning form)

(19,Intelligent learning engine)

(47,Learning more convenient)

(72,From the real demand for more close to the enterprise)

        很明显,键并不是行号。一般情况下,很难取得行号,因为文件按字节而不是按行切分为分片。

2.2 KeyValueTextInputFormat案例

统计输入文件中每一行第一个单词相同的行数:

  1. 编写Mapper类:
    public class KVTextMapper extends Mapper {
        // 设置value
        LongWritable v = new LongWritable(1);
        @Override
        protected void map(Text key, Text value, Context context) throws IOException, InterruptedException {
            context.write(key, v);
        }
    }
  2. 编写Reducer类:
    public class KVTextReducer extends Reducer {
        LongWritable v = new LongWritable();
        @Override
        protected void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException {
            long sum = 0l;
            // 汇总统计
            for (LongWritable value : values) {
                sum += value.get();
            }
            v.set(sum);
            // 输出
            context.write(key, v);
        }
    }
  3. 编写Driver类:
    public class KVTextDriver {
        public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
            Configuration configuration = new Configuration();
            // 设置切割符号
            configuration.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, " ");
            // 获取job对象
            Job job = Job.getInstance(configuration);
            // 设置jar包位置,关联mapper和reducer
            job.setJarByClass(KVTextDriver.class);
            job.setMapperClass(KVTextMapper.class);
            job.setReducerClass(KVTextReducer.class);
            // 设置map输出kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(LongWritable.class);
            // 设置最终输出kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(LongWritable.class);
            // 设置输入格式
            job.setInputFormatClass(KeyValueTextInputFormat.class);
            // 设置输入输出数据路径
            FileInputFormat.setInputPaths(job, new Path("d:/input"));
            FileOutputFormat.setOutputPath(job, new Path("d:/mr"));
            boolean b = job.waitForCompletion(true);
        }
    }
  4. 结果输出: →

2.3 NLineInputFormat案例

按固定行数(例如3行)切片,实现单词统计:

  • Mapper类:
    public class NLineMapper extends Mapper {
        private Text k = new Text();
        private LongWritable v = new LongWritable(1);
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            // 获取一行
            String line = value.toString();
            // 切割
            String[] split = line.split(" ");
            // 循环写出
            for (String s : split) {
                k.set(s);
                context.write(k, v);
            }
        }
    }
  • Reducer类:
    public class NLineReducer extends Reducer {
        LongWritable v = new LongWritable();
        @Override
        protected void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException {
            long sum = 0L;
            for (LongWritable value : values) {
                sum += value.get();
            }
            v.set(sum);
            context.write(key, v);
        }
    }
  • Driver类:
    public class NLineDriver {
        public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
            // 获取job对象
            Configuration configuration = new Configuration();
            Job job = Job.getInstance(configuration);
            // 设置每个切片InputSplit中划分三条记录
            NLineInputFormat.setNumLinesPerSplit(job,3);
            // 使用NLineInputFormat处理记录数
            job.setInputFormatClass(NLineInputFormat.class);
            // 设置jar包位置,关联mapper和reducer
            job.setJarByClass(NLineDriver.class);
            job.setMapperClass(NLineMapper.class);
            job.setReducerClass(NLineReducer.class);
            // 设置map输出kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(LongWritable.class);
            // 设置最终输出kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(LongWritable.class);
            // 设置输入输出数据路径
            FileInputFormat.setInputPaths(job, new Path("d:/input"));
            FileOutputFormat.setOutputPath(job, new Path("d:/mr"));
            boolean b = job.waitForCompletion(true);
        }
    }
  • 测试结果:

2.4 自定义InputFormat与小文件合并

处理大量小文件时,自定义InputFormat可将多个小文件合并为SequenceFile格式,提升处理效率。实现步骤:

  1. 继承FileInputFormat:
    public class WholeFileInputFormat extends FileInputFormat {
        @Override
        protected boolean isSplitable(JobContext context, Path filename) {
            return false;
        }
        @Override
        public RecordReader createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext)
                throws IOException, InterruptedException {
            // 自定义一个recordReader
            WholeRecordReader recordReader = new WholeRecordReader();
            // 初始化recordReader
            recordReader.initialize(inputSplit, taskAttemptContext);
            return recordReader;
        }
    }
  2. 改写RecordReader:
    public class WholeRecordReader extends RecordReader {
        private Configuration configuration;
        private FileSplit split;
        private boolean isProgress = true;
        private BytesWritable v = new BytesWritable();
        private Text k = new Text();
        @Override
        public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
            this.split = (FileSplit) inputSplit;
            configuration = taskAttemptContext.getConfiguration();
        }
        @Override
        public boolean nextKeyValue() throws IOException, InterruptedException {
            if (isProgress) {
                // 定义缓存区
                byte[] contents = new byte[(int) split.getLength()];
                FileSystem fs = null;
                FSDataInputStream fis = null;
                try {
                    // 获取文件系统
                    Path path = split.getPath();
                    fs = path.getFileSystem(configuration);
                    // 读取数据
                    fis = fs.open(path);
                    // 读取文件内容
                    IOUtils.readFully(fis, contents, 0, contents.length);
                    // 输出文件内容
                    v.set(contents, 0, contents.length);
                    // 获取文件路径及名称
                    String name = split.getPath().toString();
                    k.set(name);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                } finally {
                    IOUtils.closeStream(fis);
                }
                isProgress = false;
                return true;
            }
            return false;
        }
        @Override
        public Text getCurrentKey() throws IOException, InterruptedException {
            return k;
        }
        @Override
        public BytesWritable getCurrentValue() throws IOException, InterruptedException {
            return v;
        }
        @Override
        public float getProgress() throws IOException, InterruptedException {
            return 0;
        }
        @Override
        public void close() throws IOException {
        }
    }
  3. Mapper类:
    public class SequenceFileMapper extends Mapper {
        @Override
        protected void map(Text key, BytesWritable value, Context context) throws IOException, InterruptedException {
            context.write(key, value);
        }
    }
  4. Reducer类:
    public class SequenceFileReducer extends Reducer {
        @Override
        protected void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException {
            context.write(key, values.iterator().next());
        }
    }
  5. Driver类:
    public class SequenceFileDriver {
        public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
            // 获取job对象
            Configuration configuration = new Configuration();
            Job job = Job.getInstance(configuration);
            // 设置jar包存储位置、关联自定义的mapper和reducer
            job.setJarByClass(SequenceFileDriver.class);
            job.setMapperClass(SequenceFileMapper.class);
            job.setReducerClass(SequenceFileReducer.class);
            // 设置输入的inputFormat
            job.setInputFormatClass(WholeFileInputFormat.class);
            // 设置输出的outputFormat
            job.setOutputFormatClass(SequenceFileOutputFormat.class);
            // 设置map输出端的kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(BytesWritable.class);
            // 设置最终输出端的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(BytesWritable.class);
            // 设置输入输出路径
            FileInputFormat.setInputPaths(job, new Path("d:/input"));
            FileOutputFormat.setOutputPath(job, new Path("d:/mr"));
            // 提交job
            job.waitForCompletion(true);
        }
    }

3. FileInputFormat切片机制与配置

FileInputFormat的切片机制直接决定MapTask的并行度。切片大小默认等于HDFS块大小(128MB),计算公式为:Math.max(minSize, Math.min(maxSize, blockSize))。

waitForCompletion()
submit();
// 1建立连接
connect();
// 1)创建提交job的代理
new Cluster(getConfiguration());
// (1)判断是本地yarn还是远程
initialize(jobTrackAddr, conf);
// 2 提交job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取jobid ,并创建job路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝jar包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向Stag路径写xml配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());

1、获取文件大小fs.sizeOf(hello.txt);

2、计算切片大小

computeSliteSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M

默认情况下,切片大小=blocksize

3、开始切,形成第1个切片:hello.txt—0:128M 第2个切片hello.txt—128:256M 第3个切片hello.txt—256M:300M(每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片)

4、将切片信息写到一个切片规划文件中

5、整个切片的核心过程在getSplit()方法中完成。

6、数据切片只是在逻辑上对输入数据进行分片,并不会再磁盘上将其切分成分片进行存储。InputSplit只记录了分片的元数据信息,比如起始位置、长度以及所在的节点列表等。

7、注意:block是HDFS上物理上存储的存储的数据,切片是对数据逻辑上的划分。

切片时不考虑数据集整体,而是逐个文件独立切片。可通过mapreduce.input.fileinputformat.split.minsize和maxsize参数调整切片大小。

比如有两个待处理的数据文件:
file1.txt 320M
file2.txt 10M


经过FileInputFormat的切片机制运算后,形成的切片信息如下: 
file1.txt.split1-- 0~128
file1.txt.split2-- 128~256
file1.txt.split3-- 256~320
file2.txt.split1-- 0~10M

 切片主要由这几个值来运算决定:
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
因此,默认情况下,切片大小=blocksize。
maxsize(切片最大值):参数如果调得比blocksize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blocksize还大。

// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
// 获取切片的文件名称
String name = inputSplit.getPath().getName();

4. CombineTextInputFormat:小文件优化利器

CombineTextInputFormat通过虚拟存储和切片过程,将多个小文件合并为一个逻辑切片,显著减少MapTask数量。切片机制包括:

  1. 虚拟存储过程:文件大小与setMaxInputSplitSize比较,按规则划分虚拟块
  2. 切片过程:虚拟块大小超过阈值则独立切片,否则合并

CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);        // 4m

注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。

CombineTextInputFormat.setMinInputSplitSize(job, 2097152);        // 2m

举例:

0.5m+1m+0.3m+5m=2m + 4.8m=2m + 4m + 0.8m

例如:setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。

举例:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,则虚拟存储之后形成6个文件块,大小分别为:

1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M)

最终会形成3个切片,大小分别为:

(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M

案例:4个小文件,默认产生4个切片;设置setMaxInputSplitSize(20M)后合并为2个切片;设置为10M时合并为1个切片。

// 如果不设置InputFormat,它默认用的是TextInputFormat.class job.setInputFormatClass(CombineTextInputFormat.class);

//虚拟存储切片最大值设置2k 

CombineTextInputFormat.setMaxInputSplitSize(job, 2048);

    public static void main(String[] args) throws Exception {
        // 获取配置信息
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);
        // 设置jar的加载路径
        job.setJarByClass(WordCountDriver.class);
        // 如果不设置InputFormat,它默认用的是TextInputFormat.class
        job.setInputFormatClass(CombineTextInputFormat.class);
        //虚拟存储切片最大值设置2k
        CombineTextInputFormat.setMaxInputSplitSize(job, 2048);
        // 设置map类和reduce类
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);
        // 设置map输出类
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);
        // 设置输出KV类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);
        // 设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path("d:/input"));
        FileOutputFormat.setOutputPath(job, new Path("d:/mr"));
        // 提交
        boolean b = job.waitForCompletion(true);
    }

// 如果不设置InputFormat,它默认用的是TextInputFormat.class job.setInputFormatClass(CombineTextInputFormat.class);

//虚拟存储切片最大值设置4m

CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);

5. MapTask与Shuffle核心机制

MapTask的并行度决定作业处理速度。切片是逻辑概念,Block是物理存储单位。MapTask工作流程包括:

  • Read阶段:从InputSplit解析key/value
  • Map阶段:调用map()函数处理数据
  • Collect阶段:分区后写入环形缓冲区
  • Spill阶段:缓冲区满时排序并溢写到磁盘
  • Combine阶段:合并临时文件

溢写阶段详情:

        步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。

        步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。

        步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。

        当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。

        在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。

        让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

5.1 Shuffle与分区机制

Shuffle是Map方法之后、Reduce方法之前的数据处理过程,确保每个Reducer的输入按键排序。默认分区基于key的hashCode对reduceTasks取模。

        如果要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)

public class HashPartitioner extends Partitioner {
    /** Use {@link Object#hashCode()} to partition. */
    public int getPartition(K key, V value, int numReduceTasks) {
        return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
    }
}

自定义Partitioner步骤:

  1. 继承Partitioner,重写getPartition():
    public class ProvincePartitioner extends Partitioner {
        @Override
        public int getPartition(Text text, MyFlowBean myFlowBean, int i) {
            // 1 获取电话号码的前三位
            String s = text.toString().substring(0, 3);
            int partition = 4;
            // 2 判断是哪个省
            switch (s) {
                case "136":
                    partition = 0;
                    break;
                case "137":
                    partition = 1;
                    break;
                case "138":
                    partition = 2;
                    break;
                case "139":
                    partition = 3;
                    break;
            }
            return partition;
        }
    }
  2. 设置自定义Partitioner和ReduceTask数量:
    public class FlowSumDriver {
        public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
            // 配置信息
            Configuration configuration = new Configuration();
            Job job = Job.getInstance(configuration);
            // 指定jar包所在的本地路径
            job.setJarByClass(FlowSumDriver.class);
            // job使用的mapper/Reducer业务类
            job.setMapperClass(FlowCountMapper.class);
            job.setReducerClass(FlowCountReducer.class);
            // 指定mapper输出数据的kv类型
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(MyFlowBean.class);
            // 最终输出的数据的kv类型
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(MyFlowBean.class);
            // =========================增加==========================
            // 指定自定义数据分区
            job.setPartitionerClass(ProvincePartitioner.class);
            // 指定相应数量的reduce task
            job.setNumReduceTasks(5);
            //=======================================================
            // job的输入原始文件所在目录
            FileInputFormat.setInputPaths(job, new Path("D:/input"));
            FileOutputFormat.setOutputPath(job, new Path("d:/mr"));
            // 将job中配置的相关参数,以及job所用的java类所在的jar包, 提交给yarn去运行
            boolean b = job.waitForCompletion(true);
        }
    }
  3. 结果输出: →

job.setPartitionerClass(CustomPartitioner.class);

job.setNumReduceTasks(5);

例如:假设自定义分区数为5,则

1)job.setNumReduceTasks(1); 会正常运行,只不过会产生一个输出文件

2)job.setNumReduceTasks(2);会报错

3)job.setNumReduceTasks(6);大于5,程序会正常运行,会产生空文件

在 【第三节 序列化案例的基础上进行修改】

        将统计结果按照手机归属地不同省份输出到不同文件中(分区),手机号136、137、138、139开头都分别放到一个独立的4个文件中,其他开头的放到一个文件中。

5.2 WritableComparable排序与分组

排序是MapReduce的默认行为,分为:

  • 部分排序:每个文件内部有序
  • 全排序:通过分区实现全局有序
  • 辅助排序:在Reduce阶段对值排序
  • 二次排序:多个排序条件

自定义排序实现WritableComparable接口:

@Override
public int compareTo(FlowBean o) {
    // 倒序排列,从大到小
    return this.sumFlow > o.getSumFlow() ? -1 : 1;
}

全排序案例:

public class MyFlowBean implements WritableComparable {
    // 上行流量
    private long upFlow;
    // 下行流量
    private long downFlow;
    // 总流量
    private long sumFlow;
    // 反序列化时,需要反射调用空参构造函数,所以必须有
    public MyFlowBean() {
        super();
    }
    public MyFlowBean(long upFlow, long downFlow) {
        super();
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }
    // 重写序列化方法
    @Override
    public void write(DataOutput dataOutput) throws IOException {
        dataOutput.writeLong(upFlow);
        dataOutput.writeLong(downFlow);
        dataOutput.writeLong(sumFlow);
    }
    // 重写反序列化方法 注意反序列化的顺序和序列化的顺序完全一致
    @Override
    public void readFields(DataInput dataInput) throws IOException {
        this.upFlow = dataInput.readLong();
        this.downFlow = dataInput.readLong();
        this.sumFlow = dataInput.readLong();
    }
    // 要想把结果显示在文件中,需要重写toString(),且用”\t”分开,方便后续用
    @Override
    public String toString() {
        return upFlow + "\t" + downFlow + "\t" + sumFlow;
    }
    @Override
    public int compareTo(MyFlowBean o) {
        // 按照总流量大小,倒序排列
        if (sumFlow > o.getSumFlow()) {
            return -1;
        } else if (sumFlow < o.getSumFlow()) {
            return 1;
        } else {
            return 0;
        }
    }
    public void sumFlow(long upFlow, long downFlow) {
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }
    public long getUpFlow() {
        return upFlow;
    }
    public void setUpFlow(long upFlow) {
        this.upFlow = upFlow;
    }
    public long getDownFlow() {
        return downFlow;
    }
    public void setDownFlow(long downFlow) {
        this.downFlow = downFlow;
    }
    public long getSumFlow() {
        return sumFlow;
    }
    public void setSumFlow(long sumFlow) {
        this.sumFlow = sumFlow;
    }
}
public class FlowCountMapper extends Mapper {
    MyFlowBean k = new MyFlowBean();
    Text v = new Text();
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // 获取行数据
        String line = value.toString();
        // 切割字符串
        String[] fields = line.split("\t");
        // 封装
        // 手机号
        String phone = fields[1];
        // 流量
        long upFlow = Long.parseLong(fields[fields.length - 3]);
        long downFlow = Long.parseLong(fields[fields.length - 2]);
        v.set(phone);
        k.sumFlow(upFlow, downFlow);
        // 输出
        context.write(k, v);
    }
}
public class FlowCountReducer extends Reducer {
    @Override
    protected void reduce(MyFlowBean key, Iterable values, Context context) throws IOException, InterruptedException {
        // 循环输出,避免总流量相同情况
        for (Text value : values) {
            context.write(value, key);
        }
    }
}
public class FlowSumDriver {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        // 配置信息
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);
        // 指定jar包所在的本地路径
        job.setJarByClass(FlowSumDriver.class);
        // job使用的mapper/Reducer业务类
        job.setMapperClass(FlowCountMapper.class);
        job.setReducerClass(FlowCountReducer.class);
        // 指定mapper输出数据的kv类型
        job.setMapOutputKeyClass(MyFlowBean.class);
        job.setMapOutputValueClass(Text.class);
        // 最终输出的数据的kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(MyFlowBean.class);
        // job的输入原始文件所在目录
        FileInputFormat.setInputPaths(job, new Path("D:/input"));
        FileOutputFormat.setOutputPath(job, new Path("d:/mr"));
        // 将job中配置的相关参数,以及job所用的java类所在的jar包, 提交给yarn去运行
        boolean b = job.waitForCompletion(true);
    }
}
→

区内排序案例:

public class ProvincePartitioner extends Partitioner {
    @Override
    public int getPartition(MyFlowBean myFlowBean, Text text, int i) {
        // 1 获取电话号码的前三位
        String s = text.toString().substring(0, 3);
        int partition = 4;
        // 2 判断是哪个省
        switch (s) {
            case "136":
                partition = 0;
                break;
            case "137":
                partition = 1;
                break;
            case "138":
                partition = 2;
                break;
            case "139":
                partition = 3;
                break;
        }
        return partition;
    }
}
public class FlowSumDriver {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        // 配置信息
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);
        // 指定jar包所在的本地路径
        job.setJarByClass(FlowSumDriver.class);
        // job使用的mapper/Reducer业务类
        job.setMapperClass(FlowCountMapper.class);
        job.setReducerClass(FlowCountReducer.class);
        // =========================增加==========================
        // 指定自定义数据分区
        job.setPartitionerClass(ProvincePartitioner.class);
        // 指定相应数量的reduce task
        job.setNumReduceTasks(5);
        //=======================================================
        // 指定mapper输出数据的kv类型
        job.setMapOutputKeyClass(MyFlowBean.class);
        job.setMapOutputValueClass(Text.class);
        // 最终输出的数据的kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(MyFlowBean.class);
        // job的输入原始文件所在目录
        FileInputFormat.setInputPaths(job, new Path("D:/input"));
        FileOutputFormat.setOutputPath(job, new Path("d:/mr"));
        // 将job中配置的相关参数,以及job所用的java类所在的jar包, 提交给yarn去运行
        boolean b = job.waitForCompletion(true);
    }
}
→

如何用Hadoop产生一个全局排序的文件?最简单的方法是使用一个分区。但该方法在处理大型文件时效率极低,因为一台机器必须处理所有输出文件,从而完全丧失了MapReduce所提供的并行架构。

在 【第三节 序列化案例的基础上进行修改】

        将统计结果按照总流量倒序排序(全排序)。

要求每个省份手机号输出的文件中按照总流量内部排序。

5.3 GroupingComparator分组(辅助排序)

对Reduce阶段的数据按字段分组,实现复杂业务逻辑。案例:

  • 自定义订单类:
    public class OrderBean implements WritableComparable {
        // 订单id
        private int orderId;
        // 价格
        private double price;
        public OrderBean() {
            super();
        }
        public OrderBean(int orderId, double price) {
            this.orderId = orderId;
            this.price = price;
        }
        @Override
        public void write(DataOutput dataOutput) throws IOException {
            dataOutput.writeInt(orderId);
            dataOutput.writeDouble(price);
        }
        @Override
        public void readFields(DataInput dataInput) throws IOException {
            orderId = dataInput.readInt();
            price = dataInput.readDouble();
        }
        @Override
        public String toString() {
            return orderId + "\t" + price;
        }
        @Override
        public int compareTo(OrderBean o) {
            if (orderId > o.getOrderId()) {
                return 1;
            } else if (orderId < o.getOrderId()) {
                return -1;
            } else {
                return price > o.getPrice() ? -1 : 1;
            }
        }
        public int getOrderId() {
            return orderId;
        }
        public void setOrderId(int orderId) {
            this.orderId = orderId;
        }
        public double getPrice() {
            return price;
        }
        public void setPrice(double price) {
            this.price = price;
        }
    }
  • Mapper类:
    public class OrderMapper extends Mapper {
        OrderBean k = new OrderBean();
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            // 获取行数据
            String line = value.toString();
            String[] split = line.split("\t");
            // 对象封装
            k.setOrderId(Integer.parseInt(split[0]));
            k.setPrice(Double.parseDouble(split[2]));
            // 输出
            context.write(k, NullWritable.get());
        }
    }
  • 分组类:
    public class OrderGroupingComparator extends WritableComparator {
        public OrderGroupingComparator() {
            super(OrderBean.class, true);
        }
        @Override
        public int compare(WritableComparable a, WritableComparable b) {
            OrderBean aBean = (OrderBean) a;
            OrderBean bBean = (OrderBean) b;
            if (aBean.getOrderId() > bBean.getOrderId()) {
                return 1;
            } else if (aBean.getOrderId() < bBean.getOrderId()) {
                return  -1;
            } else {
                return 0;
            }
        }
    }

步骤:

1、自定义类继承WritableComparator

2、重写compare()方法

3、创建一个构造将比较对象的类传给父类

求出每一个订单中最贵的商品:

(1)利用“订单id和成交金额”作为key,可以将map阶段读取到的所有订单数据按照id分区,按照金额排序,发送到reduce。

(2)在reduce端利用groupingcomparator将订单id相同的kv聚合成组,然后取第一个即是最大值。

6. Combiner合并与ReduceTask优化

Combiner在Map端进行局部合并,减少网络传输数据量。ReduceTask的数量通过job.setNumReduceTasks()设置,默认1个。注意点:

  • ReduceTask数量需与Partitioner逻辑匹配
  • 过多ReduceTask会导致小文件过多
  • 合理设置Combine可提升性能

7. OutputFormat数据输出与Join应用

OutputFormat负责将Reduce结果写入文件系统。常见实现包括TextOutputFormat、SequenceFileOutputFormat和自定义OutputFormat。Join操作分为Reduce Join和Map Join两种方式。

计数器用于统计作业运行信息,数据清洗是MapReduce的经典应用场景。开发总结:

  • 合理设置切片大小和并行度
  • 充分利用Combine和分区优化
  • 小文件优先使用CombineTextInputFormat
  • 自定义排序和分组满足复杂业务
[AFFILIATE_SLOT_1]

通过本文的系统解析,您已掌握MapReduce框架从输入到输出的完整原理。在实际项目中,建议结合AI数据处理和自然语言处理场景,灵活运用分区、排序和分组机制,构建高效的大数据管道。

[AFFILIATE_SLOT_2]

总结:MapReduce框架的核心在于InputFormat切片、Shuffle排序分区、以及ReduceTask输出。理解这些机制,结合机器学习和深度学习需求,能有效提升数据处理效率。掌握这些原理,您将能轻松应对复杂的大数据处理挑战。

posted on 2026-05-06 15:51  ljbguanli  阅读(59)  评论(0)    收藏  举报