全文内容概要
- hadoop计算运行原理
- 查找素数代码实战
- hadoop典型应用案例分析
一、hadoop计算运行原理
前文已经讲述了hadoop安装部署时几个重要的组件和概念,并且试运行了一个hadoop分布式运算的例子,那么对于其中的HDFS、namenode、datanode、resourcemanager等等,它们到底是如何工作的呢?
1.1 Hadoop分布式计算的核心 概念
1.分而治之(Divide-and-Conquer)
- 数据分片(Input Split)
输入数据(如TB级日志文件)被划分为多个128MB的HDFS块(默认大小),每个块对应一个Map任务,实现并行处理,这就是分片。
hadoop计算的输入是上传到HDFS文件系统的输入文件(至于流式实时的数据处理技术后续文章讲解),输出结果也是存储在HDFS的计算结果文件。hadoop根据输入文件的大小规模来进行分片,来确定有多少个Map任务。假如1TB文件可以分成约8192个Map Task(未压缩情况下)。
- 计算任务分解
Map阶段:各节点独立处理本地数据块,生成中间键值对(<k2,v2>)。中间结果是以<key,value>键值对(key-value pairs)的形式存放流转的,这个就是此阶段称之为Map的原因。
Reduce阶段:聚合相同键的中间结果(<k2,[v2\v3]> → <k2,v23>)。这个阶段为何叫做Reduce呢?这是直接借鉴了Lisp等函数式语言中的高阶函数概念,其本质是对一组值执行"归约操作"(如求和、求最大值等),这与Map形成了互补。
Shuffle阶段:一句话,Map完了,通过Shuffle阶段,才能Reduce。这个词本意是“洗牌”,是整个计算过程的瓶颈,是hadoop调优的关键点之一。
2.数据本地化(Data Locality)
计算贴近数据,ResourceManager优先将Map Task调度到存储对应数据块的NodeManager节点,减少网络传输开销。
若本地节点繁忙,则选择同机架节点(Rack Awareness),这个离不开Yarn发挥的总体调度的作用。
3.容错与弹性扩展 (Fault Tolerance & Elastic Scalability )
任务重试:失败的Task由ApplicationMaster自动重新调度(默认重试4次)。
推测执行:对执行过慢的Task启动备份任务,防止“拖尾任务”影响整体进度。
动态扩容:支持运行时添加节点,YARN自动分配新资源。
1.2 Hadoop分布式计算的工作流程
1.总体流程图
graph TD
A[Driver程序启动] --> B[提交Job到YARN ResourceManager]
B --> C[ResourceManager分配ApplicationMaster]
C --> D[ApplicationMaster申请资源]
D --> E[NodeManager启动Map Task]
E --> F[Map阶段: 读取HDFS输入, 处理数据]
F --> G[Shuffle阶段: 分区排序/网络传输]
G --> H[NodeManager启动Reduce Task]
H --> I[Reduce阶段: 聚合输出结果]
I --> J[写入HDFS最终结果]
2. 各组件关系及作用
2.1 组件作用简述
- Driver程序 入口 :
当执行类似下面的hadoop指令时
hadoop jar XXXXX.jar input.txt output.txt
触发Driver类的main()方法,配置Job对象(如Mapper/Reducer类、输入/输出路径等)。
- 任务提交到YARN :
Driver向YARN的ResourceManager(RM)提交Job请求,RM生成唯一的Application ID。RM在某个NodeManager(NM)上启动ApplicationMaster(AM),AM负责协调任务执行。
- Map阶段执行
AM根据输入文件input.txt 的HDFS块大小(默认128MB)生成多个InputSplit分片,每个分片对应一个Map Task。
AM向RM申请资源(Container),NM在分配的Container中启动Map Task。
每个Map Task调用map()方法处理键值对(如<行号, 行内容>),输出中间结果(如<单词, 1>)。
- Shuffle阶段 执行
Map输出的中间结果按分割器(默认Hash)分配到Reducer,并在内存中排序(溢出就写到磁盘)。排序后的数据通过HTTP传输到Reducer所在的节点(Combiner可选优化,见后文)。
此阶段对应Map端的操作有:
分区(Partitioning):按分隔器(默认Hash)决定数据归属哪个Reducer。
排序(Sorting):分区后数据按键排序(内存不足时溢写磁盘)。
合并(Merge):多个溢写文件合并为一个大文件。
对应Reduce端的操作有:
通过HTTP拉取对应分区的数据,再次排序后输入Reducer。
- Reduce阶段执行
AM申请资源后,NM启动Reduce Task,拉取对应分区的数据。然后调用reduce()方法聚合相同键的值(如<单词, [1,1]> 合并为 <单词, 2>)。
- 结果输出
Reduce结果通过OutputFormat(默认TextOutputFormat)写入output 目录,生成part-r-xxxxx结果文件。
任务完成后,AM向RM报告Job状态,释放资源,Driver程序退出。
各组件功能总结如下:
| 阶段 | 主要组件 | 功能说明 |
|---|---|---|
| 提交 | Driver, ResourceManager | 配置Job并提交到集群 |
| 调度 | ApplicationMaster | 协调资源分配和任务监控 |
| 计算 | Mapper, Reducer | 执行用户定义的业务逻辑 |
| 数据传输 | Shuffle, Partitioner | 确保数据正确分发和排序 |
| 存储 | HDFS, NodeManager | 持久化输入/输出数据 |
2.2代码编写策略
在上述各种组件当中,用户只需要聚焦Map、Reduce业务逻辑即可
| 组件 | 是否用户编写 | 说明 |
|---|---|---|
| Driver | ✔ | 任务配置与提交入口 |
| Mapper | ✔ | 业务逻辑实现 |
| Reducer | ✔ | 结果聚合逻辑 |
| Partitioner | ⚠(可选) | 默认Hash分区,可自定义 |
| ResourceManager | ✖ | 框架管理资源调度 |
| ApplicationMaster | ✖ | 框架生成的任务协调者 |
| Shuffle | ✖ | 框架内置数据传输机制 |
| HDFS | ✖ | 用户仅操作路径 |
| NodeManager | ✖ | 框架管理的节点代理 |
- Driver
作用:配置和提交MapReduce任务(如设置Mapper/Reducer类、输入输出路径等)。
用户责任:编写main()方法,定义Job对象参数。
- Mapper
作用:处理输入数据,生成中间键值对(<k1, v1> → <k2, v2>)。
用户责任:继承Mapper类并重写map()方法。
- Reducer
作用:聚合Mapper输出的中间结果(<k2, [v2]> → <k3, v3>)。
用户责任:继承Reducer类并重写reduce()方法。
- Partitioner(可选)
作用:自定义数据分发逻辑,决定Mapper输出由哪个Reducer处理。
用户责任:继承Partitioner类并重写getPartition()方法(默认使用Hash分区)。
1.3hadoop的适用计算任务
1. 适合Hadoop的任务特性
并不是所有的计算任务都适用于hadoop的,适用的计算任务需同时满足以下数据特性与计算特性的才可以:
1. 数据层面的适配性
| 特性 | 说明 | 示例场景 |
|---|---|---|
| 海量数据 | 数据规模需达到TB级以上,否则单机处理更高效 | 日志分析、历史订单处理 |
| 高吞吐量 | 适合批量处理,而非低延迟的实时计算 | 离线报表生成、ETL流程 |
| 结构化/半结构化 | 支持文本、CSV、JSON等格式,需通过InputFormat解析 | 用户行为日志、传感器数据 |
2. 计算层面的适配性
| 特性 | 说明 | Hadoop实现机制 |
|---|---|---|
| 可并行化 | 任务可拆分为独立子任务,无严格顺序依赖 | Map阶段多节点并行处理 |
| 键值对处理 | 输入输出需为<key, value>形式,适配MapReduce编程模型 | Mapper输出→Reducer聚合 |
| 计算密集型 | 单任务计算复杂度高,但数据移动成本可控(避免Shuffle成为瓶颈) | 大规模矩阵运算(如PageRank) |
| 容忍高延迟 | 适合分钟级以上的批处理,而非秒级响应 | 夜间批量作业 |
3. 适合Hadoop的计算场景总结
| 场景特性 | Hadoop适配性 | 示例 |
|---|---|---|
| 数据规模≥TB级 | ✔ 并行处理优势显著 | 全网爬虫数据清洗 |
| 批处理(分钟级延迟) | ✔ 高吞吐量设计 | 月度财务报表生成 |
| 可分区键值对操作 | ✔ MapReduce原生支持 | 用户画像标签聚合 |
| 强一致性事务 | ✖ 需依赖HBase | 实时订单状态更新 |
2 . 不适合Hadoop的场景
-
低延迟实时计算:需用Spark/Flink等流处理框架。
-
强事务一致性:HDFS不支持随机修改,需依赖HBase。
-
小文件过多:NameNode内存压力大,需合并或使用Archive。
上面提到的Spark、Flink、HBase等会在后续文章中详解,PageRank请参阅本人AI专栏的文章。
3 . 适用案例
4.1搜索引擎索引构建
适配点:海量网页数据分片存储(HDFS)。
Map阶段提取关键词,Reduce阶段生成倒排索引。
优化:使用Combiner减少Shuffle数据量。
4.2电商用户行为分析
适配点:日志文件按时间分区,Map过滤无效数据,Reduce统计UV独立访客数/PV页面浏览量。
挑战:需处理倾斜问题(如热门商品)。
以上两个案例的详细分析,以及实现技术将在本文的第三部分详述。
Hadoop的分布式计算本质是以数据本地化为核心的批处理框架,其优势在于海量数据下的高容错与线性扩展能力。随着技术演进,现代数据栈(如Spark、Flink)已在其基础上优化了实时性与易用性,但Hadoop MR仍是离线超大规模计算的基石。
1.4hadoop任务的执行方式
在Hadoop中执行分布式计算任务主要有以下几种模式,每种模式适用于不同的场景和需求。
1. 本地模式(Local Mode)
描述:在单机上模拟Hadoop环境,无需启动HDFS或YARN,直接运行MapReduce或其他计算任务。
适用场景:开发调试、单元测试。
特点:无需集群,资源开销小,使用本地文件系统而非HDFS。
2. 伪分布式模式(Pseudo-Distributed Mode)
描述:在单机模拟完整的Hadoop集群(HDFS/YARN),所有守护进程(如NameNode、DataNode)运行在同一台机器。
适用场景:学习Hadoop原理或小规模功能验证。
特点:配置与真实集群一致,但性能受限于单机资源。需手动启动HDFS/YARN服务。
3. 完全分布式模式(Fully Distributed Mode)
描述:标准的Hadoop集群模式,任务分布在多台物理或虚拟节点上执行。
适用场景:生产环境的大规模数据处理。
特点:依赖HDFS存储数据和YARN调度资源。支持高可用(HA)和容错。
4. 通过YARN REST API远程提交
描述:通过YARN提供的RESTful API(如ResourceManager Web API)远程提交任务。
适用场景:需要集成到外部系统(如Web应用或自动化工具)。
特点:支持JSON/XML格式的请求。需处理认证(如Kerberos)。
流程:获取YARN的Application ID,上传JAR包至HDFS,通过API提交任务描述(如资源需求、主类名)。
5. 通过Oozie调度工作流
描述:使用Apache Oozie编排复杂的多步骤任务(如MapReduce→Hive→Sqoop)。
适用场景:定时任务或依赖关系复杂的流水线。
特点:支持基于时间或数据依赖的触发。通过XML定义工作流(workflow.xml )。
6. 通过HUE(Hadoop User Experience)可视化提交
描述:通过Web界面(如HUE)上传JAR包并配置参数。
适用场景:非技术用户或快速原型设计。
特点:图形化操作,无需命令行。支持Hive、Pig等工具的集成。
7. 使用第三方工具(如Apache Spark/Flink)
描述:通过其他计算框架(如Spark)调用Hadoop集群资源。
适用场景:需要更低延迟或迭代计算。
特点:Spark可通过yarn-cluster模式提交任务。需额外配置依赖(如spark-submit)。
8. 通过DistributedShell运行非MapReduce任务
描述:使用YARN的DistributedShell直接在容器中执行Shell脚本或命令。
适用场景:非Java任务或简单并行化脚本。
总结对比表
| 模式 | 依赖组件 | 适用场景 | 复杂度 |
|---|---|---|---|
| 本地模式 | 无 | 开发调试 | 低 |
| 伪分布式 | HDFS/YARN | 学习测试 | 中 |
| 完全分布式 | 完整集群 | 生产环境 | 高 |
| YARN REST API | ResourceManager | 系统集成 | 高 |
| Oozie | Oozie Server | 工作流调度 | 中高 |
其中Spark/Flink后续文章讲解,本文第二部分的代码实战将采用完全分布式方式运行。
二、查找素数代码实战
2.1需求分析
所谓素数就是只能被1和自身整除的正整数,素数是数学中最古老且神秘的研究对象之一,其研究历程贯穿了数千年的人类文明。从欧几里得首次证明素数无限性,到黎曼猜想试图揭示质数分布规律,再到现代密码学中素数的核心应用,其意义不断拓展。目前已知的最大素数是梅森素数,其发现推动了分布式计算技术的发展。
寻找新素数的意义主要体现在三个方面:
理论突破:素数分布规律仍是未解之谜,新素数的发现可能为黎曼猜想等难题提供线索;
技术革新:大素数在加密算法中具有不可替代性,新素数能提升密码系统安全性;
计算挑战:素数搜索需要高性能计算,这一过程促进了算法优化和硬件发展。
本文就通过查找100万以内的素数来实战一下Hadoop计算任务编程。
2.2 先运行起来
为了更好的理解其中的代码,我们先运行起来。如前文所述,我们采用和实际生产环境完全一致的方式,集群模式来运行,关于集群模式的搭建和配置,参见我上一篇文章。
先把开发生成的jar包上传到Hadoop集群的主节点机内
1、HDFS的常用操作
前文讲过,Hadoop的计算任务是HDFS的输入数据文件的,因此第一步是生成数据文件,并上传进HDFS
在当前路径下,生成一个文本的输入数据文件,从1到1000000,每一行一个数字
seq 1 1000000 >input.txt
将输入数据文件上传至HDFS的用户路径下
hdfs dfs -put input.txt /user/root/input.txt
注意,如果文件已经存在于HDFS中,上面指令会失败
要先检查一下是否已经存在
hdfs dfs -ls /user/root/input.txt
如果已经存在,用下面的命令先删除,再上传
hdfs dfs -rm /user/root/input.txt
常用HDFS 操作指令
| 指令 | 功能 | 示例 |
|---|---|---|
| hdfs dfs -ls <路径> | 列出目录内容 | hdfs dfs -ls /user |
| hdfs dfs -mkdir [-p] <路径> | 创建目录(-p递归创建) | hdfs dfs -mkdir -p /data/input |
| hdfs dfs -put <本地路径> <HDFS路径> | 上传文件 | hdfs dfs -put local.txt /data/ |
| hdfs dfs -get <HDFS路径> <本地路径> | 下载文件 | hdfs dfs -get /data/output ./ |
| hdfs dfs -cat <文件路径> | 查看文件内容 | hdfs dfs -cat /data/log.txt |
| hdfs dfs -rm [-r] [-skipTrash] <路径> | 删除文件/目录(-r递归删除,-skipTrash跳过回收站) | hdfs dfs -rm -r /tmp/old |
| hdfs dfs -cp <源路径> <目标路径> | 复制文件 | hdfs dfs -cp /data/a.txt /backup/ |
| hdfs dfs -mv <源路径> <目标路径> | 移动/重命名文件 | hdfs dfs -mv /data/old.txt /data/new.txt |
| hdfs dfs -chmod [-R] <权限> <路径> | 修改权限(-R递归) | hdfs dfs -chmod 755 /data/script.sh |
| hdfs dfs -chown [-R] <用户:组> <路径> | 修改所有者 | hdfs dfs -chown hdfs:analysts /data/report |
| hdfs dfs -stat <格式> <路径> | 查看文件状态(如 %o块大小) | hdfs dfs -stat "%o" /data/file |
| hdfs dfs -df [-h] | 查看HDFS空间使用(-h人性化显示) | hdfs dfs -df -h |
| hdfs dfs -du [-h] [-s] <路径> | 统计大小(-s汇总,-h易读格式) | hdfs dfs -du -h /user/hive |
| hdfs dfs -count [-q] <路径> | 统计文件/目录数量(-q配额信息) | hdfs dfs -count /data |
有些读者可能注意到了,hdfs dfs 指令完成的功能,有的资料用的是 hadoop fs,它们有什么区别和联系呢?
原来,在Hadoop中,hadoop fs 和 hdfs dfs 是指令的两种形式,它们的功能高度重叠但存在一些细微区别。
两者均可操作HDFS文件系统,绝大多数指令(如ls、put、get)完全通用。均通过File System API与HDFS交互,最终执行效果一致。
| 对比项 | hadoop fs | hdfs dfs |
|---|---|---|
| 适用范围 | 支持多种文件系统(如HDFS、S3、本地文件系统) | 仅针对HDFS文件系统操作 |
| 语义明确性 | 更通用,但需依赖配置指定文件系统类型 | 直接明确操作HDFS,避免歧义 |
| 版本兼容性 | 所有Hadoop版本均支持 | Hadoop 2.x+引入,推荐在新版本中使用 |
要优先使用 hdfs dfs,当明确操作HDFS时(如集群管理),使用hdfs dfs更直观且减少配置依赖。
仅当需要跨文件系统操作(如从本地拷贝到S3),旧脚本兼容(Hadoop 1.x)考虑hadoop fs。
在Hadoop 2.x/3.x中,操作HDFS时优先使用hdfs dfs以提升可读性。
2、运行查找素数计算
上传完成输入数据文件后,执行下面的指令,运行我们的运算任务
hadoop jar primenumber-2.7.3.jar PrimeNumberHadoop input.txt output
指令 hadoop jar :运行MapReduce计算任务jar包
参数1 primenumber-2.7.3.jar:用户编制的计算任务打的jar包
参数2 PrimeNumberHadoop: jar包的Main方法入口类名称
参数3 input.txt: 计算任务输入HDFS文件名
参数4 output: 计算结果输出HDFS目录名称
经过一段时间等待,集群主节点终端输出如下
作业配置完成,开始提交
25/08/08 04:30:50 INFO client.RMProxy: Connecting to ResourceManager at hadoop-server-00/192.168.200.25:8032
25/08/08 04:30:52 WARN mapreduce.JobResourceUploader: Hadoop command-line option parsing not performed. Implement the Tool interface and execute your application with ToolRunner to remedy this.
25/08/08 04:30:55 INFO input.FileInputFormat: Total input paths to process : 1
25/08/08 04:30:56 INFO mapreduce.JobSubmitter: number of splits:1
25/08/08 04:30:56 INFO mapreduce.JobSubmitter: Submitting tokens for job: job_1754594692226_0002
25/08/08 04:30:57 INFO impl.YarnClientImpl: Submitted application application_1754594692226_0002
25/08/08 04:30:58 INFO mapreduce.Job: The url to track the job: http://hadoop-server-00:8088/proxy/application_1754594692226_0002/
25/08/08 04:30:58 INFO mapreduce.Job: Running job: job_1754594692226_0002
25/08/08 04:31:52 INFO mapreduce.Job: Job job_1754594692226_0002 running in uber mode : false
25/08/08 04:31:52 INFO mapreduce.Job: map 0% reduce 0%
25/08/08 04:32:16 INFO mapreduce.Job: map 12% reduce 0%
25/08/08 04:32:19 INFO mapreduce.Job: map 34% reduce 0%
25/08/08 04:32:22 INFO mapreduce.Job: map 48% reduce 0%
25/08/08 04:32:25 INFO mapreduce.Job: map 64% reduce 0%
25/08/08 04:32:26 INFO mapreduce.Job: map 100% reduce 0%
25/08/08 04:32:43 INFO mapreduce.Job: map 100% reduce 100%
25/08/08 04:32:47 INFO mapreduce.Job: Job job_1754594692226_0002 completed successfully
25/08/08 04:32:48 INFO mapreduce.Job: Counters: 49
File System Counters
FILE: Number of bytes read=863484
FILE: Number of bytes written=1964433
FILE: Number of read operations=0
FILE: Number of large read operations=0
FILE: Number of write operations=0
HDFS: Number of bytes read=6889009
HDFS: Number of bytes written=538468
HDFS: Number of read operations=6
HDFS: Number of large read operations=0
HDFS: Number of write operations=2
Job Counters
Launched map tasks=1
Launched reduce tasks=1
Data-local map tasks=1
Total time spent by all maps in occupied slots (ms)=29470
Total time spent by all reduces in occupied slots (ms)=15083
Total time spent by all map tasks (ms)=29470
Total time spent by all reduce tasks (ms)=15083
Total vcore-milliseconds taken by all map tasks=29470
Total vcore-milliseconds taken by all reduce tasks=15083
Total megabyte-milliseconds taken by all map tasks=30177280
Total megabyte-milliseconds taken by all reduce tasks=15444992
Map-Reduce Framework
Map input records=1000000
Map output records=78498
Map output bytes=706482
Map output materialized bytes=863484
Input split bytes=113
Combine input records=0
Combine output records=0
Reduce input groups=78498
Reduce shuffle bytes=863484
Reduce input records=78498
Reduce output records=78498
Spilled Records=156996
Shuffled Maps =1
Failed Shuffles=0
Merged Map outputs=1
GC time elapsed (ms)=1077
CPU time spent (ms)=28570
Physical memory (bytes) snapshot=442507264
Virtual memory (bytes) snapshot=4190257152
Total committed heap usage (bytes)=293601280
Shuffle Errors
BAD_ID=0
CONNECTION=0
IO_ERROR=0
WRONG_LENGTH=0
WRONG_MAP=0
WRONG_REDUCE=0
File Input Format Counters
Bytes Read=6888896
File Output Format Counters
Bytes Written=538468
任务总耗时: 117310毫秒 (117.31秒)
然后执行下面的命令查看HDFS中的结果输出目录内容
hdfs dfs -ls /user/root/output
结果如下
-rw-r--r-- 2 root supergroup 0 2025-08-08 04:32 /user/root/output/_SUCCESS
-rw-r--r-- 2 root supergroup 538468 2025-08-08 04:32 /user/root/output/part-r-00000
其中形如part-r-XXXXX的文件就是结果数据文件,若有多个 Reducer,会生成 part-r-00001、part-r-00002 等,由于我们的输入数据文件很小,仅使用一个reduce就足够了。我们把它从HDFS拉到本地
hdfs dfs -get /user/root/output/part-r-00000 output.txt
从文件中得知,我们找到了100万范围内的素数78498个。
2.3 编程实现解析
1、项目结构
使用你熟悉的java开发IDE,新建一个Maven项目,在pom.xml中引入如下依赖等关键配置
.........................................................................
<groupId>com.example.hadoop</groupId>
<artifactId>primenumber</artifactId>
<version>2.7.3</version>
<name>primenumber</name>
.........................................................................
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
.........................................................................
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-jobclient</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-jobclient</artifactId>
<version>${project.version}</version>
<scope>test</scope>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>${project.version}</version>
<scope>test</scope>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>${project.version}</version>
<scope>test</scope>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-yarn-server-tests</artifactId>
<version>${project.version}</version>
<scope>test</scope>
<type>test-jar</type>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-app</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-app</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.jersey.jersey-test-framework</groupId>
<artifactId>jersey-test-framework-grizzly2</artifactId>
<version>1.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-hs</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>11.0.2</version>
<scope>provided</scope>
</dependency>
.........................................................................
要点:java 编译级别要和hadoop集群上安装的java环境一致,依赖包要和hadoop版本匹配兼容。
计算任务的结构,如下图:
整个计算程序只有一个类文件PrimeNumberHadoop:
其静态内部类PrimeMapper是org.apache.hadoop.mapreduce.Mapper的子类,负责完成map方法;
其静态内部类PrimeReducer是org.apache.hadoop.mapreduce.Reducer的子类,负责完成reduce方法;
其main方法就是整个程序的入口。
就这么简单!
2、main代码解析
main方法
public static void main(String[] args) throws Exception {
// 1. 创建Hadoop配置对象,用于加载集群配置
Configuration conf = new Configuration();
// 2. 初始化MapReduce作业实例,指定配置和作业名称
Job job = Job.getInstance(conf, "Prime Number Finder");
// 3. 设置作业的主类(通过该类找到JAR包路径)
job.setJarByClass(PrimeNumberHadoop.class);
// 4. 设置自定义Mapper类
job.setMapperClass(PrimeMapper.class);
// 5. 设置自定义Reducer类
job.setReducerClass(PrimeReducer.class);
// 6. 设置Map阶段输出的键类型(素数数值)
job.setOutputKeyClass(LongWritable.class);
// 7. 设置Map阶段输出的值类型(是否为素数的标记)
job.setOutputValueClass(BooleanWritable.class);
// 8. 设置输入格式为文本文件格式
job.setInputFormatClass(TextInputFormat.class);
// 9. 设置输出格式为文本文件格式
job.setOutputFormatClass(TextOutputFormat.class);
// 10. 打印作业配置状态日志
System.out.println("作业配置完成,开始提交");
// 11. 设置输入文件路径(从命令行参数args[0]获取)
FileInputFormat.addInputPath(job, new Path(args[0]));
// 12. 设置输出文件路径(从命令行参数args[1]获取)
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 13. 记录任务开始时间(毫秒级)
long startTime = System.currentTimeMillis();
// 14. 提交作业并等待完成,参数true表示打印详细执行日志
boolean success = job.waitForCompletion(true);
// 15. 记录任务结束时间并计算总耗时
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 16. 打印任务耗时(同时显示毫秒和秒两种单位)
System.out.println("任务总耗时: " + duration + "毫秒 (" +
String.format("%.2f", duration / 1000.0) + "秒)");
// 17. 根据作业执行结果退出程序(0表示成功,1表示失败)
System.exit(success ? 0 : 1);
}
job.setOutputKeyClass 要点说明:
在Hadoop MapReduce中,job.setOutputKeyClass()支持多种键类型,这些类型均实现了WritableComparable接口以支持序列化和排序。以下是常用键类型及其应用场景:
一般类型
| 键类型 | 对应Java类型 | 应用场景示例 |
|---|---|---|
| IntWritable | int | 小范围整数数据(如计数、数组索引) |
| LongWritable | long | 大范围整数数据(如ID、时间戳、大数值) |
| FloatWritable | float | 单精度浮点数据(如温度、评分) |
| DoubleWritable | double | 双精度浮点数据(如科学计算、经纬度) |
| BooleanWritable | boolean | 二值判断场景(如标记是否有效) |
| Text | String | 文本数据(如日志中的URL、用户名) |
| BytesWritable | byte[] | 二进制数据(如图片、视频) |
| NullWritable | null | 无键值对场景(如只进行排序或聚合) |
特殊类型
| 键类型 | 特点 | 应用场景 |
|---|---|---|
| NullWritable | 无实际值(单例模式) | 仅需值数据的场景(如纯输出值) |
| BytesWritable | 字节数组,可存储任意二进制数据 | 二进制文件处理(如图像、序列化对象) |
| VIntWritable | 可变长度整数,节省小数值存储空间 | 稀疏整数数据集(如ID列表) |
| VLongWritable | 可变长度长整数,优化存储效率 | 大范围稀疏数值(如时间戳序列) |
| ArrayWritable | 存储同类型Writable数组 | 批量处理相同类型数据(如多个数值) |
| MapWritable | 存储键值对集合(类似HashMap) | 非结构化数据(如JSON-like结构) |
| ObjectWritable | 包装任意Java对象(需自定义序列化) | 复杂业务对象(如自定义POJO) |
当内置类型无法满足需求时,可通过实现WritableComparable接口定义组合键,例如:
public class CompositeKey implements WritableComparable<CompositeKey> {
private Text category;
private LongWritable timestamp;
// 实现compareTo、write、readFields方法
}
自定义组合键的应用场景是需多字段排序(如先按类别排序,再按时间戳排序)。
那这么多的键类型的选择建议是
-
数值计算:优先使用LongWritable/IntWritable(整数)或DoubleWritable(浮点数)
-
文本处理:Text是字符串键的标准选择
-
存储优化:小整数用VIntWritable,大整数用LongWritable
-
复杂数据:优先考虑ArrayWritable或自定义组合键,避免过度使用MapWritable(序列化开销大)
当前代码中使用LongWritable作为素数数值的键类型,正是利用了其支持大范围整数且排序高效的特性,适合素数判定这类数值计算场景。
job.setOutputValueClass 要点说明
在Hadoop MapReduce中,job.setOutputValueClass()用于指定Map或Reduce阶段输出值的数据类型。除了当前使用的BooleanWritable判断是否素数外,常用的值类型及其应用场景如下:
基础数据类型
| 值类型 | 对应Java类型 | 应用场景示例 |
|---|---|---|
| BooleanWritable | boolean | 二值判断(如是否为素数、是否包含关键词) |
| IntWritable | int | 计数统计(如单词出现次数、用户访问量) |
| LongWritable | long | 大数值计算(如文件大小、时间戳差值) |
| FloatWritable | float | 精度要求不高的浮点数据(如百分比) |
| DoubleWritable | double | 高精度浮点数据(如平均值、经纬度) |
| Text | String | 文本内容(如日志详情、描述信息) |
特殊用途类型
| 值类型 | 特点 | 应用场景 |
|---|---|---|
| NullWritable | 空值标记(无实际数据) | 仅需输出键的场景(如去重结果) |
| BytesWritable | 二进制数据容器 | 存储序列化对象、图片等二进制数据 |
| VIntWritable | 可变长度整数(节省存储空间) | 小整数集合(如标签ID列表) |
| VLongWritable | 可变长度长整数 | 大范围数值集合(如多个时间戳) |
| ArrayWritable | 存储同类型Writable数组 | 批量数值(如多个传感器读数) |
| MapWritable | 键值对集合(类似HashMap) | 非结构化数据(如用户属性集合) |
| ObjectWritable | 包装任意Java对象(需自定义序列化) | 复杂业务对象(如订单信息) |
| PairWritable | 存储两个关联值(键值对) | 需同时传递两个相关值(如键+计数) |
当内置类型无法满足需求时,可通过实现Writable接口自定义值类型,例如:
public class UserInfoWritable implements Writable {
private Text name;
private IntWritable age;
private DoubleWritable score;
// 实现write()和readFields()方法
}
自定义的值类型的应用场景是需传递多字段组合数据(如用户完整信息)。
那这么多的值类型的选择建议是
-
简单标记:BooleanWritable适合二值状态场景(如当前素数判断)
-
数值计算:优先使用IntWritable/LongWritable(整数)或DoubleWritable(浮点数)
-
文本数据:Text是字符串值的标准选择
-
存储优化:小数值用VIntWritable/VLongWritable
-
复杂数据:优先考虑ArrayWritable或自定义类型,避免过度使用MapWritable(序列化效率较低)
当前代码中使用BooleanWritable作为值类型,正是利用其轻量级特性标记数值是否为素数,符合"是/否"二值判断的场景需求。
job.setInputFormatClass 要点说明
在Hadoop MapReduce中,job.setInputFormatClass()用于指定输入数据的格式解析器。除了当前代码中使用的TextInputFormat外,常用的输入类型及其应用场景如下:
基础输入格式
| 输入格式类 | 特点 | 应用场景 |
|---|---|---|
| TextInputFormat | 默认格式,按行读取文本,键为偏移量 | 普通文本文件(如日志、CSV) |
| KeyValueTextInputFormat | 每行按分隔符分割为键值对(默认\t) | 键值对结构文本(如配置文件) |
| NLineInputFormat | 固定行数为一个Split(可配置) | 需要按行分组处理的场景(如批量任务) |
特殊输入格式
| 输入格式类 | 特点 | 应用场景 |
|---|---|---|
| SequenceFileInputFormat | 读取Hadoop二进制序列文件(键值对) | MapReduce中间结果存储、高效IO场景 |
| SequenceFileAsTextInputFormat | 将SequenceFile转换为文本键值对 | 需要文本化查看二进制数据的场景 |
| FixedLengthInputFormat | 读取固定长度记录的二进制文件 | 结构化二进制数据(如金融交易记录) |
| CombineFileInputFormat | 合并小文件为一个Split,减少Map任务数 | 大量小文件场景(如日志文件合并) |
| MultipleInputs | 支持多路径、多格式混合输入 | 异构数据源合并(如文本+数据库) |
| XMLInputFormat | 按XML标签提取记录(需指定开始/结束标签) | XML文件解析(如RSS订阅、配置文件) |
| DBInputFormat | 从关系型数据库读取数据 | 数据库数据导入Hadoop(如MySQL表) |
| MongoDBInputFormat | 从MongoDB读取文档数据(第三方库) | NoSQL数据库集成 |
| AvroKeyInputFormat | 读取Avro序列化文件 | 大数据生态系统数据交换(如Kafka) |
选择建议
-
普通文本:优先使用TextInputFormat(默认)或KeyValueTextInputFormat(键值对文本)
-
性能优化:小文件场景用CombineFileInputFormat,中间结果用SequenceFileInputFormat
-
结构化数据:固定长度用FixedLengthInputFormat,XML用XMLInputFormat
-
跨系统集成:数据库用DBInputFormat,多源数据用MultipleInputs
当前代码使用TextInputFormat处理普通文本输入,适合素数判定任务中读取纯数字文本的场景。若需处理其他格式数据(如二进制文件或数据库表),可根据上述场景选择对应输入格式。
job.setOutputFormatClass 要点说明
在Hadoop MapReduce中,job.setOutputFormatClass()用于指定输出数据的格式和存储方式。除了当前使用的TextOutputFormat外,常用的输出类型及其应用场景如下:
基础文本输出格式
| 输出格式类 | 特点 | 应用场景 |
|---|---|---|
| TextOutputFormat | 默认格式,键值对用\t分隔,文本存储 | 通用文本输出(如日志、报告) |
| KeyValueTextOutputFormat | 键值对用指定分隔符(默认\t),可自定义 | 需要特定分隔符的文本输出(如CSV) |
| LazyOutputFormat | 延迟创建输出文件(空目录不生成) | 可能无输出结果的场景(如过滤任务) |
特殊输出格式
| 输出格式类 | 特点 | 应用场景 |
|---|---|---|
| SequenceFileOutputFormat | 二进制键值对序列文件,高效压缩存储 | MapReduce中间结果、后续处理输入 |
| SequenceFileAsBinaryOutputFormat | 纯二进制序列文件,无键值对结构 | 原始二进制数据存储(如加密内容) |
| MapFileOutputFormat | 排序的SequenceFile,支持快速查找 | 需要随机访问的场景(如字典数据) |
| MultipleOutputs | 支持多路径、多格式输出 | 分类输出(如按日期/类型拆分文件) |
| DBOutputFormat | 直接写入关系型数据库表 | 结果回写数据库(如统计结果入库) |
| NullOutputFormat | 不产生任何输出文件 | 仅需副作用的场景(如数据清洗后上传) |
| AvroOutputFormat | 写入Avro序列化文件(带Schema) | 大数据生态系统交换(如Hive集成) |
| ParquetOutputFormat | 列式存储格式,高效压缩和查询 | 数据分析场景(如Spark、Impala查询) |
| ORCFileOutputFormat | 优化的行列存储格式,Hive原生支持 | Hive表数据存储、BI工具分析 |
选择建议
-
通用文本:TextOutputFormat(默认)或KeyValueTextOutputFormat(自定义分隔符)
-
性能优先:中间结果用SequenceFileOutputFormat,需随机访问用MapFileOutputFormat
-
分类输出:多路径场景用MultipleOutputs,空结果用LazyOutputFormat
-
数据仓库:列式存储用ParquetOutputFormat或ORCFileOutputFormat
-
跨系统集成:数据库用DBOutputFormat,大数据生态用AvroOutputFormat
当前代码使用TextOutputFormat输出素数结果,适合人类可读的文本场景。若需后续MapReduce处理,建议改用SequenceFileOutputFormat以提高IO效率;若需入库分析,可选择ParquetOutputFormat或DBOutputFormat。
3、map代码解析
静态内部类的完整代码如下:
public static class PrimeMapper
extends Mapper<LongWritable, Text, LongWritable, BooleanWritable> {
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
System.out.println("Map阶段处理输入: " + value.toString());
long num = Long.parseLong(value.toString());
boolean isPrime = isPrime(num);
if (isPrime) {
context.write(new LongWritable(num), new BooleanWritable(true));
}
}
private boolean isPrime(long n) {
if (n <= 1)
return false;
if (n <= 3)
return true;
if (n % 2 == 0 || n % 3 == 0)
return false;
for (long i = 5; i * i <= n; i += 6) {
if (n % i == 0 || n % (i + 2) == 0)
return false;
}
return true;
}
}
Mapper<LongWritable, Text, LongWritable, BooleanWritable> 父类传参要点说明
父类Mapper的代码如下:
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
public Mapper() { }
protected void setup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException { }
protected void map(KEYIN key, VALUEIN value, Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
context.write(key, value);
}
protected void cleanup(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException { }
public void run(Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException { this.setup(context);
try {
while(context.nextKeyValue()) {
this.map(context.getCurrentKey(), context.getCurrentValue(), context);
}
} finally {
this.cleanup(context);
}
}
}
可见其有四个泛型参数:KEYIN, VALUEIN, KEYOUT, VALUEOUT,分别代表输入输出的数据类型,具体含义如下:
- KEYIN
含义:输入键的数据类型
来源:由InputFormat决定,默认是LongWritable
作用:通常表示输入文件中数据的字节偏移量(如第100字节开始的记录)
示例:在PrimeNumberHadoop中对应LongWritable(行偏移量)
- VALUEIN
含义:输入值的数据类型
来源:由InputFormat决定,默认是Text
作用:表示输入的实际数据内容(如文件中的一行文本)
示例:在PrimeNumberHadoop中对应Text(输入的数字字符串)
- KEYOUT
含义:输出键的数据类型
来源:由用户业务逻辑决定
作用:作为Map阶段输出的键值对中的键,将被传递给Reducer
示例:在PrimeNumberHadoop中对应LongWritable(待判断的数字)
- VALUEOUT
含义:输出值的数据类型
来源:由用户业务逻辑决定
作用:作为Map阶段输出的键值对中的值,与KEYOUT共同组成中间结果
示例:在PrimeNumberHadoop中对应BooleanWritable(是否为素数的判断结果)
所有类型必须是Hadoop序列化类型(实现Writable接口),不能使用Java原生类型,实际类型由job.setInputFormatClass()和业务逻辑共同决定,键值对的设计需考虑数据倾斜和Reducer负载均衡。
所谓数据倾斜是分布式计算中常见的性能问题,指数据在各计算节点上分布不均匀,导致部分节点负载过重而其他节点资源闲置的现象。
map方法要点解析
你可以简单地理解为,hadoop每读到输入文件中的一行就调用一次map方法,在Hadoop MapReduce的map方法中,三个参数分别承担不同的数据传递和框架交互职责,具体作用如下:
- LongWritable key
类型:Hadoop的长整型序列化类型
来源:由InputFormat定义(默认是TextInputFormat)
核心作用:表示输入数据在文件中的字节偏移量(从文件开头算起的字节位置)
具体示例:若输入文件某一行从第1024字节开始,则处理到这一行时,传入的key的值为1024
在本例的PrimeNumberHadoop中该参数未被实际使用(素数判断仅依赖输入值),但框架必须传递此参数。
- Text value
类型:Hadoop的字符串序列化类型
来源:由InputFormat定义(默认是TextInputFormat)
核心作用:存储实际待处理的业务数据(对应文件中的一行文本)
String numStr = value.toString(); // 将Text转换为Java字符串
long number = Long.parseLong(numStr); // 解析为长整型数字
boolean isPrime = isPrime(number); // 判断是否为素数
在PrimeNumberHadoop中的关键作用:承载待判断是否为素数的原始数字字符串
- Context context
类型:Hadoop的上下文对象(org.apache.hadoop.mapreduce.Mapper.Context)
核心作用:作为Map任务与Hadoop框架之间的通信桥梁
主要功能:
1、输出键值对:
context.write(new LongWritable(number), new BooleanWritable(isPrime));
2、获取作业配置:
Configuration conf = context.getConfiguration();
String threshold = conf.get("prime.threshold"); // 读取自定义配置
3、进度跟踪:自动向框架汇报任务进度
4、计数器功能:统计处理记录数
context.getCounter("PrimeCounter", "TOTAL_NUMBERS").increment(1);
参数传递关系图
输入文件 → InputFormat → Key(偏移量)/Value(数据) → map()方法 → Context → Reducer
↑ ↑ ↓
└── 文件系统 └── 业务逻辑处理 输出结果到HDFS
key和value的类型必须与Mapper类定义的泛型参数匹配(KEYIN, VALUEIN),Context是线程安全的,但在多线程环境下需注意输出顺序,在本例PrimeNumberHadoop场景中,value的格式直接决定素数判断的准确性,需确保输入是纯数字字符串。
计算素数isPrime方法解析
private boolean isPrime(long n) {
if (n <= 1)
return false;
if (n <= 3)
return true;
if (n % 2 == 0 || n % 3 == 0)
return false;
for (long i = 5; i * i <= n; i += 6) {
if (n % i == 0 || n % (i + 2) == 0)
return false;
}
return true;
}
这个方法是典型的发现素数的数学方法,基于以下原理
1、素数定义:排除n<=1的情况。
2、已知小素数:2和3直接返回true。
3、排除2和3的倍数。
4、大于3的素数必为6k±1的形式,因此只需检查这种形式的除数。
所有自然数可表示为 6k, 6k+1, 6k+2, 6k+3, 6k+4, 6k+5 (k为整数),其中 6k/6k+2/6k+4 是偶数,6k+3 是3的倍数,均不可能是素数,仅剩 6k+1 和 6k+5 (即 6k-1) 两种形式可能为素数。
5、只检查到平方根,减少计算量。
i * i <= n等价于 i <= √n(直接计算平方根很昂贵),若 n 存在因子,必有一个因子 ≤ √n。例如 100 的因子对 (2,50)、(4,25)、(5,20)、(10,10),其中较小因子均 ≤ 10 (√100),因此只试算到这个数的平方根即可,不用算到头。
4、reduce代码解析
静态内部类的完整代码如下:
public static class PrimeReducer
extends Reducer<LongWritable, BooleanWritable, LongWritable, NullWritable> {
@Override
protected void setup(Context context) {
System.out.println("Reducer Task " + context.getTaskAttemptID() + " 初始化完成");
}
@Override
protected void reduce(LongWritable key, Iterable<BooleanWritable> values, Context context)
throws IOException, InterruptedException {
System.out.println("Reduce阶段处理键: " + key.toString());
context.write(key, NullWritable.get());
}
}
PrimeReducer类通过Hadoop的Reduce阶段实现数据聚合,其核心逻辑是对Mapper输出的键值对进行最终处理与结果输出。结合素数判断的业务场景,其聚合实现方式如下:
Reducer的泛型定义与输入输出
public static class PrimeReducer extends Reducer<LongWritable, BooleanWritable, LongWritable, BooleanWritable> {
// ...
}
输入键类型:LongWritable(待判断的数字,继承自Mapper的输出键)
输入值类型:BooleanWritable(Mapper判断的素数结果,可能有多个相同键的结果)
输出键类型:LongWritable(最终输出的数字)
输出值类型:BooleanWritable(聚合后的素数判断结果)
核心聚合逻辑(reduce方法)
改写成如下易于理解的一般情况通用写法
protected void reduce(LongWritable key, Iterable<BooleanWritable> values, Context context) throws IOException, InterruptedException {
boolean isPrime = false;
// 遍历所有Mapper输出的结果(理论上应为单个结果)
for (BooleanWritable value : values) {
isPrime = value.get(); // 获取布尔值
break; // 素数判断结果唯一,取第一个值即可
}
context.write(key, new BooleanWritable(isPrime)); // 输出最终结果
}
聚合过程解析
- 数据输入阶段
输入来源:经过Shuffle阶段后,相同数字(Key)的所有判断结果(Value)被分组传递
特殊处理:在素数判断场景中,每个数字理论上只会被一个Mapper处理,因此values迭代器通常只有一个元素
- 聚合逻辑实现
遍历取值:通过for (BooleanWritable value : values)循环遍历同一数字的所有判断结果
结果确定:由于素数判断结果是唯一的(非素即素),直接取第一个值并跳出循环
输出结果:通过context.write()将数字及其最终素数判断结果写入HDFS
在素数判断任务中的特殊作用
去重保障:即使因数据分片异常导致同一数字被多个Mapper处理,Reducer会确保只输出一个结果
结果校验:可扩展添加校验逻辑(如判断多个结果是否一致)
for (BooleanWritable value : values) {
if (!value.get()) {
isPrime = false; // 只要有一个判断为非素数,则最终结果为非素数
break;
}
isPrime = true;
}
格式统一:标准化输出格式,为后续数据处理提供一致结构
本例 与标准聚合场景的差异
| 场景 | 传统WordCount聚合 | PrimeReducer聚合 |
|---|---|---|
| 核心操作 | 统计相同Key的数量(Sum) | 取相同Key的唯一结果(First) |
| 输入值特征 | 多个相同Key的计数值(如1,1,1) | 理论上单个Key对应单个值 |
| 处理逻辑 | 累加求和 | 直接取值或简单校验 |
性能优化点
提前终止循环:通过break减少无效迭代
避免复杂计算:素数判断的核心逻辑已在Mapper完成,Reducer仅做结果传递
低内存占用:无需缓存所有值,逐个处理即可
这种聚合实现充分适配了素数判断任务的特性——结果唯一性与低计算复杂度,同时保留了Hadoop框架的容错能力,确保在极端情况下(如数据重分片)仍能输出正确结果。
代码解析就此完成,然后利用Maven打成jar包,即可运行了。
三、hadoop典型案例分析
3.1 搜索引擎索引构建
1、需求分析
搜索引擎是用户输入一些词语,引擎返回包含这些词语的网站页面。这个需求看起来是不是很简单呢,怎么实现呢?你可能会想到,浏览器接收用户输入的词语,然后运用网络爬虫技术搜索所有网站的所有网页,然后按照爬到的内容返回给用户。
你只说对了一半,这个方案行不通的原因是每次都去爬,速度太成问题了!我们常讲以空间获取时间,因此缓存查询词语的索引才是解决问题的关键,对于这种海量的数据,hadoop就发挥了作用。
爬虫源源不断地提供给Hadoop这样的数据<网页的URL,网页的HTML内容>,Hadoop永不停歇的计算每个网页包含词汇的出现频次,并输出<关键词, {URL1:频次, URL2:频次,...,URLN:频次}>这样的计算结果,用户要搜索某一个关键词时,只需要在这个结果里搜索即可。
2、各阶段代码要点
1. 海量网页数据分片存储(HDFS)
技术实现:
网页数据按<URL, HTML内容>格式存储,每个文件块(128MB)包含约10万篇网页(假设平均每页1KB)。
分片策略:通过NLineInputFormat(见上文)确保每个Map任务处理固定行数(如1万行),避免单个Map负载不均。
冷热数据分层:高频更新的新网页存于SSD缓存池(HDFS Storage Policy设置为ALL_SSD),历史数据存于HDD。充分发挥固态硬盘的高速特性。
2. Map阶段关键词提取
处理逻辑:Map阶段用分词工具处理每一个网页,示意伪代码如下:
public class IndexMapper extends Mapper<LongWritable, Text, Text, Text> {
protected void map(LongWritable offset, Text html, Context context) {
String url = 获取网页的链接(html);
List<String> keywords = 处理分词(html); // 使用某种NLP分词库
for (String word : keywords) {
context.write(new Text(word), new Text(url + ":1")); // 输出<关键词, URL:频次1>
}
}
}
这里的性能瓶颈是分词计算,中文分词消耗CPU资源(如jieba的算法),建议通过Native库加速。
以上关于NLP(自然语言处理)中分词的内容请参阅本人在AI专栏的文章。
3. Reduce阶段倒序生成索引
聚合逻辑,输入键值对<Text(keyword), Text(url:count)>,输出键值对<Text(keyword), Text({url1:count1, url2:count2...})>示意代码如下
public class IndexReducer extends Reducer<Text, Text, Text, Text> {
protected void reduce(Text keyword, Iterable<Text> urls, Context context) {
Map<String, Integer> url计数映射= new HashMap<>();
for (Text entry : urls) {
String[] parts = entry.toString().split(":");
url计数映射.put(parts[0], url计数映射.getOrDefault(parts[0], 0) + Integer.parseInt(parts[1]));
}
context.write(keyword, new Text(url计数映射.toString())); // 输出<关键词, {URL1:频次, URL2:频次}>
}
}
3、优化及增强
3.1 Shuffle阶段专项优化方案
使用Combiner(合成器)设计
作用:在Map端局部聚合相同关键词的频次,减少传输数据量。就是在Reduce聚合之前先聚合一下。合成器是Hadoop中的一个组件,写一个Combiner有如下要点,要实现一个和Reduce的reduce方法签名完全一致的方法,然后在Drive里注册一下,它就能在shuffle到reduce之前就聚合一下,大大减少传输数据。是一种计算向数据移动思想的体现。
Combiner的设计有一定的适用条件:
-
操作需满足结合律:如求和(Sum)、计数(Count)、最大值(Max)。
-
不可用于非幂等操作:如平均值(Avg)、方差(需全局数据)。
与Reducer的关系
| 特性 | Combiner | Reducer |
|---|---|---|
| 执行位置 | Map节点本地 | 独立的Reduce节点 |
| 输入输出 | 必须与Reducer格式一致 | 最终结果聚合 |
| 调用次数 | 可能多次(溢写时触发) | 每个Key仅一次 |
Combiner的代码实现例子
public class IndexCombiner extends Reducer<Text, Text, Text, Text> {
protected void reduce(Text keyword, Iterable<Text> urls, Context context) {
int sum = 0;
for (Text url : urls) sum += Integer.parseInt(url.toString().split(":")[1]);
context.write(keyword, new Text("merged:" + sum)); // 输出局部聚合结果
}
}
然后注册在Drivre中
job.setCombinerClass(IndexCombiner.class); // 在Driver中设置
3.2 Shuffle阶段参数调优
在mapred-site.xml中调整相关参数
| 参数 | 作用 | 索引构建推荐值 |
|---|---|---|
| mapreduce.map.output.compress | Map输出压缩 | true(使用Zstandard编解码器) |
| mapreduce.job.reduce.slowstart.completedmaps | 启动Reduce的Map完成阈值 | 0.95(避免Reduce过早阻塞) |
| yarn.nodemanager.resource.memory-mb | 单节点可用内存 | 根据SSD缓存大小动态调整(如64GB) |
3.3 前沿技术增强方案
GPU分词:
将NLP分词任务卸载到GPU(需使用支持CUDA的分词库,如FasterTransformer)。
FPGA网络加速:
在Shuffle阶段使用FPGA压缩/解压缩数据。
Lucene集成:
Reduce阶段直接生成Lucene(著名流行搜索引擎)格式的Segment文件,避免二次转换。
存算分离:
原始网页存于对象存储(如S3),通过Alluxio加速HDFS兼容访问。
弹性调度:
Map阶段使用Spot实例,Reduce阶段切换为按需实例(通过YARN的Node Labels实现)。
注1:Alluxio(原Tachyon)是一款以内存为中心的分布式数据编排平台,位于计算框架(如Spark、Flink)与底层存储系统(如HDFS、S3)之间,通过统一的数据访问层提升跨存储系统的性能与效率.
注2:Spot实例是云计算服务(如AWS、阿里云等)提供的一种低成本竞价型计算资源。
到如今,搜索引擎索引构建已形成分层优化体系:
基础层:HDFS分片+MapReduce批处理保证吞吐量
加速层:Combiner/GPU/FPGA(现场可编程逻辑门阵列,一种硬件)减少Shuffle与计算耗时
存储层:Lucene格式直接落地,对接实时检索服务
实际部署时需通过jmx监控各阶段资源消耗,动态调整参数(如Combiner内存配额)。对于千亿级网页索引,建议采用Lambda架构:Hadoop处理全量数据,Spark Streaming处理增量更新。
3.2电商用户行为分析
1、需求分析
电商用户行为分析课题属于推荐系统优化范畴。其核心需求是对用户进行画像构建,统计用户历史购买频次、品类偏好、消费金额分布等等。它对实时性要求比较高,往往T+1更新数据(隔日分析),比如支持促销活动效果评估。数据规模一般情况极大,日均订单几千万+就能达到约30TB原始日志。
2、各阶段要点
1. 数据流程设计
graph LR
A[用户行为日志] --> B(实时采集)
B --> C{HDFS存储}
C --> D[MapReduce离线分析]
D --> E[数仓聚合]
E --> F(BI可视化)
2. 关键组件与逻辑
MAP阶段
// 输入: <用户ID, JSON行为日志>
public class BehaviorMapper extends Mapper<LongWritable, Text, Text, Text> {
protected void map(...) {
JSONObject log = parseJson(value);
String userId = log.get("user_id");
String behaviorType = log.get("type"); // "点击"/"采购/收藏"
context.write(new Text(userId), new Text(behaviorType + ":" + log));
}
}
Reduce阶段
// 输出: <用户ID, 聚合行为标签>
public class BehaviorReducer extends Reducer<Text, Text, Text, Text> {
protected void reduce(...) {
int purchaseCount = 0;
Set<String> viewedCategories = new HashSet<>();
for (Text record : values) {
String[] parts = record.toString().split(":");
if (parts[0].equals("purchase")) purchaseCount++;
else viewedCategories.add(parts[1]);
}
context.write(key, new Text(purchaseCount + "," + String.join(" |", viewedCategories)));
}
}
3. 优化及增强
3.1 性能优化
| 问题 | 解决方案 | 参数推荐 |
|---|---|---|
| Shuffle数据倾斜 | 使用Salting技术分散热点用户 | mapreduce.job.reduces=500 |
| 小文件过多 | 合并HDFS块(hadoop archive命令) | 目标块大小256MB(Zstd压缩) |
| UV统计内存溢出 | 改用HyperLogLog算法 | 误差率≤1%,内存节省90% |
| 跨机房数据传输 | 拓扑感知调度 | yarn.nodemanager.network.topology.awareness |
| Shuffle数据倾斜 | 动态分桶技术 | Spark 4.0 |
2. 成本控制
- 混合实例池的智能调度
Map阶段:使用Spot实例
Reduce阶段:预留实例 + 按需实例弹性伸缩(通过YARN的ResourceManager REST API动态调整)
- 存储分层的精细化设计
冷热数据分离:
| 数据层级 | 存储介质 | 访问频率 | 压缩算法 |
|---|---|---|---|
| Hot | NVMe SSD缓存 | 每日多次 | LZ4 |
| Warm | HDFS + Ozone | 每周访问 | Zstd |
| Cold | Glacier Deep Archive | 月度归档 | Brotli |
如 本文所述,Hadoop作为分布式计算与存储的核心框架,主要应用于金融风控、用户行为分析 、 电商推荐系统 等 大数据分析领域,同时在互联网企业的海量日志存储与实时处理方面发挥巨大的作用。而诸如生物基因测序、天文数据模拟等需要PB级存储的科学计算场景也出现其忙碌的身影。
HDFS通过副本机制保障数据安全,MapReduce支持任务重试体现了它的高容错性 ; 增加廉价节点轻松扩展至数千台集群体现了它的横向扩展能力 ; 生态丰富,大量衍生工具弥补了它的短板。
我们不得不看到,云原生技术(如Kubernetes)和实时计算框架(Flink)的崛起,挤压了Hadoop在部分场景的份额。
政务大数据、传统企业数字化转型等行业深耕也给Hadoop带来了机遇,YARN资源调度优化及与AI框架(TensorFlow/PyTorch)的集成探索使得Hadoop的技术迭代从未停止。
Hadoop虽面临新兴技术竞争,但其成熟度、成本优势及生态完整性使其在中长期仍是大数据基础设施的重要组成,尤其在离线批处理和混合云场景中不可替代。
本文谈及的关联工具及技术名词解释
(以在本文中的出现为序)
HBase:
HBase是Apache的Hadoop项目子项目,是一个分布式的、面向列的开源数据库。HBase是Hadoop生态系统的重要组成部分,两者通过HDFS实现数据存储的协同。
Spark:
Apache Spark是专为大规模数据处理而设计的快速通用的计算引擎。Spark通过内存计算,显著提升迭代计算、补偿hadoop实时流处理等场景的性能。
Flink:
Apache Flink是开源流处理框架,其核心是分布式数据流引擎。Hadoop作为底层存储(HDFS)或资源调度(YARN)与Flink处理实时流数据,形成批流一体的解决方案。
Oozie:
是Hadoop生态系统中的核心组件,主要用于管理和协调Hadoop生态系统内的任务调度。
Pig:
是由Yahoo!开发的Hadoop生态系统数据流编程语言,通过将声明式脚本转换为MapReduce任务链实现分布式数据处理,属于数据流语言,适合构建多阶段数据处理流程。
Hive:
是基于Hadoop的数据仓库基础构架,它利用简单的SQL语句(HQL)来查询、分析存储在HDFS中的数据,并把SQL语句转换成MapReduce程序来进行数据的处理。
Sqoop:
是Hadoop生态系统中的一款开源工具,专门用于在关系型数据库(如MySQL、Oracle)与Hadoop组件(如HDFS、Hive、HBase)之间高效传输数据。
Lucene:
Apache Lucene是一个开源的全文检索引擎工具包,提供完整的查询引擎和索引引擎,帮助开发者快速实现全文检索功能,广泛应用于Elasticsearch、Solr等现代搜索引擎的底层架构。
Alluxio:
作为虚拟分布式存储系统,统一了数据访问方式,位于计算框架(如Spark)与存储系统(如HDFS、S3)之间,为计算框架和底层存储系统搭建桥梁。
S3:
amazon(S3)是一个公开的服务,Web应用程序开发人员可以使用它存储数字资产,包括图片、视频、音乐和文档。
Lambda架构:
是一种大数据处理架构,通过结合批处理和实时处理,旨在平衡数据处理的准确性、延迟和容错性。其核心设计包含三层结构:批处理层(BatchLayer)速度层(SpeedLayer)服务层(ServingLayer)。
Salting技术:
在大数据处理领域,指通过在键(key)上添加随机值(盐值)来分散热点数据,避免数据倾斜问题。例如在分区时人为增加随机值,使热点数据分散到多个分区中,从而平衡负载。
Zstd压缩:
ZSTD是一种高性能无损压缩算法,由Facebook于2016年开源,以高压缩比、快速压缩/解压、无损性、支持实时压缩场景著称,广泛应用于大数据处理。
HyperLogLog算法:
是一种用于高效估算大数据集基数的概率性算法,通过牺牲一定精度来减少内存消耗。其核心原理基于哈希函数映射和概率统计,适用于独立访客统计、活跃用户数统计等场景。
Ozone存储:
是HDFS的扩展组件,提供对象存储功能,兼容S3协议和HDFS RPC协议,支持混合云场景。两者结合可满足更复杂的数据存储需求。
Glacier Deep Archive:
是Amazon提供的一种云存储服务,专为长期保存极少访问的数据设计。其核心优势在于成本极低,适用于需要长期保留数据但访问频率极低的场景,如金融、医疗、媒体等行业。
Kafka:
作为分布式事件流平台,核心能力是实时捕获、缓冲和分发数据流,支持每秒百万级消息吞吐,适用于日志聚合、实时监控等场景。Kafka和Hadoop是大数据生态系统中互补的分布式系统,前者专注实时数据流处理,后者专精批量数据存储与计算,两者通过数据管道实现协同工作。