1. 先把概念捋清楚:Spark到底是个什么东西
1.1 一句话说清Spark的定位
Spark是一个分布式计算引擎,专门用来处理“单台机器搞不定”的数据计算问题。什么叫做单台机器搞不定?两个典型场景:一是数据量大到内存放不下,二是计算逻辑复杂到单机跑完要好几个小时甚至直接卡死。
你可能听说过Hadoop MapReduce,Spark和它的定位很像,都是做分布式计算,但Spark最大的杀手锏是“内存计算”。MapReduce每个步骤都要落盘,每一步读写硬盘都是一次IO开销,而Spark尽量把中间结果留在内存里,所以跑迭代式算法、交互式查询的时候速度能快出好几个数量级。
我用一个不太严谨但很好懂的类比:MapReduce像是去银行办事,每办一个窗口业务就要重新排一次队;Spark则是提前把所有材料准备好,进一个大厅就能连着把几件事办完。数据还在内存里热着,下一个算子直接用,不用反复冷启动加载。
我在实际带新人入门的时候发现,很多初学者一上来就盯着RDD、DataFrame、shuffle这些术语,结果看了一周概念还是不会动手。所以这篇教程的定位很简单——让你今晚就能在本机跑出一个真正的Spark程序,同时把背后的原理和常见的坑讲清楚。
1.2 Spark生态全景:这么多组件分别干嘛的
打开Spark官网或者看招聘JD,你会发现Spark从来不是一个光秃秃的计算引擎,它是一整套生态。先记住下面这张表,不用全懂,但至少面试和看文档时不至于懵。
| 组件 | 作用 | 类比 |
|---|---|---|
| Spark Core | 基础计算引擎,负责任务调度、内存管理、容错 | 操作系统内核 |
| Spark SQL | 用SQL或DataFrame API做结构化数据处理 | 数据库查询引擎 |
| Spark Streaming / Structured Streaming | 处理实时流式数据,按微批次切分 | 水龙头分段接水 |
| MLlib | 机器学习算法库,包括分类、回归、聚类、推荐 | 算法工具箱 |
| GraphX | 图计算组件,处理社交网络、关系图谱等 | 图数据库专用计算器 |
选择Spark的很大一部分原因,就是这个生态太全了。同一个集群上既能跑离线批处理,又能跑实时流,还能直接做机器学习模型训练前的特征工程,不用来回搬数据换技术栈。
1.3 你该用哪个版本、哪门语言入门
先说版本。现在稳定主线是Spark 3.x,3.5已经是比较成熟的版本。网上还能搜到Spark 2.0、2.4的源码下载和旧教程,但除非你维护的是老集群,否则别在版本上纠结太久,直接选3.3+以上的新版本。
再说语言。Spark支持Java、Scala、Python、R四种语言,我的建议很明确:
- 如果你未来做偏数据工程、跑批、SQL类工作:Python + PySpark完全够用。
- 如果你做偏底层调优、二次开发、写UDF:建议直接学Scala,因为Spark源码本身就是Scala写的,很多底层算子Scala才暴露完整能力。
- 如果只是公司有现成集群,你只负责写分析任务:先用SQL + DataFrame解决问题,这是上手最快的路径。
我自己带过不少从零开始的人,绝大多数都是从PySpark入门的。语法简单、调试方便、本地装了Python就能折腾,不用像Scala那样先搞懂sbt编译、类型推断这些烦心事。等跑通了几个实战项目,再去补Scala,是性价比最高的路线。
2. 动手前的准备:装环境、跑起来的两种姿势
2.1 本地模式:最快速度验证“Spark能跑”
很多人一听到“Spark集群”就以为必须搞三台服务器,其实Spark最简单的运行方式就是本地模式。本地模式下,Spark用一台机器上的多线程来模拟分布式计算,不需要Hadoop、不需要YARN,非常适合学习和初步开发。
准备步骤非常简单:
第一步:安装JDK(8/11/17均可)
Spark是基于JVM的,无论你用Python还是Scala,JDK都是必须的。官方要求JDK 8/11/17,我用的是JDK 11,稳定没毛病。装完记得配置JAVA_HOME.
第二步:安装Python(3.8及以上)
我是直接装的Anaconda,好处是numpy、pandas这些常用库都一起带了,后续做数据处理样例时不用单独装包。
第三步:下载Spark安装包
从官网下载spark-3.5.x-bin-hadoop3.tgz,解压到一个没有空格的路径,比如/opt/spark或者D:\spark. 然后配置环境变量:
export SPARK_HOME=/opt/spark export PATH=$SPARK_HOME/bin:$PATHWindows用户记得把%SPARK_HOME%\bin加到Path环境变量里。
第四步:验证安装
pyspark看到SparkSession已经在localhost:4040启动,就说明本地环境OK了。整个安装过程熟练的话不到十分钟。
2.2 集群模式:从单机到一堆机器的思路
本地模式是学习用的,生产环境肯定还是集群模式。Spark常见的集群部署方式有三种:
- Standalone模式:Spark自带的一套简单集群管理器,不需要额外组件就可以把多台机器组成集群,适合中小规模场景和测试环境。
- YARN模式:大数据生态里最常用的资源管理器,如果你公司已经有Hadoop集群,Spark跑在YARN上最省事,因为资源和权限体系都统一管理。
- Kubernetes模式:容器化部署中的主流方案(配套DGX这类GPU服务器做Spark部署时也会用),适合云原生环境、需要弹性伸缩的场景。
如果你现在只是个人学习,完全不需要着急搭集群。先把本地模式玩熟,理解了任务怎么提交、资源怎么分配,再去搭集群会顺畅得多。反过来说,连单机Spark都跑不起来,直接去搭集群大概率会被各种环境配置折腾到怀疑人生。
2.3 开发工具推荐与起步调试技巧
很多初学者用pyspark命令行一句一句地敲,这适合测试,但不适合写完整任务。我更推荐两个方案:
- VS Code + Python插件:本地写
.py文件,用spark-submit提交,或直接右键Run。 - Jupyter Notebook:配合
findspark可以很方便地在Notebook里初始化Spark环境,逐段调试非常直观。
import findspark findspark.init() from pyspark.sql import SparkSession spark = SparkSession.builder.appName("MyFirstApp").master("local[*]").getOrCreate()注意local[*]这个参数,星号代表用本机所有可用的CPU核心数来跑。如果你是8核机器,Spark就会创建8个并发任务来模拟分布式并行计算。学习阶段这样设置就够了,别贪多去调local[1]或local[2]这种手动控制,留到后面理解了并发模型再调整也不迟。
3. 核心概念一次讲透:RDD、DataFrame、Dataset
3.1 RDD:Spark最底层的抽象
RDD的全称是弹性分布式数据集(Resilient Distributed Dataset),它代表一个只读的、分区的记录集合。我从四个特性帮你理解:
- 只读:RDD一旦创建就不能修改,要做变换只能生成新的RDD。这是为了容错方便,每个RDD都知道自己是由哪个父RDD演算来的。
- 分区:一个RDD会被切分成多个分区,分布式地存储在不同Executor上,Spark任务就是针对每个分区并行计算的。
- 容错(弹性):如果一个分区数据丢失了,RDD可以通过血缘关系重新计算出来,不需要像Hadoop那样做冗余备份。
- 延迟计算:RDD上的
map、filter这类变换不会立即执行,只有遇到count、collect、saveAsTextFile这类行动操作时才会真正触发计算。
我把RDD类比成一张“菜谱”,你往里面加各种步骤(变换操作),但真正开火做菜要等你说“开饭”(行动操作)那一刻才发生。
但RDD有一个很不友好的点:它没有Schema,也就是告诉Spark“那一列是整数、这一列是字符串”的信息完全靠你自己管理。数据量大、列多的时候,用RDD写代码会非常痛苦。
3.2 DataFrame:让Spark真正“好用起来”的关键
DataFrame的出现解决了RDD“缺乏结构”的问题。你可以把它理解为一张带列名和类型的分布式表格,每一行数据都遵循相同的Schema。
DataFrame能让Spark变快的原因有两个:
第一,Catalyst优化器。你写的DataFrame代码会被自动转换为逻辑执行计划,优化器会帮你做谓词下推、列剪枝、常量折叠等优化。比如你只需要查两列,Spark会尽可能跳过其他列的读取和计算。这个优化对于RDD是做不到的。
第二,Tungsten执行引擎。Spark通过二进制存储和代码生成技术,让DataFrame的计算在底层被编译成高性能的Java字节码,避免对象序列化和反序列化的开销,内存利用率更高。
一个很典型的现象:同一个数据统计任务,RDD写法可能跑4分钟,DataFrame写法只要40秒。差距就是这么夸张。
3.3 到底选哪个API:我的建议
| 维度 | RDD | DataFrame | Dataset |
|---|---|---|---|
| 适用人群 | 框架开发者、底层功能需求 | 数据分析、ETL、报表开发 | Scala/Java用户、需要类型安全 |
| Schema支持 | 无 | 有 | 有 |
| 性能 | 低 | 高(有优化器) | 高(有优化器) |
| 开发效率 | 低 | 高 | 中 |
如果你写Python,乖乖用DataFrame。如果你写Scala,优先用Dataset/DataFrame,结合类型安全用Case Class。至于RDD,了解它的特性和原理就够了,除非你要自定义分区器、操作非结构化数据,否则别直接拿RDD去写业务逻辑。
我见过很多从老教程学Spark的人,满脑子还是sc.textFile().flatMap().reduceByKey()这一套,结果同事用DataFrame写的同类任务执行效率高出一截。这年头入门Spark,直接拥抱DataFrame和Spark SQL,才是正道。
4. 从零到能跑:第一个WordCount实战
4.1 环境自检与数据准备
万事开头难,先验证环境是否一切正常。打开终端执行pyspark,如果能够看到SparkSession启动信息,说明环境已经就绪。
接着准备一个用于测试的文本文件,我用/tmp/hello.txt作为示例,内容放几行单词:
hello world spark hello spark hello spark is awesome word count example学习阶段我建议用这种几百字节的小文件调通流程,别上来拿几个G的大文件跑,否则问题排查时单机和分布式混在一起,你根本分不清是代码问题还是环境问题。
4.2 逐步写出WordCount
我分三个级别来演示,你对比着看就明白RDD和DataFrame的差异了。
级别一:RDD版本(学习底层原理用)
from pyspark import SparkContext sc = SparkContext.getOrCreate() # 读文件,每行变成一个元素 lines = sc.textFile("file:///tmp/hello.txt") # 按空格拆成单词 words = lines.flatMap(lambda line: line.split(" ")) # 每个单词计数为1 pairs = words.map(lambda word: (word, 1)) # 按单词聚合求和 counts = pairs.reduceByKey(lambda a, b: a + b) # 触发执行并收集结果 for word, count in counts.collect(): print(f"{word}: {count}")这段代码的每一步都有清晰的语义,也是理解Spark执行模型最好的教科书式示例。
级别二:DataFrame + SQL版本(实际开发推荐)
from pyspark.sql import SparkSession spark = SparkSession.builder.appName("WordCountDF").master("local[*]").getOrCreate() df = spark.read.text("file:///tmp/hello.txt") # 方法1:用DataFrame的API from pyspark.sql.functions import explode, split, col word_counts = df.select(explode(split(col("value"), " ")).alias("word")) \ .groupBy("word").count() # 方法2:直接注册成临时表,写SQL df.createOrReplaceTempView("lines") word_counts_sql = spark.sql(""" SELECT word, COUNT(*) as cnt FROM (SELECT explode(split(value, ' ')) AS word FROM lines) GROUP BY word """) word_counts.show()注意用了file:///前缀,这是告诉Spark读取本地文件系统。如果不加这个前缀,Spark会默认走HDFS路径,单机模式下会报错找不到文件。这是初学者最容易踩的坑之一。
4.3 代码里这几个细节最容易踩坑
第一,collect()慎用。collect()会把所有计算结果拉回到Driver端内存,结果集稍大一点就直接OOM。学习阶段无所谓,但生产环境里统计几十亿行数据时,你用了collect()就等着爆内存吧。要保存到文件用write.csv()或write.parquet()才对。
第二,开多个SparkSession会冲突。有些同学在同一个Python进程里反复执行SparkSession.builder().getOrCreate(),最后报错Cannot call getOrCreate() after SparkSession was stopped或者端口冲突。原因是用完一个SparkSession后调用spark.stop(),然后又想重新创建,同一进程内这样操作很容易出问题。最简单的方式是每个脚本只创建一个SparkSession,别重复创建、重复关闭。
第三,文件路径别写错。Windows用户尤其小心,路径分隔符要用/而不是\,或者使用绝对路径。相对路径在不同工作目录下读取结果完全不一样,建议一开始就写死绝对路径。
5. 一个真实需求:用Spark分析用户复购率(附Redshift集成)
5.1 需求拆解:什么叫“复购率”
结构化的例子比WordCount更能让你感受到Spark在生产中的价值。我们拿电商场景的“用户复购率”来练手,这也是招聘和日常工作中很常见的分析需求。
复购率的口径要先明确。一般指的是:在某段时间内首次购买过的用户中,有多少比例的人再次购买了商品。
计算公式:
复购率 = 至少购买两次的用户数 / 至少购买一次的用户数这里的“两次”和“不同商品”还是“不同订单”要根据业务来定义。比如“用不同订单号算”还是“同一订单内不同商品算”,结果差异很大。我们在这里用最简单也最常用的口径:一个用户在两个不同的订单里出现,就算复购。
5.2 用DataFrame完成清洗、聚合、计算
先造一批模拟数据,两个CSV文件,一个用户表一个订单表:
orders.csv: order_id, user_id, order_time 1001, u001, 2024-01-05 1002, u001, 2024-02-10 1003, u002, 2024-01-08 1004, u003, 2024-01-15 1005, u003, 2024-02-20完整代码如下:
from pyspark.sql import SparkSession from pyspark.sql.functions import col, count, countDistinct spark = SparkSession.builder \ .appName("RepeatPurchaseRate") \ .master("local[*]") \ .getOrCreate() # 读订单数据 orders = spark.read.option("header", True).csv("file:///tmp/orders.csv") orders.show() # 清洗:去空格、去空值、统一用户ID格式 orders_clean = orders.select( col("order_id").cast("string").alias("order_id"), col("user_id").cast("string").alias("user_id"), col("order_time").cast("string").alias("order_time") ).dropDuplicates(["order_id", "user_id"]) # 防止同订单重复记录 # 统计每个用户实际下单次数(拿不同订单号数量) user_orders = orders_clean.groupBy("user_id").agg( countDistinct("order_id").alias("order_cnt") ) user_orders.show() # 加一列标记是否复购 user_orders_with_flag = user_orders.withColumn( "is_repurchase", col("order_cnt") >= 2 ) # 计算复购率 total_users = user_orders_with_flag.count() repurchase_users = user_orders_with_flag.filter(col("is_repurchase") == True).count() repeat_rate = repurchase_users / total_users print(f"总购买用户数: {total_users}") print(f"复购用户数: {repurchase_users}") print(f"复购率: {repeat_rate:.2%}")这个例子虽然数据量很小,但完整走了一遍“读数据 -> 清洗 -> 去重 -> 聚合 -> 业务计算”的流程。实际生产环境里,这几张表的数据量可能是几亿行,Spark并行处理的能力就在这里体现出来了。
5.3 JDBC读写Redshift的思路
热词里出现了“用户复购率 spark 脚本 redshift”,这其实是很典型的数据管道场景:原始数据在Redshift数据仓库里,你要用Spark做复杂加工,处理完再写回Redshift供BI报表查询。
Spark读写Redshift的常见方式是JDBC:
# 从Redshift读数据 df = spark.read \ .format("jdbc") \ .option("url", "jdbc:redshift://your-cluster.redshift.amazonaws.com:5439/yourdb") \ .option("dbtable", "public.orders") \ .option("user", "your_user") \ .option("password", "your_password") \ .option("driver", "com.amazon.redshift.jdbc.Driver") \ .load() # 处理逻辑... # 写回Redshift df.write \ .format("jdbc") \ .option("url", "jdbc:redshift://your-cluster.redshift.amazonaws.com:5439/yourdb") \ .option("dbtable", "public.user_repurchase_result") \ .option("user", "your_user") \ .option("password", "your_password") \ .mode("overwrite") \ .save()这里的核心坑点有三个:
- 司机会冲突。如果你项目里同时有PostgreSQL JDBC驱动和Redshift驱动,注意别把两个相关的包都扔进去,因为Redshift驱动和PostgreSQL驱动的类名容易冲突,导致各种奇怪的ClassNotFoundException。
- 写入性能。通过JDBC逐条写回Redshift非常慢,要对大数据量写回时建议把数据先落成S3上的Parquet,再用
COPY命令导入Redshift,速度能差出几十倍。 - 分区数匹配。写入Redshift前,把DataFrame重分区到合理的并行度(比如16-32个分区),减少数据库连接压力。别用默认的200个分区去写,Redshift会被打崩。
6. 内存、shuffle和任务调优:入门到进阶的必经之路
6.1 Spark为什么总报内存不足
初学Spark的人一定会遇到这个报错:
java.lang.OutOfMemoryError: Java heap space或者:
Container killed by YARN for exceeding memory limits先说Spark的内存模型。Spark的Executor内存分为三块:
- Storage内存:用来缓存RDD/DataFrame数据。
- Execution内存:用来存shuffle、join、aggregation等过程中的临时数据。
- Reserved内存:保留给系统使用。
默认情况下,Storage和Execution是动态共享的,比例为spark.memory.fraction=0.6,也就是Executor堆内存的60%用于这两块,剩下40%留给用户代码和元数据。
出现OOM通常有三种原因:
- Driver端OOM:你用了
collect()、take()这类拉取全部数据的操作。 - Executor端OOM:单个分区内的数据量太大,或者shuffle的数据量超过了Executor内存限制。
- 元数据OOM:表太多、分区太多、Schema太复杂,导致Driver端需要维护的元数据暴增。
调优方向不是一味加大内存,更核心的是减少单个Executor需要处理的数据量,也就是合理分区。
6.2 shuffle是性能杀手,怎么降低开销
shuffle这个词几乎是Spark性能优化的核心议题。简单理解,shuffle就是数据在集群里重新分布的过程,比如groupByKey、join、reduceByKey这些操作,都需要把相同key的数据拉到同一个节点处理。
shuffle慢的原因在于:数据要从当前Executor写出到磁盘,然后通过网络传到其他Executor,再读入内存。涉及磁盘IO + 网络IO,是Spark作业中最昂贵的操作。
我在实际项目中踩过一个印象很深的坑:一条join语句处理不到1亿行的数据,跑了一个多小时。后来排查发现,两个表都发生了严重的shuffle,数据在集群里来回倒腾了几轮。后来改用Broadcast方式,把小表广播到大表所在节点,避免彻底shuffle,作业直接缩短到3分钟。
优化shuffle的思想总结起来有三板斧:
- 减少shuffle数据量:在shuffle之前先
filter、先做列裁剪,尽可能减小单个分区的数据体积。 - 合理设置shuffle分区数:
spark.sql.shuffle.partitions默认是200。如果你数据量很小,200个分区纯属浪费;如果数据量巨大,200个分区又不够并行。经验法则:每个shuffle分区处理128MB到256MB的数据比较合适。 - 尽可能避免shuffle:能用map、filter、broadcast join解决的就别用wide dependency。
6.3 先学会这几招再谈优化
给初学者一个可以立即执行的优化清单:
设置合理的内存和并行度参数
spark.conf.set("spark.executor.memory", "2g") spark.conf.set("spark.executor.cores", "2") spark.conf.set("spark.sql.shuffle.partitions", "8")用缓存加速重复使用的数据
df.cache() df.count() # 触发缓存 # 后续多次使用 df 时不用重算用Broadcast Join处理小表关联
from pyspark.sql.functions import broadcast result = large_df.join(broadcast(small_df), "key", "left")避免写
.show()在生产任务里。show()会触发一次action,如果被反复调用在循环里,会导致重复计算。优先使用Parquet列式存储。读写Parquet比CSV快得多,特别是做列裁剪查询时,Parquet可以直接跳过无关列。
这些优化策略核心是理解一点:Spark任务尽量在内存里完成,尽量不让数据频繁跨节点移动。
7. 常见问题与排查技巧实录
7.1 我踩过的5个典型坑
| 报错或现象 | 原因 | 解决方案 |
|---|---|---|
Could not find or load main class org.apache.spark.launcher.Main | SPARK_HOME或JAVA_HOME没配置好 | 检查环境变量,确保Spark解压路径和JDK路径正确 |
读取文件后数据都是null | CSV文件没设置header=True,或者分隔符不对 | 确认表头参数和文件实际格式,必要时指定option("delimiter", "\t") |
PySpark提示Python worker failed to connect back | Python路径找不到,Spark启动的Worker无法连接Python进程 | 在代码里显式指定Python解释器:spark.conf.set("spark.pyspark.python", "/usr/bin/python3") |
java.io.IOException: No space left on device | 临时目录(通常是/tmp/spark-*)被写满 | 修改spark.local.dir指向有足够空间的目录 |
作业进度卡在Running jobs...不动 | 某个Executor死了,或者Driver和Executor之间连接丢失 | 查看4040端口Spark UI,定位是哪个Stage卡住,检查是否Executor内存不够导致频繁GC |
7.2 几个实用排查命令与工具
排查问题最核心的工具就是Spark自带的Web UI,默认地址是http://localhost:4040.
打开后重点看三个标签页:
- Jobs页面:看到当前作业的整体进度,以及每个Job的耗时分布。
- Stages页面:如果某个Stage的shuffle读或写数据量特别大,说明shuffle优化没做好。
- Executors页面:看每个Executor的GC时间和内存使用,如果某个Executor的GC时间占比很高,说明内存配置不合理。
日志排查的通用流程是:先看Driver端日志 -> 看出错的是哪个Stage -> 点进对应Stage看任务日志 -> 定位是数据倾斜还是OOM。
还可以用spark-submit --verbose查看提交的详细参数,确认配置是否生效。
7.3 学了这些下一步学什么
到这里,你已经能写一个能跑的Spark程序,也能理解它的核心概念和常见调优手段。接下来往哪个方向深入,我根据自己的经历给三条路线:
- 数据倾斜深入排查:当你的join结果出现某些任务要跑很久、其他任务很快完成时,大概率是数据倾斜。学习加盐、两阶段聚合等高级玩法。
- Structured Streaming流式计算:学会用同样的DataFrame API处理Kafka中的实时数据。
- SQL优化与源码理解:用
explain()看懂执行计划,自己判断哪些SQL写法效率低;想更深入的可以下Spark源码,编译跑通后在本地断点调试看执行流程。
我个人在实际学习中的体会是:Spark入门最大的障碍不是概念复杂,而是“看得多、跑得少”。很多知识点非要写到代码里、跑到报错、翻到报错原因,才会真正变成你自己的经验。所以看这篇文章的时候,最好电脑就在手边,把前面几个例子亲手敲一遍,遇到报错再回来看对应章节的排查方法,一步一个脚印,比什么都管用。
最后再分享一个小技巧:学习阶段如果你遇到奇怪的环境问题,搜索时别老盯着 “Spark xxx 教程” 这类泛泛的标题,试着直接把报错原文贴到搜索引擎里,往往一搜就中。很多牛人已经把这些坑的记录发布在各个社区了,你看到的报错,大概率别人早就踩过、修过了。