尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Pandas API on Spark:用Pandas语法无缝跑通分布式大数据处理

Pandas API on Spark:用Pandas语法无缝跑通分布式大数据处理 做数据分析的老哥老姐们肯定都有过这种体验本地用 Pandas 处理数据一切岁月静好但数据量一涨到几个 G 甚至几十个 G内存直接爆掉风扇狂转程序卡到怀疑人生。换 Spark 吧又要学 RDD、DataFrame、各种算子语法风格和 Pandas 差异很大从 Pandas 切过去总有一种“明明能写但写不明白”的别扭感。这次要聊的 Pandas API on Spark就是为了解决这个痛点而生的——它把 Spark 的分布式计算能力整个藏了起来让你继续用 Pandas 的写法来处理海量数据。说白了就是你写的还是df.groupby(...).sum()底层跑的却是 Spark 的分布式任务。这篇文章适合已经会用 Pandas、想接触 Spark 但不想从零学一套新语法的读者也适合业务侧的数据分析同学希望用最低的学习成本接管大数据量定时任务。我会从环境搭建讲到高频操作再讲到性能坑位和常见报错尽量做到看完就能直接上手干活的水平。1. 为什么说这是Pandas用户切入Spark的最优路径1.1 先搞明白Pandas API on Spark到底是个什么产物很多刚接触的人会以为 Pandas API on Spark 是另一个独立框架其实它就是在 PySpark 里面开放的一个接口层模块名叫pyspark.pandas你导入的时候一般习惯性写成import pyspark.pandas as ps它并不是简单给 Spark 套了层 Pandas 的壳而是把 Pandas 风格的 API 逐个翻译成 Spark SQL 的执行计划。你操作的是一个逻辑上的 DataFrame感觉像是单机 Pandas但数据实际上分片存在各个 worker 节点上计算也是分布式跑的。这里有个历史背景值得说一下。早期这个项目叫 Koalas由 Databricks 立项目的是“让 Pandas 用户无痛迁移到 Spark”。2020 年左右它被整体捐给了 Apache Spark 社区之后合入 Spark 主分支成了pyspark.pandas。所以你现在起个 Spark 环境只要能跑 PySpark就自带这份能力不需要额外装三方包。理解了这个演变过程你就能明白它的定位不是替代 Pandas而是让 Pandas 的编程体验拥有分布式的内核。1.2 它和原生的Spark DataFrame API有什么区别普通 PySpark 写法长这样from pyspark.sql import SparkSession spark SparkSession.builder.getOrCreate() df spark.read.csv(data.csv, headerTrue, inferSchemaTrue) result df.groupBy(category).sum(amount)这一套语法本身没毛病但如果你是 Pandas 老手会觉得groupBy(category).sum(amount)这种写法别扭尤其是遇到复杂一点的聚合逻辑比如要同时算多个不同的聚合函数原生 API 需要写agg里面还要传F.sum、F.count这类函数心智负担一下就上来了。Pandas API on Spark 的写法就熟多了df ps.read_csv(data.csv) result df.groupby(category)[amount].agg([sum, count])虽然内部同样是 Spark 在跑但代码观感和你用了好几年的 Pandas 几乎一致。说白了它就是一道翻译层把 Pandas 的函数调用翻译成 Spark 操作把用户从“两种思维模式”的切换里解放出来。1.3 适用场景和不该用它的地方没有银弹Pandas API on Spark 也不例外。我自己的经验是它最适合下面这几类场景数据量超过单机内存几个数量级但又不想全量换成 Spark SQL 的项目。团队里大部分人熟悉 Pandas不熟悉 Spark但又不得不处理大数据量的临时分析、报表任务。已有 Pandas 数据处理代码希望小改动后就能放到集群上跑减少重写成本。但有些场景我反而不推荐用。最典型的就是数据量很小的情况本来几百 MB 以内 Pandas 秒出结果你硬要用 ps.DataFrame 分布式处理光是拉起任务、序列化的开销就比计算本身还大完全没必要。此外需要大量逐行循环迭代的代码或者重度依赖行级顺序、嵌套字典结构的场景也不适合因为 Pandas API on Spark 分布式之后行间的执行顺序并不严格很多依赖“上一行结果”的写法会出错。2. 环境准备从零跑通Pandas API on Spark2.1 安装环节容易踩的版本坑先说环境很多人一上来就卡在装包环节。理论上Pandas API on Spark 不需要额外安装只要你有 PySpark 就行。但 PySpark 和本机 Python 版本、Java 版本的兼容性是我见过最多人踩坑的地方。以我目前的稳定组合为例Java 8 或者 Java 11 配 Python 3.8 到 3.10 都问题不大更高版本的 Python 需要确认你所装的 PySpark 是否支持。安装命令很简单pip install pyspark pandas但这不是全部。你还需要在本机装好 Java并把 Java 的路径配到环境变量里。很多时候报 “java.io.IOException: Cannot run program ... java.lang.RuntimeException: Java gateway process exited before sending the driver its port number” 这种错十有八九就是 Java 没装好或者版本不对。一个小建议如果不想折腾环境变量推荐用 Anaconda 建一个独立环境在环境里专门给 PySpark 用避免和原有的 pandas、numpy 版本冲突。我自己就吃了不少版本打架的亏后来学乖了PySpark 项目一律单独建环境。2.2 启动SparkSession的正确姿势虽然pyspark.pandas用起来像单机库但它骨子里还是 Spark所以必须有一个 SparkSession 在跑。在 Jupyter 或者普通 Python 脚本里你可以这样初始化from pyspark.sql import SparkSession spark SparkSession.builder \ .appName(PandasAPIDemo) \ .master(local[*]) \ .config(spark.sql.execution.arrow.pyspark.enabled, true) \ .getOrCreate()local[*]的意思是使用本机所有可用 CPU 核心跑本地模式。如果你只是想快速体验这个配置就够了。Arrow 那个配置建议打开它能让 Pandas 和 Spark 之间互传数据时更快后面做to_pandas()转换或者从 Pandas 转过来时会明显感觉到差别。还有个容易忽略的地方要把spark.sql.execution.arrow.pyspark.enabled配成 true 之前确保你装了pyarrow这个包。否则开启后会报找不到 Arrow 的错误。2.3 验证环境是否正常工作初始化完成后先别急着跑大任务用一段十行内的代码验证环境import pyspark.pandas as ps psdf ps.DataFrame({a: [1, 2, 3], b: [x, y, z]}) print(psdf) print(type(psdf))如果可以看到一个 3 行 2 列的表格并且类型显示为pyspark.pandas.frame.DataFrame那就说明环境已经通了。这里提醒一下打印psdf时它不会像 Pandas 一样直接输出全部数据而是会输出一个预览表格下面通常有个提示说只显示前 30 行。这其实是分布式系统常见的设计——数据太大不可能全拉到 driver 端先给你看个样例。3. 动手写第一段Pandas风格的Spark代码3.1 数据读取从本地文件到分布式DataFrame数据读入是每次分析的起点。ps.read_csv、ps.read_parquet、ps.read_json这些 API 和 Pandas 的基本一模一样参数也大差不差。# 本地CSV psdf ps.read_csv(sales_data.csv) # Parquet格式分布式计算里最常用的存储格式 psdf ps.read_parquet(hdfs://path/to/data.parquet) # 带schema推断和日期解析 psdf ps.read_csv(sales_data.csv, parse_dates[order_date], infer_schemaTrue)有一处要特别留意Pandas 的read_csv默认参数enginec解析性能很好。而ps.read_csv底层其实调用的是 Spark 的 CSV 读取器很多 Pandas 里细腻的参数不一定支持。比如nrows参数在 ps.read_csv 里就不太好使想抽样的话建议用df.head(n)或df.limit(n)这种读完之后再截断的方式。读取大文件时我强烈建议把文件放到分布式文件系统比如 HDFS或对象存储里而不是一股脑放在本地盘上让 Spark 去读。因为本地文件系统在每个 worker 节点上不一定都存在实际跑分布式任务时容易遇到文件找不到的诡异问题。3.2 从Pandas DataFrame转过去和转回来工作中最常见的场景是手头有一份 Pandas DataFrame想转成 Pandas API on Spark 处理更大的数据集或者反过来处理完了转回 Pandas 做画图。转换代码非常直白import pandas as pd import pyspark.pandas as ps # Pandas - Pandas API on Spark pdf pd.DataFrame({a: [1, 2, 3], b: [4, 5, 6]}) psdf ps.from_pandas(pdf) # Pandas API on Spark - Pandas小结果集才建议这么干 result_pdf psdf.to_pandas()to_pandas()会触发一次真正的计算数据从集群拉到 driver 端变成单机 Pandas DataFrame所以只有结果集小的时候才适合调用。如果你对着一张几个亿行的表用to_pandas()driver 内存直接被撑爆这个操作基本等于自杀式调用。还有一个互转方式值得记牢psdf.to_spark()可以拿到底层的 Spark DataFrame反过来用spark_df.to_pandas_on_spark()也能转成 ps.DataFrame。这样你就可以在同一个任务里Pandas API 和 Spark 原生 API 混合着用哪边方便用哪边# ps.DataFrame - Spark DataFrame sdf psdf.to_spark() # Spark DataFrame - ps.DataFrame psdf2 sdf.to_pandas_on_spark()3.3 和Pandas的几个核心行为差异使用过程中最大的一道坎不是 API 不会写而是“看起来一样实际行为不同”的隐性差异。下面几个点是我觉得新手最容易翻车的。首先是索引的连续性。Pandas 的 index 默认是 0 到 n-1 的连续整数做df.iloc[10]时按位置找很快。但 ps.DataFrame 的索引虽然默认也是从 0 开始可它并不是“真实存在”的列分布式跑的时候索引和行之间的对应关系不像单机那样稳定。所以很多依赖索引的操作比如df.loc[5]、df.iloc[5]在 ps.DataFrame 上行为会有限制部分场景会直接报错或者只返回一个近似结果。其次是列名和类型。Spark 的底层存储对列名大小写敏感取决于配置而且对列名的字符限制更严格。Pandas 里你可以用weird column!这种带空格带标点的列名Spark 里跑起来常常出幺蛾子。建议直接用下划线命名法省得后面天天处理引号问题。最后是排序稳定性。Pandas 里df.sort_values(col)排序完索引会跟着走但 ps.DataFrame 排序后索引的变化规律不一定符合你的直觉。如果你要复现一个稳定排序的逻辑最好显式reset_index(dropTrue)。4. 高频操作实战从筛选过滤到窗口函数4.1 条件筛选、过滤与空值处理先说最简单的按条件筛行。Pandas 写法filtered psdf[psdf[amount] 1000]完全继承不用改。多个条件也照样玩filtered psdf[(psdf[amount] 1000) (psdf[status] paid)]这里有一个 Python 语法层面的大坑必须用而不是and。因为 pandas/ps 的 DataFrame 重载了运算符而and是 Python 关键字没法被重载一写就报 “ValueError: The truth value of a DataFrame is ambiguous”。空值处理方面dropna()、fillna()也不错# 删除全空的行 clean_df psdf.dropna(howall) # 填充空值 filled_df psdf.fillna({amount: 0, status: unknown})但要提醒一下dropna和fillna在某些参数组合下会有性能问题比如axis1按列删空值在分布式环境里实现代价比较大实际跑起来可能要扫描多遍数据。数据量大的时候慎重使用。4.2 分组聚合groupby的写法几乎一模一样分组聚合是我日常用最多的操作。Pandas 里怎么写的这里基本就能怎么写# 单列聚合 result psdf.groupby(category)[amount].sum() # 多列、多用聚合函数 result psdf.groupby([category, region]).agg( total_amount(amount, sum), avg_amount(amount, mean), order_count(order_id, count), )在 sp 里聚合字段名还可以像上面这样用元组的形式传语义很清晰。你可以把agg里的写法理解成 SQL 里的SELECT category, SUM(amount) AS total_amount ... GROUP BY category只是换成了 Pandas API 的风格。这也带出一个处理大数据的通用原则能聚合就聚合尽量不要把明细数据全部拉到 driver 端。比如你想看每个类别的总量直接用 groupby 聚合后的结果做to_pandas()传输的数据量就只是分组后的几行几十行而不是几个亿的原始明细。4.3 多表连接merge和join的注意事项多表连接也是高频需求。merge的用法和 Pandas 基本对齐merged psdf_left.merge(psdf_right, onuser_id, howleft)支持的 how 参数包括 inner、left、right、outer和 SQL 的连接概念一一对应。在 Pandas 里你数据量大时 merge 两个几千万行的表基本就跑不动了但在这里交给 Spark 分布式 join只要 key 设计合理问题不大。关键点来了连接时尽量让 key 的数据分布均匀。如果 key 存在严重的数据倾斜——比如某个 user_id 占了全表 80% 的行——那么 join 的时候所有数据都会涌向同一个节点照样会卡死。这种问题在原生 Spark 里叫数据倾斜Pandas API 里你不一定看得到底层但表现是任务卡住不动。后面专门讲性能时会展开。concat在 ps.DataFrame 里也支持用于纵向拼接。如果你是做按月分区数据的合并可以考虑用 concat 把好几张表拼一起combined ps.concat([df_jan, df_feb, df_mar], ignore_indexTrue)但 concat 也需要谨慎拼接多个大表本质上是全量数据重写如果只是追加新分区更合理的做法是直接写多个分区的路径让 Spark 读的时候自动把分区数据并起来而不是真去 concat 到一张表里。4.4 窗口函数与排序用rated解决排名问题再举个稍微高级点的例子排名、累计值。Pandas 里你用排序加 cumsum 来写但分布式环境下全局排序的数据交换量很大。更好的选择是用 Spark 底层支持的窗口函数Pandas API 也把这层能力暴露出来了通过transform可以模拟窗口逻辑但效率和直接用spark.sql里的窗口函数比还是有差距。这里我提供一个比较务实的做法复杂窗口计算不硬用 ps API 怼而是把 DataFrame 转成 Spark DataFrame写 SQL 实现。psdf.to_spark().createOrReplaceTempView(sales) result_sdf spark.sql( SELECT category, amount, RANK() OVER (PARTITION BY category ORDER BY amount DESC) AS rank FROM sales ) result result_sdf.to_pandas_on_spark()这不是说 ps API 不支持窗口而是说遇到复杂窗口逻辑时直接用 SQL 表达更清楚性能也更可控。Pandas API on Spark 的优势本来就不是在所有场景取代 SQL而是在你熟悉的 Pandas 优势区里给你提供熟悉的操作体验。混合使用 ps 和 Spark SQL往往才是效率最高的方案。5. 性能问题为什么有的地方会慢怎么避坑5.1 先理解分布式计算的耗时从哪来很多人第一次用 ps DataFrame 跑脚本会发现有些操作比 Pandas 还慢于是吐槽 “这东西就是个玩具”。但慢的原因其实非常系统化不是能力不行而是你不该拿分布式计算去跑单机任务。分布式计算慢最大头不在计算本身而在数据混洗shuffle。什么叫 shuffle简单理解就是数据原本分散在不同的 worker 节点上每个节点保存一部分。当你做 groupby、join、distinct、全局排序这类需要“把相同 key 的数据汇总到同一个节点”的操作时所有节点必须把数据大范围地交换一遍这个交换过程就是 shuffle它涉及网络传输和磁盘落地代价极高。而 Pandas 的单机算法不需要这种跨节点传输反而在小数据量上更快。所以当你拿 ps 处理一个几十 MB 的小文件跑了一次 groupby你会明显觉得慢——那慢的不是计算是 Spark 框架启动、规划任务、序列化、shuffle 的开销。5.2 几个立竿见影的调优手段如果确实数据量很大非分布式不可那下面这些优化点会非常实用。第一个是合理设置分区数。spark 默认分区数可能偏小或偏大导致并行度不足或 task 过碎。你可以通过 SparkSession 配置调整spark.conf.set(spark.sql.shuffle.partitions, 100)这个参数直接影响 groupby、join 之后生成的 shuffle 分区数量。分区太少单个 task 处理太多数据容易 OOM分区太多任务调度开销大。经验值是根据数据量和集群核数来定通常可以设成集群可用核心数的 2 到 3 倍。第二个是减少不必要的to_pandas()和 collect 类操作。这一点我再强调一遍ps DataFrame 是分布式的所有查询默认是懒加载的只有真正触发计算时才会执行。而to_pandas()就是一个强触发操作。如果你在代码里频繁调用to_pandas()每调一次都可能触发一次全量计算性能直接崩。正确方式是先做好所有转换、聚合最后一步再转 Pandas。第三个是能先用 SQL 实现逻辑的地方就先用 SQL 跑通再用 ps 封装。Spark 的 Catalyst 优化器对 SQL 的优化非常成熟对 DataFrame 高层的 API 也会优化但有时你自己用 ps 组合出来的一连串操作并不如 SQL 里一句表达来得高效。我开发时经常先在spark.sql里跑通核心逻辑确认结果正确再决定用哪种 API 落到工程代码里。5.3 遇到数据倾斜怎么办前面提到过数据倾斜这是分布式计算里最典型的坑。表现是其他 task 一两秒跑完了就某一个 task 卡在那里跑十几分钟甚至更久最后可能直接内存爆炸报错退出。在 ps API 场景下你通常看不到 task 级别的情况但可以通过 Spark UI 看到某个 stage 卡住。解决思路有几类加盐salting把原本倾斜的 key 加上随机前缀打散到不同分区先做局部聚合再去掉前缀做全局聚合。过滤掉异常大 key如果倾斜来自某个“超大类”的脏数据比如 user_id -1 占了 90%可以考虑先过滤或单独处理。用广播变量当一边表很小、一边表很大时用广播 join 替代 shuffle join。Pandas API 里直接设置参数不太好搞但底层转成 Spark DataFrame 后可以手动 hint# 小表广播 large_sdf.join(broadcast(small_sdf), user_id, left)这些内容里有些偏底层但我觉得想用好 Pandas API on Spark必须懂一点 Spark 执行原理。不然你看到的现象是 “同样的代码有时快有时慢”完全摸不到头脑更谈不上调优。6. 常见报错与排查思路速查6.1 环境类报错与启动类问题这里我把平时交流群里被问得最多的一类错误整理一下已经遇到的朋友可以对号入座。最早遇到的往往是 Java gateway 相关报错比如py4j.Py4JException: An error occurred while trying to connect to the Java serverjava.lang.RuntimeException: Java gateway process exited before sending the driver its port number排查顺序是先确认 Java 装了没有java -version能不能看到版本再看JAVA_HOME环境变量是否指向正确路径最后看 PySpark 是否安装了能匹配当前 Spark 版本。还有个容易被忽略的点如果你装了多个 Java 版本PySpark 会优先使用JAVA_HOME指定的那个版本太新比如 17会导致某些 Spark 版本不兼容建议直接装 Java 8 或 Java 11。还有一个常见报错是AttributeError: module pandas has no attribute ...这种一般不是 ps 的问题而是你把 pandas 2.0 之后的版本和某些老版本 PySpark 混在一起用产生的冲突。解决办法是升级 PySpark 到 3.4 及以上或者在环境里把 pandas 版本降回 1.5.x 系列保持兼容。6.2 API调用层常见的坑位到 API 层面高频报错包括ValueError: The truth value of a DataFrame is ambiguous多条件筛选时误用了and/or改成/|并且给条件加括号。KeyError / ValueError: Cannot combine column references列名写错或列名包含空格/特殊字符在 ps DataFrame 里没有被正确解析建议改名后再操作。NotImplementedError某个 Pandas API 在 ps 里还没有完整实现。这种情况我没啥好办法要么换种写法实现同样逻辑要么到官方文档查一下该 API 的支持程度再决定。还有一个比较隐蔽的坑sort_values之后即使加了ascending参数结果顺序还是有些“随缘”。这是因为全局排序本身需要把数据集中到少量分区里如果 sort 后面还有依赖顺序的运算比如cumsum结果可能与 Pandas 不完全一致。我建议在分布式任务里尽量避免依赖行序的逻辑实在要依赖就用窗口函数配合rows BETWEEN来显式定义窗口范围。6.3 排查问题的通用思路真跑出问题时我的排查顺序一般是这样第一步在代码里去掉 ps 和 Spark 相关部分用纯 Pandas 在抽样的小数据集上跑通逻辑确认结果形式正确。这能帮你把“业务逻辑写错”和“分布式环境报错”分开。第二步把数据量缩小到几百行用 ps 跑一遍看结果是否和 Pandas 一致。这一步主要排查 API 行为差异特别是索引、类型、排序差异。第三步再放大数据量跑实际任务遇到慢或 OOM去 Spark UI 看各个 stage 的耗时和 shuffle 数据量定位瓶颈在读取、shuffle 还是聚合。第四步如果问题还解决不了把报错堆栈搜一搜但注意别直接抄网上的答案——很多老帖基于 Koalas 早期版本API 行为已经变了要看官方文档当前版本的支持矩阵。个人使用体会从最开始抱着“试试看”的心态到后来真拿它处理了几十 G 的日志数据我个人对 Pandas API on Spark 的评价是它是 Pandas 生态和 Spark 生态之间最顺滑的一座桥但不是万能的桥。它最大的价值在于让你复用已有的 Pandas 技能快速推进数据分析任务同时把数据规模的天花板捅高了一大截。同时我也想给刚入坑的朋友提个醒千万不要因为它在语法上像 Pandas就真把它完全当成 Pandas 来用。分布式计算有它自己的铁律减少数据交换、避免全量拉取、理解延迟计算、留意数据倾斜。掌握这几条你就能既享受 Pandas 的书写体验又享受 Spark 的分布式算力。最后聊点实际的如果你手头正好有一台多核机器强烈建议装上 Spark 本地模式把平时单纯用 Pandas 跑的数据量成倍放大主动试几次 ps 和原生 PySpark 混写的场景慢慢体会“哪些操作顺畅、哪些操作别扭”。这种亲身试出来的边界感比看我写十篇教程都管用。
返回列表