写在前面
自从上海疫情被封在家2个月之后,缺吃少用,天天发牢骚骂娘,我焦虑的头发又少了不少。不过因祸得福,时间是比以前多了不少,这不又研究起了心心念念的Spark。
Spark?不就是火花嘛,当初我可是要一周从入门到精通的。
一个月后的今天,我坐在电脑前,看着满屏的OutOfMemoryError和Task not serializable,默默地打开了Word,开始写这篇文章。
友情提示:本文适合以下人群阅读:
- 对Spark充满好奇的萌新
- 被Spark折磨得死去活来的中年人
- 想找点乐子的围观群众
第一章:Spark是什么鬼?
1.1 官方定义(假装专业)
Apache Spark是一个快速、通用、可扩展的大数据处理引擎。
翻译成人话就是:一个能让你在多台机器上同时跑代码的框架,而且它号称比MapReduce快100倍。
至于为什么快100倍...因为它把数据放内存里了啊!
这就好比你问我为什么开法拉利比骑自行车快,答案是:废话,一个烧油一个烧腿。
1.2 Spark生态系统
先来看看Spark的全家桶:
核心引擎] --> B[Spark SQL
结构化数据处理] A --> C[Spark Streaming
实时流处理] A --> D[MLlib
机器学习库] A --> E[GraphX
图计算] end subgraph "数据源" F[HDFS] --> A G[Hive] --> A H[Kafka] --> A I[MySQL] --> A J[各种乱七八糟的] --> A end subgraph "集群管理" A --> K[Standalone] A --> L[YARN] A --> M[Mesos] A --> N[Kubernetes] end style A fill:#e74c3c,stroke:#c0392b,color:#fff style B fill:#3498db,stroke:#2980b9,color:#fff style C fill:#2ecc71,stroke:#27ae60,color:#fff style D fill:#9b59b6,stroke:#8e44ad,color:#fff style E fill:#f39c12,stroke:#d68910,color:#fff
看到没?这就是为什么学Spark会掉头发的原因——它的生态系统比我的发际线还要广阔。
第二章:核心概念(掉发重灾区)
2.1 RDD:弹性分布式数据集
RDD(Resilient Distributed Dataset)是Spark的核心抽象,翻译过来叫"弹性分布式数据集"。
弹性:数据丢了能恢复(理论上)
分布式:数据分散在多台机器上
数据集:就是一堆数据
简单来说,RDD就是一个被切成很多块、散落在集群各个节点上的数据集合。
比如10TB的日志] end subgraph "Spark眼中的数据" B[Partition 1
节点A] C[Partition 2
节点B] D[Partition 3
节点C] E[Partition 4
节点D] F[Partition N
节点...] end A -->|"分区"| B A -->|"分区"| C A -->|"分区"| D A -->|"分区"| E A -->|"分区"| F style A fill:#e74c3c,stroke:#c0392b,color:#fff style B fill:#3498db,stroke:#2980b9,color:#fff style C fill:#3498db,stroke:#2980b9,color:#fff style D fill:#3498db,stroke:#2980b9,color:#fff style E fill:#3498db,stroke:#2980b9,color:#fff style F fill:#3498db,stroke:#2980b9,color:#fff
2.2 RDD的两种操作
RDD有两种操作,这是面试必考题,请务必背下来:
| 操作类型 | 英文名 | 特点 | 举例 |
|---|---|---|---|
| 转换操作 | Transformation | 懒加载,不会立即执行 | map, filter, flatMap, groupBy |
| 行动操作 | Action | 触发真正的计算 | count, collect, save, reduce |
重点来了:Transformation是懒的!
什么意思呢?就是你写了一堆map、filter,Spark根本不执行,它就记个小本本。直到你调用count()或collect()这种Action,它才开始干活。
这就像我老婆让我洗碗,我说"好的好的",但直到她站在厨房门口盯着我,我才真正开始洗。
2.3 DAG:有向无环图
当你写完一堆Transformation后,Spark会生成一个DAG(Directed Acyclic Graph)。
翻译:一个没有环的有方向的图。
再翻译:Spark会记录你对数据做的所有操作,形成一个执行计划。
第三章:Spark架构(开始头秃)
3.1 集群架构图
大脑,负责调度] end subgraph "Cluster Manager" B[YARN / Mesos / K8s
资源管理] end subgraph "Worker Node 1" C[Executor 1] C --> C1[Task] C --> C2[Task] C --> C3[Cache] end subgraph "Worker Node 2" D[Executor 2] D --> D1[Task] D --> D2[Task] D --> D3[Cache] end subgraph "Worker Node 3" E[Executor 3] E --> E1[Task] E --> E2[Task] E --> E3[Cache] end A <-->|"申请资源"| B B -->|"分配资源"| C B -->|"分配资源"| D B -->|"分配资源"| E A -->|"分发任务"| C A -->|"分发任务"| D A -->|"分发任务"| E style A fill:#e74c3c,stroke:#c0392b,color:#fff style B fill:#9b59b6,stroke:#8e44ad,color:#fff style C fill:#3498db,stroke:#2980b9,color:#fff style D fill:#3498db,stroke:#2980b9,color:#fff style E fill:#3498db,stroke:#2980b9,color:#fff
3.2 名词解释
| 角色 | 职责 | 类比 |
|---|---|---|
| Driver | 运行main函数,创建SparkContext | 包工头 |
| Cluster Manager | 管理集群资源 | 人力资源部 |
| Worker Node | 运行计算任务的节点 | 搬砖工人 |
| Executor | Worker上的JVM进程 | 工人的手 |
| Task | 最小的执行单元 | 一块砖 |
所以整个流程就是:
- 包工头(Driver)向人力资源部(Cluster Manager)申请工人
- 人力资源部分配工人(Worker)
- 包工头把活分给工人的手(Executor)
- 手开始搬砖(执行Task)
第四章:代码实战(正式开始放弃)
4.1 WordCount:大数据界的Hello World
每个学大数据的人,都逃不过WordCount。这就像每个学编程的人都要写Hello World一样。
// Scala版本 - 简洁到令人发指
val textFile = spark.read.textFile("hdfs://path/to/file.txt")
val counts = textFile
.flatMap(line => line.split(" "))
.groupByKey(identity)
.count()
counts.show()// Java版本 - 啰嗦到令人窒息
JavaRDD<String> textFile = sc.textFile("hdfs://path/to/file.txt");
JavaPairRDD<String, Integer> counts = textFile
.flatMap(s -> Arrays.asList(s.split(" ")).iterator())
.mapToPair(word -> new Tuple2<>(word, 1))
.reduceByKey((a, b) -> a + b);
counts.saveAsTextFile("hdfs://path/to/output");# Python版本 - 人生苦短,我用PySpark
text_file = sc.textFile("hdfs://path/to/file.txt")
counts = text_file.flatMap(lambda line: line.split(" ")) \
.map(lambda word: (word, 1)) \
.reduceByKey(lambda a, b: a + b)
counts.saveAsTextFile("hdfs://path/to/output")4.2 Spark SQL:终于像个正常人了
如果你觉得RDD太难用,恭喜你,你可以用Spark SQL。
// 创建DataFrame
val df = spark.read.json("path/to/people.json")
// 像写SQL一样操作数据
df.createOrReplaceTempView("people")
val result = spark.sql("""
SELECT
name,
age,
CASE
WHEN age < 18 THEN '未成年'
WHEN age < 35 THEN '青年'
WHEN age < 60 THEN '中年(脱发高危)'
ELSE '老年'
END as age_group
FROM people
WHERE age IS NOT NULL
ORDER BY age DESC
""")
result.show()4.3 DataFrame操作流程
JSON/CSV/Parquet] -->|"spark.read"| B[DataFrame] B -->|"select/filter/groupBy"| C[转换后的DataFrame] C -->|"write"| D[输出
HDFS/Hive/MySQL] B -->|"createTempView"| E[临时视图] E -->|"spark.sql"| C style A fill:#95a5a6,stroke:#7f8c8d,color:#fff style B fill:#3498db,stroke:#2980b9,color:#fff style C fill:#2ecc71,stroke:#27ae60,color:#fff style D fill:#e74c3c,stroke:#c0392b,color:#fff style E fill:#9b59b6,stroke:#8e44ad,color:#fff
第五章:性能调优(脱发加速器)
5.1 常见的坑
我来总结一下,在使用Spark的过程中,你会遇到的各种令人想砸电脑的问题:
5.2 数据倾斜:万恶之源
数据倾斜是什么?就是某个key的数据特别多,导致某个Task干得累死,其他Task早就下班了。
100万条] A2[Partition 2
98万条] A3[Partition 3
102万条] A4[Partition 4
100万条] end subgraph "数据倾斜" B1[Partition 1
10万条] B2[Partition 2
5万条] B3[Partition 3
380万条
😱] B4[Partition 4
5万条] end style B3 fill:#e74c3c,stroke:#c0392b,color:#fff
解决方案:
- 加盐打散:给倾斜的key加随机前缀
- 增加并行度:让数据分得更散
- 广播小表:避免shuffle
- 换个框架:开玩笑的...除非你真的受不了了
5.3 内存配置
spark.executor.memory] A --> B[执行内存
spark.memory.fraction × 60%] A --> C[存储内存
spark.memory.fraction × 40%] A --> D[用户内存
剩余部分] A --> E[预留内存
300MB] B --> B1[Shuffle/Join/Sort/Aggregation] C --> C1[Cache/Broadcast] end style A fill:#e74c3c,stroke:#c0392b,color:#fff style B fill:#3498db,stroke:#2980b9,color:#fff style C fill:#2ecc71,stroke:#27ae60,color:#fff style D fill:#9b59b6,stroke:#8e44ad,color:#fff style E fill:#f39c12,stroke:#d68910,color:#fff
5.4 常用调优参数
| 参数 | 说明 | 建议值 | 我的血泪经验 |
|---|---|---|---|
| spark.executor.memory | Executor内存 | 4-8G | 太大会GC,太小会OOM |
| spark.executor.cores | Executor核数 | 2-4 | 别太贪心 |
| spark.sql.shuffle.partitions | Shuffle分区数 | 100-500 | 默认200太小了 |
| spark.default.parallelism | 默认并行度 | CPU核数×2~3 | 看数据量 |
| spark.memory.fraction | 内存占比 | 0.6 | 一般不用改 |
第六章:踩坑实录(真实经历)
6.1 Task not serializable
这个错误,我愿称之为"Spark第一坑"。
症状:
org.apache.spark.SparkException: Task not serializable原因:你在RDD操作里引用了不可序列化的对象。
解决方案:
// ❌ 错误示范
class MyService {
val db = new DatabaseConnection() // 不可序列化!
def process(rdd: RDD[String]) = {
rdd.map(x => db.query(x)) // 💥 爆炸
}
}
// ✅ 正确姿势
class MyService extends Serializable {
def process(rdd: RDD[String]) = {
rdd.mapPartitions { partition =>
val db = new DatabaseConnection() // 每个分区创建一个
partition.map(x => db.query(x))
}
}
}6.2 OOM三连
6.3 小文件问题
症状:任务跑得奇慢无比,但CPU和内存都没打满。
原因:你有10万个小文件,每个只有几KB。
后果:Spark会创建10万个Task,调度开销比实际计算还大。
解决:
// 合并小文件
spark.conf.set("spark.sql.files.maxPartitionBytes", "128MB")
spark.conf.set("spark.sql.files.openCostInBytes", "4MB")
// 或者用coalesce
val df = spark.read.parquet("path/to/small/files")
.coalesce(100) // 合并成100个分区第七章:Spark vs 其他框架
7.1 大数据框架对比
元老级,但慢] B[Spark
快,但费内存] C[Flink
真·流批一体] end subgraph "流处理" D[Storm
老牌流处理] E[Spark Streaming
微批处理] F[Flink
真·流处理] G[Kafka Streams
轻量级] end A -.->|"进化"| B B -.->|"进化"| C D -.->|"进化"| F style A fill:#95a5a6,stroke:#7f8c8d,color:#fff style B fill:#e74c3c,stroke:#c0392b,color:#fff style C fill:#2ecc71,stroke:#27ae60,color:#fff style F fill:#2ecc71,stroke:#27ae60,color:#fff
7.2 选型建议
| 场景 | 推荐框架 | 原因 |
|---|---|---|
| 离线批处理 | Spark | 生态成熟,资料多 |
| 实时流处理 | Flink | 真正的流处理,延迟低 |
| 简单ETL | Spark SQL | 写SQL就行 |
| 机器学习 | Spark MLlib | 和Spark无缝集成 |
| 图计算 | Spark GraphX | 没什么好选的 |
| 想不加班 | 外包出去 | 真心建议 |
第八章:学习路线图
第九章:常用命令速查
9.1 spark-submit参数
spark-submit \
--master yarn \
--deploy-mode cluster \
--driver-memory 4g \
--executor-memory 8g \
--executor-cores 4 \
--num-executors 10 \
--conf spark.sql.shuffle.partitions=500 \
--conf spark.default.parallelism=500 \
--conf spark.yarn.maxAppAttempts=1 \
--class com.joey.spark.MyApp \
my-spark-app.jar \
arg1 arg29.2 常用Spark Shell命令
// 启动spark-shell
spark-shell --master yarn --executor-memory 4g
// 查看SparkContext信息
sc.version
sc.master
sc.defaultParallelism
// 查看当前配置
spark.conf.getAll.foreach(println)
// 设置日志级别
sc.setLogLevel("WARN")写在最后
如果你能看到这里,说明你还没有放弃,或者你直接拉到底了。
Spark确实是一个强大的分布式计算框架,但它的学习曲线也确实陡峭。我写这篇文章的时候,又掉了几根头发。
几点忠告:
- 不要急:Spark的概念很多,慢慢来
- 多实践:看100遍文档不如自己跑一遍
- 学会看日志:Spark的错误信息虽然长,但其实挺有用的
- 善用UI:Spark Web UI能帮你定位很多问题
- 保持健身:身体是革命的本钱,头发没了身体不能没
最后送大家一句话:
代码虐我千百遍,我待代码如初恋。
参考资料
作者:Joey | 写于某个深夜 | 头发状态:危