未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用范围与待维护事项
本文适用于已有Java、HDFS及可用Hadoop环境的批处理学习。Shuffle默认值未按具体发行版核验,tools.jar编译命令依赖旧版JDK;运行前需检查兼容性并确保输出目录不存在。日志、气温示例只覆盖所示简化格式,未完整处理脏数据。随机前缀代码缺少去盐后二次聚合,不能直接产生最终词频;自定义键、MultipleOutputs及调优代码也需补齐集成和验证。固定内存、并发、压缩与推测执行配置仅供实验,不代表生产性能保证。
⚡ MapReduce编程模型 - 分而治之的计算艺术
abstract 📋 本节概览
- 🎯 学习目标:掌握MapReduce编程思想、执行流程和开发技巧
- 📚 前置知识:HDFS分布式文件系统 | Java编程基础 | SQL
- ⏱️ 预计用时:110分钟
- 🔧 技术重点:Map-Reduce范式、Shuffle机制、分区策略、性能优化
🧠 MapReduce核心思想
note 💡 MapReduce就像工厂流水线
想象一个大型汽车工厂:
- Map阶段(分工):把复杂的汽车组装任务分解成简单的零件加工
- Shuffle阶段(物流):将相同类型的零件收集到一起
- Reduce阶段(汇总):将零件组装成最终产品
这样几千个工人可以同时工作,效率远超一个工人独自完成整辆车
分而治之的哲学
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
A[大问题<br/>Big Problem]
B[分解<br/>Divide]
A --> B --> C[子问题1<br/>Sub-problem 1]
B --> D[子问题2<br/>Sub-problem 2]
B --> E[子问题3<br/>Sub-problem 3]
B --> F[子问题n<br/>Sub-problem n]
C --> G[并行处理<br/>Parallel Processing]
D --> G
E --> G
F --> G
G --> H[合并结果<br/>Combine Results]
H --> I[最终答案<br/>Final Answer]
class A,B,C,D,E,F,G,H,I default
🏗️ MapReduce执行框架
整体执行流程
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
flowchart TD
classDef default fill:#FDFBF5,stroke:#6E6A60,color:#2D2A26,stroke-width:1.5px
classDef startend fill:#2D2A26,stroke:#2D2A26,color:#FDFBF5,font-weight:bold
classDef decision fill:#F6EAD8,stroke:#C08A1A,color:#2D2A26,font-weight:bold
classDef hub fill:#3B5A80,stroke:#3B5A80,color:#FDFBF5,font-weight:bold
classDef io fill:#E5EAF0,stroke:#3B5A80,color:#2D2A26
classDef accent fill:#A6432C,stroke:#A6432C,color:#FDFBF5,font-weight:bold
classDef storage fill:#F3EEDF,stroke:#9A9488,color:#2D2A26
subgraph "输入阶段"
A[输入数据<br/>Input Data]
B[InputSplit<br/>数据切分]
end
subgraph "Map阶段"
C[Mapper 1]
D[Mapper 2]
E[Mapper n]
end
subgraph "Shuffle阶段"
F[Partitioner<br/>分区]
G[Sort & Group<br/>排序分组]
end
subgraph "Reduce阶段"
H[Reducer 1]
I[Reducer 2]
J[Reducer m]
end
subgraph "输出阶段"
K[输出文件1]
L[输出文件2]
M[输出文件m]
end
A --> B
B --> C
B --> D
B --> E
C --> F
D --> F
E --> F
F --> G
G --> H
G --> I
G --> J
H --> K
I --> L
J --> M
class A,B,C,D,E,F,G,H,I,J,K,L,M default
1. Map阶段 - 数据转换
note 💡 Map就像数据预处理工厂
每个Mapper负责处理一小块数据:
- 读取输入记录
- 执行用户定义的map函数
- 输出键值对(key-value)
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 输入:一行文本
// 默认TextInputFormat下,key为该行起始位置的文件字节偏移量,value为文本内容
String line = value.toString().toLowerCase();
// 分词处理
String[] words = line.split("\\s+");
// 输出:(单词, 1) 键值对
for (String w : words) {
if (w.isEmpty()) {
continue;
}
word.set(w);
context.write(word, one); // 输出 (word, 1)
}
}
}
2. Shuffle阶段 - 数据重组
note 💡 Shuffle就像快递分拣中心
负责将Map输出按照key重新分组:
- 分区(Partition):决定数据发往哪个Reducer
- 排序(Sort):按key对数据排序
- 分组(Group):将相同key的值聚合在一起
Shuffle 是 MapReduce 中最复杂、也是最影响性能的阶段,分为 Map 端和 Reduce 端两部分:
Map 端 Shuffle 流程:
Map 输出
↓
环形缓冲区(默认 100MB,由 mapreduce.task.io.sort.mb 控制)
↓(使用率达到 80%,由 mapreduce.map.sort.spill.percent 控制)
溢写(Spill)到磁盘
├─ 按 Partitioner 分区
├─ 分区内按 key 排序
└─ 可选:调用 Combiner 做本地预聚合
↓(多次溢写产生多个溢写文件)
归并(Merge):将所有溢写文件合并为一个有序文件
| 参数 | 默认值 | 说明 |
|---|---|---|
mapreduce.task.io.sort.mb |
100(MB) | Map 端环形缓冲区大小 |
mapreduce.map.sort.spill.percent |
0.80 | 缓冲区达到此比例时触发溢写 |
mapreduce.task.io.sort.factor |
10 | 归并时每次合并的文件数 |
Reduce 端 Shuffle 流程:
从所有 Mapper 拉取(Fetch)属于自己分区的数据
↓(并行拉取,由 mapreduce.reduce.shuffle.parallelcopies 控制,默认 5)
内存缓冲区(默认堆内存的 70%)
↓(内存不足时溢写到磁盘)
归并排序(Merge Sort):将内存和磁盘的数据合并为全局有序序列
↓
交给 Reducer 逐组处理
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
sequenceDiagram
participant M1 as Mapper1
participant M2 as Mapper2
participant S as Shuffle
participant R1 as Reducer1
participant R2 as Reducer2
M1->>S: (apple,1), (banana,1)
M2->>S: (apple,1), (cherry,1)
Note over S: Map端:缓冲区→溢写→分区排序→归并
Note over S: 分区:apple→R1, banana→R1, cherry→R2
Note over S: Reduce端:拉取→归并排序→分组
S->>R1: apple:[1,1], banana:[1]
S->>R2: cherry:[1]
warning Shuffle 是性能调优的核心战场
- 减少 Map 输出数据量(用 Combiner 预聚合,压缩中间结果)
- 在 Map JVM 堆内存允许的范围内调整缓冲区(
mapreduce.task.io.sort.mb),评估是否能减少溢写,避免挤占业务对象内存 - 结合网络、磁盘和 Shuffle 服务负载调整并行拉取数(
mapreduce.reduce.shuffle.parallelcopies),增大并发不保证提速
3. Reduce阶段 - 数据聚合
note 💡 Reduce就像统计汇总部门
每个Reducer处理一组相同key的数据:
- 接收已排序的键值对组
- 执行用户定义的reduce函数
- 输出最终结果
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
// 输入:(单词, [1,1,1,...]) - 相同单词的所有计数
int sum = 0;
// 累加所有的1
for (IntWritable value : values) {
sum += value.get();
}
// 输出:(单词, 总次数)
result.set(sum);
context.write(key, result);
}
}
🎯 WordCount完整实例
程序结构
import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordCount {
// Mapper类
public static class TokenizerMapper
extends Mapper<LongWritable, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString().toLowerCase();
StringTokenizer tokenizer = new StringTokenizer(line);
while (tokenizer.hasMoreTokens()) {
word.set(tokenizer.nextToken());
context.write(word, one);
}
}
}
// Reducer类
public static class IntSumReducer
extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
public void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
// 主函数:配置作业
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCount.class);
job.setMapperClass(TokenizerMapper.class);
job.setCombinerClass(IntSumReducer.class); // 使用Reducer作为Combiner
job.setReducerClass(IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
编译和运行
# 1. 编译Java代码
export HADOOP_CLASSPATH=$JAVA_HOME/lib/tools.jar
hadoop com.sun.tools.javac.Main WordCount.java
# 2. 创建JAR包
jar cf wordcount.jar WordCount*.class
# 3. 准备输入数据
hdfs dfs -mkdir /input
echo "Hello World Bye World" > input.txt
echo "Hello Hadoop Goodbye Hadoop" >> input.txt
hdfs dfs -put input.txt /input
# 4. 运行MapReduce作业
hadoop jar wordcount.jar WordCount /input /output
# 5. 查看结果
hdfs dfs -cat /output/part-r-00000
# 输出:
# bye 1
# goodbye 1
# hadoop 2
# hello 2
# world 2
🔧 MapReduce高级特性
1. Combiner - 本地聚合
note 💡 Combiner就像地区预汇总
在数据发送到Reducer之前,先在本地做一次预聚合:
- 减少网络传输数据量
- 提高整体作业性能
- 必须满足结合律和交换律(不是所有操作都适合)
Combiner 使用条件:Combiner 的输入、输出键值类型须兼容 Map 输出的中间键值类型及 Reducer 输入类型,不要求与 Reducer 的最终输出类型相同。聚合操作应满足结合律和交换律,并保证不执行或执行多次 Combiner 都不改变最终结果。
| 操作 | 适合使用 Combiner | 原因 |
|---|---|---|
| 求和(sum) | 适合 | (a+b)+c = a+(b+c) |
| 计数(count) | 适合 | 本质是求和 |
| 最大值(max) | 适合 | max(max(a,b), c) = max(a,b,c) |
| 最小值(min) | 适合 | 同上 |
| 求平均值(average) | 不适合 | 平均值不满足结合律,需要传递 (sum, count) 对 |
| 中位数(median) | 不适合 | 局部中位数不等于全局中位数 |
Combiner 与 Reducer 的关系:
- Combiner 在 Map 端本地执行(可能执行 0 次或多次,不保证一定执行)
- Reducer 任务按分区处理数据,每个键分组调用一次 reduce;任务可能因失败重试或推测执行产生多个执行尝试,不保证只执行一次,也不保证位于远端节点
- WordCount 中可以直接用 Reducer 类作为 Combiner(因为求和满足结合律)
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
// Combiner的输出作为Reducer的输入
context.write(key, new IntWritable(sum));
}
}
// 在主函数中设置
job.setCombinerClass(WordCountCombiner.class);
2. Partitioner - 数据分区
note 💡 Partitioner就像快递分拣规则
决定Map输出的每个键值对发送到哪个Reducer:
- 默认使用HashPartitioner
- 可自定义分区逻辑
- 影响数据分布和负载均衡
public class CustomPartitioner extends Partitioner<Text, IntWritable> {
@Override
public int getPartition(Text key, IntWritable value, int numPartitions) {
// 示例:按单词首字母分区
char firstChar = key.toString().charAt(0);
if (firstChar >= 'a' && firstChar <= 'm') {
return 0 % numPartitions; // 前半字母表发送到第一个分区
} else {
return 1 % numPartitions; // 后半字母表发送到第二个分区
}
}
}
// 在主函数中设置
job.setPartitionerClass(CustomPartitioner.class);
job.setNumReduceTasks(2); // 设置Reducer数量
3. WritableComparable - 自定义数据类型
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.WritableComparable;
public class TemperatureWritable implements WritableComparable<TemperatureWritable> {
private int year;
private int temperature;
// 构造函数
public TemperatureWritable() {}
public TemperatureWritable(int year, int temperature) {
this.year = year;
this.temperature = temperature;
}
// 序列化:写入数据
@Override
public void write(DataOutput out) throws IOException {
out.writeInt(year);
out.writeInt(temperature);
}
// 反序列化:读取数据
@Override
public void readFields(DataInput in) throws IOException {
year = in.readInt();
temperature = in.readInt();
}
// 比较方法:用于排序
@Override
public int compareTo(TemperatureWritable other) {
int result = Integer.compare(this.year, other.year);
if (result == 0) {
result = Integer.compare(this.temperature, other.temperature);
}
return result;
}
// getter和setter方法
public int getYear() { return year; }
public int getTemperature() { return temperature; }
public void setYear(int year) { this.year = year; }
public void setTemperature(int temperature) { this.temperature = temperature; }
@Override
public String toString() {
return year + "," + temperature;
}
}
4. InputFormat - 输入格式
InputFormat 负责两件事:
- 数据切分(getSplits):将输入数据切分为 InputSplit,每个 Split 对应一个 Map 任务
- 记录读取(createRecordReader):将 Split 解析为键值对,逐条喂给 Mapper
| InputFormat 类 | 适用场景 | 切分逻辑 |
|---|---|---|
TextInputFormat(默认) |
普通文本文件,每行一条记录;key 为行起始位置的文件字节偏移量,value 为行内容 | 对可切分文件通常参考 Block 大小及切分配置生成 Split;行可跨 Split 边界,由 RecordReader 协调读取完整记录 |
KeyValueTextInputFormat |
文本文件,每行有 tab 分隔的 key-value | 同上,自动以 tab 为分隔符解析 key |
NLineInputFormat |
需要精确控制每个 Mapper 处理的行数 | 每 N 行一个 Split,N 由 mapreduce.input.lineinputformat.linespermap 控制 |
SequenceFileInputFormat |
Hadoop 二进制序列文件(高效、可压缩) | 按 Block 切分 |
CombineFileInputFormat |
大量小文件场景,合并多个小文件为一个 Split | 将多个小文件合并,减少 Map 任务数 |
DBInputFormat |
从关系型数据库读取数据 | 按行号范围切分 |
// 默认是 TextInputFormat,可显式设置
job.setInputFormatClass(SequenceFileInputFormat.class);
// NLineInputFormat:每个 Mapper 处理 100 行
job.setInputFormatClass(NLineInputFormat.class);
NLineInputFormat.setNumLinesPerSplit(job, 100);
5. OutputFormat - 输出格式
OutputFormat 控制任务结果如何写出,有 Reduce 的作业通常由 Reducer 输出,纯 Map 作业由 Mapper 输出。下表列出常用 OutputFormat 及辅助输出工具:
| OutputFormat 类 | 说明 |
|---|---|
TextOutputFormat(默认) |
每条记录写成一行,key 和 value 以 tab 分隔 |
SequenceFileOutputFormat |
输出 Hadoop 二进制序列文件,适合作为下一个 MR 作业的输入 |
MapFileOutputFormat |
输出带索引的 MapFile,支持按 key 随机查找 |
NullOutputFormat |
不产生任何输出(结果通过 Counter 或 Side Effect 输出) |
MultipleOutputs(辅助工具,非 OutputFormat 子类) |
配合 OutputFormat 将不同数据写到不同输出文件,可通过基础输出路径组织多个目录 |
// Driver 中注册输出路径
MultipleOutputs.addNamedOutput(job, "year2023", TextOutputFormat.class,
Text.class, IntWritable.class);
MultipleOutputs.addNamedOutput(job, "year2024", TextOutputFormat.class,
Text.class, IntWritable.class);
// Reducer 中按条件写入不同路径
MultipleOutputs<Text, IntWritable> mos = new MultipleOutputs<>(context);
if ("2024".equals(year)) {
mos.write("year2024", key, value);
} else {
mos.write("year2023", key, value);
}
mos.close();
6. Counter - 计数器
Counter 是 MapReduce 的内置监控机制,用于统计程序运行时的各种指标。
内置计数器分类:
| 计数器组 | 代表计数器 | 说明 |
|---|---|---|
FileSystemCounters |
HDFS_BYTES_READ、HDFS_BYTES_WRITTEN |
HDFS 读写字节数 |
Job Counters |
LAUNCHED_MAP_TASKS、LAUNCHED_REDUCE_TASKS |
启动的任务数 |
Map-Reduce Framework |
MAP_INPUT_RECORDS、REDUCE_OUTPUT_RECORDS |
Map 输入记录数、Reduce 输出记录数 |
Map-Reduce Framework |
SPILLED_RECORDS |
溢写记录数;同一记录可能在溢写和归并中重复计数,高值需结合输出规模、归并次数及缓冲区配置分析 |
自定义计数器:
// 定义计数器枚举(推荐方式)
enum MyCounters {
VALID_RECORDS,
INVALID_RECORDS,
EMPTY_LINES
}
// 在 Mapper/Reducer 中使用
context.getCounter(MyCounters.VALID_RECORDS).increment(1);
// 也可以使用字符串组名(不推荐,容易拼写错误)
context.getCounter("Data Quality", "Invalid Records").increment(1);
// Driver 中读取计数器结果(作业完成后)
boolean success = job.waitForCompletion(true);
Counters counters = job.getCounters();
long validCount = counters.findCounter(MyCounters.VALID_RECORDS).getValue();
System.out.println("有效记录数: " + validCount);
tip Counter 的实际价值
Counter 是排查数据质量问题的利器。当结果不符合预期时,通过 INVALID_RECORDS 计数器可以快速定位是否有大量脏数据被过滤,比在日志里找报错高效得多。
📊 实际应用案例
案例1:网站访问日志分析
public class LogAnalysisMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text outputKey = new Text();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 日志格式:IP timestamp method URL status size
// 例:192.168.1.1 [01/Jan/2024:00:00:01] GET /index.html 200 1024
String line = value.toString();
String[] fields = line.split(" ");
if (fields.length >= 6) {
String ip = fields[0];
String url = fields[3];
String status = fields[4];
// 统计不同维度
context.write(new Text("ip:" + ip), one); // IP访问次数
context.write(new Text("url:" + url), one); // URL访问次数
context.write(new Text("status:" + status), one); // 状态码统计
// 错误请求分析:按HTTP状态码统计4xx和5xx;非200不一定表示错误
if (status.matches("[45]\\d{2}")) {
context.write(new Text("error:" + ip), one);
}
}
}
}
案例2:气温数据最值分析
// Mapper:提取年份和气温
public class TemperatureMapper extends Mapper<LongWritable, Text, IntWritable, IntWritable> {
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 数据格式:year,month,day,temperature
String line = value.toString();
String[] fields = line.split(",");
if (fields.length == 4) {
int year = Integer.parseInt(fields[0]);
int temperature = Integer.parseInt(fields[3]);
context.write(new IntWritable(year), new IntWritable(temperature));
}
}
}
// Reducer:计算每年最高温度
public class MaxTemperatureReducer extends Reducer<IntWritable, IntWritable, IntWritable, IntWritable> {
@Override
protected void reduce(IntWritable key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int maxTemperature = Integer.MIN_VALUE;
for (IntWritable temperature : values) {
maxTemperature = Math.max(maxTemperature, temperature.get());
}
context.write(key, new IntWritable(maxTemperature));
}
}
⚡ 性能优化技巧
1. 作业级优化
public static void optimizeJob(Job job) {
Configuration conf = job.getConfiguration();
// 1. Map端优化
conf.set("mapreduce.map.memory.mb", "2048"); // Map任务内存
conf.set("mapreduce.map.java.opts", "-Xmx1638m"); // Map JVM堆内存
conf.set("mapreduce.task.io.sort.mb", "512"); // 排序缓冲区大小
conf.set("mapreduce.map.sort.spill.percent", "0.9"); // 溢写阈值
// 2. Reduce端优化
conf.set("mapreduce.reduce.memory.mb", "4096"); // Reduce任务内存
conf.set("mapreduce.reduce.java.opts", "-Xmx3276m"); // Reduce JVM堆内存
conf.set("mapreduce.reduce.shuffle.parallelcopies", "10"); // 并行拷贝数
// 3. 压缩优化
conf.set("mapreduce.map.output.compress", "true");
conf.set("mapreduce.map.output.compress.codec", "org.apache.hadoop.io.compress.SnappyCodec");
conf.set("mapreduce.output.fileoutputformat.compress", "true");
conf.set("mapreduce.output.fileoutputformat.compress.codec", "org.apache.hadoop.io.compress.GzipCodec");
// 4. 推测执行
conf.set("mapreduce.map.speculative", "true");
conf.set("mapreduce.reduce.speculative", "true");
}
2. 数据倾斜处理
warning ⚠️ 数据倾斜问题
就像工厂某个工位积压太多工件:
- 现象:某些Reducer处理时间远超其他
- 原因:数据分布不均匀,某些key对应的数据量过大
- 影响:整个作业的完成时间取决于最慢的任务
// 方案1:使用随机前缀打散数据
public class AntiSkewMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private Random random = new Random();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
String[] words = line.split("\\s+");
for (String word : words) {
// 为热点key添加随机前缀
if (isHotKey(word)) {
int randomPrefix = random.nextInt(10); // 0-9随机前缀
context.write(new Text(randomPrefix + "_" + word), new IntWritable(1));
} else {
context.write(new Text(word), new IntWritable(1));
}
}
}
private boolean isHotKey(String word) {
// 判断是否为热点词汇的逻辑
return word.equals("the") || word.equals("and") || word.equals("of");
}
}
// 方案2:两阶段聚合
// 第一阶段:局部聚合,添加随机后缀
// 第二阶段:去除第一阶段添加的随机前缀或后缀,按原始key汇总局部计数;此处未提供该阶段的实现,不能把第一阶段输出直接作为最终结果
🔗 下一步学习
掌握MapReduce编程模型后,建议继续学习:
最后更新:2024-06-29