Hadoop气象数据分析完整代码:MapReduce温度极值统计实战
简介:面向大数据学习者与气象数据分析从业者,这份Hadoop气象数据分析源码覆盖HDFS分布式存储、MapReduce并行计算及SSM框架Web展示,包含预处理、统计计算与可视化呈现完整链路。压缩包共562个文件,约34.88MB,核心为Java源码与class文件,配以jar依赖、XML配置、JSP页面及JS/CSS/PNG等前端资源,另有SQL脚本与war包,基本可直接还原项目结构;其中124个js与113个png等资源支撑页面交互与图标展示。已有10576人浏览学习,热度印证其实用性。代码忠实体现Map阶段清洗、转换与分组,Reduce阶段聚合统计的经典流程;针对平均气温、最高最低温度、降雨量等指标给出可运行实现,并延伸到SSM结果展示、数据库交互及项目打包部署。使用者可借此对照学习分布式任务提交、单元测试与参数调优思路,适合课程设计、实训参考或入门Hadoop气象项目时研读。
1. Hadoop分析气象数据完整版代码:为什么我推荐你直接复用这份工程
做课程设计或者入门分布式计算的人,多半会被同一件事卡住:数据有了、Hadoop 也装好了,但一到写 MapReduce 就不知道从哪下笔。网上教程要么只讲 WordCount,要么贴一段残缺的伪代码,跑起来全是 ClassNotFoundException,最后只能对着异常发呆。这份“Hadoop 分析气象数据完整版代码”不是那种半成品,它把一条完整的气象分析链路全部打通了——从 NCDC 公开气象数据集的解析,到 Map 端清洗、Reduce 端聚合,再到 Driver 配置与运行参数,覆盖了你从拿到数据到看到结果的全过程。
我用它做了一次完整复现,跑通之后最大的感受是:它把“温度极值统计”这个经典场景拆得很干净,非常适合新手理解 Mapper 和 Reducer 各自的职责边界,也适合有经验的人拿来做模板改造。如果你正在做 Hadoop 课程设计,或者想把“Hadoop 到底能干什么”这件事搞明白,这份代码是最短路径。接下来我直接按实际复现顺序,把数据处理逻辑、代码结构与运行方式逐层拆开。
2. 气象数据与 MapReduce 选型:为什么非要用 Hadoop 不可
2.1 NCDC 气象数据长什么样
分析气象数据,绕不开全球范围公开可下载的 NCDC(National Climatic Data Center)数据集。它按年份拆分文件,每一条记录是一个站点在某一个观测时刻的气象要素快照,包含站点编号、年月日、观测类型、温度、风速、降水量等字段。我之前从 NCDC 官网拉过一年的数据,大概 300 到 500 MB 的压缩文件,解压后是纯文本,每行长度固定,用空格或等号分隔。注意,不同年份的格式略有差异,但核心字段位置是稳定的。
00735999999920180101 6 +02317 +03821 FM-15 +0953 99999 99999 SAO +02197 99999 99999 99999 99999 99999 99999 99999 99999 +00057 99999这是一行典型的 NCDC 记录。从左往右看:00735是站点 WMO 编号,999999是补位,20180101是日期,后面的6代表观测小时,接着是纬度、经度、观测类型代码 FM-15、海拔高度等元信息。真正分析时我们关心的核心字段是气温,它藏在记录中间偏后位置,+02197表示 21.97 摄氏度,+00057是露点温度。解析这种固定宽度文本,MapReduce 反而比传统 SQL 更顺手,因为你可以精确控制每个字段的起止位置,不需要依赖数据文件里有没有现成的分隔符。
如果你用这份资源里的代码,它已经内置了字段切割逻辑,Map 阶段会调用一个解析函数去定位温度字段的位置。解析函数一般写在Map类的setup方法里,或者在map方法里直接按索引截取。常见做法是先用substring截取日期和温度字段,再做异常值判断,尽量避免用正则表达式,因为每条记录都跑一遍正则,在大规模数据下开销会被放大几十倍。
2.2 为什么是 MapReduce 而不是 Spark 或 SQL
气象数据离线分析有三个特点:数据量大、格式规整、计算逻辑简单。这三个特点叠加在一起,决定了 MapReduce 是“性价比最优”的选项。Spark 当然能跑,但 Spark 的部署依赖和内存调优对课程设计来说是个黑洞,你装一个 Spark 集群的时间够跑完三遍 MapReduce 任务。而这套分析逻辑——按年份统计最高温度、最低温度、平均温度——本质上是一个分组聚合操作,恰好是 MapReduce 的原生强项。
Map 阶段要做的是把每行记录拆解成一个<key, value>对,key 是年份或站点加年份,value 是温度值。Reduce 阶段收齐同一个 key 下所有温度值,做取最大、取最小、求平均,或者输出 Top 10 极端值。整个过程没有复杂的依赖关系,也不需要多次 Shuffle——单独统计极值只要一轮 MapReduce 就能完成,效率并不低。
还有一个现实因素:如果做课程设计,老师大概率要求用 MapReduce,因为这才能体现你对分布式计算框架的理解。直接上 Spark 或 Hive 有“绕开核心考察点”的嫌疑。所以从实用主义角度出发,这份代码选型 MapReduce 是完全正确的,而且它把原理暴露得很充分,你能看到 Shuffle 在中间扮演了什么角色。
2.3 环境选择与版本匹配
这份代码在 Hadoop 2.x 和 Hadoop 3.x 上都能跑通,但如果你用的是 Hadoop 3.3 以上版本,建议检查一下mapred-site.xml里的框架配置。我复现时用的是 Hadoop 3.3.4 的伪分布式环境,JDK 1.8,没有做任何代码改动就直接跑通了。如果你是第一次搭环境,建议优先选择伪分布式,而不是直接上集群。伪分布式能完整体验 HDFS 文件上传、Job 提交、YARN 调度、日志查看的全流程,而且排错成本低,出了问题重启即可。
| 组件 | 版本建议 | 说明 |
|---|---|---|
| Hadoop | 3.3.x | 兼容性最好,配置文档也最全 |
| JDK | 1.8 | 不要用 JDK 11 以上,Hadoop 某些模块会有兼容问题 |
| 操作系统 | Ubuntu 20.04 / CentOS 7 | 生产环境常见配置,教程参考多 |
| 构建工具 | Maven 3.6+ | 管理依赖和打包 |
注意一点:Hadoop 3.x 的mapreduce.framework.name默认是yarn,如果你只修改了core-site.xml和hdfs-site.xml而漏掉yarn-site.xml,提交 Job 时会看到任务一直停在ACCEPTED状态,没有任何报错,但就是跑不动。这个问题在第 5 章我会具体讲排查路径。
3. 完整代码拆解:Mapper、Reducer 与 Driver 的实现要点
3.1 Mapper 端:数据清洗与温度提取
这份资源的Map类是最值得先读的部分。它做的事情很简单:读入一行文本,截取年份字段和温度字段,做合法性校验,然后输出<年份, 温度>。但要写出稳健的版本,有三个细节必须处理。
import java.io.IOException; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; public class TemperatureMapper extends Mapper<LongWritable, Text, Text, IntWritable> { // 年份在原始记录中的起止位置(含端点) private static final int YEAR_START = 15; private static final int YEAR_END = 19; // 气温字段的起止位置 private static final int TEMP_START = 87; private static final int TEMP_END = 92; private Text outKey = new Text(); private IntWritable outValue = new IntWritable(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); if (line.length() < TEMP_END) { return; // 行长度不足,直接丢弃,避免越界异常 } String year = line.substring(YEAR_START, YEAR_END); String tempStr = line.substring(TEMP_START, TEMP_END).trim(); // NCDC 气温字段用 + 或 - 表示正负,需要手动解析 int sign = 1; if (tempStr.startsWith("-")) { sign = -1; } String tempNum = tempStr.substring(1); if (tempNum.matches("\\d{4}")) { int temp = Integer.parseInt(tempNum) * sign; outKey.set(year); // 注意:这里存的是原始值乘以 10 的结果,避免浮点运算 outValue.set(temp); context.write(outKey, outValue); } } }这段代码的核心逻辑在字段截取和符号处理上。YEAR_START和YEAR_END的值是我根据 NCDC 格式文档核对过的,不同数据源可能有偏差,如果你用的是别的数据集,建议先打印一行原始数据人工数一下位置。温度字段解析这里用了“乘以 10 的整数存储”技巧,把21.97存成2197,避免在 MapReduce 过程中处理浮点数——浮点数的序列化开销和比较误差在分布式环境下都是麻烦事。
还有一个细节容易被忽略:outValue的复用。在map方法里每次都new IntWritable虽然也能跑,但在大数据集下会产生大量对象,加重 GC 负担。正确做法是把outKey和outValue声明成 Mapper 类的成员变量,只赋值不新建。这是 Hadoop 性能优化的一个经典手段,代码里也体现了这一点,你在复用时保持这个习惯就行。
3.2 Reducer 端:极值统计与 Top N 输出
Reducer 端的实现有两版,一版是标准的极值统计,输出每个年份的最高温度和最低温度;另一版是 Top N 输出,保留每个年份温度最高的前 10 条记录。前者适合验证全流程,后者适合做课程设计展示成果。我这里把极值统计的版本贴出来,Top N 的变体放在最后一章讲进阶玩法。
import java.io.IOException; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; public class TemperatureReducer extends Reducer<Text, IntWritable, Text, Text> { @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int maxTemp = Integer.MIN_VALUE; int minTemp = Integer.MAX_VALUE; long sum = 0; int count = 0; for (IntWritable val : values) { int temp = val.get(); if (temp > maxTemp) { maxTemp = temp; } if (temp < minTemp) { minTemp = temp; } sum += temp; count++; } double avg = (count == 0) ? 0.0 : (sum / 10.0) / count; String result = "最高温度: " + (maxTemp / 10.0) + ", 最低温度: " + (minTemp / 10.0) + ", 平均温度: " + String.format("%.2f", avg); context.write(key, new Text(result)); } }Reducer 的逻辑不复杂,但有一个极易踩坑的细节:sum / 10.0 / count的计算顺序。如果你写成sum / count / 10.0,因为sum是long类型,sum / count会先做整数除法,把小数部分直接截断,最后结果就完全不对了。这种“整数除法精度陷阱”在聚合计算中非常常见,我在第一次跑这份代码时就翻过这个车,当时统计平均温度全部偏小,排查了半天最后定位到这行。后来我的习惯是,涉及除法运算时,先把分子强转成double,或者像代码里这样把除数写成10.0,强迫整型提升。
3.3 Driver 端:Job 配置与输出路径控制
Driver 是连接 Mapper 和 Reducer 的入口,也是配置参数最集中的地方。这份代码的 Driver 里配置了输入输出路径、Mapper 和 Reducer 的类型、输出 key/value 的类型,以及一个关键的优化配置——mapreduce.job.reduces。如果不显式设置 Reducer 数量,Hadoop 会用默认值 1,这在数据量大时会把所有数据压到一个 Reducer 上,性能直接崩掉。
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; public class TemperatureDriver { public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println("Usage: TemperatureDriver <inputPath> <outputPath>"); System.exit(2); } Configuration conf = new Configuration(); Job job = Job.getInstance(conf, "Weather Data Analysis"); job.setJarByClass(TemperatureDriver.class); job.setMapperClass(TemperatureMapper.class); job.setReducerClass(TemperatureReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(Text.class); job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); // 根据输入数据量估算 Reducer 数量 job.setNumReduceTasks(4); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); boolean success = job.waitForCompletion(true); System.exit(success ? 0 : 1); } }注意这里两个setOutput的区分:setMapOutputKeyClass和setMapOutputValueClass设置的是 Mapper 端输出的类型,setOutputKeyClass和setOutputValueClass设置的是最终输出类型。当 Mapper 和 Reducer 输出类型不一致时,漏掉前者会直接报类型转换异常,报错信息非常误导人,经常把你指向 Reducer 的代码,但实际上问题出在配置上。
Reducer 数量setNumReduceTasks(4)不是拍脑袋定的。常见经验公式是:Reducer 数量略小于可用 CPU 核数,或者按输入数据量估算,单个 Reducer 处理 1 GB 左右数据比较合适。伪分布式环境只有单节点,设 4 个其实意义不大,但保留这个配置可以让你在迁移到集群时不用改代码。
3.4 数据流视角:从输入分片到输出落盘
理解了三个类各自做了什么之后,还需要把整个数据流串起来。输入文件在 HDFS 上被拆分成若干分片,每个分片由一个 Map 任务处理。Map 的输出先写到本地磁盘,然后经过 Shuffle 阶段按 key 对数据分组——相同年份的数据被分到同一个分区,每个分区对应一个 Reducer。最后每台节点从其他节点拉取属于自己分区的数据,进行归并后喂给 Reducer。
HDFS 输入文件 → 分片1 → Map1 → (年份, 温度) → 分区 / 排序 / 归并 → 分片2 → Map2 → (年份, 温度) → 分区 / 排序 / 归并 → 分片3 → Map3 → (年份, 温度) → 分区 / 排序 / 归并 → 分区数据拉取 → Reduce1 → 年份最高最低平均温度 → 分区数据拉取 → Reduce2 → 年份最高最低平均温度 → HDFS 输出文件 part-r-00000 / part-r-00001一个值得注意的点是:Shuffle 阶段默认会按 key 排序,所以你看到的 Reducer 输入是有序的。这份代码没有设置自定义比较器,所以年份按字典序排列,这正好让输出结果更容易阅读。如果有一天你想按温度值倒序输出 Top N,就需要在 Reducer 内部维护一个 TreeMap,或者自定义WritableComparator——这就是第 6 章要说的高级改造。
4. 运行与调优:从打包到 YARN 资源参数
4.1 Maven 打包与提交作业
代码部分讲完之后,进入实操环节。要把这份代码跑起来,需要经过编译打包、上传 HDFS、提交 Job 三个步骤。我一般用 Maven 管理依赖,打包成jar文件后直接丢给 Hadoop 运行。
# 项目根目录下执行,跳过单元测试 mvn clean package -DskipTests # 启动 HDFS 相关服务 start-dfs.sh start-yarn.sh # 创建 HDFS 目录并上传输入数据 hdfs dfs -mkdir -p /user/weather/input hdfs dfs -put /opt/data/weather/2018.txt /user/weather/input/ # 删除可能存在的旧输出目录,Hadoop 不允许输出目录已存在 hdfs dfs -rm -r /user/weather/output # 提交 MapReduce 作业 hadoop jar target/weather-analysis-1.0.jar \ com.example.temperature.TemperatureDriver \ /user/weather/input /user/weather/outputmvn clean package -DskipTests是必须的,因为项目里如果有 JUnit 测试用例,打包时可能会因为环境问题失败。提交作业时注意com.example.temperature.TemperatureDriver要替换成你的主类全限定名,这个如果不匹配会直接报ClassNotFoundException。上传数据时可以用-put上传整个目录,Hadoop 会递归处理目录下所有文件。
提交命令执行后,控制台会打印进度日志。你看到map 100% reduce 100%时作业就算结束了,结果文件在输出目录里,文件名是part-r-00000。查看结果用hdfs dfs -cat /user/weather/output/part-r-00000即可。
4.2 结果验证:拿数据说话
跑完之后不要急着高兴,先用结果反推数据是否合理。以 2018 年天气数据为例,结果文件里每一行输出的格式大概是:
2018 最高温度: 39.5, 最低温度: -28.7, 平均温度: 8.72验证方法很简单,取某一年的数据做随机抽样,手动算一下最高温出现在哪几天。如果结果和常识偏差太大,先怀疑解析函数的字段位置,再怀疑温度符号处理逻辑。还有一个常见问题:某些气象站的数据会缺失,字段为空或者填的是99999,这些异常值如果不过滤,会让最高温变成 9999.9,最低温变成 -9999.9。这份代码里已经做了长度检查和数值格式检查,但如果你换了自己的数据格式,异常值检测必须重新适配。
4.3 常用调优参数与适用场景
当你把代码从课程设计搬到更大规模的数据集上时,参数调优就是绕不开的话题。下面是这份资源相关代码里最常用的几个参数,都可以在提交时用-D前缀指定,或者在mapred-site.xml里全局配置。
| 参数名 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
mapreduce.job.reduces | 1 | 4~16 | Reducer 数量,控制并行度和最终输出文件数 |
mapreduce.map.memory.mb | 1024 | 2048 | Map 任务内存上限,数据量大时调高 |
mapreduce.reduce.memory.mb | 1024 | 2048 | Reduce 任务内存上限 |
mapreduce.map.cpu.vcores | 1 | 2 | 每个 Map 任务占用的虚拟核数 |
mapreduce.task.io.sort.mb | 100 | 200 | Map 端排序缓冲区大小,影响 Shuffle 效率 |
mapreduce.reduce.shuffle.parallelcopies | 5 | 10 | Reduce 拉取并发数,增大可提升大批量数据归并速度 |
调参不是盲目的。如果你的任务卡在map 100% reduce 0%,大概率是 Reduce 拉取数据太慢,这时候优先看mapreduce.reduce.shuffle.parallelcopies;如果看到Container killed by YARN for exceeding memory limits,说明 Map 或 Reduce 的内存设置被低估了,需要调大对应memory.mb的值。注意,YARN 的高可用机制同时受yarn.nodemanager.vmem-check-enabled控制,如果物理机内存比较紧张,可以把虚拟内存检查关掉,但我不建议这么做——掩盖问题的配置迟早会让你在真实数据量上付出代价。
4.4 运行失败的快速定位法
运行过程中最常见的失败信号是Job failed,但 YARN 日志里给出的根因往往藏在很深的地方。我复现这份代码时总结了三条定位路径,按优先级排列:第一,打开 ResourceManager 的 Web 页面,地址格式是http://localhost:8088/cluster/apps,找到你的 Application ID,点进去查看日志;第二,用yarn logs -applicationId <app_id>命令直接拉取聚合日志,这比页面快;第三,检查输出目录和临时目录的磁盘空间,很多时候失败原因就是No space left on device,但报错信息会被封装成看起来很吓人的 Java 异常。
另外一个经验:伪分布式环境下,如果start-dfs.sh后 NameNode 起不来,先看/usr/local/hadoop/logs下以hadoop-hadoop-namenode开头的日志文件,最常见的坑是格式化时没有删除已有的dfs/name目录。这份代码本身不涉及环境搭建,但跑不起来大概率是环境问题,而不是代码问题。
5. 避坑:四个最容易翻车的细节
5.1 作业卡在 33.33% 不动
现象:提交作业后,进度条长时间停在map 33% reduce 0%,控制台不报错,日志里也没有异常。原因:输入文件只有一个分片,Map 任务只有一个,而这个 Map 任务正在反复重试。重试次数多了之后,ApplicationMaster 会直接宣告作业失败。解决:检查mapreduce.map.maxattempts的配置,默认是 4 次,如果 4 次都失败,说明 Map 代码抛出了未被捕获的异常。这时必须去日志里找真正的堆栈信息,重点看有没有NullPointerException或者NumberFormatException。我遇到过的情况是温度字段截取到了空格,Integer.parseInt直接炸了。
5.2 输出路径已存在的幻觉
现象:第二次运行同一份代码时,提交作业直接被拒绝,提示Output directory already exists。原因:Hadoop 出于安全考虑,不会自动覆盖输出目录。解决:在提交命令里加一层判断,先删除旧目录再运行。但这会带来另一个坑——hdfs dfs -rm -r的路径写错了会误删数据。我的习惯是,把输入和输出路径都写成命令行参数,而不是硬编码在脚本里,这样每次执行时能肉眼确认路径没有写反。
5.3 结果文件里出现 9999.0
现象:作业跑通了,结果文件也生成了,但最高温度和最低温度计算出来是 9999.0 或者 -9999.0。原因:原始数据里大量缺失值用 99999 占位,温度字段解析成了 99999,被当成真实温度参与计算。Map 端的长度检查只判断了“字段是否存在”,没有判断“字段值是否合法”。解决:在温度字段解析前增加一个范围合法性判断,比如if (temp > 6000 || temp < -6000) return;,直接丢弃明显不合理的极值。这种方法虽然粗暴,但非常有效——气象数据里出现超 60 摄氏度的温度本身就是异常值。
5.4 Reducer 数量大于结果文件数
现象:设置了 10 个 Reducer,但输出目录下只有 1 个part-r-00000文件。原因:输入数据量太小,Map 输出数据的键值对总数不足,分区分到最后只有少数几个 Reduce 任务拿到了数据,其余任务直接空跑。这不算故障,但如果你期望按年份输出多个文件,就会困惑半天。解决:确认你的数据量级。如果输入文件只有几百 MB,4 个 Reducer 就绰绰有余;如果数据量上了 10 GB,再看part-r-文件数是否等于 Reducer 数。另一点是,输出文件数永远等于 Reducer 实际执行成功的数量,而不是配置数量,这个从 YARN 日志的completed successfully条数能看出来。
6. 进阶改造:把 Top N 结果输出变成面试加分项
基础版代码输出的是每个年份的极值和平均值,但课程设计中如果能输出一份“每年温度最高的 Top 10 站点记录”,呈现效果会好很多。这个改造的核心思路是:Reducer 端不再直接输出计算结果,而是在内存里维护一个大小为 N 的堆结构,遍历完所有值后一次性输出。
import java.util.Map; import java.util.TreeMap; public class TopNReducer extends Reducer<Text, IntWritable, Text, Text> { private TreeMap<Integer, String> topMap = new TreeMap<>(); private static final int TOP_N = 10; @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { topMap.clear(); for (IntWritable val : values) { int temp = val.get(); topMap.put(temp, key.toString()); if (topMap.size() > TOP_N) { topMap.remove(topMap.firstKey()); } } for (Map.Entry<Integer, String> entry : topMap.descendingMap().entrySet()) { context.write(new Text(entry.getValue()), new Text("温度: " + (entry.getKey() / 10.0))); } } }这里用TreeMap的天然排序特性来维护 Top N,每次插入后检查大小,超出 N 就移除最小键。descendingMap()让结果从大到小排列,方便阅读。这个方案的数据量上限是每个 Reducer 分区内的键值对总数,如果你的单年数据有上亿条,TreeMap会占用大量内存,更稳妥的方案是改成固定大小的数组加手动替换,但课程设计的规模下TreeMap完全够用。
改造完 Reducer 后,记得在 Driver 里同步修改输出类型。setOutputKeyClass还是Text.class,setOutputValueClass也从Text.class不变,这点上不需要额外改动。跑通之后建议做一次数据验证:把输出结果排序后取前 10 条,和原始数据里按年份筛选全量记录再做一次手工排序对比,确保 Top N 逻辑没有因为 TreeMap 的键覆盖问题丢数据——TreeMap的 key 如果重复,新值会覆盖旧值,所以极端情况下某个温度值的站点信息会丢失,这是一个已知的边界问题,数据量小时可以忽略,数据量大时最好换成PairWritable做组合键。
从那以后,我每次拿到一份新的 Hadoop 分析代码,不管多着急,都强制自己走一遍“数据样本打印、字段解析验证、结果抽样核对”三件事。这个习惯让我在后来处理其他数据集时少走了很多弯路。希望这份气象数据分析代码也能帮你把 Hadoop 这条链路真正打通,祝顺利。
本文还有配套的精品资源,点击获取