1. Spark 应用的基本形态
- Driver(驱动程序) :运行你的
main函数,负责构建 DAG、提交任务、汇总结果。 - Executors(执行器):分布在集群各节点,执行并行任务。
- RDD(Resilient Distributed Dataset) :Spark 的核心抽象------可并行操作、具容错能力的分布式数据集。RDD 的每个分区在不同节点处理。
获取 RDD 的两条路径:
- 从外部存储读取(HDFS、本地、S3、HBase、任意 Hadoop InputFormat)
- 从 Driver 的本地集合并行化(
sc.parallelize)
容错性 :RDD 基于 lineage(血缘)自动从失败节点恢复。
持久化:可将 RDD 缓存在内存或磁盘,重复使用时显著加速。
2. 共享变量:Broadcast 与 Accumulator
- 默认行为 :并行任务会收到"闭包"的拷贝,各 Executor 内部修改对 Driver 不可见。
- Broadcast(广播变量) :把只读的大对象高效分发并缓存到各节点。
- Accumulator(累加器) :只支持加法 (交换结合),用于计数/求和等全局聚合;仅 Driver 可读值。
示例(Python):
python
# Broadcast
bvar = sc.broadcast({"threshold": 10})
rdd.map(lambda x: x > bvar.value["threshold"]).count()
# Accumulator
acc = sc.accumulator(0)
sc.parallelize([1,2,3,4]).foreach(lambda x: acc.add(x))
print(acc.value) # 10
生产建议:广播对象创建后不要再修改 ;累加器的更新应在 action 内进行(在 transformation 中可能被重复执行)。
3. 语言与环境:如何链接 Spark
-
PySpark 版本 :Spark 4.0.1 适配 Python 3.9+,支持 CPython / PyPy 7.3.6+。
-
两种运行方式:
bin/spark-submit/bin/pyspark(无需 pip 安装 PySpark)pip install pyspark后,直接python your_app.py或继续用spark-submit
-
在项目里声明依赖(示例):
pythoninstall_requires = ["pyspark==4.0.1"] -
选择 Python 解释器:
bashPYSPARK_PYTHON=python3.10 bin/pyspark PYSPARK_PYTHON=/path/to/pypy bin/spark-submit app.py
4. 初始化 Spark:SparkConf / SparkContext
python
from pyspark import SparkConf, SparkContext
conf = SparkConf().setAppName("MyApp").setMaster("local[4]") # 本地4核
sc = SparkContext(conf=conf)
在 生产集群 中常用
spark-submit --master ...传入 master,不在代码里硬编码。
5. 用 Shell 快速实验(强烈推荐)
-
进入交互式 Shell:
bash./bin/pyspark --master "local[4]" \ --py-files code.py \ --packages groupId:artifactId:version \ --repositories https://repo1.maven.org/maven2 -
IPython / Jupyter:
bashPYSPARK_DRIVER_PYTHON=ipython ./bin/pyspark PYSPARK_DRIVER_PYTHON=jupyter \ PYSPARK_DRIVER_PYTHON_OPTS=notebook \ ./bin/pyspark
在 Notebook 里可用
%pylab inline做内嵌可视化。
6. 创建 RDD:并行化 vs. 外部数据集
6.1 并行化本地集合
python
data = [1, 2, 3, 4, 5]
dist = sc.parallelize(data, numSlices=4)
print(dist.reduce(lambda a,b: a+b)) # 15
分区数经验值:每个 CPU 2~4 份。
6.2 外部数据集(Text / WholeText / SequenceFile / 任意 InputFormat)
python
# 行式文本
distFile = sc.textFile("hdfs:///data/input/*.txt")
total = distFile.map(lambda s: len(s)).reduce(lambda a,b: a+b)
# small files -> (filename, content)
pairs = sc.wholeTextFiles("s3a://bucket/path/")
# SequenceFile(KV)
rdd = sc.parallelize(range(1,4)).map(lambda x: (x, "a"*x))
rdd.saveAsSequenceFile("path/to/file")
print(sorted(sc.sequenceFile("path/to/file").collect()))
# [(1,'a'), (2,'aa'), (3,'aaa')]
# 自定义 InputFormat(例:Elasticsearch)
# ./bin/pyspark --jars elasticsearch-hadoop.jar
conf = {"es.resource": "index/type"}
es_rdd = sc.newAPIHadoopRDD(
"org.elasticsearch.hadoop.mr.EsInputFormat",
"org.apache.hadoop.io.NullWritable",
"org.elasticsearch.hadoop.mr.LinkedMapWritable",
conf=conf
)
print(es_rdd.first()) # => Python dict
读取本地路径时需确保 worker 也能访问;通配与压缩文件开箱即用。
7. RDD 编程模型:转换(Transformations)与行动(Actions)
-
Lazy:转换是延迟的,仅在遇到行动时触发计算。
-
常用转换(部分):
map/flatMap/filtermapPartitions/mapPartitionsWithIndexunion/intersection/distinctgroupByKey(聚合推荐reduceByKey/aggregateByKey)reduceByKey/aggregateByKeyjoin/leftOuterJoin/cogrouprepartition/coalesce/repartitionAndSortWithinPartitions
-
常用行动(部分):
reduce/collect/count/first/take/takeSampletakeOrdered/saveAsTextFile/countByKey/foreach
示例:
python
lines = sc.textFile("data.txt")
lineLens = lines.map(lambda s: len(s)) # 转换
total = lineLens.reduce(lambda a,b: a+b) # 行动
复用 :如果
lineLens后续还会多次使用,建议先lineLens.persist()。
8. 闭包与副作用:为什么"直接改全局变量"会翻车
python
counter = 0
rdd = sc.parallelize([1,2,3])
def inc(x):
global counter
counter += x
# 错误示范:Executor 改的是闭包副本,Driver 看不到
rdd.foreach(inc)
print(counter) # 仍可能是 0(分布式下)
- 在分布式模式,任务执行前闭包会被序列化 并拷贝到各 Executor;修改发生在副本上。
- 正确做法 :使用 Accumulator 聚合,或把需要的"配置"提前做成 Broadcast。
打印元素 :想在 Driver 打印,用 take/collect 后在 Driver 打印;不要在 Executor 里 print 期待回到 Driver。
python
for x in rdd.take(100):
print(x)
9. Shuffle:最昂贵但不可避免的重分布
触发场景 :reduceByKey/aggregateByKey/groupByKey/join/repartition/coalesce(扩大分区) 等。
开销来源 :网络传输、磁盘 I/O、序列化/反序列化、堆上数据结构(溢出后落盘)。
内部机制 :map 端写按目标分区排序的中间文件;reduce 端按需拉取并聚合。
排序与顺序 :Shuffle 后分区集合与分区顺序可确定 ,但分区内元素顺序不保证;需要有序请用:
mapPartitions(...)内排序repartitionAndSortWithinPartitions(...)sortBy(...)进行全局排序
调优提示:
- 适度提高并行度(
spark.sql.shuffle.partitions/rdd.repartition(n)) - 避免滥用
groupByKey,优先reduceByKey/aggregateByKey - 监控临时目录磁盘(
spark.local.dir),长作业注意中间文件清理时机
10. 持久化(缓存)策略
python
rdd.cache() # = persist(StorageLevel.MEMORY_ONLY)
# or
from pyspark.storagelevel import StorageLevel
rdd.persist(StorageLevel.MEMORY_AND_DISK)
Python 存储级别: MEMORY_ONLY[_2]、MEMORY_AND_DISK[_2]、DISK_ONLY[_2|_3] 等;对象会通过 Pickle 序列化。
选择指南:
- 能放下就用
MEMORY_ONLY(CPU 最省) - 放不下 →
MEMORY_ONLY_SER/MEMORY_AND_DISK(Scala/Java 下 SER 更省内存;PySpark天然序列化) - 不轻易"溢出到磁盘",除非重算成本更高
- Web 服务低延迟 + 快速容错 →
_2级别做副本 - 释放缓存:
rdd.unpersist(blocking=True)
11. 用 Key-Value RDD 做聚合与 Join(PySpark)
python
lines = sc.textFile("data.txt")
pairs = lines.map(lambda s: (s, 1))
counts = pairs.reduceByKey(lambda a,b: a+b)
topN = counts.sortBy(lambda kv: kv[1], ascending=False).take(10)
12. 部署到集群与作业启动
- 打包 :Scala/Java → JAR;Python →
.py/.zip(依赖走--py-files或集群镜像) - 提交 :
bin/spark-submit可投递到 Standalone / YARN / Kubernetes - 嵌入式启动 (Java/Scala):
org.apache.spark.launcher提供子进程 API
13. 单元测试友好
- 测试中用
local[*]创建SparkContext - 在
tearDown或finally中sc.stop(),确保同一进程只存在一个SparkContext
14. 实用清单(Troubleshooting & Best Practices)
- 闭包陷阱:不要在 Executor 修改 Driver 变量;用 Accumulator。
- Driver OOM :避免对大 RDD
collect();先take/sample/saveAsTextFile。 - 分区数 :Map 阶段/Shuffle 后适度提升并行度;小结果写出可
coalesce(1)。 - 数据路径 :本地/共享/分布式路径要一致可见;S3/HDFS 用
s3a:///hdfs://。 - 依赖管理 :第三方连接器走
--packages或--jars,Python 依赖用--py-files/镜像。 - 热数据缓存:确实"常用再缓存",配合 WebUI 观察命中与内存占用。