news 2026/10/17 23:30:44

PySpark入门实战:用Python玩转分布式计算与大数据处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PySpark入门实战:用Python玩转分布式计算与大数据处理

不用纠结这个项目标题是不是太“基础”,很多人一开始都容易把它当成“又一个大数据框架介绍”,但实际上,Spark的Python版本也就是PySpark,已经把大数据的门槛拉得非常低了。我见过不少只写过几年Python、连分布式系统都没碰过的开发者,靠着PySpark直接上手处理几十个G甚至上T的数据。这篇文章就把Spark当作一个“用Python就能玩转的分布式计算工具”来讲,讲清楚它到底是什么、核心概念有哪些、底层是怎么跑的,以及你第一次用PySpark写任务时最容易踩的那些坑。

不适合什么人看呢?如果你已经用Spark写过一年以上的生产任务,那这篇对你来说确实偏入门了。但如果你正在纠结“要不要学Spark”“PySpark和普通Python处理数据到底有什么区别”“分布式到底是怎么个分布法”,那这篇可以把这些问题一次性讲明白。

1. 到底什么是Spark,为什么大家都在用

1.1 Spark是什么:批处理、流处理、SQL一把抓

先说结论:Spark是一个通用的分布式计算引擎。这里的“通用”不是随便说说的,因为它能干的活确实很杂:

  • 处理大规模结构化数据,跑SQL分析;
  • 做ETL,也就是从各种数据源里抽取数据、清洗转换、再写回目标存储;
  • 处理实时流数据,比如从消息队列里消费数据做实时统计;
  • 跑机器学习算法,像分类、聚类、协同过滤这类常见算法它都内置了;
  • 处理图数据,做图计算。

一个引擎能把这些事全包了,这是它流行起来的最核心原因。在我早年做数据开发的时候,公司的技术栈是割裂的:离线分析用一套工具,实时计算又用另一套框架,机器学习还要再起一个单独的环境。维护成本高,数据从离线到实时之间的口径还经常对不上。Spark出来后,至少批处理和流处理在API层面统一了,写离线任务的代码改一改就能跑实时场景,这省下来的工作量不是一星半点。

有一件事需要先明确:Spark本身不是存储系统。它不负责把你的数据存起来,它负责的是“算”。你的数据可能放在分布式文件系统里,可能放在各种云存储里,也可能在数据库里,Spark只是把它们读进来,在内存里做分布式计算,再把结果写出去。

所以你看,Spark解决的核心问题,就是“单机算不动”的那类场景。单机Python处理数据,内存上限决定了你最多能处理多大的数据量。就算你的机器有256G内存,一旦数据超过这个规模,要么等死,要么OOM。Spark的思路是把计算分散到很多台机器上,每台机器只处理自己那一份数据,然后合并结果。这个思路不复杂,但把它做得稳定、高效、易用,就非常难了。

1.2 为什么Python版(PySpark)这么火

这个要追溯到开发者的使用习惯。早期Spark只提供Scala和Java接口,那个年代写大数据基本是Java工程师的活。后来Spark加入了Python API,也就是PySpark,局面立刻不一样了——Python生态在数据分析和机器学习领域本来就有绝对统治力,PySpark相当于给这批人开了一条通往分布式计算的路。

PySpark能火还有一个很实在的原因:代码写起来比Scala版本省心太多。我对比过同一个逻辑的两种实现,用Scala写要定义各种类型、处理序列化和编译问题,而用Python写几乎就是按着数据处理的直觉来。尤其是你本来就会pandas的话,PySpark的DataFrame API上手会非常快,很多方法名和习惯都是类似的。

另外,PySpark还能配合Python生态里的其他库一起用。你在一个PySpark任务里引入pandas、numpy、scikit-learn这些库完全没障碍。虽然跑分布式任务时不建议在主流程里大量用纯Python库,但做一些小规模预处理、写UDF的时候,Python生态的优势就体现出来了。

我接触到的团队里,现在用PySpark的比例已经明显超过了Scala。不是说Scala不好,而是对一个业务团队来说,招一个会Python又能理解数据的工程师,比招一个Scala专家容易太多了。而且PySpark的社区活跃度很高,踩到坑基本都能搜到解决方案。

2. Spark核心概念盘点:先搞懂这几个词,文档也能读下去了

2.1 RDD:最基础的数据抽象

RDD全称是弹性分布式数据集(Resilient Distributed Dataset)。这个概念是Spark的骨架级抽象,虽然有经验的开发者现在写代码很少直接操作RDD,都是操作DataFrame,但不懂RDD的话,你很难真正理解Spark的容错和调度原理。

你可以把RDD理解为“一个被分片存储、可以被并行操作的数据集合”。它有几个关键特性:

  • 分区:数据被切分成若干个分片,每个分片就是一台机器上的一个计算单元;
  • 不可变:一旦创建就不能修改,任何“修改”操作都会生成新的RDD;
  • 懒加载:真正触发计算的是动作,而不是转换;
  • 容错:每个RDD都记录着自己是从哪个父RDD通过什么操作得来的,也就是血缘关系,某个分片坏了可以按血缘重新计算。

用生活化的比喻来理解:RDD就是一摞被拆分成好几叠的卡片,每张卡片写着一行数据。你没法直接改某张卡片,只能拿着一叠卡片去做变换,最后交给统计员汇总。这个过程不动原卡片,而是生成一叠新卡片。

虽然现在日常开发已经很少写sc.parallelize这种代码了,但理解RDD对后面理解DataFrame的底层逻辑非常有帮助。DataFrame其实就是带有schema信息的RDD,如果你遇到DataFrame层面的诡异问题,最终还是要到RDD层面去排查。

2.2 DataFrame与DataSet:面向结构化的好朋友

先说Dataset,这是Spark 1.6开始引入的强类型数据抽象,提供了编译时类型安全检查,但主要在Scala/Java里体现价值。在PySpark里,你接触到的Dataset其实就是DataFrame,因为Python是动态类型语言,所谓强类型检查在Python里不存在。

DataFrame在PySpark里就是最核心的数据抽象。它有两个特点:

  1. 有Schema,也就是“每一列叫什么名字、是什么类型”,这一点和pandas、SQL表很像;
  2. 底层分布式存储,自动分区并行计算。

为什么DataFrame比RDD更流行?主要是两个原因:

一是,DataFrame的优化空间大。Spark的优化器(Catalyst优化器)会对DataFrame操作做逻辑优化和物理优化,就像SQL数据库里的查询优化器一样。你写的代码会被重写成执行效率更高的计划。而RDD层面没有这种优化空间,你写的什么逻辑就怎么执行。

二是,DataFrame更接近数据分析师和工程师的思考习惯。大部分人对数据处理的理解天然是“表”和“行列”,而不是分布式抽象。

我实际用下来的感觉是,能用DataFrame就不用RDD。DataFrame的API足够丰富,而且不断在更新,除非你要做非常底层的自定义逻辑,否则RDD真的没必要直接碰。

2.3 SparkSession、Transformation与Action:先搞清楚懒执行

Spark 2.0开始,SparkSession成了统一的入口。以前要分别创建SparkContext、SQLContext、HiveContext,现在一个入口全搞定。你写PySpark代码的第一行,基本就是创建或者获取一个SparkSession:

from pyspark.sql import SparkSession spark = SparkSession.builder \ .appName("my_first_job") \ .master("local[*]") \ .getOrCreate()

appName是这个任务的名字,会在集群管理界面上显示,用来区分不同任务。master决定了任务跑在哪里,local[*]表示本地模式,使用本机所有CPU核。

接下来是神仙打架级别的关键概念:Transformation和Action。

Transformation叫转换操作,比如select、filter、groupBy,这类操作的特点是懒执行——你调用了它,Spark并不会立刻计算,只是记录了一个操作步骤。

Action叫动作操作,比如count、collect、show,这类操作会真正触发Spark去执行计算流程。

用一个比喻解释懒执行:比如你要做一道菜,转换操作就是“把菜谱看了一遍又一遍”,动作操作才是“真正下锅开炒”。

懒执行带来的好处是高性能。Spark会把一连串转换操作记录下来,形成一个DAG(有向无环图),然后在执行前对整个链条做优化调整。比如你连续做了多个filter,Spark可能会把它们合并成一个过滤条件,减少数据扫描次数。这就是为什么Spark能在大规模数据上保持高效率的秘密之一。

刚接触PySpark的人,最常见的疑惑就是“我明明写了df.filter(...),为什么数据没变?”答案就是:你只写了转换操作,没有触发的动作,数据当然不会变。记住一句话:没有Action,就没有计算,也没有结果。

3. 从宏观到微观:Spark架构与运行原理

3.1 集群里到底谁在干活:Driver、Executor、Cluster Manager

Spark是典型的主从架构,跑一个Spark任务时,集群里有三类角色:

  • Driver:任务的“大脑”,负责解析你的代码、生成执行计划、把任务分发给各个工作节点、汇总结果。它运行在你的提交端,或者集群的一个专用节点上。
  • Executor:任务的“苦力”,真正干活的进程,负责执行具体的计算任务、把结果缓存到内存或磁盘。每个节点上可以有多个Executor进程。
  • Cluster Manager:负责“分配地盘”,也就是管理集群资源,决定在哪些机器上启动Executor。Spark支持不同的集群管理器,有自带的Standalone,也有通用的资源调度系统。

我经常把这三个角色类比成项目团队的组成:Driver是项目经理,Executor是干活执行的工程师,Cluster Manager是行政人事,负责组团队和分配场地。

一个容易让人迷惑的点是:Spark任务执行时,你的代码到底在哪里运行?答案是:Driver端和Executor端都有运行。比如你写了一个df.groupBy("city").count(),Driver把这段逻辑翻译成执行计划,分发给Executor执行,Executor会把分组和计数的具体计算跑起来。如果是自定义UDF,Spark会把这个函数的序列化副本发给每个Executor,各自在本地执行。

3.2 一个Spark任务从提交到出结果的完整旅程

我来完整地梳理一遍,当你执行一个PySpark任务时,后台到底发生了什么:

  1. 你提交PySpark应用,Driver进程启动,创建SparkContext(或者SparkSession,底层一样)。
  2. SparkContext向Cluster Manager申请资源,即在多台机器上启动Executor进程。
  3. 你的代码被解析成一系列转换操作,构建成DAG(有向无环图),DAG描述了整个计算步骤的依赖关系。
  4. 当遇到Action操作时,DAG调度器把DAG切割成多个Stage。切割规则是:遇到宽依赖就切分Stage。
  5. 每个Stage由多个Task组成,这些Task被分发到各个Executor执行。
  6. Executor执行任务,把结果写回Driver或者外部存储(比如分布式文件系统、数据库)。
  7. 全部任务完成后,Driver退出,释放资源。

这里面最关键的是第4步提到的“宽依赖”。什么是宽依赖呢?简单说就是父RDD的某个分区数据被多个子RDD分区使用,比如groupByKey、join,这种操作通常需要发生数据混淆(Shuffle),也就是数据要在集群节点间重新分配。

而窄依赖就是父RDD的每个分区只对应一个子RDD分区,比如map、filter,不需要网络传输。窄依赖的阶段执行效率很高,宽依赖所在的Stage则是优化和调优的重点。

3.3 部署模式怎么选:local、Standalone、YARN还是K8s

部署模式决定了你的Spark任务跑在哪里。下面这几种我都实际用过,可以给你一些选择参考。

Local模式就是本地跑,不依赖集群。适合开发调试、跑演示Demo,master("local[*]")就是这种。它好在零部署成本,坏在只能用一台机器的资源。

Standalone模式是Spark自带的集群部署方式,不依赖其他系统就能搭一个简单的Spark集群。适合中小规模实验环境、学习环境,或者不想引入额外组件的时候。

YARN模式是目前很多生产环境的选择,因为它可以把Spark任务和其他计算框架的资源统一管理起来,集群利用率高。适合在Hadoop生态体系成熟的大数据平台里跑。如果你所在的公司已经有一套大数据平台,大概率用的YARN。

Kubernetes模式是近几年越来越流行的方式,Spark可以直接以容器的方式跑在K8s集群里,资源隔离性、弹性都更强。适合云原生环境,尤其是容器化架构已经比较完整的团队。

我的建议很简单:学习就Local,生产环境哪个和你现有体系融合度高就选哪个。不要为了追新而强行迁移部署模式,分布式系统最怕迁移时的各种环境问题。

4. 用Python版Spark实操:从零跑通一个PySpark任务

4.1 本地环境搭建:一条命令还真能搞定

如果你是Python派,环境搭建非常简单。前提是你机器上已经装了Python 3.8以上版本。我用的是pip直接装:

pip install pyspark

没错,就这么简单。pyspark这个PyPI包里面自带Spark的JVM运行环境,装完就能直接跑Local模式。不用单独下载Spark、不用配置JAVA_HOME,什么都不用配。我当年第一次用Spark的时候,配环境花了一整天,现在新入门的朋友真的幸福太多。

装完之后,你在命令行敲一下:

pyspark

就能进到Spark的交互式界面,可以直接写SQL、执行DataFrame操作,跟用Jupyter差不多。

如果你是更习惯写脚本的人,那就建一个Python文件,比如叫app.py,里面写SparkSession初始化,然后直接spark-submit app.py提交运行。不过本地测试时直接python app.py也行,因为Local模式下SparkSession自己就能把JVM拉起来。

4.2 读取数据并做转换:最小可用示例

我拿一个最常见的场景举例:读一个CSV文件,做简单的统计和过滤操作。

假设现在有个CSV文件,里面是用户订单数据,字段包括:order_id,user_id,city,amount,order_date。

from pyspark.sql import SparkSession from pyspark.sql.functions import col, sum spark = SparkSession.builder \ .appName("order_analysis") \ .master("local[*]") \ .getOrCreate() # 读取CSV,自动推断schema df = spark.read.option("header", True) \ .option("inferSchema", True) \ .csv("orders.csv") # 查看数据 df.show(5) # 计算每个城市的总订单金额 result = df.groupBy("city").agg(sum("amount").alias("total_amount")) # 只保留金额超过10000的城市 result = result.filter(col("total_amount") > 10000) # 触发Action,真正执行计算 result.show()

看到这坨代码,如果你是写pandas的,应该会觉得很多地方很眼熟。groupBy、agg、filter,这些API和SQL的语法非常贴近。不同点是,pandas是在内存里一口气算完,而PySpark把这个计算分布式地拆到了多台机器上,数据量更大时优势非常明显。

有一点要注意:不要直接用df.collect()把全量数据拉到本地。collect()会把分布式所有分区数据汇总到Driver端内存里,如果数据量太大,你的Driver会直接OOM。正确方式是show()看几条,或者write到目标存储。

4.3 PySpark新手必踩的几个坑

第一个坑,是类型问题。CSV自带字段类型需要推理,但推理经常“猜错”,比如日期被读成了字符串、空值被读成了null导致类型变复杂。我的建议是,正式任务里不要单纯依赖inferSchema,最好在建表或读取时手动指定schema。这样做虽然多写几行代码,但能避免无数后续问题。

from pyspark.sql.types import StructType, StructField, StringType, DoubleType schema = StructType([ StructField("order_id", StringType(), True), StructField("user_id", StringType(), True), StructField("city", StringType(), True), StructField("amount", DoubleType(), True), StructField("order_date", StringType(), True), ]) df = spark.read \ .option("header", True) \ .schema(schema) \ .csv("orders.csv")

第二个坑,是UDF性能太差。很多从pandas转过来的开发者,习惯把Python函数包成udf在PySpark里跑。但UDF有一个致命问题:它会打破Spark的优化机制,数据要在JVM和Python进程之间来回序列化传输。如果数据量大,UDF上的数据量等于“火山式”性能灾难。

我的经验是:能用内置函数解决的就用内置函数,比如pyspark.sql.functions提供了一大批常见函数;实在要写UDF,优先用pandas_udf,它通过Apache Arrow做了优化,性能比普通UDF好很多。

第三个坑,是分区数不调。刚用PySpark时,我把数据读进来直接处理,跑得特别慢。后来发现默认的分区数并不一定适合你的数据和集群配置。分区太多,调度开销大;分区太少,每块数据太大,执行不均匀。具体怎么调,我放在下一节详细说。

5. 调优与排查:跑大数据不能只靠玄学

5.1 分区设置:影响速度的关键参数

分区的数量和大小是PySpark任务性能的决定性因素之一。每个分区对应一个计算单元,数据被分到多少分区,就意味着最多能有多少任务并行执行。

分区太少:有些Executor可能没活干,资源闲置;单个分区数据量过大,处理时间变长,内存压力也大。分区太多:任务调度和元数据开销增加,每个任务启动、结束都有成本,你的时间都浪费在“发任务”上了。

怎么判断分区数合不合适?先看两个指标:

  • 如果每个任务执行时间只有几百毫秒,那很可能分区太多了;
  • 如果某个Stage只有几个任务,但每个任务跑了几十分钟,那很可能分区太少或者数据倾斜了。

一个简单的经验公式(基于我的实践,不是什么玄学):目标让每个分区的数据量在100MB到200MB左右。假设你的数据有2GB,那设置10到20个分区比较合理。

调整分区的常用方式:

# 重新分区,会触发Shuffle df = df.repartition(20) # 合并分区,减少分区数,尽量不触发Shuffle df = df.coalesce(5)

注意,repartition和coalesce是有区别的。repartition会把数据重新均匀打散,代价是触发一次完整的Shuffle,适合需要调整并行度的时候。coalesce只能减少分区数,而且尽量不移动数据,适合输出结果前缩小分区避免产生太多小文件。

5.2 内存管理:执行内存和存储内存怎么平衡

Spark把Executor的内存分成了两大块:执行内存和存储内存。执行内存用于计算,比如Shuffle、Join、Aggregation时的临时数据;存储内存用于缓存RDD或DataFrame。

在Spark 1.6之后引入了统一内存管理器,执行内存和存储内存不再严格隔离,而是可以互相抢占。举个例子,如果缓存数据占用了存储内存,而执行时需要更多内存,它会主动驱逐一部分缓存数据来给执行让路。

这解释了一个常见的困惑:为什么我看到Storage Memory里有大量缓存,任务还是OOM了?因为这个缓存占的是存储内存,而执行时需要的内存不够时,虽然会让缓存让路,但如果缓存没有及时释放,就会触发额外开销甚至失败。

实际调优时不需要太激进改内存比例,除非你明确知道自己在做什么。我更建议你先关注:

  • Executor数量与CPU核数的关系;
  • 每个Executor的内存大小;
  • 是否每个Executor都能获得足够的“并发任务”资源。

spark.executor.memory控制Executor的堆内存,spark.executor.cores控制并发任务数。不需要机械遵循某个“标准配置”,但有一个原则:单个Executor的内存不要设置太大,因为内存越大的JVM,GC(垃圾回收)压力越大。一般推荐单个Executor的堆内内存在2GB到8GB之间,这个区间是我踩过各种坑之后觉得比较稳的范围。

5.3 数据倾斜:最常见的性能杀手及缓解思路

数据倾斜是分布式计算中“人人见面都叹气”的问题。它的表现是:其他任务都跑完了,只有一两个任务卡在那里跑不动,整个Stage都等在那几个慢任务上。

数据倾斜的本质是:某个分区里的数据量远大于其他分区。比如你按城市分组统计订单量,结果“上海”这一个城市的订单占了总数据量的一半,那么处理上海这个分区的任务就会比处理其他小城市分区的任务慢几十倍。

解决倾斜有几种常用手段,我按推荐顺序列一下:

  1. 提高Shuffle并行度,让每个任务处理的数据变少。这是最省事的解法:spark.sql.shuffle.partitions调大,比如从默认200调到500甚至更高。但它只是缓解,不能根治。

  2. 加盐(salting)。针对热点Key,给它加上一个随机前缀或后缀,把它打散成多个Key再处理。比如把“上海”变成“上海_0”、“上海_1”……这样就能把原先一个分区的数据分散到多个分区去算,算完再合并结果。

from pyspark.sql.functions import concat, lit, rand, split, col # 给城市加随机盐,打散热点 salted = df.withColumn( "salted_city", concat(col("city"), lit("_"), (rand() * 10).cast("int")) ) # 按加盐后的key聚合 salted_result = salted.groupBy("salted_city").agg(sum("amount").alias("total")) # 把盐去掉后再聚合一次 result = salted_result.withColumn( "city", split(col("salted_city"), "_")[0] ).groupBy("city").agg(sum("total").alias("total_amount"))
  1. 数据量悬殊的Join可以用广播。如果小表很小,几百MB以内,可以把小表广播到每个Executor,避免Shuffle。PySpark里用join时指定广播提示:
from pyspark.sql.functions import broadcast result = large_df.join(broadcast(small_df), "key")

广播Join在数据量差距悬殊时效果立竿见影,是我最常用的优化手段之一。

  1. 如果倾斜严重,考虑对倾斜Key单独处理。把热点数据和非热点数据分开计算,最后再union回来。操作复杂一些,但能真正解决问题。

调优这条路没有银弹,关键是要学会看Spark UI。每个Stage的统计信息里能看到任务运行时间分布、Shuffle数据量等,定位到具体瓶颈后,再用上面的方法对症下药。

6. 写在最后:一点个人体会

我从第一次接触Spark到现在,最大的感受是:这玩意儿的学习曲线其实没有传说中那么陡。只要你抓住“懒执行、分区、Shuffle、宽窄依赖”这几个核心概念,再带着几个真实的数据集去跑一跑,很快就能建立感觉。

用Python写Spark更是把门槛又降了一层。你在pandas里的数据处理思路,完全可以迁移到PySpark上,只是要多记一个前提:你的数据是分布式的,你的操作是懒执行的,你的结果最终要靠Action才能触发。

如果你正要开始做一个PySpark项目,我的建议是:先从一份小数据集开始,把流程整个跑通,包括读取、转换、写出。然后慢慢换成大数据集,在压力下观察Spark UI上的指标,再反推去理解那些调优参数是怎么回事。最后再多记一句话:不要一开始就想着“调优”,先让任务正确跑起来,再去追踪慢在哪里。

分布式计算这门手艺,跑得通只是起点,跑得快才是经验。希望这些内容能帮你把起点站稳了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 23:27:27

机房搬迁与网络割接实战:数据中心迁移方案与避坑指南

简介:这份文档面向数据中心管理员与IT基础设施运维人员,聚焦机房搬迁与网络设备割接两大核心场景,提供从物理搬迁到业务切换的一揽子技术方案。内容涵盖搬迁目标、前提条件、前期准备与职责分工,并细化到设备下电、拆卸、包装、清…

作者头像 李华
网站建设 2026/10/17 23:25:22

AI编程工具选型指南:Claude Code、Codex、OpenCode与Cursor深度对比

1. 从一堆工具名里找到真正的痛点1.1 这些工具到底在解决什么问题先把场景说清楚。现在市面上围绕 AI 辅助编程的工具已经多到让人眼花缭乱,Claude Code、Codex、OpenCode、Cursor 这几个名字几乎每周都会在技术社区里被反复提起。它们各自定位不同,但核…

作者头像 李华
网站建设 2026/10/17 23:18:50

HTML模板与插槽复用实战:从template到Web Components

前不久帮一个朋友维护他的个人博客,碰到一个特别典型的问题:导航栏在五个页面里各有一份,他改一个链接要全局搜索替换五次;页脚那段备案信息就更离谱,每篇文章底部都复制了一遍,改一次要动十几个文件。我俩…

作者头像 李华
网站建设 2026/10/17 23:08:14

虚拟机安装Ubuntu 18.04与ROS Melodic完整教程

每个做机器人相关的开发者,不管是做SLAM、机械臂还是无人车,基本都绕不过一个坎:在Linux环境下搭一套ROS开发环境。而大多数人的第一选择不是直接装双系统,而是“虚拟机 安装Ubuntu18.04和ROS Melodic”。我第一次搭这套环境的时候…

作者头像 李华
网站建设 2026/10/17 23:07:39

UE4悬停与点击效果的底层实现原理与多端适配

1. 为什么UE4里一个“鼠标悬停”要专门写一篇长文?在某跨平台系统开发中,我接手过一个被反复退回的UI模块:按钮在PC端测试时一切正常,但一接入手柄模式或切换到触摸屏设备,悬停态就彻底消失;更奇怪的是&…

作者头像 李华