一文通关Spark

Spark 分布式计算框架深度解析

从入门到源码,全面掌握大数据处理引擎


目录


一、项目背景和应用场景

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 基准测试

核心设计理念
  1. 内存计算优先

    • 中间结果保存在内存而非磁盘
    • 支持手动缓存策略(CACHE/PERSIST)
    • 内存不足时自动溢出到磁盘
  2. DAG 执行引擎

    • 将计算任务表示为有向无环图
    • 自动优化执行计划(管道化、谓词下推)
    • 比 MapReduce 的 Map→Shuffle→Reduce 更灵活
  3. 统一技术栈

    • 批处理、流处理、SQL、ML、图计算一套 API
    • 降低学习和维护成本
    • 数据在不同组件间无缝流转
  4. 容错机制

    • 基于 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 的操作,分为 TransformationAction 两类。

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 写 + 读)

优化建议

  1. 减少 Shuffle:能用窄依赖实现的就不用宽依赖
  2. 合并操作:多个窄依赖操作可合并到一个 Stage
  3. 预分区:对需要多次 join/aggregation 的 Key 预先 partitionBy
  4. 调整并行度spark.sql.shuffle.partitions 设置合理的 Shuffle 分区数
  5. 使用广播:小表 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 exceededSparkOutOfMemoryError

原因

  • 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 处理批和流

十、技术选型对比

维度 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 无缝集成
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 核心优势

  1. 速度快:基于内存计算,比 MapReduce 快 10-100 倍
  2. 易用性高:支持 Scala/Java/Python/R,API 丰富
  3. 生态完善:SQL、Streaming、MLlib、GraphX 全覆盖
  4. 通用性:批处理、流处理、交互式查询、机器学习
  5. 成熟稳定:大量企业生产验证,社区活跃

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)"
相关推荐
ACP广源盛139246256732 小时前
ASW3810@ACP#4 路差分 2:1/1:2 双向多路复用 / 解复用器 产品规格与应用总结
大数据·单片机·嵌入式硬件·计算机外设·电脑
AI营销资讯站2 小时前
AI营销内容增长瓶颈?原圈科技以AI Agents破局之道
大数据·人工智能
hellolianhua2 小时前
测试集群hdfs和mapreduce
大数据·hadoop·hdfs
颜颜yan_2 小时前
面向工业物联网的大数据底座选型:Apache IoTDB 的架构能力与落地价值分析
大数据·物联网·apache
Cx330❀2 小时前
Linux System V标准简介
大数据·linux·运维·服务器·人工智能
jerryinwuhan2 小时前
Spark RDD 编程入门
大数据·分布式·spark
小陈工2 小时前
ModelEngine智能体开发实战:知识库自动生成与多Agent协作
大数据·网络·数据库·人工智能·python·django·异步
一叶飘零_sweeeet2 小时前
分布式协调双雄深度拆解:ZooKeeper 与 Nacos 从底层原理到生产实战全指南
分布式·zookeeper·nacos
llilian_164 小时前
IRIG-B码产生器立足用户痛点,提供精准授时解决方案
大数据·数据库·功能测试·单片机·嵌入式硬件·测试工具