Spark 重要概念及工作原理详解
基于 JavaWordCount 项目的完整梳理
目录
核心概念
1. Spark 是什么?
Spark 是一个分布式计算框架,用于处理大规模数据。
特点:
✅ 分布式:数据和计算分散在多台机器上
✅ 内存计算:充分利用内存,比 Hadoop MapReduce 快 100 倍
✅ 容错性:支持节点失败自动恢复
✅ 易用性:提供高级 API(RDD、DataFrame、SQL)
2. 核心组件
| 组件 | 说明 |
|---|---|
| RDD | 弹性分布式数据集,Spark 的基础数据结构 |
| DataFrame | 带有列名和类型的分布式表格 |
| Executor | 执行任务的工作进程 |
| Driver | 协调和调度任务的主进程 |
| Partition | 数据分片,并行处理的基本单位 |
| Task | 在单个分区上执行的计算单元 |
3. 关键术语
┌─────────────────────────────────────────────────────────┐
│ Spark 应用 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Job 1: 从数据加载到第一个 Action │ │
│ │ ├─ Stage 1: 分词 (flatMap + filter) │ │
│ │ ├─ Stage 2: 映射 (mapToPair) │ │
│ │ └─ Stage 3: 聚合 (reduceByKey) + Shuffle │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Job 2: 排序 │ │
│ │ ├─ Stage 4: 交换 key-value (mapToPair) │ │
│ │ ├─ Stage 5: 排序 (sortByKey) + Shuffle │ │
│ │ └─ Stage 6: 交换回来 (mapToPair) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Job 3: 收集结果 │ │
│ │ └─ Stage 7: collect() 到 Driver │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
术语解释:
- Job:由一个 Action 触发的完整计算流程
- Stage:Job 中的一个阶段,由 Shuffle 边界划分
- Task:在单个分区上执行的最小计算单元
架构设计
1. 主从架构
┌─────────────────────────────────────────────────────────────┐
│ Spark 集群 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Driver 进程(主节点) │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ SparkContext / SparkSession │ │ │
│ │ │ - 解析用户代码 │ │ │
│ │ │ - 生成执行计划 │ │ │
│ │ │ - 调度 Task 到 Executor │ │ │
│ │ │ - 监控 Executor 状态 │ │ │
│ │ │ - 收集结果 │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ (通过网络通信) │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Executor 进程(工作节点) │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ Executor 1 (4GB, 2 核) │ │ │
│ │ │ - 执行 Task │ │ │
│ │ │ - 管理内存 │ │ │
│ │ │ - 缓存数据 │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ Executor 2 (4GB, 2 核) │ │ │
│ │ │ - 执行 Task │ │ │
│ │ │ - 管理内存 │ │ │
│ │ │ - 缓存数据 │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ │ ... (更多 Executor) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
2. 部署模式
Local 模式(本地开发)
// 在单台机器上运行,用于开发测试
SparkConf conf = new SparkConf()
.setMaster("local[4]"); // 4 个线程模拟 4 个 Executor
Standalone 模式(独立集群)
# 需要手动部署 Spark 集群
spark-submit --master spark://master:7077 \
--executor-memory 4G \
--executor-cores 2 \
--num-executors 10 \
app.jar
YARN 模式(生产环境)
# 由 YARN 管理资源,推荐用于生产环境
spark-submit --master yarn \
--deploy-mode cluster \
--executor-memory 4G \
--executor-cores 2 \
--num-executors 20 \
app.jar
3. 一个 Executor 对应一台机器吗?
答案:不一定
一台机器可以有多个 Executor:
Machine 1 (16GB 内存, 8 核)
├─ Executor 1 (4GB, 2 核)
├─ Executor 2 (4GB, 2 核)
├─ Executor 3 (4GB, 2 核)
└─ Executor 4 (4GB, 2 核)
→ 1 台机器 = 4 个 Executor
4. 一个 task 一般是由一个 executor 去执行吧?
不完全是。
🔄 Task 和 Executor 的关系
简单答案:
- 一个 Executor 可以执行多个 Task(不是一对一)
- 一个 Task 只能由一个 Executor 执行(一对一)
┌─────────────────────────────────────────────────────────────┐
│ Executor 和 Task 的关系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Executor 1 (4GB, 2 核) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 线程池(2 个核心 = 2 个线程) │ │
│ │ │ │
│ │ 时间轴 → │ │
│ │ ├─ 线程 1: [Task 1-1] [Task 1-3] [Task 1-5] ... │ │
│ │ └─ 线程 2: [Task 1-2] [Task 1-4] [Task 1-6] ... │ │
│ │ │ │
│ │ 特点: │ │
│ │ ✅ 一个 Executor 有多个线程 │ │
│ │ ✅ 每个线程可以顺序执行多个 Task │ │
│ │ ✅ 多个线程可以并行执行不同的 Task │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Executor 2 (4GB, 2 核) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 线程池(2 个核心 = 2 个线程) │ │
│ │ │ │
│ │ 时间轴 → │ │
│ │ ├─ 线程 1: [Task 2-1] [Task 2-3] [Task 2-5] ... │ │
│ │ └─ 线程 2: [Task 2-2] [Task 2-4] [Task 2-6] ... │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ... (更多 Executor) │
│ │
└─────────────────────────────────────────────────────────────┘
工作流程
1. 完整执行流程
用户代码
↓
┌─────────────────────────────────────────────────────────┐
│ Step 1: 初始化 Spark 环境 │
│ - 创建 SparkConf(配置) │
│ - 创建 SparkSession(入口点) │
│ - 获取 JavaSparkContext(上下文) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Step 2: 构建 RDD DAG(有向无环图) │
│ - parallelize() 创建初始 RDD │
│ - flatMap() 创建新 RDD │
│ - filter() 创建新 RDD │
│ - mapToPair() 创建新 RDD │
│ - reduceByKey() 创建新 RDD │
│ - sortByKey() 创建新 RDD │
│ (此时还没有执行任何计算,只是构建计划) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Step 3: 触发 Action(执行计算) │
│ - collect() 将结果收集到 Driver │
│ (这一刻,Spark 开始真正执行计算) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Step 4: DAG 调度器分析依赖关系 │
│ - 识别 Shuffle 边界 │
│ - 划分 Stage │
│ - 生成执行计划 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Step 5: 任务调度器分配任务 │
│ - 为每个分区创建 Task │
│ - 根据数据本地性分配 Task 到 Executor │
│ - 提交 Task 到 Executor 执行 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Step 6: Executor 执行 Task │
│ - 从 HDFS/内存 读取数据 │
│ - 执行计算逻辑 │
│ - 将结果写入内存或磁盘 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Step 7: 收集结果到 Driver │
│ - 所有 Executor 的结果汇总 │
│ - 返回给用户代码 │
└─────────────────────────────────────────────────────────┘
↓
结果输出
2. 关键概念:Lazy Evaluation(懒加载)
// 这些操作都不会立即执行,只是构建计划
JavaRDD<String> lines = sc.parallelize(Arrays.asList(inputData));
JavaRDD<String> words = lines.flatMap(...).filter(...);
JavaPairRDD<String, Integer> wordCounts = words.mapToPair(...).reduceByKey(...);
// 直到这里,才真正开始执行计算!
wordCounts.collect(); // ← Action,触发执行
优点:
- ✅ Spark 可以优化整个计算流程
- ✅ 避免不必要的中间结果存储
- ✅ 提高性能
RDD 详解
1. RDD 是什么?
RDD(Resilient Distributed Dataset)= 弹性分布式数据集
特性:
┌─────────────────────────────────────────────────────────┐
│ 1. 弹性(Resilient) │
│ - 支持节点失败自动恢复 │
│ - 通过 Lineage(血缘关系)重新计算 │
│ │
│ 2. 分布式(Distributed) │
│ - 数据分散在多个分区 │
│ - 可以并行处理 │
│ │
│ 3. 数据集(Dataset) │
│ - 包含任意类型的数据 │
│ - 可以是 String、Integer、自定义对象等 │
└─────────────────────────────────────────────────────────┘
2. RDD 的创建方式
// 方式 1:从集合创建
JavaRDD<String> rdd1 = sc.parallelize(Arrays.asList(inputData));
// 方式 2:从外部存储创建
JavaRDD<String> rdd2 = sc.textFile("hdfs://path/to/file");
// 方式 3:从其他 RDD 转换创建
JavaRDD<String> rdd3 = rdd1.flatMap(line -> Arrays.asList(line.split("\s+")).iterator());
3. RDD 的两种操作
Transformation(转换)- 返回新 RDD
// 不会立即执行,只是构建计划
JavaRDD<String> words = lines.flatMap(...); // 返回新 RDD
JavaRDD<String> filtered = words.filter(...); // 返回新 RDD
JavaPairRDD<String, Integer> pairs = words.mapToPair(...); // 返回新 RDD
常见 Transformation:
map():一对一转换flatMap():一对多转换filter():过滤mapToPair():转换为键值对reduceByKey():按 key 聚合sortByKey():按 key 排序join():两个 RDD 连接union():两个 RDD 合并
Action(动作)- 返回结果到 Driver
// 会立即执行计算
List<Tuple2<String, Integer>> results = wordCounts.collect(); // 收集所有数据
wordCounts.saveAsTextFile("hdfs://path/to/output"); // 保存到文件
long count = wordCounts.count(); // 计数
Tuple2<String, Integer> first = wordCounts.first(); // 获取第一个
常见 Action:
collect():收集所有数据到 Drivercount():计数first():获取第一个元素take(n):获取前 n 个元素saveAsTextFile():保存到文件foreach():对每个元素执行操作
4. RDD 的血缘关系(Lineage)
RDD 之间的依赖关系:
RDD1 (parallelize)
↓
RDD2 (flatMap)
↓
RDD3 (filter)
↓
RDD4 (mapToPair)
↓
RDD5 (reduceByKey) ← Shuffle 边界
↓
RDD6 (mapToPair)
↓
RDD7 (sortByKey) ← Shuffle 边界
↓
RDD8 (mapToPair)
↓
collect() ← Action,触发执行
如果某个 Executor 失败,Spark 可以根据 Lineage 重新计算该 RDD
执行计划
1. DAG(有向无环图)
Spark 将 RDD 转换序列表示为 DAG:
parallelize()
↓
flatMap() ─────┐
↓ │
filter() │
↓ │
mapToPair() │
↓ │
reduceByKey() ←┘ (Shuffle)
↓
mapToPair()
↓
sortByKey() (Shuffle)
↓
mapToPair()
↓
collect()
2. Stage 划分
划分stage的依据就是宽依赖
Shuffle 是 Stage 的分界线:
Stage 1: parallelize → flatMap → filter → mapToPair
(没有 Shuffle,窄依赖)
Stage 2: reduceByKey → mapToPair
(有 Shuffle,宽依赖)
Stage 3: sortByKey → mapToPair
(有 Shuffle,宽依赖)
Stage 4: collect()
(收集结果)
3. Task 分配
每个 Stage 中,每个分区对应一个 Task:
Stage 1 (20 个分区):
Task 1-1: 处理 Partition 0
Task 1-2: 处理 Partition 1
...
Task 1-20: 处理 Partition 19
Stage 2 (20 个分区):
Task 2-1: 处理 Partition 0
Task 2-2: 处理 Partition 1
...
Task 2-20: 处理 Partition 19
... (其他 Stage)
Shuffle 机制
1. 什么是 Shuffle?
Shuffle 是指数据根据 key 重新分配到不同分区的过程。
Shuffle 前:
┌─────────────────────────────────────┐
│ Partition 0: ("the",3), ("fox",2) │
│ Partition 1: ("the",5), ("dog",1) │
│ Partition 2: ("fox",1), ("and",3) │
└─────────────────────────────────────┘
Shuffle 后(按 key 的 hash 重新分配):
┌─────────────────────────────────────┐
│ Partition 0: ("the",3), ("the",5) │ ← 所有 "the"
│ Partition 1: ("fox",2), ("fox",1) │ ← 所有 "fox"
│ Partition 2: ("dog",1), ("and",3) │ ← 所有 "dog"
└─────────────────────────────────────┘
2. Shuffle 的工作流程
Map 端(写入):
┌─────────────────────────────────────┐
│ Executor 1 处理 Partition 0 │
│ - 执行 reduceByKey 的 map 端 │
│ - 本地预聚合(Combiner) │
│ - 按 key 的 hash 分配到 bucket │
│ - 写入本地磁盘 │
└─────────────────────────────────────┘
网络传输:
┌─────────────────────────────────────┐
│ 从各个 Executor 的磁盘读取数据 │
│ 通过网络传输到目标 Executor │
│ 写入目标 Executor 的磁盘 │
└─────────────────────────────────────┘
Reduce 端(读取):
┌─────────────────────────────────────┐
│ Executor 2 处理 Partition 0 │
│ - 从磁盘读取所有 "the" 的数据 │
│ - 执行 reduceByKey 的 reduce 端 │
│ - 最终聚合结果 │
└─────────────────────────────────────┘
3. 本地预聚合(Combiner)的优化
不使用 Combiner:
300M 条记录 → 网络传输 3.6GB
使用 Combiner:
300M 条记录 → 本地预聚合 → 100K 条记录 → 网络传输 1.2MB
优化效果:减少 99.97% 的网络传输!
容错机制
1. 故障恢复原理
Spark 通过 RDD Lineage(血缘关系)实现容错:
如果 Executor 3 在处理 Partition 4 时崩溃:
┌─────────────────────────────────────┐
│ 1. Driver 检测到 Executor 3 失败 │
│ 2. 查询 Partition 4 的 Lineage │
│ 3. 重新计算 Partition 4 │
│ 4. 在其他 Executor 上执行 │
│ 5. 继续处理其他分区 │
└─────────────────────────────────────┘
2. 持久化(Caching)
// 缓存 RDD 到内存,避免重复计算
wordCounts.cache(); // 或 persist()
// 使用缓存的 RDD
wordCounts.collect();
wordCounts.count(); // 不需要重新计算,直接从缓存读取
// 释放缓存
wordCounts.unpersist();
缓存级别:
MEMORY_ONLY:仅内存MEMORY_AND_DISK:内存不足时 Spill 到磁盘DISK_ONLY:仅磁盘MEMORY_ONLY_2:内存,2 个副本
性能优化
1. 分区优化
// 增加分区数,提高并行度
JavaRDD<String> lines = sc.parallelize(Arrays.asList(inputData), 40);
// 40 个分区,而不是默认的 20 个
// 重新分区
JavaRDD<String> repartitioned = lines.repartition(50);
// 合并分区
JavaRDD<String> coalesced = lines.coalesce(10);
2. 序列化优化
// 使用 Kryo 序列化器(比 Java 默认序列化快)
SparkConf conf = new SparkConf()
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.set("spark.kryo.referenceTracking", "false");
3. 内存管理
// 配置 Executor 内存
spark-submit --executor-memory 4G \
--driver-memory 2G \
app.jar
// 内存分配:
// - 60% 用于 RDD 缓存
// - 20% 用于 Shuffle
// - 20% 用于其他
4. 避免常见性能问题
// ❌ 错误:collect() 会将所有数据加载到 Driver 内存
List<Tuple2<String, Integer>> all = wordCounts.collect();
// ✅ 正确:只收集需要的数据
List<Tuple2<String, Integer>> top10 = wordCounts.take(10);
// ❌ 错误:在 Driver 端处理大数据
wordCounts.collect().forEach(tuple -> {
// 处理逻辑
});
// ✅ 正确:在 Executor 端处理
wordCounts.foreach(tuple -> {
// 处理逻辑
});
项目代码解析
1. 初始化阶段
// 第 24-27 行:创建 SparkConf(配置)
SparkConf conf = new SparkConf()
.setAppName("JavaWordCount")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.set("spark.kryo.referenceTracking", "false");
// 说明:
// - setAppName():应用名称,用于 UI 和日志
// - spark.serializer:使用 Kryo 序列化器(性能更好)
// - spark.kryo.referenceTracking:禁用引用追踪(减少开销)
// 第 37-40 行:创建 SparkSession(推荐方式)
SparkSession sparkSession = SparkSession.builder()
.config(conf)
.appName("JavaWordCount")
.getOrCreate();
// 说明:
// - SparkSession 是 Spark 2.0+ 的标准入口点
// - 自动创建 SparkContext
// - 支持 SQL、DataFrame 等高级 API
// 第 43 行:获取 JavaSparkContext
JavaSparkContext sc = new JavaSparkContext(sparkSession.sparkContext());
// 说明:
// - JavaSparkContext 是 Java API 的入口
// - 用于创建 RDD 和执行操作
2. 数据加载阶段
// 第 63 行:创建初始 RDD
JavaRDD<String> lines = sc.parallelize(Arrays.asList(inputData));
// 工作流程:
// 1. parallelize() 将数据分成 20 个分区(默认)
// 2. 分配到 10 个 Executor(每个 2 个分区)
// 3. 每个分区约 100MB(假设 2GB 数据)
// 分区示意:
// Partition 0: 100MB (15M 单词)
// Partition 1: 100MB (15M 单词)
// ...
// Partition 19: 100MB (15M 单词)
3. 转换阶段
// 第 66-68 行:分词
JavaRDD<String> words = lines
.flatMap(line -> Arrays.asList(line.split("\s+")).iterator())
.filter(word -> !word.isEmpty());
// 工作流程:
// 1. flatMap():每行分成多个单词
// "The quick brown fox" → ["The", "quick", "brown", "fox"]
// 2. filter():过滤空字符串
// 3. 这是窄依赖,无需 Shuffle
// 第 71-73 行:映射和聚合
JavaPairRDD<String, Integer> wordCounts = words
.mapToPair(word -> new Tuple2<>(word.toLowerCase(), 1))
.reduceByKey(Integer::sum);
// 工作流程:
// 1. mapToPair():转换为 (word, 1) 的键值对
// "The" → ("the", 1)
// "quick" → ("quick", 1)
// 2. reduceByKey():按 key 聚合
// ("the", 1) + ("the", 1) + ("the", 1) → ("the", 3)
// 3. 这会触发 Shuffle(宽依赖)
// 第 76-79 行:排序
JavaPairRDD<String, Integer> sortedWordCounts = wordCounts
.mapToPair(tuple -> new Tuple2<>(tuple._2, tuple._1)) // 交换
.sortByKey(false) // 降序排序
.mapToPair(tuple -> new Tuple2<>(tuple._2, tuple._1)); // 交换回来
// 工作流程:
// 1. 交换 key-value:("the", 45000) → (45000, "the")
// 2. sortByKey(false):按数字降序排序
// (45000, "the") > (12000, "fox") > (8500, "dog")
// 3. 交换回来:(45000, "the") → ("the", 45000)
// 4. 这也会触发 Shuffle(宽依赖)
4. 执行阶段
// 第 84-86 行:收集结果(Action,触发执行)
sortedWordCounts.collect().forEach(tuple ->
System.out.println(tuple._1 + ": " + tuple._2)
);
// 工作流程:
// 1. collect() 是 Action,触发整个计算流程
// 2. 所有 Executor 的结果汇总到 Driver
// 3. Driver 打印结果
// ⚠️ 注意:
// - collect() 会将所有数据加载到 Driver 内存
// - 如果结果集很大(如 1000 万条),会导致 Driver OOM
// - 对于大结果集,应该使用 saveAsTextFile() 等方式
5. 完整执行时间线
300M 词表,10 个 Executor 的执行时间线:
0s ─────────────────────────────────────────────────────
│ 初始化 Spark 环境
5s ├─────────────────────────────────────────────────────
│ 数据加载与分区
15s ├─────────────────────────────────────────────────────
│ 分词 + mapToPair + 本地预聚合
30s ├─────────────────────────────────────────────────────
│ Shuffle 1 (reduceByKey)
40s ├─────────────────────────────────────────────────────
│ 全局聚合
45s ├─────────────────────────────────────────────────────
│ Shuffle 2 (sortByKey)
55s ├─────────────────────────────────────────────────────
│ 收集结果到 Driver
60s ─────────────────────────────────────────────────────
完成!
总耗时:约 1 分钟
总结
核心要点
1. RDD 是 Spark 的基础数据结构
- 分布式:数据分散在多个分区
- 弹性:支持节点失败自动恢复
- 懒加载:Transformation 不立即执行
2. Transformation 和 Action 的区别
- Transformation:返回新 RDD,不执行计算
- Action:返回结果到 Driver,触发执行
3. Shuffle 是性能瓶颈
- 需要网络传输和磁盘 I/O
- 使用 Combiner 优化(本地预聚合)
- 减少 99.97% 的网络传输
4. 容错机制
- 通过 Lineage(血缘关系)实现
- 节点失败时自动重新计算
5. 分布式执行
- Driver 调度,Executor 执行
- 充分利用集群资源
- 比单机处理快 100 倍以上
demo
package org.example;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.sql.SparkSession;
import scala.Tuple2;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
public class JavaWordCount {
public static void main(String[] args) throws Exception {
// 打印执行时间(精确到秒)
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String executionTime = now.format(formatter);
System.out.println("\n========== sparkSession任务执行时间: " + executionTime + " ==========");
// 1. 初始化 Spark 配置
SparkConf conf = new SparkConf()
.setAppName("JavaWordCount")
.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.set("spark.kryo.referenceTracking", "false");
// 只在本地开发环境设置 Master,YARN 环境会自动配置
// String master = System.getenv("SPARK_MASTER");
// if (master == null || master.isEmpty()) {
// conf.setMaster("local[*]"); // 本地开发模式
// }
// 2. 创建 SparkSession(推荐方式,正确初始化 YARN ApplicationMaster)
// 这是 Spark 2.0+ 的标准入口点,会正确处理 YARN 模式
SparkSession sparkSession = SparkSession.builder()
.config(conf)
.appName("JavaWordCount")
.getOrCreate();
// 3. 从 SparkSession 获取 JavaSparkContext
JavaSparkContext sc = new JavaSparkContext(sparkSession.sparkContext());
// 设置日志级别为 WARN,减少控制台输出
sc.setLogLevel("WARN");
try {
// 4. 加载数据(直接写死在代码里,避免文件交互)
String[] inputData = {
"The quick brown fox jumps over the lazy dog",
"The dog is lazy",
"The fox is quick",
"A quick brown fox",
"The lazy dog sleeps",
"Fox and dog are animals",
"Quick and lazy are adjectives",
"The brown fox is quick and clever",
"Dogs are loyal animals",
"Foxes are cunning animals"
};
JavaRDD<String> lines = sc.parallelize(Arrays.asList(inputData));
// 5. 分词 - 使用正则表达式分割,过滤空字符串
JavaRDD<String> words = lines
.flatMap(line -> Arrays.asList(line.split("\s+")).iterator())
.filter(word -> !word.isEmpty());
// 6. 映射成 (word, 1) 形式并统计
JavaPairRDD<String, Integer> wordCounts = words
.mapToPair(word -> new Tuple2<>(word.toLowerCase(), 1)) // 转小写统一处理
.reduceByKey(Integer::sum);
// 7. 按照词频降序排序
JavaPairRDD<String, Integer> sortedWordCounts = wordCounts
.mapToPair(tuple -> new Tuple2<>(tuple._2, tuple._1)) // 交换 key-value
.sortByKey(false) // 降序排序
.mapToPair(tuple -> new Tuple2<>(tuple._2, tuple._1)); // 交换回来
// 8. 收集结果到 driver 并输出(确保在 driver 端完成所有操作)
System.out.println("\n========== Word Count Results ==========");
// 使用 collect() 将结果收集到 driver,避免在 YARN 模式下的异步问题
sortedWordCounts.collect().forEach(tuple ->
System.out.println(tuple._1 + ": " + tuple._2)
);
// 打印完成时间
LocalDateTime endTime = LocalDateTime.now();
String endTimeStr = endTime.format(formatter);
System.out.println("========== 任务完成时间: " + endTimeStr + " ==========\n");
// 9. 成功完成,关闭 Spark 上下文
sc.close();
} catch (Exception exception) {
System.err.println("执行出错: " + exception.getMessage());
exception.printStackTrace();
// 10. 发生异常时也要关闭 Spark 上下文
sc.close();
// 11. 重新抛出异常,让 YARN 知道任务失败
throw exception;
}
}
}