Spark 分布式计算框架深度解析
从入门到源码,全面掌握大数据处理引擎
目录
- 一、项目背景和应用场景
- 二、架构设计
- 三、关键概念
- [四、Spark 内存模型](#四、Spark 内存模型)
- 五、基本用法
- 六、任务优化:数据倾斜处理
- 七、性能调优实战
- [八、Spark 源码解析](#八、Spark 源码解析)
- [九、Spark Streaming 流处理](#九、Spark Streaming 流处理)
- 十、技术选型对比
- [十一、Spark 3.x 新特性](#十一、Spark 3.x 新特性)
- 十二、总结
- 附录:环境搭建
一、项目背景和应用场景
1.1 诞生背景
历史时间线
2004-2006 │ Google 发表 MapReduce 论文,Hadoop 诞生
│ → 批处理成为主流,但迭代计算效率低
│
2009 │ UC Berkeley AMPLab 启动 Spark 项目
│ → Matei Zaharia 的 PhD 研究课题
│
2010 │ Spark 开源发布
│ → 社区开始关注
│
2013 │ 成为 Apache 顶级项目
│ → 进入快速发展期
│
2014 │ Spark 1.0 发布
│ → 企业级生产就绪
│
2015 │ Spark 超越 MapReduce 成为最活跃 Apache 项目
│ → 成为事实上的大数据计算标准
│
2016-至今 │ 持续迭代,Spark 3.x 引入 Adaptive Query Execution
│ → 性能持续优化,云原生支持
技术演进驱动力
Spark 诞生的核心原因是 Hadoop MapReduce 在以下场景的局限性:
| 问题 | MapReduce 表现 | Spark 解决方案 |
|---|---|---|
| 迭代计算 | 每步操作都写 HDFS,磁盘 IO 开销大 | 基于内存,中间结果缓存 |
| 交互式查询 | 启动慢,延迟高(分钟级) | 秒级响应,支持即席查询 |
| 流处理 | 原生不支持,需配合 Storm | Spark Streaming 微批处理 |
| 机器学习 | 多轮迭代效率极低 | 内存迭代,MLlib 库 |
| 开发效率 | Map/Reduce 两个阶段,表达力有限 | 丰富的算子,DAG 执行引擎 |
性能对比
MapReduce vs Spark 性能对比(100TB 排序基准)
| 场景 | MapReduce | Spark | 提升倍数 |
|---|---|---|---|
| 磁盘批处理 | 100% | 50% | 2 倍 |
| 内存迭代计算 | 100% | 1% | 100 倍 |
| 交互式查询 | 100% | 5% | 20 倍 |
| 流处理延迟 | 秒级 | 毫秒级 | 10 倍 |
测试环境 :100 节点集群,每节点 16 核 64GB 内存,10Gbps 网络
数据来源:Daytona Gray Sort 基准测试
核心设计理念
-
内存计算优先
- 中间结果保存在内存而非磁盘
- 支持手动缓存策略(CACHE/PERSIST)
- 内存不足时自动溢出到磁盘
-
DAG 执行引擎
- 将计算任务表示为有向无环图
- 自动优化执行计划(管道化、谓词下推)
- 比 MapReduce 的 Map→Shuffle→Reduce 更灵活
-
统一技术栈
- 批处理、流处理、SQL、ML、图计算一套 API
- 降低学习和维护成本
- 数据在不同组件间无缝流转
-
容错机制
- 基于 Lineage(血统)的容错
- RDD 不可变,可通过依赖关系重算
- 比 checkpoint 更轻量
1.2 应用场景详解
1.2.1 离线批处理(Batch Processing)
适用场景:T+1 报表、数据仓库 ETL、历史数据分析
典型数据流:
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 数据源 │ → │ 清洗 │ → │ 转换 │ → │ 输出 │
│ HDFS/S3 │ │ 过滤 │ │ 聚合 │ │ Hive/ │
│ MySQL │ │ 去重 │ │ 关联 │ │ MySQL │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
│ │ │ │
▼ ▼ ▼ ▼
原始数据 数据质量提升 业务逻辑处理 下游消费
典型案例:
| 行业 | 场景 | 数据规模 | 处理频率 |
|---|---|---|---|
| 电商 | 每日销售报表 | 100GB-1TB | 每日 |
| 金融 | 交易对账 | 10-100GB | 每日 |
| 广告 | 投放效果分析 | 1-10TB | 每小时 |
| 物流 | 路径优化分析 | 100GB-1TB | 每日 |
| 游戏 | 玩家行为分析 | 100GB-5TB | 每日 |
代码示例:
python
# 电商每日销售报表
daily_report = (spark
.read.parquet("hdfs://raw/orders/*")
.filter(col("date") == "2024-01-15")
.groupBy("category", "region")
.agg(
sum("amount").alias("total_sales"),
count("*").alias("order_count"),
avg("amount").alias("avg_order_value")
)
.write.mode("overwrite")
.parquet("hdfs://warehouse/daily_sales/")
)
1.2.2 实时流处理(Stream Processing)
适用场景:实时监控、风控告警、实时指标
实时数据流架构:
Kafka → Spark Streaming → 实时计算 → 告警/可视化
↓ ↓ ↓ ↓
事件产生 微批接收 指标聚合 实时响应
(毫秒级) (秒级) (秒级) (秒级)
典型案例:
| 行业 | 场景 | 延迟要求 | 吞吐量 |
|---|---|---|---|
| 金融 | 欺诈交易检测 | < 5 秒 | 10 万条/秒 |
| 电商 | 实时 GMV 大屏 | < 10 秒 | 50 万条/秒 |
| 运维 | 系统监控告警 | < 30 秒 | 100 万条/秒 |
| 社交 | 热搜榜更新 | < 1 分钟 | 20 万条/秒 |
| 物流 | 实时轨迹追踪 | < 5 秒 | 30 万条/秒 |
代码示例:
python
# 实时交易风控
from pyspark.sql.functions import *
transactions = (spark
.readStream.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("subscribe", "transactions")
.load()
)
# 检测异常交易(单笔超过阈值或短时间多次交易)
alerts = (transactions
.withColumn("amount", col("value").cast("double"))
.withWatermark("timestamp", "5 minutes")
.groupBy(window("timestamp", "5 minutes"), "user_id")
.agg(
count("*").alias("tx_count"),
sum("amount").alias("total_amount")
)
.filter((col("tx_count") > 10) | (col("total_amount") > 100000))
.writeStream
.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("topic", "alerts")
.start()
)
1.2.3 机器学习(Machine Learning)
适用场景:大规模特征工程、模型训练、推荐系统
ML 流水线:
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 原始 │ → │ 特征 │ → │ 特征 │ → │ 模型 │ → │ 模型 │
│ 数据 │ │ 提取 │ │ 选择 │ │ 训练 │ │ 评估 │
└────────────┘ └────────────┘ └────────────┘ └────────────┘ └────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
用户行为 TF-IDF/ 相关性分析 随机森林/ AUC/准确率
日志数据 Word2Vec 降维 神经网络 F1-score
典型案例:
| 行业 | 场景 | 数据规模 | 模型类型 |
|---|---|---|---|
| 电商 | 商品推荐 | 10 亿 + 用户行为 | 协同过滤、DeepFM |
| 金融 | 信用评分 | 千万级用户 | 逻辑回归、XGBoost |
| 社交 | 内容推荐 | 亿级内容 | NLP、图神经网络 |
| 医疗 | 疾病预测 | 百万级病历 | 分类模型 |
| 广告 | CTR 预估 | 百亿级曝光 | Deep Learning |
代码示例:
python
from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler, StandardScaler
from pyspark.ml.classification import RandomForestClassifier
# 构建 ML 流水线
pipeline = Pipeline(stages=[
# 特征组装
VectorAssembler(inputCols=["age", "income", "score"], outputCol="features"),
# 特征标准化
StandardScaler(inputCol="features", outputCol="scaled_features"),
# 模型训练
RandomForestClassifier(
labelCol="label",
featuresCol="scaled_features",
numTrees=100,
maxDepth=10
)
])
# 训练模型
model = pipeline.fit(train_data)
# 预测
predictions = model.transform(test_data)
1.2.4 交互式查询(Interactive Query)
适用场景:BI 自助分析、数据探索、Ad-hoc 查询
交互式查询架构:
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ BI 工具 │ → │ Spark │ → │ 查询 │ → │ 结果 │
│ Tableau │ │ SQL │ │ 优化 │ │ 返回 │
│ Superset │ │ Engine │ │ Catalyst │ │ <5 秒 │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
│ │ │ │
▼ ▼ ▼ ▼
用户发起 SQL 解析 执行计划 可视化展示
查询请求 逻辑计划 物理计划 图表/表格
典型案例:
| 用户角色 | 查询类型 | 典型问题 | 响应要求 |
|---|---|---|---|
| 数据分析师 | 探索性查询 | "上周哪个地区销售最好?" | < 10 秒 |
| 产品经理 | 指标查看 | "今日 DAU 是多少?" | < 5 秒 |
| 运营人员 | 报表生成 | "活动转化率统计" | < 30 秒 |
| 高管 | 决策支持 | "季度营收趋势" | < 1 分钟 |
代码示例:
python
# 注册临时视图
df.createOrReplaceTempView("sales")
# SQL 查询
result = spark.sql("""
SELECT
region,
category,
SUM(amount) as total_sales,
COUNT(*) as order_count,
AVG(amount) as avg_order_value
FROM sales
WHERE date >= '2024-01-01'
GROUP BY region, category
ORDER BY total_sales DESC
LIMIT 100
""")
# 显示结果
result.show()
# 或导出到 BI 工具
result.write.jdbc(
url="jdbc:mysql://bi-server:3306/analytics",
table="sales_summary",
properties={"user": "bi_user", "password": "***"}
)
1.2.5 图计算(Graph Processing)
适用场景:社交网络分析、推荐系统、风控反作弊
图计算应用:
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 图数据 │ → │ 图构建 │ → │ 算法 │ → │ 结果 │
│ 边/顶点 │ │ Graph │ │ PageRank │ │ 应用 │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
│ │ │ │
▼ ▼ ▼ ▼
用户关系 GraphX/ 社区发现 推荐好友
交易网络 GraphFrame 关键节点 风险识别
典型案例:
| 行业 | 场景 | 图规模 | 算法 |
|---|---|---|---|
| 社交 | 好友推荐 | 10 亿 + 边 | PageRank、连通分量 |
| 金融 | 反洗钱 | 千万级交易 | 社区发现、路径分析 |
| 电商 | 商品推荐 | 亿级关联 | 协同过滤图 |
| 安全 | 欺诈检测 | 百万级设备 | 异常子图 |
| 知识图谱 | 实体关联 | 亿级三元组 | 图嵌入 |
代码示例:
python
from graphframes import GraphFrame
# 构建顶点 DataFrame
vertices = spark.createDataFrame([
("1", "Alice", 30),
("2", "Bob", 25),
("3", "Charlie", 35)
], ["id", "name", "age"])
# 构建边 DataFrame
edges = spark.createDataFrame([
("1", "2", "friend"),
("2", "3", "friend"),
("3", "1", "follow")
], ["src", "dst", "relationship"])
# 创建图
graph = GraphFrame(vertices, edges)
# PageRank 计算
results = graph.pageRank(resetProbability=0.15, tol=0.01)
# 查找三角形(三方关系)
triangles = graph.motifs("(a)-[e1]->(b); (b)-[e2]->(c); (c)-[e3]->(a)")
1.3 生态系统
┌─────────────────────────────────────────────────────────────────────────┐
│ Spark 生态系统 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ ┌───────────┐ │
│ │ Spark Core │ │ Spark SQL │ │ Spark Streaming │ │ MLlib │ │
│ │ (核心引擎) │ │ (SQL 查询) │ │ (流处理) │ │ (机器学习)│ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ └───────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │
│ │ GraphX │ │ SparkR │ │ PySpark │ │ Spark on │ │
│ │ (图计算) │ │ (R 语言) │ │ (Python) │ │ Kubernetes │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └───────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 存储层 │ │
│ │ HDFS │ S3 │ HBase │ Cassandra │ MySQL │ Kafka │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
二、架构设计
2.1 整体架构图
┌─────────────────────────────────────────────────────────────────────────┐
│ Client Stage │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ spark-submit │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Driver │ │ Context │ │ Job │ │ │
│ │ │ Program │ │ (SparkConf) │ │ Submission │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Cluster Manager Stage │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Standalone │ │ YARN │ │ Kubernetes │ │
│ │ │ │ (Hadoop) │ │ (K8s) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Worker Stage │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Worker 1 │ │ Worker 2 │ │ Worker N │ │
│ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │
│ │ │ Executor │ │ │ │ Executor │ │ │ │ Executor │ │ │
│ │ │ ┌─────┐ │ │ │ │ ┌─────┐ │ │ │ │ ┌─────┐ │ │ │
│ │ │ │Task │ │ │ │ │ │Task │ │ │ │ │ │Task │ │ │ │
│ │ │ └─────┘ │ │ │ │ └─────┘ │ │ │ │ └─────┘ │ │ │
│ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
2.2 主要角色
| 角色 | 职责 | 说明 |
|---|---|---|
| Driver | 应用程序主进程 | 创建 SparkContext,分解任务,调度执行 |
| Executor | 工作节点进程 | 执行 Task,存储缓存数据 |
| Cluster Manager | 集群资源管理 | 分配资源(Standalone/YARN/K8s) |
| Worker | 节点管理进程 | 管理 Executor 生命周期 |
| Task | 最小执行单元 | 一个 Partition 的计算逻辑 |
2.3 Spark-Submit 执行流程
┌─────────────────────────────────────────────────────────────────────────┐
│ Spark-Submit 执行流程 │
│ │
│ 1. 提交应用 │
│ $ spark-submit --class com.example.WordCount \ │
│ --master yarn --deploy-mode cluster \ │
│ --num-executors 10 --executor-cores 4 \ │
│ wordcount.jar │
│ │
│ 2. 启动 Driver │
│ ┌─────────────┐ │
│ │ Driver │ ← 创建 SparkContext │
│ │ (AM/YARN) │ ← 向 ResourceManager 申请资源 │
│ └─────────────┘ │
│ │
│ 3. 资源分配 │
│ ResourceManager ──→ 分配 Container ──→ 启动 Executor │
│ │
│ 4. 任务执行 │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Driver │ │
│ │ ↓ 创建 Job │ │
│ │ ↓ 分解为 Stage │ │
│ │ ↓ 生成 Task │ │
│ │ ↓ TaskScheduler 调度 │ │
│ │ ↓ 分发到 Executor │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 5. 结果返回 │
│ Executor ──→ 执行 Task ──→ 返回结果 ──→ Driver │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.4 核心组件交互
┌─────────────────────────────────────────────────────────────────────────┐
│ 运行时组件交互 │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ SparkContext │◄──────事件─────────┤ ListenerBus │ │
│ └──────┬───────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ 任务提交 ┌──────────────┐ │
│ │ DAGScheduler │ ────────────────►│TaskScheduler │ │
│ │ (Stage 划分) │ │ (Task 调度) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ │ Stage 信息 │ Task 分发 │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ RDD Lineage │ │ Cluster │ │
│ │ (依赖图) │ │ Manager │ │
│ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Executor │ │
│ │ ┌────────┐ │ │
│ │ │ Task │ │ │
│ │ └────────┘ │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
三、关键概念
3.1 算子(Operator)
算子是 Spark 对 RDD/DataFrame 的操作,分为 Transformation 和 Action 两类。
3.1.1 Transformation(转换)
惰性执行,只记录计算逻辑,不触发计算。
python
from pyspark import SparkContext
sc = SparkContext("local", "OperatorDemo")
# 基础转换
rdd = sc.parallelize([1, 2, 3, 4, 5])
# map: 一对一转换
mapped = rdd.map(lambda x: x * 2) # [2, 4, 6, 8, 10]
# filter: 过滤
filtered = rdd.filter(lambda x: x > 2) # [3, 4, 5]
# flatMap: 一对多转换
words = sc.parallelize(["hello world", "spark sql"])
flattened = words.flatMap(lambda x: x.split(" ")) # ["hello", "world", "spark", "sql"]
# reduceByKey: 按键聚合
pairs = sc.parallelize([("a", 1), ("b", 2), ("a", 3)])
reduced = pairs.reduceByKey(lambda x, y: x + y) # [("a", 4), ("b", 2)]
# join: 连接
rdd1 = sc.parallelize([("a", 1), ("b", 2)])
rdd2 = sc.parallelize([("a", 3), ("c", 4)])
joined = rdd1.join(rdd2) # [("a", (1, 3))]
# 宽依赖 vs 窄依赖
# 窄依赖:map, filter, union (父 RDD 分区被子 RDD 一个分区使用)
# 宽依赖:reduceByKey, join (父 RDD 分区被子 RDD 多个分区使用,需要 Shuffle)
3.1.2 Transformation 算子完整列表
| 类别 | 算子名称 | 方法签名 | 作用说明 | 宽窄依赖类型 |
|---|---|---|---|---|
| filter | filter(func) |
过滤满足条件的元素 | 窄依赖 | |
| flatMap | flatMap(func) |
一对多转换,每个元素生成多个输出 | 窄依赖 | |
| mapPartitions | mapPartitions(func) |
对每个分区应用函数,减少函数实例化开销 | 窄依赖 | |
| mapPartitionsWithIndex | mapPartitionsWithIndex(func) |
同 mapPartitions,但可获取分区索引 | 窄依赖 | |
| glom | glom() |
将每个分区元素合并为数组 | 窄依赖 | |
| 聚合操作 | reduceByKey | reduceByKey(func) |
按键聚合,相同 Key 的值合并 | 宽依赖 |
| groupByKey | groupByKey() |
按键分组,返回 (K, Iterable) | 宽依赖 | |
| aggregateByKey | aggregateByKey(zeroValue, seqFunc, combFunc) |
按键聚合,可自定义初始值和聚合函数 | 宽依赖 | |
| foldByKey | foldByKey(zeroValue, func) |
按键折叠,初始值相同 | 宽依赖 | |
| combineByKey | combineByKey(createCombiner, mergeValue, mergeCombiners) |
最通用的按键聚合 | 宽依赖 | |
| reduce | reduce(func) |
聚合所有元素为单个值 | - | |
| fold | fold(zeroValue, func) |
带初始值的 reduce | - | |
| aggregate | aggregate(zeroValue, seqFunc, combFunc) |
通用聚合,可返回不同类型 | - | |
| 连接操作 | join | join(otherRDD) |
内连接,返回 (K, (V1, V2)) | 宽依赖 |
| leftOuterJoin | leftOuterJoin(otherRDD) |
左外连接 | 宽依赖 | |
| rightOuterJoin | rightOuterJoin(otherRDD) |
右外连接 | 宽依赖 | |
| fullOuterJoin | fullOuterJoin(otherRDD) |
全外连接 | 宽依赖 | |
| cogroup | cogroup(otherRDD) |
按键分组,返回 (K, (Iterable, Iterable)) | 宽依赖 | |
| 集合操作 | union | union(otherRDD) |
合并两个 RDD | 窄依赖 |
| intersection | intersection(otherRDD) |
求交集 | 宽依赖 | |
| distinct | distinct() |
去重 | 宽依赖 | |
| subtract | subtract(otherRDD) |
差集,返回当前 RDD 有但 otherRDD 没有的元素 | 宽依赖 | |
| cartesian | cartesian(otherRDD) |
笛卡尔积 | 宽依赖 | |
| 排序操作 | sortByKey | sortByKey(ascending=True) |
按键排序 | 宽依赖 |
| sortBy | sortBy(func, ascending=True) |
按函数返回值排序 | 宽依赖 | |
| repartitionAndSortWithinPartitions | repartitionAndSortWithinPartitions(partitioner) |
重分区并排序 | 宽依赖 | |
| 分区操作 | partitionBy | partitionBy(partitioner) |
按指定分区器分区 | 宽依赖 |
| coalesce | coalesce(numPartitions) |
减少分区数(无 Shuffle) | 窄依赖 | |
| repartition | repartition(numPartitions) |
重分区(有 Shuffle) | 宽依赖 | |
| repartitionAndSortWithinPartitions | repartitionAndSortWithinPartitions(numPartitions) |
重分区并排序 | 宽依赖 | |
| 采样操作 | sample | sample(withReplacement, fraction, seed) |
随机采样 | 窄依赖 |
| takeSample | takeSample(withReplacement, num, seed) |
采样固定数量元素 | - | |
| 其他操作 | pipe | pipe(command) |
通过外部程序处理 RDD | 窄依赖 |
| checkpoint | checkpoint() |
切断 Lineage,保存到可靠存储 | - | |
| localCheckpoint | localCheckpoint() |
本地 checkpoint,不切断 Lineage | - | |
| setName | setName(name) |
设置 RDD 名称(用于 UI 显示) | - |
3.1.3 Action(行动)
触发实际计算,返回结果或写入存储。
python
# 收集结果
result = rdd.collect() # 返回所有元素到 Driver
# 计数
count = rdd.count() # 返回元素个数
# 聚合
total = rdd.reduce(lambda x, y: x + y) # 返回聚合结果
# 取前 N 个
top3 = rdd.take(3) # 返回前 3 个元素
# 保存到文件
rdd.saveAsTextFile("output/result")
# foreach: 对每个元素执行操作
rdd.foreach(lambda x: print(x))
3.1.4 Action 算子完整列表
| 类别 | 算子名称 | 方法签名 | 作用说明 | 返回值类型 |
|---|---|---|---|---|
| 收集结果 | collect | collect() |
返回所有元素到 Driver(慎用,可能 OOM) | Array[T] |
| collectAsMap | collectAsMap() |
返回 KV 对的 Map(仅 RDD[(K,V)]) | Map[K, V] | |
| toLocalIterator | toLocalIterator() |
迭代器方式逐个返回元素,节省内存 | Iterator[T] | |
| 计数操作 | count | count() |
返回元素个数 | Long |
| countByKey | countByKey() |
统计每个 Key 的出现次数(仅 RDD[(K,V)]) | Map[K, Long] | |
| countApprox | countApprox(timeout, confidence) |
近似计数,可设置超时和置信度 | Approximate[Long] | |
| countByValue | countByValue() |
统计每个值的出现次数 | Map[T, Long] | |
| 获取元素 | first | first() |
返回第一个元素 | T |
| take | take(num) |
返回前 num 个元素 | Array[T] | |
| takeOrdered | takeOrdered(num, ordering) |
按指定顺序返回前 num 个元素 | Array[T] | |
| takeSample | takeSample(withReplacement, num, seed) |
随机采样 num 个元素 | Array[T] | |
| top | top(num, ordering) |
返回最大的 num 个元素 | Array[T] | |
| max | max(ord) |
返回最大元素 | T | |
| min | min(ord) |
返回最小元素 | T | |
| 聚合操作 | reduce | reduce(func) |
聚合所有元素为单个值 | T |
| fold | fold(zeroValue, func) |
带初始值的 reduce,初始值参与每次聚合 | T | |
| aggregate | aggregate(zeroValue, seqFunc, combFunc) |
通用聚合,可返回不同类型 | U | |
| treeAggregate | treeAggregate(zeroValue, seqFunc, combFunc, depth) |
树形聚合,减少 Driver 压力 | U | |
| treeReduce | treeReduce(func, depth) |
树形 reduce,减少 Driver 压力 | T | |
| 遍历操作 | foreach | foreach(func) |
对每个元素执行操作(无返回值) | Unit |
| foreachPartition | foreachPartition(func) |
对每个分区执行操作,减少函数实例化 | Unit | |
| 保存操作 | saveAsTextFile | saveAsTextFile(path) |
保存为文本文件 | Unit |
| saveAsSequenceFile | saveAsSequenceFile(path) |
保存为 Hadoop SequenceFile | Unit | |
| saveAsHadoopFile | saveAsHadoopFile(path, outputFormat) |
保存为 Hadoop 文件格式 | Unit | |
| saveAsNewAPIHadoopFile | saveAsNewAPIHadoopFile(path, keyClass, valueClass, outputFormat) |
保存为新 Hadoop API 格式 | Unit | |
| saveAsPickleFile | saveAsPickleFile(path) |
保存为 Python Pickle 格式 | Unit | |
| saveAsObjectFile | saveAsObjectFile(path) |
保存为 Java 序列化对象 | Unit | |
| 其他操作 | isEmpty | isEmpty() |
判断 RDD 是否为空 | Boolean |
| lookup | lookup(key) |
查找指定 Key 的所有值(仅 RDD[(K,V)]) | Seq[V] | |
| histogram | histogram(buckets) |
计算数值分布直方图 | Array[Long] | |
| stats | stats() |
返回统计信息(count, mean, stddev 等) | StatCounter | |
| mean | mean() |
返回平均值 | Double | |
| variance | variance() |
返回方差 | Double | |
| stdev | stdev() |
返回标准差 | Double |
3.2 缓存(Cache/Persist)
缓存是 Spark 性能优化的关键,避免重复计算。
python
from pyspark import StorageLevel
# 创建 RDD
rdd = sc.textFile("hdfs://data/large_file.txt")
processed = rdd.map(lambda x: x.split(",")).filter(lambda x: len(x) > 3)
# 缓存策略
processed.cache() # 等价于 persist(StorageLevel.MEMORY_ONLY)
processed.persist(StorageLevel.MEMORY_AND_DISK) # 内存 + 磁盘
processed.persist(StorageLevel.MEMORY_ONLY_SER) # 序列化存储
processed.persist(StorageLevel.DISK_ONLY) # 仅磁盘
# 使用缓存
result1 = processed.count() # 首次计算并缓存
result2 = processed.collect() # 从缓存读取
# 取消缓存
processed.unpersist()
# 缓存检查点(切断 Lineage)
processed.checkpoint() # 需要预先设置 checkpoint 目录
3.2.1 缓存级别对比
| 级别 | 内存 | 磁盘 | 序列化 | 适用场景 |
|---|---|---|---|---|
| MEMORY_ONLY | ✅ | ❌ | ❌ | 内存充足,追求速度 |
| MEMORY_ONLY_SER | ✅ | ❌ | ✅ | 节省内存空间 |
| MEMORY_AND_DISK | ✅ | ✅ | ❌ | 内存不足,避免重算 |
| MEMORY_AND_DISK_SER | ✅ | ✅ | ✅ | 大数据量,节省空间 |
| DISK_ONLY | ❌ | ✅ | ❌ | 内存非常有限 |
3.3 宽依赖与窄依赖详解
依赖关系定义
Spark 根据 RDD 之间的依赖关系将 Job 划分为多个 Stage,依赖关系分为 窄依赖 和 宽依赖 两种:
┌─────────────────────────────────────────────────────────────────────────┐
│ RDD 依赖关系示意图 │
│ │
│ 窄依赖(Narrow Dependency): │
│ ┌───────────┐ │
│ │ Parent RDD│ 分区 0 ────────→ 分区 0 │
│ │ │ 分区 1 ────────→ 分区 1 │
│ │ │ 分区 2 ────────→ 分区 2 │
│ └───────────┘ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ Child RDD │ │
│ └───────────┘ │
│ │
│ 特点:每个父 RDD 分区最多被子 RDD 的一个分区使用 │
│ 优势:可流水线执行,无需 Shuffle,数据本地性好 │
│ │
│ 宽依赖(Wide/Shuffle Dependency): │
│ ┌───────────┐ │
│ │ Parent RDD│ 分区 0 ──┬──────→ 分区 0 │
│ │ │ ├───────→ 分区 1 │
│ │ │ 分区 1 ──┼──────→ 分区 0 │
│ │ │ ├───────→ 分区 2 │
│ │ │ 分区 2 ──┴──────→ 分区 1 │
│ └───────────┘ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ Child RDD │ │
│ └───────────┘ │
│ │
│ 特点:父 RDD 分区被子 RDD 的多个分区使用 │
│ 代价:需要 Shuffle,数据需要跨节点传输,性能开销大 │
└─────────────────────────────────────────────────────────────────────────┘
算子与依赖类型对照表
| 依赖类型 | Transformation 算子 | 是否需要 Shuffle | Stage 边界 |
|---|---|---|---|
| 窄依赖 | map, mapPartitions, mapPartitionsWithIndex | ❌ | 否 |
| filter, distinct(无 Shuffle 实现) | ❌ | 否 | |
| union, glom | ❌ | 否 | |
| coalesce(无 Shuffle) | ❌ | 否 | |
| pipe, checkpoint | ❌ | 否 | |
| 宽依赖 | reduceByKey, groupByKey, aggregateByKey | ✅ | 是 |
| foldByKey, combineByKey | ✅ | 是 | |
| join, leftOuterJoin, rightOuterJoin, fullOuterJoin | ✅ | 是 | |
| cogroup, groupWith | ✅ | 是 | |
| sortByKey, sortBy, repartition | ✅ | 是 | |
| partitionBy, repartitionAndSortWithinPartitions | ✅ | 是 | |
| distinct(默认实现), intersection | ✅ | 是 | |
| subtract, cartesian | ✅ | 是 |
依赖关系对执行的影响
┌─────────────────────────────────────────────────────────────────────────┐
│ Stage 划分示例 │
│ │
│ 代码: │
│ rdd1.textFile("input") │
│ .map(parse) ← 窄依赖 │
│ .filter(valid) ← 窄依赖 │
│ .map(transform) ← 窄依赖 │
│ .reduceByKey(_ + _) ← 宽依赖 (Stage 边界) ✂️ │
│ .map(format) ← 窄依赖 │
│ .saveAsTextFile("output") │
│ │
│ Stage 划分: │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Stage 0 │ │
│ │ textFile → map → filter → map │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │ P0 │→ │ P0 │→ │ P0 │→ │ P0 │ │ 可流水线执行 │
│ │ │ P1 │→ │ P1 │→ │ P1 │→ │ P1 │ │ 无需 Shuffle │
│ │ │ P2 │→ │ P2 │→ │ P2 │→ │ P2 │ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │ Shuffle Write │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Stage 1 │ │
│ │ reduceByKey → map → save │ │
│ │ ┌───────────┐ ┌─────┐ ┌─────────┐ │ │
│ │ │ Shuffle │→ │ P0 │→ │ Output │ │ 需要等待 Stage 0 完成 │
│ │ │ Read │ │ P1 │ │P0,P1,P2 │ │ │
│ │ │ │ │ P2 │ │ │ │ │
│ │ └───────────┘ └─────┘ └─────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 关键点: │
│ • 窄依赖算子可合并到同一 Stage,流水线执行 │
│ • 宽依赖算子是 Stage 边界,前后 Stage 必须串行执行 │
│ • 优化方向:减少宽依赖,合并窄依赖 │
└─────────────────────────────────────────────────────────────────────────┘
宽窄依赖判断技巧
判断方法:看父 RDD 的每个分区是否最多被子 RDD 的一个分区使用。
python
# 窄依赖示例
rdd1 = sc.parallelize(range(100), 4) # 4 个分区
rdd2 = rdd1.map(lambda x: x * 2) # 窄依赖,分区数不变
rdd3 = rdd2.filter(lambda x: x > 50) # 窄依赖,分区数不变
# 检查分区数
print(rdd1.getNumPartitions()) # 4
print(rdd2.getNumPartitions()) # 4 (窄依赖)
print(rdd3.getNumPartitions()) # 4 (窄依赖)
# 宽依赖示例
rdd4 = rdd3.map(lambda x: (x % 10, x)) # 转为 KV 对
rdd5 = rdd4.reduceByKey(lambda a, b: a + b) # 宽依赖,需要 Shuffle
print(rdd5.getNumPartitions()) # 默认 200 (spark.default.parallelism)
# 使用 toDebugString 查看依赖关系
print(rdd5.toDebugString())
# 输出:
# (200) PythonRDD[...] at reduceByKey at ... []
# | MapPartitionsRDD[...] at map at ... []
# | PythonRDD[...] at parallelize at ... []
# | ReducedPartition 200 ← 宽依赖标记
性能影响与优化建议
| 特性 | 窄依赖 | 宽依赖 |
|---|---|---|
| Shuffle | 不需要 | 需要 |
| 数据本地性 | 好(数据不移动) | 差(跨节点传输) |
| 执行方式 | 流水线(Pipeline) | 分 Stage 串行 |
| 容错成本 | 低(重算单个分区) | 高(可能需要重算多个分区) |
| 内存占用 | 低 | 高(Shuffle 缓冲区) |
| 网络 IO | 无 | 大量 |
| 磁盘 IO | 无 | 大量(Shuffle 写 + 读) |
优化建议:
- 减少 Shuffle:能用窄依赖实现的就不用宽依赖
- 合并操作:多个窄依赖操作可合并到一个 Stage
- 预分区:对需要多次 join/aggregation 的 Key 预先 partitionBy
- 调整并行度 :
spark.sql.shuffle.partitions设置合理的 Shuffle 分区数 - 使用广播:小表 join 大表时用 broadcast 避免 Shuffle
3.4 广播变量(Broadcast Variable)
广播变量用于高效分发大变量到所有节点,避免每个 Task 都传输一份。
python
# 不使用广播变量(低效)
blacklist = ["user1", "user2", "user3"] # 小列表没问题
# 大列表会导致每个 Task 都传输一份
large_blacklist = load_blacklist() # 100MB 数据
rdd.filter(lambda x: x[0] not in large_blacklist)
# 使用广播变量(高效)
broadcast_blacklist = sc.broadcast(large_blacklist) # 只传输一次
rdd.filter(lambda x: x[0] not in broadcast_blacklist.value)
# 广播累加器
broadcast_config = sc.broadcast({"threshold": 0.8, "max_iter": 100})
def process(record):
config = broadcast_config.value
if record.score > config["threshold"]:
return process_with_iter(record, config["max_iter"])
return record
result = rdd.map(process)
3.5 累加器(Accumulator)
累加器用于分布式计数和聚合。
python
# 创建累加器
error_count = sc.accumulator(0)
invalid_records = sc.accumulator(0)
def parse_record(record):
global error_count, invalid_records
try:
fields = record.split(",")
if len(fields) < 3:
invalid_records.add(1)
return None
return fields
except Exception:
error_count.add(1)
return None
# 使用
rdd = sc.textFile("hdfs://data/input")
parsed = rdd.map(parse_record).filter(lambda x: x is not None)
parsed.count() # 触发计算
print(f"错误记录数:{error_count.value}")
print(f"无效记录数:{invalid_records.value}")
3.6 Graph(RDD 依赖图)
Spark 根据 RDD 依赖关系构建 DAG(有向无环图),用于 Stage 划分。
┌─────────────────────────────────────────────────────────────────────────┐
│ RDD Lineage Graph │
│ │
│ ┌─────────────────┐ │
│ │ textFile │ (HadoopRDD) │
│ └────────┬────────┘ │
│ │ 窄依赖 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ map │ (MapPartitionsRDD) │
│ └────────┬────────┘ │
│ │ 窄依赖 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ filter │ (MapPartitionsRDD) │
│ └────────┬────────┘ │
│ │ 窄依赖 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ reduceByKey │ (ShuffleRDD) ← Stage 边界 │
│ └────────┬────────┘ │
│ │ 窄依赖 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ map │ (MapPartitionsRDD) │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ collect │ (Action) │
│ └─────────────────┘ │
│ │
│ Stage 0: textFile → map → filter → reduceByKey (Shuffle Write) │
│ Stage 1: reduceByKey (Shuffle Read) → map → collect │
│ │
└─────────────────────────────────────────────────────────────────────────┘
四、Spark 内存模型
4.1 内存区域划分
Spark 的内存管理是性能优化的核心。理解内存模型有助于避免 OOM(Out Of Memory)并提升性能。
┌─────────────────────────────────────────────────────────────────────────┐
│ Spark Executor 内存布局 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Executor Heap (堆内存) │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ Reserved Memory (保留内存) │ │ │
│ │ │ (300MB, 用于内部对象、序列化缓冲等) │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ User Memory (用户内存) │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ • RDD Transformation 临时对象 │ │ │ │
│ │ │ │ • Shuffle 缓冲区 │ │ │ │
│ │ │ │ • Join/Sort 的中间结果 │ │ │ │
│ │ │ │ • Broadcast 变量 │ │ │ │
│ │ │ └─────────────────────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ Storage Memory (存储内存) │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ • RDD Cache (缓存) │ │ │ │
│ │ │ │ • Unroll 对象(反序列化) │ │ │ │
│ │ │ │ • Task 结果缓冲 │ │ │ │
│ │ │ └─────────────────────────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Off-Heap Memory (堆外内存) │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ • 序列化数据(Kryo/Tungsten) │ │ │
│ │ │ • Sort Shuffle 缓冲区 │ │ │
│ │ │ • 网络传输缓冲 │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
4.2 内存配置参数
| 参数 | 默认值 | 说明 | 推荐设置 |
|---|---|---|---|
spark.executor.memory |
1g | Executor 总堆内存 | 总内存的 60-70% |
spark.memory.fraction |
0.6 | 堆内存中用于执行 + 存储的比例 | 0.6-0.7 |
spark.memory.storageFraction |
0.5 | 存储内存占执行 + 存储的比例 | 0.3-0.5 |
spark.memory.offHeap.enabled |
false | 是否启用堆外内存 | true(大数据量) |
spark.memory.offHeap.size |
0 | 堆外内存大小 | 堆内存的 20-30% |
spark.executor.memoryOverhead |
max(384MB, 10%*executor.memory) | 堆外内存开销 | 大数据量时调大 |
┌─────────────────────────────────────────────────────────────────────────┐
│ 内存配置计算示例 │
│ │
│ 假设:Executor 总内存 = 10GB │
│ │
│ 1. Reserved Memory(保留内存) │
│ = 300MB(固定) │
│ │
│ 2. 可用堆内存 = 10GB - 300MB = 9.7GB │
│ │
│ 3. Execution + Storage Memory │
│ = 9.7GB × spark.memory.fraction (0.6) = 5.82GB │
│ │
│ 4. Storage Memory(存储内存) │
│ = 5.82GB × spark.memory.storageFraction (0.5) = 2.91GB │
│ → 用于 RDD 缓存、Unroll 等 │
│ │
│ 5. Execution Memory(执行内存) │
│ = 5.82GB - 2.91GB = 2.91GB │
│ → 用于 Shuffle、Join、Sort 等中间计算 │
│ │
│ 6. 剩余堆内存(用于任务代码、用户变量等) │
│ = 9.7GB - 5.82GB = 3.88GB │
│ │
│ 7. 堆外内存(如启用) │
│ = spark.memory.offHeap.size │
│ 或 spark.executor.memoryOverhead │
└─────────────────────────────────────────────────────────────────────────┘
4.3 内存管理策略
4.3.1 统一内存管理(Spark 1.6+)
Spark 1.6 引入统一内存管理,Execution 和 Storage 内存可以互相借用。
┌─────────────────────────────────────────────────────────────────────────┐
│ 统一内存管理机制 │
│ │
│ 场景 1:缓存优先 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Storage: ████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░ 40% │ │
│ │ Execution: ████████████████████████████░░░░░░░░░░░░░░░░ 60% │ │
│ │ │ │
│ │ → 缓存未占满时,Execution 可使用空闲 Storage 内存 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 场景 2:执行优先 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Storage: ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 20% │ │
│ │ Execution: █████████████████████████████████████████ 80% │ │
│ │ │ │
│ │ → 执行任务需要更多内存时,可借用 Storage 内存 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 借用规则: │
│ • Execution 可借用 Storage 空闲内存 │
│ • Storage 缓存时,如内存不足可驱逐 Execution 借用部分 │
│ • 已缓存的 RDD 不会被驱逐(除非显式 unpersist) │
└─────────────────────────────────────────────────────────────────────────┘
4.3.2 内存驱逐策略
当 Storage 内存不足时,Spark 会按以下策略驱逐缓存:
驱逐优先级(从高到低):
1. 未使用的缓存 RDD(最近最少访问)
↓
2. 反序列化对象(Unroll 数据)
↓
3. 临时中间结果
不会被驱逐:
• 正在被 Task 使用的数据
• 显式 cache() 且正在使用的 RDD
• Broadcast 变量
4.4 常见 OOM 场景与解决方案
4.4.1 Executor OOM(堆内存溢出)
症状 :java.lang.OutOfMemoryError: Java heap space
原因:
- 单个 Partition 数据量过大
- 数据倾斜导致某些 Task 数据过多
- 缓存了过大的 RDD
- 反序列化对象占用过多内存
解决方案:
python
# 1. 增加 Partition 数,减小单个 Partition 大小
spark.conf.set("spark.sql.shuffle.partitions", "2000") # 默认 200
spark.conf.set("spark.default.parallelism", "2000")
# 2. 避免缓存过大的 RDD
# ❌ 不推荐
large_rdd.cache()
# ✅ 推荐:只缓存需要的列
small_rdd = large_rdd.select("col1", "col2").cache()
# 3. 使用序列化缓存
from pyspark import StorageLevel
large_rdd.persist(StorageLevel.MEMORY_ONLY_SER) # 序列化存储
# 4. 增加 Executor 内存
# spark-submit 参数
# --executor-memory 8g
# --conf spark.memory.fraction=0.7
4.4.2 Driver OOM
症状 :java.lang.OutOfMemoryError: GC overhead limit exceeded
原因:
collect()返回数据量过大- Broadcast 变量过大
- Driver 端聚合了太多数据
解决方案:
python
# ❌ 避免在 Driver 端收集大量数据
all_data = large_rdd.collect() # 可能 OOM!
# ✅ 使用 take 或采样
sample_data = large_rdd.take(10000)
# ✅ 保存到文件而非返回 Driver
large_rdd.saveAsParquetFile("hdfs://output/")
# ✅ 使用 takeOrdered 而非 collect + sort
top100 = large_rdd.takeOrdered(100, key=lambda x: -x[1])
# ✅ 增加 Driver 内存
# spark-submit 参数
# --driver-memory 4g
4.4.3 Shuffle OOM
症状 :java.lang.OutOfMemoryError: GC overhead limit exceeded 或 SparkOutOfMemoryError
原因:
- Shuffle 数据量过大
- 数据倾斜导致某些 Shuffle 任务数据过多
- 堆外内存不足
解决方案:
python
# 1. 启用堆外内存
spark.conf.set("spark.memory.offHeap.enabled", "true")
spark.conf.set("spark.memory.offHeap.size", "2g")
# 2. 调整 Shuffle 相关参数
spark.conf.set("spark.shuffle.spill", "true") # 启用 Shuffle 溢出到磁盘
spark.conf.set("spark.shuffle.file.buffer", "64k") # Shuffle 文件缓冲区
spark.conf.set("spark.reducer.maxSizeInFlight", "96m") # Reduce 端拉取缓冲区
# 3. 处理数据倾斜(参考第五章)
# - 加盐
# - 两阶段聚合
# - Broadcast Join
4.5 内存监控与调优
4.5.1 Spark UI 监控
┌─────────────────────────────────────────────────────────────────────────┐
│ Spark UI 内存监控页面 │
│ │
│ Storage 页面: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ RDD Name │ Partitions │ Size │ Memory Used │ Disk │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ user_events │ 200 │ 5GB │ 3.2GB │ 0B │ │
│ │ orders │ 500 │ 10GB │ 6.1GB │ 100MB │ │
│ │ products │ 50 │ 500MB │ 480MB │ 0B │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Executors 页面: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Executor │ Heap Used │ Heap Max │ Off-Heap │ GC Time │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ 1 │ 4.2GB │ 8GB │ 1GB │ 5% │ │
│ │ 2 │ 7.8GB │ 8GB │ 1GB │ 25% ⚠️ │ │
│ │ 3 │ 3.5GB │ 8GB │ 1GB │ 3% │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 关键指标: │
│ • GC Time > 20% → 内存压力大,考虑增加内存或优化数据 │
│ • Heap Used > 90% → 接近 OOM,需要调优 │
│ • Disk Used > 0 → 内存不足,数据溢出到磁盘 │
└─────────────────────────────────────────────────────────────────────────┘
4.5.2 调优检查清单
┌─────────────────────────────────────────────────────────────────────────┐
│ 内存调优检查清单 │
│ │
│ □ 1. 检查 Partition 大小 │
│ → 目标:每个 Partition 100MB-200MB │
│ → 调整:spark.sql.shuffle.partitions │
│ │
│ □ 2. 检查数据倾斜 │
│ → 查看各 Task 处理数据量是否均匀 │
│ → 使用加盐、两阶段聚合等优化 │
│ │
│ □ 3. 检查缓存策略 │
│ → 只缓存需要的 RDD │
│ → 使用序列化缓存(MEMORY_ONLY_SER) │
│ → 及时 unpersist 不需要的缓存 │
│ │
│ □ 4. 检查堆外内存 │
│ → 大数据量时启用 off-heap │
│ → 设置合理的 memoryOverhead │
│ │
│ □ 5. 检查 GC 情况 │
│ → GC Time > 20% 需要优化 │
│ → 使用 G1 GC:-XX:+UseG1GC │
│ │
│ □ 6. 检查 Driver 内存 │
│ → 避免 collect() 大数据集 │
│ → 增加 --driver-memory │
│ │
│ □ 7. 检查序列化方式 │
│ → 使用 Kryo 序列化:spark.serializer=org.apache.spark.serializer.KryoSerializer │
│ → 注册自定义类:spark.kryo.classesToRegister │
└─────────────────────────────────────────────────────────────────────────┘
4.6 内存管理要点
python
"""
Spark 内存管理核心要点
详细调优请参见第七章
"""
# 1. 选择性缓存(只缓存需要的数据)
needed_data = full_data.select("col1", "col2", "col3").cache()
# 2. 使用合适的存储级别
from pyspark import StorageLevel
rdd.persist(StorageLevel.MEMORY_ONLY_SER) # 序列化存储节省空间
# 3. 及时清理缓存
rdd.unpersist() # 手动释放不需要的缓存
# 4. 避免 Driver OOM
result = large_rdd.take(10000) # 而非 collect()
五、基本用法
5.1 WordCount 经典示例
python
"""
Spark WordCount - 统计词频
"""
from pyspark import SparkContext, SparkConf
# 创建配置和上下文
conf = SparkConf().setAppName("WordCount").setMaster("local[4]")
sc = SparkContext(conf=conf)
# 读取文件
text_file = sc.textFile("hdfs://data/input/*.txt")
# 方式 1:基础版本
counts = (text_file
.flatMap(lambda line: line.split(" ")) # 分词
.map(lambda word: (word, 1)) # 映射为 (word, 1)
.reduceByKey(lambda a, b: a + b) # 聚合
.sortBy(lambda x: x[1], ascending=False) # 排序
)
# 输出结果
for word, count in counts.take(100):
print(f"{word}: {count}")
# 保存结果
counts.saveAsTextFile("hdfs://data/output/wordcount")
sc.stop()
5.2 DataFrame 版本(推荐)
💡 为什么推荐 DataFrame API?
特性 RDD API DataFrame API 优势说明 优化器 无 Catalyst 自动优化查询计划(谓词下推、列剪枝、常量折叠) 执行引擎 解释执行 Tungsten 生成字节码,CPU 缓存友好,性能提升 3-5 倍 内存效率 Java 对象 二进制格式 内存占用减少 5-10 倍 类型安全 编译时检查 运行时检查 DataFrame[CaseClass] 支持编译时类型检查 易用性 函数式 API SQL-like API 更接近 SQL,学习成本低 语言互通 各语言独立 统一优化 Scala/Java/Python/R 共享同一优化器 数据源 有限 丰富 原生支持 Parquet、JSON、JDBC、Hive 等 结论 :除非需要底层 RDD 的细粒度控制,否则 优先使用 DataFrame API。
python
"""
Spark SQL WordCount - 使用 DataFrame API
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import explode, split, col, count
# 创建 SparkSession
spark = SparkSession.builder \
.appName("WordCountSQL") \
.master("local[4]") \
.getOrCreate()
# 读取文本文件
df = spark.read.text("hdfs://data/input/*.txt")
# 使用 DataFrame API
word_counts = (df
.select(explode(split(col("value"), "\\s+")).alias("word"))
.filter(col("word") != "")
.groupBy("word")
.agg(count("*").alias("count"))
.orderBy(col("count").desc())
)
# 显示结果
word_counts.show(100, truncate=False)
# 使用 SQL
word_counts.createOrReplaceTempView("words")
result = spark.sql("""
SELECT word, count
FROM words
ORDER BY count DESC
LIMIT 100
""")
result.show()
spark.stop()
5.3 日志分析示例
python
"""
Spark 日志分析 - 统计访问日志
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql.types import *
spark = SparkSession.builder.appName("LogAnalysis").getOrCreate()
# 读取日志
logs = spark.read.text("hdfs://data/logs/access.log")
# 解析日志(假设格式:IP - - [timestamp] "METHOD URL PROTOCOL" status size)
log_pattern = r'^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) \S+" (\d+) (\d+)'
def parse_log(line):
import re
match = re.match(log_pattern, line)
if match:
return {
"ip": match.group(1),
"timestamp": match.group(2),
"method": match.group(3),
"url": match.group(4),
"status": int(match.group(5)),
"size": int(match.group(6)) if match.group(6) != "-" else 0
}
return None
# 解析日志
parsed_logs = (logs
.rdd
.map(lambda r: parse_log(r[0]))
.filter(lambda x: x is not None)
.toDF()
)
# 缓存解析结果
parsed_logs.cache()
# 分析 1:Top 10 访问 IP
top_ips = (parsed_logs
.groupBy("ip")
.agg(count("*").alias("visit_count"))
.orderBy(col("visit_count").desc())
.limit(10)
)
top_ips.show()
# 分析 2:HTTP 状态码分布
status_dist = (parsed_logs
.groupBy("status")
.agg(count("*").alias("count"))
.orderBy("status")
)
status_dist.show()
# 分析 3:每小时访问量
hourly_traffic = (parsed_logs
.withColumn("hour", hour(to_timestamp(col("timestamp"), "dd/MMM/yyyy:HH:mm:ss Z")))
.groupBy("hour")
.agg(count("*").alias("requests"))
.orderBy("hour")
)
hourly_traffic.show()
# 分析 4:错误请求分析(4xx, 5xx)
errors = (parsed_logs
.filter(col("status") >= 400)
.groupBy("url", "status")
.agg(count("*").alias("error_count"))
.orderBy(col("error_count").desc())
.limit(20)
)
errors.show()
spark.stop()
5.4 用户行为分析
python
"""
Spark 用户行为分析 - 漏斗分析
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql import Window
spark = SparkSession.builder.appName("FunnelAnalysis").getOrCreate()
# 读取用户行为数据
events = spark.read.parquet("hdfs://data/events/user_events")
# 数据格式:user_id, event_type, event_time, page_id
# event_type: view, click, add_cart, purchase
# 定义漏斗
funnel_stages = ["view", "click", "add_cart", "purchase"]
# 计算各阶段转化率
def calculate_funnel(df, stages):
results = []
# 获取每个用户在各阶段的首次时间
for stage in stages:
stage_df = (df
.filter(col("event_type") == stage)
.groupBy("user_id")
.agg(min("event_time").alias(f"{stage}_time"))
)
results.append(stage_df)
# 合并各阶段数据
funnel = results[0]
for i, stage_df in enumerate(results[1:], 1):
funnel = funnel.join(
stage_df,
on="user_id",
how="left"
)
# 计算阶段间转化率
prev_stage = stages[i-1]
curr_stage = stages[i]
funnel = funnel.withColumn(
f"{prev_stage}_to_{curr_stage}",
when(col(f"{curr_stage}_time") > col(f"{prev_stage}_time"), 1).otherwise(0)
)
return funnel
funnel_df = calculate_funnel(events, funnel_stages)
# 统计转化率
funnel_stats = funnel_df.agg(
count("user_id").alias("view_count"),
sum("view_to_click").alias("click_count"),
sum("click_to_add_cart").alias("add_cart_count"),
sum("add_cart_to_purchase").alias("purchase_count")
).collect()[0]
print(f"浏览人数:{funnel_stats['view_count']}")
print(f"点击转化率:{funnel_stats['click_count']/funnel_stats['view_count']:.2%}")
print(f"加购转化率:{funnel_stats['add_cart_count']/funnel_stats['click_count']:.2%}")
print(f"购买转化率:{funnel_stats['purchase_count']/funnel_stats['add_cart_count']:.2%}")
spark.stop()
六、任务优化:数据倾斜处理
💡 本章专注数据倾斜问题,完整的性能调优指南请参见 [第七章 性能调优实战](#本章专注数据倾斜问题,完整的性能调优指南请参见 第七章 性能调优实战)
6.1 数据倾斜识别
python
"""
数据倾斜检测 - 查看各 Partition 数据量
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
spark = SparkSession.builder.appName("SkewDetection").getOrCreate()
df = spark.read.parquet("hdfs://data/input")
# 方法 1:查看各 Partition 数据量
partition_sizes = (df
.rdd
.mapPartitionsWithIndex(lambda idx, iter: [(idx, sum(1 for _ in iter))])
.collect()
)
print("Partition 数据分布:")
for idx, size in partition_sizes:
print(f"Partition {idx}: {size} rows")
# 计算倾斜度
sizes = [size for _, size in partition_sizes]
avg_size = sum(sizes) / len(sizes)
max_size = max(sizes)
min_size = min(sizes)
print(f"\n平均:{avg_size:.0f}, 最大:{max_size}, 最小:{min_size}")
print(f"倾斜比:{max_size/min_size:.2f}")
if max_size > avg_size * 3:
print("⚠️ 检测到数据倾斜!")
spark.stop()
6.2 数据倾斜解决方案
方案 1:加盐(Salting)
python
"""
数据倾斜优化 - 加盐法
适用场景:groupBy/join 时某个 Key 数据量过大
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
spark = SparkSession.builder.appName("SaltingOptimization").getOrCreate()
# 读取数据
orders = spark.read.parquet("hdfs://data/orders")
# 数据格式:order_id, user_id, amount, ...
# 问题:某些大 V 用户订单量巨大,导致 reduce 倾斜
# 解决:给热点 Key 添加随机盐,分散到不同 Partition
# 步骤 1:识别热点 Key
hot_users = (orders
.groupBy("user_id")
.agg(count("*").alias("order_count"))
.filter(col("order_count") > 10000) # 阈值
.select("user_id")
)
# 步骤 2:加盐处理
SALT_NUM = 10 # 盐的数量
# 为订单数据加盐
salted_orders = (orders
.join(hot_users.withColumn("is_hot", lit(True)), on="user_id", how="left")
.withColumn("salt",
when(col("is_hot").isNull(), lit(0)) # 非热点用户不加盐
.otherwise(floor(rand() * SALT_NUM)) # 热点用户随机加盐
)
.withColumn("salted_user_id",
concat(col("user_id"), lit("_"), col("salt"))
)
)
# 为关联的用户表加盐(爆炸复制)
users = spark.read.parquet("hdfs://data/users")
salted_users = (users
.join(hot_users.withColumn("is_hot", lit(True)), on="user_id", how="left")
.withColumn("salt", explode(array([lit(i) for i in range(SALT_NUM)])))
.withColumn("salted_user_id",
when(col("is_hot").isNull(), col("user_id"))
.otherwise(concat(col("user_id"), lit("_"), lit("_"), col("salt")))
)
)
# 使用加盐后的 Key 进行关联
result = (salted_orders
.join(salted_users, on="salted_user_id", how="inner")
.drop("salt", "salted_user_id", "is_hot")
)
result.show()
spark.stop()
方案 2:两阶段聚合
python
"""
数据倾斜优化 - 两阶段聚合
适用场景:groupBy 聚合时某个 Key 数据量过大
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
spark = SparkSession.builder.appName("TwoPhaseAggregation").getOrCreate()
# 读取数据
clicks = spark.read.parquet("hdfs://data/clicks")
# 数据格式:user_id, item_id, click_time
# 问题:某些热门商品点击量巨大
# 解决:先局部聚合,再全局聚合
# 方案 1:加盐 + 两阶段聚合
SALT_NUM = 20
# 第一阶段:加盐后局部聚合
salted_clicks = (clicks
.withColumn("salt", floor(rand() * SALT_NUM))
.withColumn("salted_item_id", concat(col("item_id"), lit("_"), col("salt")))
)
local_agg = (salted_clicks
.groupBy("salted_item_id")
.agg(
count("*").alias("partial_count"),
sum("click_value").alias("partial_sum")
)
)
# 第二阶段:去除盐值,全局聚合
global_agg = (local_agg
.withColumn("item_id", split(col("salted_item_id"), "_")[0])
.groupBy("item_id")
.agg(
sum("partial_count").alias("total_count"),
sum("partial_sum").alias("total_sum")
)
)
global_agg.show()
# 方案 2:分离热点数据
# 识别热点
hot_items = (clicks
.groupBy("item_id")
.count()
.filter(col("count") > 100000)
.select("item_id")
)
# 分离热点和非热点数据
hot_clicks = clicks.join(hot_items, on="item_id", how="inner")
normal_clicks = clicks.join(hot_items, on="item_id", how="left_anti")
# 分别聚合
hot_agg = (hot_clicks
.withColumn("salt", floor(rand() * SALT_NUM))
.groupBy("item_id", "salt")
.count()
.groupBy("item_id")
.sum("count")
)
normal_agg = normal_clicks.groupBy("item_id").count()
# 合并结果
final_agg = hot_agg.unionByName(normal_agg)
final_agg.show()
spark.stop()
方案 3:Broadcast Join
python
"""
数据倾斜优化 - Broadcast Join
适用场景:大表 join 小表,避免 Shuffle
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql import Broadcast
spark = SparkSession.builder.appName("BroadcastJoin").getOrCreate()
# 读取数据
orders = spark.read.parquet("hdfs://data/orders") # 大表:10 亿行
users = spark.read.parquet("hdfs://data/users") # 小表:100 万行
# 方式 1:自动广播(小表小于 spark.sql.autoBroadcastJoinThreshold)
# 默认阈值:10MB
result = orders.join(users, on="user_id", how="inner")
# 方式 2:强制广播
from pyspark.sql.functions import broadcast
result = orders.join(broadcast(users), on="user_id", how="inner")
# 方式 3:手动广播大一点的表(如果内存允许)
broadcast_users = spark.sparkContext.broadcast(
users.collect() # 收集到 Driver 并广播
)
def lookup_user(order):
user_dict = {u["user_id"]: u for u in broadcast_users.value}
user = user_dict.get(order["user_id"])
return {**order, "user_info": user}
result = orders.rdd.map(lookup_user).toDF()
spark.stop()
方案 4:自定义分区器
python
"""
数据倾斜优化 - 自定义分区
适用场景:默认哈希分区不均匀
"""
from pyspark import SparkContext
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("CustomPartitioner").getOrCreate()
sc = spark.sparkContext
# 读取数据
data = sc.textFile("hdfs://data/input")
# 解析为 Key-Value
pairs = data.map(lambda line: (line.split(",")[0], line))
# 方式 1:使用采样获取分区策略
# 采样 1% 数据查看 Key 分布
sample = pairs.sample(False, 0.01).collect()
key_counts = {}
for key, _ in sample:
key_counts[key] = key_counts.get(key, 0) + 1
# 根据 Key 频率分配分区
sorted_keys = sorted(key_counts.keys(), key=lambda k: key_counts[k], reverse=True)
num_partitions = 100
# 热点 Key 单独分配分区
hot_keys = sorted_keys[:10] # 前 10 个热点 Key
key_to_partition = {key: i for i, key in enumerate(hot_keys)}
# 其他 Key 均匀分配
for key in sorted_keys[10:]:
key_to_partition[key] = hash(key) % (num_partitions - len(hot_keys)) + len(hot_keys)
# 自定义分区函数
def custom_partition(key):
return key_to_partition.get(key, hash(key) % num_partitions)
# 应用自定义分区
partitioned = pairs.partitionBy(num_partitions, custom_partition)
# 查看分区分布
distribution = partitioned.mapPartitionsWithIndex(
lambda idx, iter: [(idx, sum(1 for _ in iter))]
).collect()
print("分区分布:")
for idx, count in distribution:
print(f"Partition {idx}: {count}")
spark.stop()
6.3 综合优化示例
python
"""
数据倾斜综合优化 - 电商订单分析
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
from pyspark.sql import Window
spark = SparkSession.builder \
.appName("EcommerceOptimization") \
.config("spark.sql.autoBroadcastJoinThreshold", "104857600") \ # 100MB
.config("spark.sql.shuffle.partitions", "2000") \ # 增加 Shuffle 分区
.getOrCreate()
# 读取数据
orders = spark.read.parquet("hdfs://data/orders")
users = spark.read.parquet("hdfs://data/users")
items = spark.read.parquet("hdfs://data/items")
# 问题 1:大 V 用户订单倾斜
# 解决:分离热点用户
hot_users = (orders
.groupBy("user_id")
.count()
.filter(col("count") > 50000)
.select("user_id")
)
hot_orders = orders.join(hot_users, on="user_id", how="inner")
normal_orders = orders.join(hot_users, on="user_id", how="left_anti")
# 热点订单加盐处理
SALT = 50
salted_hot = (hot_orders
.withColumn("salt", floor(rand() * SALT))
.withColumn("salted_user_id", concat(col("user_id"), lit("_"), col("salt")))
)
# 用户表爆炸复制
salted_users = (users
.join(hot_users, on="user_id", how="inner")
.withColumn("salt", explode(array([lit(i) for i in range(SALT)])))
.withColumn("salted_user_id", concat(col("user_id"), lit("_"), col("salt")))
)
normal_users = users.join(hot_users, on="user_id", how="left_anti")
# 关联
hot_result = salted_hot.join(
salted_users.select("salted_user_id", "user_level"),
on="salted_user_id"
)
normal_result = normal_orders.join(
broadcast(normal_users.select("user_id", "user_level")),
on="user_id"
)
# 合并
final_result = hot_result.unionByName(normal_result)
# 聚合分析
result = (final_result
.groupBy("user_level")
.agg(
count("*").alias("order_count"),
sum("order_amount").alias("total_amount"),
avg("order_amount").alias("avg_amount")
)
.orderBy("user_level")
)
result.show()
spark.stop()
七、性能调优实战
7.1 批处理性能调优
7.1.1 调优目标与指标
核心目标:
- 缩短作业执行时间
- 提高资源利用率
- 减少数据倾斜影响
- 避免 OOM 错误
关键监控指标:
| 指标 | 正常范围 | 说明 |
|---|---|---|
| Task 执行时间 | 100ms - 10s | 过短说明并行度不足,过长说明数据倾斜 |
| GC 时间占比 | < 10% | > 20% 说明内存压力大 |
| Shuffle 读写比 | 1:1 - 1:3 | 过高说明数据倾斜 |
| 数据本地性 | > 80% | PROCESS_LOCAL + NODE_LOCAL 占比 |
| Executor 利用率 | > 70% | CPU 和内存使用率 |
7.1.2 资源配置调优
Executor 配置:
python
# 推荐配置(根据集群规模调整)
spark-submit \
--num-executors 50 \ # Executor 数量
--executor-cores 5 \ # 每 Executor 核心数(4-8 为宜)
--executor-memory 10g \ # 每 Executor 内存
--driver-memory 4g \ # Driver 内存
--conf spark.memory.fraction=0.6 \ # 执行 + 存储内存比例
--conf spark.memory.storageFraction=0.5 \ # 存储内存占比
--conf spark.executor.memoryOverhead=2g # 堆外内存开销
配置计算公式:
┌─────────────────────────────────────────────────────────────────────────┐
│ Executor 内存分配计算 │
│ │
│ 假设:节点总内存 = 64GB,预留 4GB 给 OS 和系统进程 │
│ │
│ 可用内存 = 64GB - 4GB = 60GB │
│ │
│ 方案 1:少核多线程(适合计算密集型) │
│ executor-cores = 5 │
│ num-executors-per-node = 60 / (5 × 2GB) ≈ 6 │
│ executor-memory = 10GB │
│ │
│ 方案 2:多核少线程(适合 IO 密集型) │
│ executor-cores = 3 │
│ num-executors-per-node = 60 / (3 × 2GB) ≈ 10 │
│ executor-memory = 6GB │
│ │
│ 经验法则: │
│ • 每核心分配 2-4GB 内存 │
│ • executor-cores 不要超过 8(过多会导致调度开销) │
│ • 预留 10-20% 内存给 overhead │
└─────────────────────────────────────────────────────────────────────────┘
并行度调优:
python
# Shuffle 并行度(最重要)
spark.conf.set("spark.sql.shuffle.partitions", "2000") # 默认 200,根据数据量调整
# 通用并行度
spark.conf.set("spark.default.parallelism", "2000")
# 动态分配
spark.conf.set("spark.dynamicAllocation.enabled", "true")
spark.conf.set("spark.dynamicAllocation.minExecutors", "10")
spark.conf.set("spark.dynamicAllocation.maxExecutors", "100")
spark.conf.set("spark.dynamicAllocation.initialExecutors", "20")
# 计算推荐并行度
# 公式:并行度 = 总数据量 / 目标分区大小(100-200MB)
# 例:1TB 数据,目标 200MB/分区 → 1TB/200MB = 5000 分区
7.1.3 内存调优
内存配置参数:
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
spark.executor.memory |
1g | 总内存 60-70% | Executor 总堆内存 |
spark.memory.fraction |
0.6 | 0.6-0.7 | 执行 + 存储内存比例 |
spark.memory.storageFraction |
0.5 | 0.3-0.5 | 存储内存占比 |
spark.memory.offHeap.enabled |
false | true | 启用堆外内存 |
spark.memory.offHeap.size |
0 | 2-4GB | 堆外内存大小 |
序列化优化:
python
# 使用 Kryo 序列化(比 Java 序列化快 10 倍)
spark.conf.set("spark.serializer",
"org.apache.spark.serializer.KryoSerializer")
# 注册自定义类(可选,进一步提升性能)
spark.conf.set("spark.kryo.classesToRegister",
"com.example.MyClass1,com.example.MyClass2")
# Kryo 缓冲区大小
spark.conf.set("spark.kryoserializer.buffer.max", "512m")
缓存策略:
python
from pyspark import StorageLevel
# 根据场景选择缓存级别
# 内存充足,追求速度 → MEMORY_ONLY
rdd.persist(StorageLevel.MEMORY_ONLY)
# 内存紧张,节省空间 → MEMORY_ONLY_SER
rdd.persist(StorageLevel.MEMORY_ONLY_SER)
# 内存非常紧张 → MEMORY_AND_DISK_SER
rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)
# 仅磁盘(极少使用)→ DISK_ONLY
rdd.persist(StorageLevel.DISK_ONLY)
# 选择性缓存(只缓存需要的列)
# ❌ 避免
full_df.cache()
# ✅ 推荐
needed_df = full_df.select("col1", "col2", "col3").cache()
# 及时清理
df.unpersist() # 手动释放不需要的缓存
7.1.4 Shuffle 调优
Shuffle 参数配置:
python
# 启用 Shuffle 溢出到磁盘(防止 OOM)
spark.conf.set("spark.shuffle.spill", "true")
# Shuffle 文件缓冲区(默认 32k)
spark.conf.set("spark.shuffle.file.buffer", "64k")
# Reduce 端拉取缓冲区(默认 48m)
spark.conf.set("spark.reducer.maxSizeInFlight", "96m")
# Shuffle 压缩(启用 Lz4 压缩)
spark.conf.set("spark.shuffle.compress", "true")
spark.conf.set("spark.io.compression.codec", "lz4")
# Map 端聚合(减少 Shuffle 数据量)
spark.conf.set("spark.shuffle.mapOutput.compression.codec", "lz4")
避免不必要的 Shuffle:
python
# ❌ 避免:多个宽依赖导致多次 Shuffle
df1 = df.groupBy("key1").count() # Shuffle 1
df2 = df1.groupBy("key2").sum() # Shuffle 2
df3 = df2.groupBy("key3").avg() # Shuffle 3
# ✅ 推荐:合并操作,减少 Shuffle 次数
result = (df
.groupBy("key1", "key2", "key3")
.agg(
count("*").alias("cnt"),
sum("value").alias("total"),
avg("value").alias("avg")
)
) # 只需 1 次 Shuffle
7.1.5 数据倾斜处理
识别数据倾斜:
python
# 方法 1:查看各 Partition 数据量
partition_sizes = (df.rdd
.mapPartitionsWithIndex(lambda idx, iter: [(idx, sum(1 for _ in iter))])
.collect()
)
# 方法 2:Spark UI 查看
# http://driver:4040 → Stages → 查看 Task 执行时间分布
# 如果某些 Task 执行时间远超其他,说明存在倾斜
# 方法 3:Key 分布统计
key_dist = df.groupBy("key").count().orderBy(col("count").desc()).limit(100)
key_dist.show()
解决方案汇总:
| 方案 | 适用场景 | 实现复杂度 |
|---|---|---|
| Broadcast Join | 小表 join 大表 | ⭐ |
| 加盐(Salting) | groupBy/Join 热点 Key | ⭐⭐ |
| 两阶段聚合 | 聚合热点 Key | ⭐⭐ |
| 自定义分区器 | 默认哈希分区不均匀 | ⭐⭐⭐ |
| 分离热点数据 | 少量热点 Key | ⭐⭐ |
Broadcast Join 示例:
python
from pyspark.sql.functions import broadcast
# 大表 join 小表(小表 < 100MB)
result = large_df.join(broadcast(small_df), on="user_id")
# 手动设置广播阈值
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "104857600") # 100MB
加盐示例:
python
from pyspark.sql.functions import floor, rand, concat, lit
SALT_NUM = 20
# 加盐
salted_df = (df
.withColumn("salt", floor(rand() * SALT_NUM))
.withColumn("salted_key", concat(col("key"), lit("_"), col("salt")))
)
# 聚合
local_agg = salted_df.groupBy("salted_key").agg(sum("value").alias("partial"))
# 去盐,全局聚合
global_agg = (local_agg
.withColumn("key", split(col("salted_key"), "_")[0])
.groupBy("key")
.sum("partial")
)
7.1.6 代码层优化
使用 DataFrame API 而非 RDD:
python
# ❌ RDD API(无 Catalyst 优化)
rdd = sc.textFile("data.txt")
result = rdd.map(parse).filter(valid).map(transform).reduceByKey(add)
# ✅ DataFrame API(Catalyst + Tungsten 优化)
df = spark.read.text("data.txt")
result = (df
.select(parse_expr(col("value")))
.filter(col("valid") == True)
.groupBy("key")
.sum("value")
)
# 性能提升 3-5 倍
避免 UDF,使用内置函数:
python
from pyspark.sql.functions import *
# ❌ Python UDF(慢)
def parse_json(json_str):
import json
return json.loads(json_str)["field"]
df.withColumn("field", udf(parse_json)(col("json")))
# ✅ 内置函数(快 10-100 倍)
df.select(from_json(col("json"), schema).getField("field"))
列剪枝与谓词下推:
python
# ✅ Spark 自动优化
result = (spark
.read.parquet("data/")
.filter(col("date") == "2024-01-01") # 谓词下推
.select("user_id", "amount") # 列剪枝
.groupBy("user_id")
.sum("amount")
)
# Spark 会自动将 filter 和 select 下推到数据源
7.1.7 SparkSQL 性能优化参数
SparkSQL 提供了丰富的配置参数,合理配置可显著提升查询性能。
查询优化参数:
python
# ========== Catalyst 优化器参数 ==========
# 启用自适应查询执行(AQE,Spark 3.0+)
spark.conf.set("spark.sql.adaptive.enabled", "true") # 默认 true
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true") # 合并小分区
spark.conf.set("spark.sql.adaptive.coalescePartitions.initialPartitionNum", "200")
spark.conf.set("spark.sql.adaptive.coalescePartitions.minPartitionSize", "1m")
# 自适应 Shuffle
spark.conf.set("spark.sql.adaptive.shuffle.targetPostShuffleInputSize", "64m")
spark.conf.set("spark.sql.adaptive.shuffle.minNumPostShufflePartitions", "10")
# 自动处理倾斜 Join
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "256m")
# 动态分区裁剪(Spark 3.0+)
spark.conf.set("spark.sql.optimizer.dynamicPartitionPruning.enabled", "true")
# 谓词下推
spark.conf.set("spark.sql.optimizer.nestedSchemaPruning.enabled", "true")
spark.conf.set("spark.sql.optimizer.pruneNestedColumns.enabled", "true")
Join 优化参数:
python
# ========== Join 优化参数 ==========
# 自动广播 Join 阈值(默认 10MB)
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "104857600") # 100MB
# 禁用广播(大表关联时)
spark.conf.set("spark.sql.autoBroadcastJoinThreshold", "-1")
# Shuffle Hash Join 阈值
spark.conf.set("spark.sql.join.preferSortMergeJoin", "false") # 优先使用 Hash Join
# 广播超时时间
spark.conf.set("spark.sql.broadcastTimeout", "300") # 5 分钟
# 大表 Join 策略提示
# Spark 3.0+ 支持运行时统计
spark.conf.set("spark.sql.optimizer.joinReorder.enabled", "true")
Shuffle 与并行度参数:
python
# ========== Shuffle 参数 ==========
# Shuffle 分区数(最重要参数之一)
spark.conf.set("spark.sql.shuffle.partitions", "2000") # 默认 200,根据数据量调整
# Shuffle 分区数上限
spark.conf.set("spark.sql.shuffle.partitions", "5000") # 大数据量
# Shuffle 服务模式
spark.conf.set("spark.shuffle.service.enabled", "true") # 启用外部 Shuffle 服务
spark.conf.set("spark.shuffle.service.port", "7337")
# Shuffle 压缩
spark.conf.set("spark.shuffle.compress", "true")
spark.conf.set("spark.io.compression.codec", "lz4") # lz4/snappy/zstd
spark.conf.set("spark.io.compression.zstd.level", "1") # zstd 压缩级别
# Shuffle 文件合并
spark.conf.set("spark.shuffle.consolidateFiles", "true") # 合并 Shuffle 文件
# Shuffle 溢出
spark.conf.set("spark.shuffle.spill", "true")
spark.conf.set("spark.shuffle.spill.numElementsForceSpillThreshold", "100000")
文件读写优化参数:
python
# ========== 文件读写参数 ==========
# Parquet 优化
spark.conf.set("spark.sql.parquet.compression.codec", "snappy") # snappy/gzip/lzo/zstd
spark.conf.set("spark.sql.parquet.filterPushdown", "true") # 谓词下推
spark.conf.set("spark.sql.parquet.enableVectorizedReader", "true") # 向量化读取
# ORC 优化
spark.conf.set("spark.sql.orc.filterPushdown", "true")
spark.conf.set("spark.sql.orc.splits.include.file.footer", "true")
# 文件合并(小文件问题)
spark.conf.set("spark.sql.files.maxPartitionBytes", "134217728") # 128MB
spark.conf.set("spark.sql.files.openCostInBytes", "4194304") # 4MB
spark.conf.set("spark.sql.files.ignoreCorruptFiles", "true")
spark.conf.set("spark.sql.files.ignoreMissingFiles", "true")
# 输入分片大小
spark.conf.set("spark.sql.hadoop.parallelism", "2000")
spark.conf.set("spark.sql.files.maxPartitionBytes", "268435456") # 256MB
内存与执行参数:
python
# ========== 执行引擎参数 ==========
# Tungsten 执行引擎
spark.conf.set("spark.sql.tungsten.enabled", "true") # 默认 true
spark.conf.set("spark.sql.codegen.wholeStage", "true") # 全阶段代码生成
# 代码生成回退阈值
spark.conf.set("spark.sql.codegen.fallback", "false") # 禁止回退
# 内存管理
spark.conf.set("spark.sql.execution.arrow.enabled", "true") # PySpark Arrow 优化
spark.conf.set("spark.sql.execution.arrow.maxRecordsPerBatch", "10000")
# 执行内存
spark.conf.set("spark.memory.fraction", "0.6")
spark.conf.set("spark.memory.storageFraction", "0.5")
# 堆外内存
spark.conf.set("spark.memory.offHeap.enabled", "true")
spark.conf.set("spark.memory.offHeap.size", "4g")
分区与统计信息参数:
python
# ========== 分区优化参数 ==========
# 动态分区模式
spark.conf.set("spark.sql.sources.partitionOverwriteMode", "dynamic") # 动态分区覆盖
# 分区发现
spark.conf.set("spark.sql.hive.manageFilesourcePartitions", "true")
spark.conf.set("spark.sql.hive.filesourcePartitionFileCacheSize", "250m")
# 统计信息收集
spark.conf.set("spark.sql.statistics.histogram.enabled", "true") # 直方图统计
# 分区剪枝
spark.conf.set("spark.sql.optimizer.partitionPruning", "true")
# 分区大小限制
spark.conf.set("spark.sql.optimizer.partitionSizeEstimation", "true")
缓存与持久化参数:
python
# ========== 缓存参数 ==========
# 缓存压缩
spark.conf.set("spark.sql.inMemoryColumnarStorage.compressed", "true")
spark.conf.set("spark.sql.inMemoryColumnarStorage.batchSize", "10000")
# 缓存自动清理
spark.conf.set("spark.sql.legacy.allowNonEmptyLocationInCTAS", "false")
SparkSQL 性能优化参数汇总表:
| 参数类别 | 参数名称 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|---|
| 自适应优化 | spark.sql.adaptive.enabled | true | true | 启用 AQE |
| spark.sql.adaptive.coalescePartitions.enabled | true | true | 合并小分区 | |
| spark.sql.adaptive.skewJoin.enabled | false | true | 自动处理倾斜 Join | |
| Join 优化 | spark.sql.autoBroadcastJoinThreshold | 10MB | 100MB | 广播 Join 阈值 |
| spark.sql.join.preferSortMergeJoin | true | false | 优先 SortMergeJoin | |
| Shuffle | spark.sql.shuffle.partitions | 200 | 2000+ | Shuffle 分区数 |
| spark.io.compression.codec | lz4 | lz4/snappy | Shuffle 压缩 | |
| 文件读取 | spark.sql.files.maxPartitionBytes | 128MB | 128-256MB | 单分区最大字节数 |
| spark.sql.parquet.filterPushdown | true | true | Parquet 谓词下推 | |
| 执行引擎 | spark.sql.codegen.wholeStage | true | true | 全阶段代码生成 |
| spark.sql.execution.arrow.enabled | false | true | PySpark Arrow 优化 | |
| 内存 | spark.memory.offHeap.enabled | false | true | 启用堆外内存 |
| spark.memory.offHeap.size | 0 | 2-4GB | 堆外内存大小 |
配置示例:
python
"""
SparkSQL 性能优化配置模板
"""
from pyspark.sql import SparkSession
spark = (SparkSession.builder
.appName("SparkSQLOptimized")
# 自适应查询执行
.config("spark.sql.adaptive.enabled", "true")
.config("spark.sql.adaptive.coalescePartitions.enabled", "true")
.config("spark.sql.adaptive.skewJoin.enabled", "true")
# Join 优化
.config("spark.sql.autoBroadcastJoinThreshold", "104857600") # 100MB
# Shuffle 优化
.config("spark.sql.shuffle.partitions", "2000")
.config("spark.io.compression.codec", "lz4")
# 文件读取优化
.config("spark.sql.parquet.filterPushdown", "true")
.config("spark.sql.files.maxPartitionBytes", "134217728")
# 执行引擎优化
.config("spark.sql.codegen.wholeStage", "true")
.config("spark.sql.execution.arrow.enabled", "true")
# 内存优化
.config("spark.memory.offHeap.enabled", "true")
.config("spark.memory.offHeap.size", "4g")
.getOrCreate()
)
7.2 流处理性能调优
7.2.1 调优目标与指标
Structured Streaming 关键指标:
| 指标 | 正常范围 | 说明 |
|---|---|---|
| inputRate | 稳定 | 输入速率(条/秒) |
| processedRowsPerSecond | ≥ inputRate | 处理速率应 ≥ 输入速率 |
| triggerDuration | < batchInterval | 批次处理时间应小于批次间隔 |
| stateOperators | 稳定增长或平稳 | 状态大小 |
| batchDelay | < batchInterval | 批次延迟 |
监控方式:
python
# Spark UI 监控
# http://driver:4040/streaming/
# 编程方式获取指标
query = df.writeStream.format("console").start()
while query.isActive:
print(query.lastProgress)
time.sleep(10)
7.2.2 批次间隔调优
延迟 vs 吞吐量权衡:
python
# 低延迟场景(实时告警)
trigger = processingTime="1 seconds" # 1 秒批次
# 平衡场景(实时指标)
trigger = processingTime="5 seconds" # 5 秒批次
# 高吞吐场景(日志处理)
trigger = processingTime="30 seconds" # 30 秒批次
# 连续处理模式(实验性,毫秒级延迟)
trigger = continuous("100 milliseconds")
批次间隔选择建议:
| 场景 | 推荐批次间隔 | 预期延迟 |
|---|---|---|
| 实时风控 | 1-2 秒 | 3-5 秒 |
| 实时大屏 | 5-10 秒 | 10-20 秒 |
| 日志聚合 | 30-60 秒 | 1-2 分钟 |
| 离线同步 | 5-10 分钟 | 10-20 分钟 |
7.2.3 反压处理(Backpressure)
启用反压:
python
# 自动调整读取速率
spark.conf.set("spark.streaming.backpressure.enabled", "true")
# 初始速率(条/秒)
spark.conf.set("spark.streaming.backpressure.initialRate", "1000")
# 最大速率(条/秒)
spark.conf.set("spark.streaming.backpressure.maxRate", "10000")
# 速率调整比例
spark.conf.set("spark.streaming.backpressure.maxRateThreshold", "0.5")
Kafka 速率限制:
python
# 限制每分区每秒读取条数
df = (spark
.readStream.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("subscribe", "topic")
.option("maxOffsetsPerTrigger", "10000") # 每触发器最大读取量
.load()
)
7.2.4 状态管理调优
状态后端选择:
python
# 默认状态后端(基于 JVM 堆内存)
# 适合中小状态量
# RocksDB 状态后端(适合大状态量)
spark.conf.set(
"spark.sql.streaming.stateStore.providerClass",
"org.apache.spark.sql.execution.streaming.state.RocksDBStateStoreProvider"
)
# RocksDB 配置
spark.conf.set("spark.sql.streaming.stateStore.rocksdb.path", "/tmp/rocksdb")
spark.conf.set("spark.sql.streaming.stateStore.rocksdb.compaction.enabled", "true")
状态清理:
python
# 使用 Watermark 自动清理过期状态
df_with_watermark = df.withWatermark("event_time", "2 hours")
# 基于时间的窗口聚合(自动清理 2 小时前的状态)
result = (df_with_watermark
.groupBy(
col("user_id"),
window(col("event_time"), "1 hour")
)
.agg(sum("amount"))
)
7.2.5 Checkpoint 调优
Checkpoint 配置:
python
# 设置 Checkpoint 目录
checkpoint_location = "hdfs://namenode:9000/checkpoint"
query = (df.writeStream
.format("parquet")
.option("checkpointLocation", checkpoint_location)
.option("path", "hdfs://output/")
.start()
)
# 优化 Checkpoint 频率
spark.conf.set("spark.sql.streaming.stopActiveRunOnRestart", "true")
Checkpoint 清理策略:
python
# 保留最近 N 个 Checkpoint
spark.conf.set("spark.sql.streaming.minBatchesToRetain", "10")
# 手动清理旧 Checkpoint(生产环境建议定期清理)
# hdfs dfs -rm -r /checkpoint/old_batches
7.2.6 输出优化
输出模式选择:
| 模式 | 适用场景 | 性能影响 |
|---|---|---|
| Update | 聚合查询(推荐) | 只输出变化,性能好 |
| Append | 无聚合 + Watermark | 只输出新增,性能最好 |
| Complete | 小结果集聚合 | 输出全量,性能较差 |
python
# ✅ 推荐:Update 模式(只输出变化)
query = (word_counts
.writeStream
.outputMode("update")
.format("console")
.start()
)
# ✅ 推荐:Append 模式(只输出新增)
query = (events
.withWatermark("event_time", "1 hour")
.writeStream
.outputMode("append")
.format("parquet")
.start()
)
# ⚠️ 慎用:Complete 模式(输出全量)
# 仅适用于结果集很小的场景
批量输出优化:
python
# 增加每批次输出条数
spark.conf.set("spark.sql.streaming.maxRowsPerTrigger", "100000")
# 使用批量 Sink(如 Kafka 批量发送)
query = (df.writeStream
.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("topic", "output")
.option("maxRowsPerTrigger", "50000")
.start()
)
7.2.7 资源调优
流处理资源配置:
python
spark-submit \
--num-executors 20 \
--executor-cores 4 \
--executor-memory 8g \
--conf spark.sql.shuffle.partitions=200 \ # 流处理可设小一点
--conf spark.sql.streaming.maxBatchesPerTrigger=1 \ # 限制并发批次
--conf spark.streaming.backpressure.enabled=true
动态资源分配:
python
# 流处理谨慎使用动态分配
# 可能导致状态丢失或重复处理
# 如使用,建议设置最小 Executor 数
spark.conf.set("spark.dynamicAllocation.enabled", "true")
spark.conf.set("spark.dynamicAllocation.minExecutors", "10")
spark.conf.set("spark.dynamicAllocation.maxExecutors", "50")
7.3 性能调优检查清单
7.3.1 批处理检查清单
┌─────────────────────────────────────────────────────────────────────────┐
│ 批处理性能调优检查清单 │
│ │
│ □ 资源配置 │
│ □ executor-cores 设置为 4-8 │
│ □ executor-memory 为每核心 2-4GB │
│ □ num-executors 充分利用集群资源 │
│ □ driver-memory 足够(避免 Driver OOM) │
│ │
│ □ 并行度 │
│ □ spark.sql.shuffle.partitions 根据数据量设置(目标 100-200MB/分区)│
│ □ spark.default.parallelism 设置为集群总核心数的 2-3 倍 │
│ □ 启用动态资源分配 │
│ │
│ □ 内存管理 │
│ □ 启用 Kryo 序列化 │
│ □ 启用堆外内存(大数据量) │
│ □ 选择性缓存(只缓存需要的 RDD/DataFrame) │
│ □ 及时 unpersist 不需要的缓存 │
│ │
│ □ Shuffle 优化 │
│ □ 启用 Shuffle 压缩(lz4) │
│ □ 调整 Shuffle 缓冲区大小 │
│ □ 减少不必要的 Shuffle 操作 │
│ □ 使用 Broadcast Join 优化小表关联 │
│ │
│ □ 数据倾斜 │
│ □ 检查 Key 分布是否均匀 │
│ □ 热点 Key 使用加盐或两阶段聚合 │
│ □ 监控 Task 执行时间分布 │
│ │
│ □ 代码优化 │
│ □ 优先使用 DataFrame API │
│ □ 避免 Python UDF,使用内置函数 │
│ □ 利用谓词下推和列剪枝 │
│ □ 使用分区裁剪(分区字段过滤) │
└─────────────────────────────────────────────────────────────────────────┘
7.3.2 流处理检查清单
┌─────────────────────────────────────────────────────────────────────────┐
│ 流处理性能调优检查清单 │
│ │
│ □ 批次配置 │
│ □ 批次间隔根据延迟要求设置(1s-60s) │
│ □ processedRowsPerSecond ≥ inputRate │
│ □ triggerDuration < batchInterval │
│ │
│ □ 反压处理 │
│ □ 启用背压(backpressure.enabled=true) │
│ □ 设置合理的 maxOffsetsPerTrigger │
│ □ 监控背压调整情况 │
│ │
│ □ 状态管理 │
│ □ 使用 Watermark 清理过期状态 │
│ □ 大状态量使用 RocksDB 后端 │
│ □ 定期清理旧 Checkpoint │
│ │
│ □ 输出优化 │
│ □ 选择合适的输出模式(Update/Append 优先) │
│ □ 批量输出减少 Sink 压力 │
│ □ 避免 Complete 模式(除非结果集很小) │
│ │
│ □ 容错配置 │
│ □ 设置 Checkpoint 目录 │
│ □ 启用预写日志(WAL) │
│ □ 测试故障恢复流程 │
│ │
│ □ 监控告警 │
│ □ 监控 inputRate 和 processedRate │
│ □ 监控批次延迟 │
│ □ 设置状态大小告警 │
│ □ 设置消费位点滞后告警 │
└─────────────────────────────────────────────────────────────────────────┘
7.4 常见性能问题排查
7.4.1 问题诊断流程
┌─────────────────────────────────────────────────────────────────────────┐
│ 性能问题诊断流程 │
│ │
│ 1. 观察现象 │
│ │ │
│ ├─ 作业执行慢 ──→ 检查 Stage/Task 执行时间 │
│ │ │
│ ├─ 频繁 GC ──→ 检查内存使用和 GC 日志 │
│ │ │
│ ├─ 数据倾斜 ──→ 检查各 Task 处理数据量 │
│ │ │
│ └─ 资源不足 ──→ 检查 Executor 使用情况 │
│ │
│ 2. 查看 Spark UI │
│ │ │
│ ├─ Jobs 页面 ──→ 查看作业执行时间 │
│ │ │
│ ├─ Stages 页面 ──→ 查看 Stage 划分和 Task 分布 │
│ │ │
│ ├─ Executors 页面 ──→ 查看资源使用和 GC 情况 │
│ │ │
│ └─ SQL 页面 ──→ 查看执行计划和物理计划 │
│ │
│ 3. 分析日志 │
│ │ │
│ ├─ Driver 日志 ──→ 查看作业提交和调度信息 │
│ │ │
│ ├─ Executor 日志 ──→ 查看 Task 执行和错误信息 │
│ │ │
│ └─ GC 日志 ──→ 分析 GC 频率和耗时 │
│ │
│ 4. 实施优化 │
│ │ │
│ ├─ 调整资源配置 │
│ │ │
│ ├─ 优化代码逻辑 │
│ │ │
│ ├─ 处理数据倾斜 │
│ │ │
│ └─ 调整并行度 │
└─────────────────────────────────────────────────────────────────────────┘
7.4.2 典型问题与解决方案
| 问题 | 症状 | 可能原因 | 解决方案 |
|---|---|---|---|
| Executor OOM | Java heap space |
Partition 过大、数据倾斜、缓存过多 | 增加内存、增加分区、减少缓存 |
| Driver OOM | GC overhead limit |
collect() 大数据、Broadcast 过大 | 避免 collect、增加 Driver 内存 |
| Shuffle OOM | SparkOutOfMemoryError |
Shuffle 数据过大、倾斜 | 启用堆外内存、处理倾斜 |
| GC 频繁 | GC Time > 20% | 内存不足、对象过多 | 增加内存、使用序列化、减少对象创建 |
| Task 倾斜 | 某些 Task 执行时间远超其他 | Key 分布不均 | 加盐、两阶段聚合、Broadcast Join |
| 流处理积压 | batchDelay > batchInterval | 处理速度 < 输入速度 | 增加资源、启用背压、优化代码 |
八、Spark 源码解析
8.1 DAGScheduler - Stage 划分
scala
// 文件:org/apache/spark/scheduler/DAGScheduler.scala
// 核心方法:handleJobSubmitted
/**
* Stage 划分核心逻辑
* 从后往前遍历 RDD 依赖图,遇到宽依赖就划分新 Stage
*/
private[scheduler] def handleJobSubmitted(jobId: Int, finalRDD: RDD[_], func: TaskContext => Unit) {
var stage: Stage = null
var newStages: HashSet[Stage] = null
// 1. 从最终 RDD 开始,递归构建 Stage
stage = newStage(finalRDD, func)
// 2. 将 Stage 加入待调度队列
waitStages += stage
stage.outputLocs.mapValues(_.size).foreach { case (partition, size) =>
logInfo(s"Stage $stage has $size output locations")
}
// 3. 提交 Stage
submitStage(stage)
}
/**
* 创建新 Stage 的核心方法
*/
private def newStage(
rdd: RDD[_],
shuffleDep: Option[ShuffleDependency[_, _, _]]
): Stage = {
// 1. 获取当前 RDD 的所有父依赖
val parents = getOrCreateParentStages(rdd)
// 2. 创建 Stage
val id = nextStageId.getAndIncrement()
val stage = new Stage(
id = id,
rdd = rdd,
numPartitions = rdd.partitions.length,
parents = parents,
jobId = jobId,
shuffleDep = shuffleDep
)
// 3. 记录 Stage 与 RDD 的映射
stageIdToStage(stage.id) = stage
stage
}
/**
* 递归获取父 Stage
* 遇到宽依赖(ShuffleDependency)就创建新 Stage
*/
private def getOrCreateParentStages(rdd: RDD[_]): List[Stage] = {
rdd.dependencies.flatMap {
case shuffleDep: ShuffleDependency[_, _, _] =>
// 宽依赖:创建新的 ShuffleMapStage
List(newStage(rdd, Some(shuffleDep)))
case narrowDep: NarrowDependency[_] =>
// 窄依赖:继续递归父 RDD
getOrCreateParentStages(narrowDep.rdd)
}
}
关键点:
- Stage 划分基于 宽依赖(Shuffle)
- 窄依赖的 RDD 会被合并到同一个 Stage
- 从后往前遍历,确保依赖顺序正确
8.2 TaskScheduler - 任务调度
scala
// 文件:org/apache/spark/scheduler/TaskSchedulerImpl.scala
// 核心方法:resourceOffers
/**
* 资源 Offer 处理 - 将 Task 分配到 Executor
*/
override def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = {
val allTasks = new ArrayBuffer[Seq[TaskDescription]](offers.size)
// 1. 遍历每个 Worker 的资源 Offer
for (offer <- offers) {
val tasks = new ArrayBuffer[TaskDescription]
// 2. 从待调度 Task 中选择合适的
for (task <- tasksWithLocality) {
// 检查本地性级别
if (isTaskLocalityOK(task, offer)) {
// 3. 检查资源是否足够
if (canLaunchTask(task, offer)) {
// 4. 创建 TaskDescription
val taskDesc = createTaskDescription(task, offer)
tasks += taskDesc
// 5. 更新资源使用
offer.cpus -= taskDesc.cores
offer.memory -= taskDesc.memory
}
}
}
allTasks += tasks
}
allTasks
}
/**
* 任务本地性判断
* PROCESS_LOCAL > NODE_LOCAL > RACK_LOCAL > ANY
*/
private def isTaskLocalityOK(task: Task, offer: WorkerOffer): Boolean = {
val taskLocality = task.taskInfo.location
val offerHost = offer.host
taskLocality match {
case ProcessLocal => task.preferredLocation == offer.executorId
case NodeLocal => task.preferredLocation == offer.host
case RackLocal => task.rackId == offer.rackId
case Any => true
}
}
/**
* 延迟调度机制 - 如果本地任务等待太久,降级到更低本地性
*/
private def scheduleTask(taskId: Long, task: Task, offer: WorkerOffer): Boolean = {
val launched = try {
launchTask(taskId, task, offer)
} catch {
case e: Exception =>
logError(s"Failed to launch task $taskId", e)
false
}
if (!launched) {
// 任务启动失败,加入延迟调度队列
addTaskToDelayedScheduleQueue(task)
}
launched
}
关键点:
- 任务调度考虑 数据本地性,减少网络传输
- 支持 延迟调度,等待本地资源
- 资源不足时降级到更低本地性级别
8.3 ShuffleManager - Shuffle 机制
scala
// 文件:org/apache/spark/shuffle/hash/HashShuffleWriter.scala
// 核心方法:write
/**
* Hash Shuffle Writer - 写阶段
*/
class HashShuffleWriter[K, V](
shuffleHandle: ShuffleHandle[K, V, V],
mapId: Long,
context: TaskContext
) extends ShuffleWriter[K, V] {
private val numPartitions = shuffleHandle.dependency.partitioner.numPartitions
private val writers = Array.fill[BufferedOutputStream](numPartitions)(null)
override def write(records: Iterator[Product2[K, V]]): Unit = {
for (record <- records) {
val key = record._1
val value = record._2
// 1. 计算 Partition
val partition = getPartition(key)
// 2. 写入对应 Partition 的文件
if (writers(partition) == null) {
writers(partition) = createWriter(partition)
}
// 3. 序列化并写入
val serializer = SparkEnv.get.serializer.newInstance()
val buffer = serializer.serialize(value)
writers(partition).write(buffer.array())
}
}
override def stop(success: Boolean): Option[MapStatus] = {
// 4. 关闭所有 Writer,返回 MapStatus
val mapStatus = MapStatus(blockManager.shuffleServerId, partitionLengths)
for (writer <- writers if writer != null) {
writer.close()
}
Some(mapStatus)
}
}
// 文件:org/apache/spark/shuffle/hash/HashShuffleReader.scala
// 核心方法:read
/**
* Hash Shuffle Reader - 读阶段
*/
class HashShuffleReader[K, C](
handle: ShuffleHandle[K, _, C],
startPartition: Int,
endPartition: Int
) extends ShuffleReader[K, C] {
override def read(): Iterator[Product2[K, C]] = {
// 1. 从 MapTask 获取数据位置
val mapStatuses = mapOutputTracker.getMapSizesByExecutorId(
handle.shuffleId, startPartition, endPartition
)
// 2. 为每个 Map 创建 Fetcher
val fetchers = mapStatuses.map { case (blockId, hostPort) =>
new BlockFetcher(blockId, hostPort)
}
// 3. 拉取数据并反序列化
val records = fetchers.flatMap(fetcher => {
val data = fetcher.fetch()
val serializer = SparkEnv.get.serializer.newInstance()
serializer.deserialize[Iterator[Product2[K, C]]](data)
})
records
}
}
关键点:
- Map 端:按 Partition 写入不同文件
- Reduce 端:从所有 Map 拉取对应 Partition 数据
- Sort Shuffle:Spark 1.2+ 引入,合并小文件,减少磁盘 IO
九、Spark Streaming 流处理
9.1 流处理概述
Spark 提供两套流处理 API:
| API | 代数 | 延迟 | 语义 | 状态 |
|---|---|---|---|---|
| Spark Streaming | 第一代 | 秒级 | 至少一次 | 有状态 |
| Structured Streaming | 第二代 | 毫秒级 | 精确一次 | 有状态 |
┌─────────────────────────────────────────────────────────────────────────┐
│ Spark 流处理演进 │
│ │
│ 2013 Spark Streaming 1.0 │
│ └─→ 基于 DStream 的微批处理 │
│ └─→ 延迟:秒级 │
│ │
│ 2016 Structured Streaming 2.0 │
│ └─→ 基于 DataFrame/Dataset 的流处理 │
│ └─→ 延迟:毫秒级(Continuous Processing) │
│ └─→ 支持 Event-Time、Watermark、多流 Join │
│ │
│ 2019 Structured Streaming 3.0 │
│ └─→ 自适应流执行(Adaptive Query Execution) │
│ └─→ 更好的状态管理和容错 │
└─────────────────────────────────────────────────────────────────────────┘
9.2 Spark Streaming(DStream API)
9.2.1 核心概念
DStream(Discretized Stream):连续的数据流,本质上是 RDD 序列。
┌─────────────────────────────────────────────────────────────────────────┐
│ DStream 模型 │
│ │
│ 时间轴: T0 T1 T2 T3 T4 │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ DStream: [RDD0] [RDD1] [RDD2] [RDD3] [RDD4] │
│ │ │ │ │ │ │
│ │←Batch Interval(批次间隔,如 5 秒)→│ │
│ │
│ 每个 RDD 包含该时间窗口内到达的所有数据 │
│ 对 DStream 的操作会转换为对底层 RDD 的操作 │
└─────────────────────────────────────────────────────────────────────────┘
9.2.2 基本用法
python
"""
Spark Streaming - DStream 示例
"""
from pyspark import SparkContext
from pyspark.streaming import StreamingContext
from pyspark.streaming.kafka import KafkaUtils
# 创建上下文
sc = SparkContext("local[2]", "StreamingDemo")
ssc = StreamingContext(sc, batchDuration=5) # 5 秒批次
# 从 Kafka 读取数据
kafka_stream = KafkaUtils.createStream(
ssc,
"zk:2181",
"spark-streaming-group",
{"topic1": 1, "topic2": 1}
)
# 提取消息内容
lines = kafka_stream.map(lambda x: x[1])
# WordCount 示例
words = lines.flatMap(lambda line: line.split(" "))
word_counts = words.map(lambda word: (word, 1)).reduceByKey(lambda a, b: a + b)
# 输出结果
word_counts.pprint()
# 启动流处理
ssc.start()
ssc.awaitTermination()
9.2.3 窗口操作
python
"""
窗口操作 - 统计过去 30 秒的数据,每 10 秒更新一次
"""
from pyspark.streaming import Duration
# 窗口长度 30 秒,滑动间隔 10 秒
window_length = Duration(30000) # 30 秒
slide_interval = Duration(10000) # 10 秒
# 窗口内的 WordCount
windowed_counts = words.map(lambda word: (word, 1)) \
.reduceByKeyAndWindow(
lambda a, b: a + b, # 累加函数
lambda a, b: a - b, # 逆函数(优化用)
window_length,
slide_interval
)
windowed_counts.pprint()
9.2.4 状态管理
python
"""
有状态流处理 - 维护全局词频统计
"""
def update_func(new_values, running_sum):
"""
更新函数:新值 + 历史值
"""
return sum(new_values) + (running_sum or 0)
# 使用 updateStateByKey 维护状态
global_counts = words.map(lambda word: (word, 1)) \
.updateStateByKey(update_func)
global_counts.pprint()
9.2.5 Checkpoint 与容错
python
"""
Checkpoint 配置 - 实现故障恢复
"""
from pyspark.streaming import StreamingContext
# 设置 checkpoint 目录
ssc = StreamingContext(sc, batchDuration=5)
ssc.checkpoint("hdfs://namenode:9000/checkpoint")
# 有状态操作必须设置 checkpoint
def update_func(new_values, running_sum):
return sum(new_values) + (running_sum or 0)
global_counts = words.map(lambda word: (word, 1)) \
.updateStateByKey(update_func)
# 从 checkpoint 恢复
def create_context():
ssc = StreamingContext(sc, batchDuration=5)
ssc.checkpoint("hdfs://namenode:9000/checkpoint")
# ... 定义 DStream 逻辑
return ssc
# 恢复或创建新上下文
ssc = StreamingContext.getOrCreate("hdfs://namenode:9000/checkpoint", create_context)
9.3 Structured Streaming(推荐)
9.3.1 核心概念
Structured Streaming 将流处理视为对一张无限增长的表的查询。
┌─────────────────────────────────────────────────────────────────────────┐
│ Structured Streaming 流表模型 │
│ │
│ 输入流: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 时间 │ 用户 │ 动作 │ 金额 │ │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ 10:00:01│ user1 │ click │ 100 │ ← 新到达 │ │
│ │ 10:00:02│ user2 │ buy │ 250 │ ← 新到达 │ │
│ │ 10:00:03│ user1 │ click │ 150 │ ← 新到达 │ │
│ │ ... │ ... │ ... │ ... │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 连续查询:SELECT user_id, SUM(amount) FROM input GROUP BY user_id │
│ │ │
│ ▼ │
│ 输出结果表(增量更新): │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 用户 │ 总金额 │ 更新时间 │ │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ user1 │ 250 │ 10:00:03 │ ← 增量更新 │ │
│ │ user2 │ 250 │ 10:00:02 │ ← 新增 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
9.3.2 基本用法
python
"""
Structured Streaming - WordCount 示例
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import *
# 创建 SparkSession
spark = SparkSession.builder \
.appName("StructuredStreaming") \
.getOrCreate()
# 读取 Kafka 流
lines = (spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("subscribe", "input-topic")
.option("startingOffsets", "latest") # 或 "earliest"
.load()
)
# 处理数据
words = (lines
.selectExpr("CAST(value AS STRING)")
.select(explode(split(col("value"), " ")).alias("word"))
.groupBy("word")
.count()
)
# 输出到控制台
query = (words
.writeStream
.outputMode("complete") # complete/append/update
.format("console")
.option("truncate", False)
.trigger(processingTime="5 seconds")
.start()
)
query.awaitTermination()
9.3.3 输出模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| Complete | 每次输出完整结果表 | 聚合查询(groupBy) |
| Append | 只输出新增行 | 无聚合、有 Watermark 的查询 |
| Update | 只输出变化的行 | 聚合查询,节省输出 |
python
# Complete 模式 - 每次输出所有词频
word_counts.writeStream.outputMode("complete")
# Append 模式 - 只输出新到达的数据
events.writeStream.outputMode("append")
# Update 模式 - 只输出变化的词频
word_counts.writeStream.outputMode("update")
9.3.4 Event-Time 与 Watermark
python
"""
Event-Time 处理 - 处理乱序数据
"""
from pyspark.sql.functions import *
# 读取带时间戳的数据
events = (spark
.readStream
.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.load()
.selectExpr("CAST(value AS JSON) AS json")
.select(
col("json.user_id"),
col("json.amount"),
to_timestamp(col("json.event_time")).alias("event_time")
)
)
# 设置 Watermark(允许 10 分钟延迟)
# Watermark = 最大事件时间 - 10 分钟
# 早于 Watermark 的数据会被丢弃
events_with_watermark = events.withWatermark("event_time", "10 minutes")
# 基于 Event-Time 的窗口聚合
windowed_counts = (events_with_watermark
.groupBy(
window(col("event_time"), "5 minutes"), # 5 分钟窗口
col("user_id")
)
.agg(sum("amount").alias("total_amount"))
)
# 输出
query = (windowed_counts
.writeStream
.outputMode("update")
.format("console")
.option("truncate", False)
.start()
)
9.3.5 多流 Join
python
"""
流 - 流 Join - 关联两个数据流
"""
# 订单流
orders = (spark
.readStream.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("subscribe", "orders")
.load()
.selectExpr("CAST(value AS JSON) AS json")
.select(
col("json.order_id").alias("order_id"),
col("json.user_id").alias("user_id"),
col("json.amount").alias("amount"),
to_timestamp(col("json.order_time")).alias("order_time")
)
.withWatermark("order_time", "2 hours")
)
# 支付流
payments = (spark
.readStream.format("kafka")
.option("kafka.bootstrap.servers", "kafka:9092")
.option("subscribe", "payments")
.load()
.selectExpr("CAST(value AS JSON) AS json")
.select(
col("json.order_id").alias("order_id"),
col("json.payment_status").alias("status"),
to_timestamp(col("json.payment_time")).alias("payment_time")
)
.withWatermark("payment_time", "2 hours")
)
# 流 - 流 Join(需要 Watermark 和 Join 条件)
joined = (orders
.join(payments,
expr("""
orders.order_id = payments.order_id
AND payment_time >= order_time
AND payment_time <= order_time + interval 2 hours
"""))
.select("order_id", "user_id", "amount", "status")
)
query = joined.writeStream.outputMode("append").format("console").start()
9.3.6 状态存储
python
"""
状态存储配置 - 管理聚合状态
"""
spark.conf.set("spark.sql.streaming.stateStore.providerClass",
"org.apache.spark.sql.execution.streaming.state.RocksDBStateStoreProvider")
# 或者使用 HDFS 作为状态后端
spark.conf.set("spark.sql.streaming.checkpointLocation",
"hdfs://namenode:9000/checkpoint")
# 带状态的聚合
user_sessions = (events
.withWatermark("event_time", "30 minutes")
.groupBy(
col("user_id"),
window(col("event_time"), "30 minutes")
)
.agg(
count("*").alias("event_count"),
sum("amount").alias("total_amount"),
min("event_time").alias("session_start"),
max("event_time").alias("session_end")
)
)
9.4 选型建议
| 需求 | 推荐 API | 理由 |
|---|---|---|
| 新项目 | Structured Streaming | 更现代、功能更全、性能更好 |
| 低延迟(毫秒级) | Structured Streaming | 支持 Continuous Processing |
| 复杂 Event-Time 处理 | Structured Streaming | Watermark、多流 Join |
| 已有 DStream 代码 | Spark Streaming | 迁移成本高,可逐步替换 |
| 简单批流一体 | Structured Streaming | 同一套 API 处理批和流 |
十、技术选型对比
10.1 Spark vs Flink vs Ray
| 维度 | Spark | Flink | Ray |
|---|---|---|---|
| 计算模型 | 微批处理 | 原生流处理 | 任务/Actor |
| 延迟 | 秒级 | 毫秒级 | 毫秒级 |
| 吞吐量 | 高 | 高 | 中 |
| 状态管理 | Checkpoint | Checkpoint + State Backend | Actor 状态 |
| API 丰富度 | ★★★★★ | ★★★★☆ | ★★★☆☆ |
| 机器学习 | MLlib(成熟) | Flink ML(发展中) | Ray ML(生态丰富) |
| 部署复杂度 | 中 | 中高 | 低 |
| 社区活跃度 | 非常活跃 | 非常活跃 | 活跃 |
10.2 架构图对比
┌─────────────────────────────────────────────────────────────────────────┐
│ Spark 架构 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Driver │────►│ DAGSched │────►│ TaskSched │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Executor Pool │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Task 1 │ │ Task 2 │ │ Task 3 │ │ Task 4 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 特点:基于 RDD 的批处理模型,流处理是微批模拟 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Flink 架构 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Client │────►│ JobManager │────►│ Resource │ │
│ └─────────────┘ └──────┬──────┘ │ Manager │ │
│ │ └─────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ TaskManager Pool │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Task Slot 1 │ │ Task Slot 2 │ │ │
│ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ │
│ │ │ │ Operator │ │ │ │ Operator │ │ │ │
│ │ │ │ Chain │ │ │ │ Chain │ │ │ │
│ │ │ └───────────┘ │ │ └───────────┘ │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 特点:原生流处理,Operator Chain 优化,精确一次语义 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Ray 架构 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Driver │────►│ GCS │────►│ Scheduler │ │
│ │ (Python) │ │(Redis-based)│ │ │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Node Pool │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Task/ │ │ Actor │ │ Task │ │ Actor │ │ │
│ │ │ Actor │ │ State │ │ │ │ State │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ (Object Store - Shared Memory) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 特点:Python 原生,Task+Actor 模型,共享内存对象存储 │
└─────────────────────────────────────────────────────────────────────────┘
10.3 选型建议
选择 Spark 的场景
python
"""
✅ 适合 Spark 的场景
"""
# 1. 离线批处理(T+1 报表、数据仓库)
# 优势:成熟的 SQL 支持,生态完善
spark.sql("""
SELECT user_id, SUM(amount)
FROM orders
WHERE date = '2024-01-01'
GROUP BY user_id
""")
# 2. 大规模 ETL
# 优势:高吞吐,容错好
etl_result = (spark
.read.parquet("hdfs://raw/*")
.filter(col("valid") == True)
.withColumn("processed", lit(current_timestamp()))
.write.partitionBy("date").parquet("hdfs://processed/")
)
# 3. 机器学习(传统 ML)
# 优势:MLlib 成熟,Pipeline 完善
from pyspark.ml import Pipeline
from pyspark.ml.classification import RandomForestClassifier
pipeline = Pipeline(stages=[
feature_extractor,
scaler,
RandomForestClassifier(numTrees=100)
])
model = pipeline.fit(train_data)
# 4. 团队已有 Hadoop 生态
# 优势:与 HDFS、Hive、HBase 无缝集成
选择 Flink 的场景
python
"""
✅ 适合 Flink 的场景
"""
# 1. 实时流处理(实时指标、风控)
# 优势:低延迟,精确一次语义
from pyflink.datastream import StreamExecutionEnvironment
env = StreamExecutionEnvironment.get_execution_environment()
# 实时计算 UV
clicks = env.add_source(KafkaConsumer(...))
uv = (clicks
.key_by(lambda x: x["user_id"])
.time_window(Time.minutes(5))
.apply(UvCountFunction())
)
# 2. 复杂事件处理(CEP)
# 优势:强大的模式匹配能力
from pyflink.cep import CEP
pattern = (Pattern
.begin("start").where(lambda x: x["type"] == "login")
.next("fail").where(lambda x: x["type"] == "login_fail")
.within(Time.minutes(5))
)
alerts = CEP.pattern(stream, pattern).select(AlertFunction())
# 3. 状态丰富的流应用
# 优势:强大的 State Backend
value_state = ValueStateDescriptor("count", Types.INT())
process_function = MyRichProcessFunction(value_state)
选择 Ray 的场景
python
"""
✅ 适合 Ray 的场景
"""
# 1. 机器学习训练(分布式训练、超参调优)
# 优势:与 PyTorch/TF 深度集成
from ray import tune
from ray.train.torch import TorchTrainer
# 超参搜索
analysis = tune.run(
train_model,
config={"lr": tune.loguniform(1e-4, 1e-1)},
num_samples=100
)
# 2. 强化学习
# 优势:RLlib 生态,多环境并行
from ray import rllib
rllib.train(
"PPO",
env="CartPole-v1",
num_workers=10 # 10 个并行环境
)
# 3. Python 原生分布式应用
# 优势:简单,无需 JVM
import ray
@ray.remote
def process(data):
return heavy_computation(data)
results = ray.get([process.remote(d) for d in data_list])
# 4. 模型服务部署
# 优势:Ray Serve 简单灵活
from ray import serve
@serve.deployment
class ModelDeployment:
def __init__(self, model):
self.model = model
def __call__(self, request):
return self.model.predict(request)
10.4 决策树
┌─────────────────────────────────────────────────────────────────────────┐
│ 技术选型决策树 │
│ │
│ 你的需求是什么? │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 离线批处理 实时流处理 ML/RL 训练 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Spark │ │ Flink │ │ Ray │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ • 数据仓库 ETL • 实时指标监控 • 分布式训练 │
│ • T+1 报表 • 风控告警 • 超参调优 │
│ • 大规模 ML • CEP 事件处理 • 强化学习 │
│ • 已有 Hadoop 生态 • 低延迟要求 • Python 原生 │
│ │
│ 特殊情况: │
│ • 需要同时支持批 + 流 → Flink (批流一体) │
│ • 团队 Python 为主 → Ray (学习成本低) │
│ • 企业级稳定性 → Spark (最成熟) │
│ • 超大规模 (>1000 节点) → Spark/Flink │
└─────────────────────────────────────────────────────────────────────────┘
10.5 混合架构
实际生产中,经常采用 混合架构:
┌─────────────────────────────────────────────────────────────────────────┐
│ 混合架构示例 │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 数据源层 │ │
│ │ Kafka │ MySQL │ HDFS │ S3 │ 日志 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Flink │ │ Spark │ │ Ray │ │
│ │ (实时层) │ │ (离线层) │ │ (ML 层) │ │
│ │ │ │ │ │ │ │
│ │ • 实时指标 │ │ • T+1 报表 │ │ • 模型训练 │ │
│ │ • 风控告警 │ │ • 数据仓库 │ │ • 超参调优 │ │
│ │ • CEP 处理 │ │ • ETL │ │ • 模型服务 │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └─────────────────────┼─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 数据存储层 │ │
│ │ ClickHouse │ StarRocks │ Redis │ MySQL │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ 数据流向: │
│ 实时数据 → Flink → ClickHouse (实时查询) │
│ 离线数据 → Spark → StarRocks (OLAP 分析) │
│ 训练数据 → Ray → 模型 → 服务部署 │
└─────────────────────────────────────────────────────────────────────────┘
十一、Spark 3.x 新特性
11.1 Adaptive Query Execution (AQE)
Spark 3.0 引入了自适应查询执行,这是 Spark SQL 最重要的性能优化特性之一。
┌─────────────────────────────────────────────────────────────────────────┐
│ AQE 工作原理 │
│ │
│ 传统执行流程: │
│ Query → 逻辑计划 → 物理计划 → 固定执行 │
│ (执行计划一旦生成就无法调整) │
│ │
│ AQE 执行流程: │
│ Query → 逻辑计划 → 物理计划 → 运行时统计 → 动态调整 │
│ ↓ │
│ ┌─────────┼─────────┐ │
│ ↓ ↓ ↓ │
│ 合并小分区 优化Join策略 处理数据倾斜 │
└─────────────────────────────────────────────────────────────────────────┘
核心特性:
| 特性 | 说明 | 配置参数 |
|---|---|---|
| 动态合并 Shuffle 分区 | 运行时合并小分区,减少 Task 数量 | spark.sql.adaptive.coalescePartitions.enabled |
| 动态切换 Join 策略 | 根据运行时统计自动选择最优 Join 方式 | spark.sql.adaptive.localShuffleReader.enabled |
| 动态优化倾斜 Join | 自动检测并处理倾斜的 Partition | spark.sql.adaptive.skewJoin.enabled |
python
# 启用 AQE(Spark 3.0+ 默认开启)
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
# 配置参数
spark.conf.set("spark.sql.adaptive.coalescePartitions.initialPartitionNum", "200")
spark.conf.set("spark.sql.adaptive.coalescePartitions.minPartitionSize", "1m")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "256m")
11.2 动态分区裁剪
Spark 3.0 引入动态分区裁剪,显著提升 Join 查询性能。
python
# 启用动态分区裁剪(默认开启)
spark.conf.set("spark.sql.optimizer.dynamicPartitionPruning.enabled", "true")
# 示例:维度表关联事实表
# 维度表(小表)
dim_table = spark.table("dim_region").filter(col("country") == "China")
# 事实表(大表,按 region_id 分区)
fact_table = spark.table("fact_sales")
# Spark 会自动将 dim_table 的 region_id 谓词下推到 fact_table
# 即使 fact_table 没有明确过滤条件,也能跳过无关分区
result = fact_table.join(dim_table, "region_id")
11.3 ANSI SQL 兼容性
Spark 3.0 增强了 ANSI SQL 兼容性:
python
# 启用 ANSI 模式
spark.conf.set("spark.sql.ansi.enabled", "true")
# 严格类型检查
spark.conf.set("spark.sql.ansi.strictTypeCoercion", "true")
# 除零错误
spark.conf.set("spark.sql.ansi.strictDivision", "true")
# 示例
spark.sql("SELECT 1/0") # 抛出异常而非返回 NULL
spark.sql("SELECT CAST('abc' AS INT)") # 抛出异常
11.4 Kubernetes 原生支持
Spark 3.x 增强了 Kubernetes 支持:
bash
# 提交任务到 Kubernetes
spark-submit \
--master k8s://https://kubernetes-master:8443 \
--deploy-mode cluster \
--name spark-pi \
--class org.apache.spark.examples.SparkPi \
--conf spark.executor.instances=5 \
--conf spark.kubernetes.container.image=spark:3.5.0 \
--conf spark.kubernetes.namespace=spark-namespace \
local:///opt/spark/examples/jars/spark-examples_2.12-3.5.0.jar
yaml
# Spark Operator 部署示例
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
name: spark-pi
namespace: spark-operator
spec:
type: Scala
mode: cluster
image: spark:3.5.0
mainClass: org.apache.spark.examples.SparkPi
mainApplicationFile: local:///opt/spark/examples/jars/spark-examples_2.12-3.5.0.jar
executor:
instances: 3
cores: 2
memory: 4g
driver:
cores: 2
memory: 4g
十二、总结
12.1 Spark 核心优势
- 速度快:基于内存计算,比 MapReduce 快 10-100 倍
- 易用性高:支持 Scala/Java/Python/R,API 丰富
- 生态完善:SQL、Streaming、MLlib、GraphX 全覆盖
- 通用性:批处理、流处理、交互式查询、机器学习
- 成熟稳定:大量企业生产验证,社区活跃
12.2 学习路线建议
入门 → 进阶 → 深入 → 专家
│ │ │ │
│ │ │ └─ 源码阅读
│ │ │ 性能调优
│ │ │ 二次开发
│ │ │
│ │ └─ 数据倾斜处理
│ │ Shuffle 优化
│ │ 内存管理
│ │
│ └─ DataFrame/SQL
│ 性能优化
│ 生产实践
│
└─ RDD 基础
算子理解
WordCount
12.3 最佳实践清单
- ✅ 优先使用 DataFrame/Dataset API(Catalyst 优化)
- ✅ 合理设置分区数(
spark.sql.shuffle.partitions) - ✅ 对复用 RDD 进行缓存(
cache()/persist()) - ✅ 使用广播变量优化小表 Join
- ✅ 监控并处理数据倾斜
- ✅ 启用动态资源分配(
spark.dynamicAllocation.enabled) - ✅ 使用 Kryo 序列化节省内存
- ✅ 合理设置 Executor 内存和核心数
- ✅ 启用 AQE 自适应查询执行(Spark 3.0+)
- ✅ 使用 Checkpoint 切断过长 Lineage
附录:环境搭建
A.1 本地开发环境
bash
# 安装 Spark(使用 Homebrew)
brew install apache-spark
# 验证安装
spark-shell --version
# 启动本地集群
start-master.sh
start-worker.sh spark://localhost:7077
A.2 Docker 环境
yaml
# docker-compose.yml
version: '3.8'
services:
spark-master:
image: bitnami/spark:3.5
environment:
- SPARK_MODE=master
ports:
- "8080:8080"
- "7077:7077"
spark-worker:
image: bitnami/spark:3.5
environment:
- SPARK_MODE=worker
- SPARK_MASTER_URL=spark://spark-master:7077
depends_on:
- spark-master
A.3 PySpark 安装
bash
# 安装 PySpark
pip install pyspark
# 验证
python -c "from pyspark import SparkContext; print(SparkContext)"