ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spark/Dask 不是放大版 Pandas:大规模特征计算的正确姿势

Spark/Dask 不是放大版 Pandas:大规模特征计算的正确姿势 把 Spark 当成“能跑更大数据的 Pandas”是我见过最多、也最致命的理解偏差。身边做推荐、风控、交易特征的同学Pandas 用得再熟练一接 Spark 或 Dask 就开始写 for 循环、频繁 collect跑出来的任务比单机还慢然后第一反应就是加内存、加节点。这篇文章想聊清楚一件事大规模特征计算真正的瓶颈和正确姿势到底是什么。我会结合网约车日志清洗、订单特征聚合这类实战场景把 Spark/Dask 里真正能救命的技巧、参数和坑一次性讲透。适合两种人看正在从 Pandas 迁移到 Spark/Dask 的算法或数据工程师以及刚搭完集群但任务总跑不快的同学。1. 为什么 Spark / Dask 绝不是“放大版的 Pandas”1.1 一个被忽略的差异Eager 与 Lazy 的思维切换Pandas 是典型的 eager 执行。你写df[df[amount] 100]这一行执行完结果立刻就在内存里了可以直接看shape、head()、.values。这种“拿到数据再想下一步”的交互方式让人非常舒服但也把人的思维养懒了。Spark 和 Dask 不一样它们是 lazy 执行。你写df.filter(col(amount) 100).groupBy(driver_id).count()这行代码执行完Spark 并没有真的去过滤、分组、计数它只是往内存里塞进了一个计算图DAG。只有当你调用.collect()、.show()、.write()这类 action 操作时任务才真正开跑。这个差异直接导致了一个经典翻车现场有人想看看中间结果长什么样随手敲了个.count()或者.show()结果整个任务被强制触发了一次完整的 action 计算后面再写下一段代码又触发一次 action。底层的父 RDD/DataFrame 被重复计算了好几遍比单机 Pandas 慢十倍都不奇怪。Dask 的 lazy 机制相对轻量一点本质也是一样.compute()之前都是“描述计算”算完之后才出数据。所以从 Pandas 切过来第一件事不是学 API而是接受“你在描述一个计算流程而不是在操作一批数据”这个事实。1.2 数据分区与 Shuffle真正决定性能的底层机制Pandas 只有一个进程、一个内存空间所有数据都在同一个进程里躺着。Spark/Dask 则把数据切成很多块每个块叫一个 partition分区分到不同 worker 上并行处理。理解分区之后还必须理解 Shuffle。所谓 shuffle就是数据在节点之间搬运重组的动作。join、groupBy、distinct、orderBy这些操作都会引发 shuffle因为原本分散在不同分区的数据需要按某个 key 重新分组这就必须把数据通过网络和磁盘搬家。我打一个比方Pandas 就像一个小工作室三个员工共用一张大桌子谁拿材料都方便。Spark 是一栋写字楼几十个工位分布在不同的楼层和房间每个工位只有自己面前的抽屉。做特征计算就是“按部门重新分组”Pandas 只需要抬头喊一嗓子Spark 却要让所有人搬着电脑和工位换楼层。这个“搬家”的成本就是 shuffle 的成本。理解了这一点你就明白为什么 Spark 任务常见的优化原则是减少 shuffle 次数、缩小 shuffle 数据量。很多在 Pandas 里习以为常的写法比如多次 groupBy、频繁 join在分布式环境下都会变成灾难。1.3 内存、容错与执行模型挂了不能直接拍大腿Pandas 跑挂了进程崩了数据在内存里的东西全没了你能做的只有修复代码然后重跑。Spark/Dask 不一样它们有血缘机制——每个 DataFrame 都记得自己是“从哪个父 DataFrame、做了什么操作”变来的。某个节点挂了以后调度器会找到对应的父分区重新计算一次就能恢复这就是容错。但容错不是免费的。如果血缘链特别长比如连续做了 20 步操作一旦深层节点失败重算的代价会非常大甚至比从头跑还慢。所以我后面会重点讲 checkpoint 的用法——它就是用来“截断血缘”的。内存模型更是完全不同。Pandas 是“一个大内存空间全塞进去不够就 OOM”。Spark 的 executor 内存被明确分成 execution执行和 storage存储两部分shuffle 数据、聚合中间结果、缓存数据都各有各的地盘。这意味着光给 Spark 加内存不一定能解决问题反而可能导致 GC 变长、任务更慢。这三层差异构成了全文所有技巧的底层逻辑不要再问“为什么我把 Pandas 的一行代码搬过来这么慢”而要问“我的计算到底触发了几次 shuffle、多少数据被搬动了”。2. 大规模特征计算的六大实用技巧2.1 先过滤、再投影、后聚合特征是“减”出来的我见过太多人做特征计算上来就select *然后全列做聚合。这个习惯在 Pandas 里不算大问题但在 Spark 里会直接影响扫描成本、序列化成本和 shuffle 量。正确顺序是尽早过滤。比如只需要近 7 天数据就在读表或读文件之后立刻where dt ...把数据量缩到最小。只保留需要的列。列越少传输到 shuffle 的数据就越少。尽量少 groupBy。把多个特征合并到一次聚合里而不是同一个 key 聚合三次。如果中间结果要反复使用才考虑 cache否则别乱缓存。举个实际例子。你有一个订单表里面有 50 列但司机特征只需要订单金额、订单时间、城市 ID、司机 ID。如果你直接df.groupBy(driver_id).agg(sum(amount))Spark 会扫描全部 50 列虽然它可能只读必要的列但很多情况下你需要显式写select来帮助优化器裁剪。我常用的写法是order_df ( spark.read.parquet(orders) .filter(col(dt) 2025-01-01) .select(driver_id, amount, order_time, city_id) )然后在order_df上做聚合。这一步对性能的影响往往比后面任何调参都大。特征工程的本质不是“造出来的”而是“先缩小数据空间再在缩小后的空间里做变换”。2.2 窗口函数时间序列特征的利器跟时间相关的特征几乎都离不开窗口函数。常见需求包括最近 N 天的累计订单量、上一单和当前单的时间间隔、按司机排名的金额、每个司机每天的第几单、未来 N 天的均值等。窗口函数的写法并不难难点在于理解它的执行代价。窗口函数通常会在指定的partitionBy键上做一次全部分区的排序和聚合这会触发 shuffle。如果partitionBy的 key 基数特别大比如一亿个司机每个分区又排一次序代价可不小。我放一个网约车场景里的经典案例计算每个司机的“上一单距离当前单的时间间隔”。这个特征对风控、司机行为分析都很重要做法是用lag窗口函数from pyspark.sql import functions as F from pyspark.sql.window import Window window_spec Window.partitionBy(driver_id).orderBy(order_time) feature_df order_df.withColumn( prev_order_time, F.lag(order_time).over(window_spec) ).withColumn( interval_seconds, (F.col(order_time).cast(long) - F.col(prev_order_time).cast(long)) )在这个例子里partitionBy(driver_id).orderBy(order_time)就相当于按司机分桶、再按时间排序Spark 会对这个 key 做全局 shuffle。如果司机数量有几百万这个窗口的执行时间会非常可观。一个实测有效的优化如果业务上不需要“所有司机全局排序”可以先按city_id做前置过滤或分组让窗口的 partition key 只覆盖当前需要的数据范围。Dask 里对应也可以用dask.dataframe的groupbyapply做类似操作不过 Dask 的窗口函数支持不如 Spark 完整复杂场景建议落到 SQL 上或者直接用dask-sql。2.3 Broadcast Join小表别和大表硬 Join特征计算里最常见的操作之一是把一个大事件表和一个小维表 join 在一起。比如订单表有 1 亿行城市维表只有 300 行天气表可能只有几万行。Pandas 里merge一下就完事Spark 里如果直接 joinSpark 会默认把两个表都做 shuffle把小表的数据广播到所有节点再和大表匹配。其实 Spark 有一个自动的广播优化spark.sql.autoBroadcastJoinThreshold默认 10MB当小表小于阈值时自动转 broadcast join。但自动优化不一定总生效。我习惯手动指定尤其是知道维表很小的时候from pyspark.sql import functions as F city_df spark.createDataFrame([(1, 上海), (2, 北京)], [city_id, city_name]) order_df order_df.join(F.broadcast(city_df), city_id, left)加了F.broadcast之后Spark 会把city_df分发到每个 executor 的本地内存里join 的时候完全不做 shuffle。这里有一个重要原则广播的表要真的小。如果广播一个 50GB 的表那么每个 executor 都塞 50GBDriver 分配广播任务时先把自己撑爆。一般超过 100MB 就需要重新考虑方案。在 Dask 中类似的需求可以用map_partitions Python 字典来做先收集小表到内存然后在每个分区内用字典映射新列效果更直接也不容易触发复杂的图优化。2.4 别用 Python 原生循环处理行数据用向量化/内置函数有一部分同学从 Pandas 迁移过来会习惯性写df.rdd.map(lambda row: ...)或者for row in df.collect()。在 Pandas 里这是可以接受的因为数据都在内存里在 Spark 里每map一次都是一次分布式任务调度每 collect 一次都是把所有数据拉回 Driver这两件事都是性能毒药。处理大规模数据正统做法是用 Spark 的内置函数和表达式。比如判断一个订单是否属于高峰时段不要写 UDF# 不建议 def is_peak(hour): return 1 if (hour 7 and hour 9) or (hour 17 and hour 20) else 0 udf_is_peak F.udf(is_peak, IntegerType()) df.withColumn(is_peak, udf_is_peak(F.hour(order_time)))推荐写法df.withColumn( is_peak, F.when( (F.hour(order_time).between(7, 9)) | (F.hour(order_time).between(17, 20)), 1 ).otherwise(0) )内置函数的好处是不需要序列化、不需要将 Python 和 JVM 之间的数据来回转换、可以被 Spark 催化剂优化器进一步优化。真遇到复杂业务逻辑非写 UDF 不可在 Spark 3.0 建议用 pandas UDF基于 Arrow它把数据处理放到 Python 的 pandas 里批量执行序列化和函数调用开销都小很多。Dask 里则是map_partitions把整块分区丢给 pandas 函数效果类似。记住一句话从“逐行逻辑”切换到“批量/向量化逻辑”是 Pandas 打法的核心迁移点。2.5 分区数不是越多越好Repartition / Coalesce 怎么用分区数是 Spark 任务性能最直接的“旋钮”。分区太少并行度不够CPU 空闲分区太多每个任务只干一点点活调度开销和文件碎片却可能拖垮整台集群。经验法则有两个对于读文件进来的 DataFrame分区数通常由文件块数量决定不需要刻意调整。对于 shuffle 之后的 DataFrame分区数由spark.sql.shuffle.partitions决定默认是 200。这个默认值很坑你只有 500MB 数据它也会给你拆成 200 个分区一群小任务白白增加调度开销你有 100TB 数据只有 200 个分区又严重不够。所以实践中要根据数据量和集群规模动态调整。常用的调法spark.conf.set(spark.sql.shuffle.partitions, 400) # 或者直接对 DataFrame 重分区 df df.repartition(400, driver_id)repartition会真正触发一次 shuffle把数据按指定列和分区数重新分布coalesce只是把现有分区合并不产生 shuffle所以当需要减少分区但不关心数据分布时用coalesce更便宜。还有一个高频场景处理数据倾斜。如果某个 key比如某个大司机的数据量远超其他 key聚合时单个 executor 就容易 OOM。常见解法是加随机盐比如把 key 后面拼一个 0-9 的随机数让一个大 key 分布到 10 个分区上做两阶段聚合先按加盐后的 key 聚合再去掉盐做第二次聚合。这个手法在特征计算里非常实用尤其是按热门商品、热门用户做聚合时。2.6 缓存与检查点用对了省事用错了更慢cache/persist 是 Spark 里最容易被滥用、也最容易用错的功能。很多人以为只要df.cache()就能快结果发现任务越跑越奇怪。cache 的真相是cache()是 lazy 的。调用.cache()之后如果后面没有 actionSpark 根本不会缓存等于白写。缓存级别要选对。默认MEMORY_ONLY数据放不进内存就扔掉下次还是要重算。如果中间结果很大建议用MEMORY_AND_DISK放不下的落盘至少不会全丢。缓存是有成本的。缓存一份数据需要把数据序列化放内存或磁盘如果某个结果只被用一次缓存反而更慢。更重要的一个技巧是 checkpoint。当血缘图谱很长比如做了 50 个步骤某一步失败后 Spark 要从头重算最原始的父 DataFrame代价极高。checkpoint 会把当前 DataFrame 的中间结果直接写到可靠存储如 HDFS然后切断血缘后续计算从 checkpoint 位置重新开始。spark.sparkContext.setCheckpointDir(hdfs:///tmp/checkpoint) df df.checkpoint()这个操作在分布式任务里相当关键。特征计算的 Pipeline 往往又长又重在中间结果做一次 checkpoint相当于给计算图拍了个快照后续随便怎么折腾都从快照开始不用回溯整条血缘。Dask 没有完全等价的全局 checkpoint但可以分步计算后把结果存 parquet 再接 reads思想是同一套。3. 实战从 JSON 原始日志到特征宽表的 Spark 完整过程3.1 场景定义与数据形态用一个经典场景网约车订单日志。原始数据是 JSON 文件每条记录包含订单 ID、司机 ID、乘客 ID、上车时间、下车时间、订单金额、城市 ID、上车点经纬度、订单状态等。现在要按司机维度产出最近 7 天的特征宽表包括订单量、日均订单金额、高峰时段占比、上一单间隔均值、平均每单时长等供下游模型使用。原始 JSON 大概长这样{ order_id: 123456, driver_id: d_9981, passenger_id: p_2233, status: completed, order_time: 2025-05-06 18:33:12, finish_time: 2025-05-06 18:51:40, amount: 42.5, city_id: 101, start_location: {lat: 31.2304, lng: 121.4737} }这种数据在 Pandas 里直接json_normalize展开也能做但一旦文件有好几百 GBPandas 连读都读不动Spark 反而是首选。3.2 读取 JSON 并处理嵌套结构与时间字段读取 JSON 最直接的方式是df spark.read.json(hdfs:///raw_data/orders/*.json)Spark 会自己推断 schema。但这里有两个坑inferSchema 开销巨大。Spark 默认需要遍历一遍数据才能推断出所有字段类型文件越多越慢。如果你已经知道字段格式最好直接显式定义 schema使用spark.read.schema(...)读取。嵌套 Struct 解析。读取之后经纬度字段会变成一个struct类型需要通过col(start_location.lat)访问不能直接当普通 DataFrame 列处理。时间字段也是一个重点。原始 JSON 里order_time是字符串Spark 推断 schema 时很可能推断成 StringType。要参与窗口排序、时间差计算必须先转成 timestampdf df.withColumn(order_time, F.to_timestamp(order_time, yyyy-MM-dd HH:mm:ss)) df df.withColumn(finish_time, F.to_timestamp(finish_time, yyyy-MM-dd HH:mm:ss))然后做初步清洗过滤掉金额异常小于 0、状态不是 completed 的记录、上下车时间颠倒的记录。清洗这一步的代码clean_df ( df.filter(F.col(status) completed) .filter(F.col(amount) 0) .filter(F.col(finish_time) F.col(order_time)) .select( order_id, driver_id, order_time, finish_time, amount, city_id, F.col(start_location.lat).alias(start_lat), F.col(start_location.lng).alias(start_lng) ) )到这里数据形态已经变成一张比较规整的订单明细宽表。接下来要做特征计算。3.3 特征计算的 Pipeline 实现现在看具体特征怎么算。我列出几个有代表性的特征及实现思路。特征一近 7 天订单量、总金额、平均金额一次性聚合避免多次 groupBydriver_agg clean_df.groupBy(driver_id).agg( F.count(*).alias(order_cnt_7d), F.sum(amount).alias(total_amount_7d), F.avg(amount).alias(avg_amount_7d) )特征二高峰时段订单占比先算每笔订单是否属于高峰时段再按司机汇总。用when函数生成 0/1 标签不需要 UDFpeak_df clean_df.withColumn( is_peak, F.when( (F.hour(order_time).between(7, 9)) | (F.hour(order_time).between(17, 20)), 1 ).otherwise(0) ) peak_agg peak_df.groupBy(driver_id).agg( F.mean(is_peak).alias(peak_ratio) )特征三上一单间隔的均值、中位数用窗口函数先算出每一单的间隔再按司机聚合window_spec Window.partitionBy(driver_id).orderBy(order_time) gap_df clean_df.withColumn( prev_order_time, F.lag(order_time).over(window_spec) ).withColumn( gap_seconds, (F.col(order_time).cast(long) - F.col(prev_order_time).cast(long)) ).filter(F.col(gap_seconds).isNotNull()) gap_agg gap_df.groupBy(driver_id).agg( F.avg(gap_seconds).alias(avg_gap_seconds), F.expr(percentile_approx(gap_seconds, 0.5)).alias(median_gap_seconds) )percentile_approx在超大分组数据里比精确百分位函数快得多误差通常可接受是生成分布类特征的常用替代方案。特征四平均每单时长直接两个时间戳相减duration_agg clean_df.withColumn( duration_seconds, (F.col(finish_time).cast(long) - F.col(order_time).cast(long)) ).groupBy(driver_id).agg( F.avg(duration_seconds).alias(avg_duration_seconds) )最后把多个特征 join 成宽表。这里有个性能细节多个聚合结果都是按driver_id这个 key 做 groupBy理论上会各自产生一次 shuffle。为避免过多 shuffle可以把多个聚合合并到一次groupBy里用多个agg表达式一次性算完如果窗口特征没法合并就把窗口生成的结果作为一列放回原表之后再一起聚合尽量少拆分组动作。把聚合结果合并成宽表feature_table ( driver_agg .join(gap_agg, driver_id, left) .join(peak_agg, driver_id, left) .join(duration_agg, driver_id, left) )然后按实际业务需要对空值做填充feature_table feature_table.fillna( { avg_gap_seconds: 0, median_gap_seconds: 0, peak_ratio: 0, avg_duration_seconds: 0 } )3.4 写出 Parquet 并处理类型转换、分区策略特征宽表算完之后最后一步是落地。我强烈推荐 Parquet 而不是 CSVParquet 是列式存储下游模型如果只需要其中几列可以跳过大部分数据扫描。体积比 CSV 小很多自带压缩。保留类型信息不会出现 CSV 写出后时间字段全变字符串、金额精度丢失的问题。写出方式按日期分区feature_table.write.mode(overwrite) \ .partitionBy(dt) \ .format(parquet) \ .save(/warehouse/features/driver_feature_7d)这里需要先把当前计算日期dt加到 DataFrame 里然后按天分区将来按天做增量特征更新就很方便。注意一点partitionBy的列会自动参与写入但如果不小心把dt加进来后又去做过滤Spark 可能没法做分区裁剪所以写完后查数要记得带上分区过滤条件。还有一个类型转换的细节Spark 在 join 时如果两个 DataFrame 的 join key 类型不一致会静默做 cast但性能会下降。我在实际项目中遇到过LongType和IntegerType字段 join 的情况小表还好大表 join 直接慢了几倍。所以建议在写 pipeline 时提前统一 key 类型比如driver_agg driver_agg.withColumn(driver_id, F.col(driver_id).cast(string)) gap_agg gap_agg.withColumn(driver_id, F.col(driver_id).cast(string))这一步骤看起来不起眼但在大规模场景里非常关键也延续了 Pandas 里astype对齐类型的好习惯。4. 常见问题与排查技巧实录4.1 Spark 内存问题OOM、Executor 丢失、GC 飙升几乎每个 Spark 项目都会碰到内存问题现象各有不同任务在某个 stage 卡死、Container killed by YARN、ExecutorLostFailure、GC 时间占比超高。排查的第一步永远是看 Spark UI而不是猜参数。具体看几个关键页面Executors 页面看到某个 executor 的 Shuffle Spill 非常大说明内存不够数据一直在溢写到磁盘需要提高 executor 内存或者减少单分区数据量。Stages 页面看每个 task 的 Shuffle Read/Write 量。如果某个 task 的输入量是其他 task 的几十倍那基本是数据倾斜。GC 时间如果 Executor 的 GC 时间占总运行时间 30% 以上说明内存管理出了问题可能需要调整spark.memory.fraction。常用参数调整--executor-memory 8g --conf spark.memory.fraction0.75 --conf spark.memory.storageFraction0.3spark.memory.fraction表示用于执行和存储的内存占 executor 总内存的比例默认 0.6如果 GC 严重可以适当调大到 0.75storageFraction表示其中专用于 cache 的占比如果不太用 cache 可以调小。但请记住内存参数永远排在不产生 shuffle 和减少数据规模的优化之后。很多人一遇到 OOM 就疯狂加内存加到 64G 还是挂原因就是 shuffle 量太大或者分区严重倾斜加机器根本缓解不了。4.2 JSON 读取与嵌套解析Schema 推断慢、时间字段解析失败刚才实战环节已经提了一些这里再补充排查要点。如果 JSON 文件特别多spark.read.json自动推断 schema 会触发一整轮全量扫描。解决办法之一是加samplingRatiospark.read.option(samplingRatio, 0.1).json(hdfs:///raw_data/*.json)只抽样 10% 的数据来推断 schema速度能快不少。但抽样有风险少数文件里多出的字段可能没被推断出来后面读出来变成 null。我的经验是线上固定格式的数据文件直接显式写 schema 最稳妥。嵌套数组的展开也是常见难点。JSON 里如果某个字段是数组需要先explode再处理。比如每个订单可能包含多个途经点你要按途经点展开成多行再做特征from pyspark.sql import functions as F df df.withColumn(waypoint, F.explode(F.col(waypoints)))展开之后行数会增多下一阶段的聚合要格外小心数据量膨胀。我见过有人对一个大 JSON 展开之后总行数翻了 100 倍后面 join 直接把集群跑挂。展开之前先想清楚这个字段是不是真的需要进特征。时间字段解析失败也是高频问题。JSON 里的时间字符串可能是2025-05-06T18:33:12Z、2025-05-06 18:33:12、甚至20250506183312Spark 默认的不一定能正确识别。最简单粗暴的办法是显式指定格式F.to_timestamp(F.col(order_time), yyyy-MM-ddTHH:mm:ssXXX)在写样例数据测试时尽量把各种时间格式都覆盖到避免上线以后在凌晨三点被某个异常格式打断。4.3 Pandas 数据类型转换和环境安装的坑热词里有一堆跟 Pandas 有关的问题比如“pandas 数据类型转换”“pycharm 怎么安装 pandas 包”“import pandas as pd 清华源下载”“手机 python 安装 pandas”。这些看似是小问题但工程里确实耗人。先解决环境安装这个比较常见的点。如果你遇到ERROR: Could not find a version that satisfies the requirement pandas (from versions: none) ERROR: No matching distribution found for pandas通常原因有三个pip 源里没有当前 Python 版本对应的 pandas 轮子、Python 版本太新或太旧、缺少依赖库导致安装脚本失败。最快的解决办法之一是切换到豆瓣或清华的镜像源pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple/Pandas 的依赖版本如果和你已有环境里的 NumPy 冲突装完之后import pandas还可能报错建议用 venv 或 conda 单独建环境避免污染系统 Python。手机 Python 环境一般用 Termux 或 Pydroid直接 pip 也一样但要注意手机架构可能没有预编译包只能走源码编译速度慢且容易失败等不了建议直接放弃。再说数据类型转换。Pandas 里常见的坑包括字符串列里有空格astype(int)直接报错时间字段混了两种格式pd.to_datetime性能骤降超大整数被当成int64转成 Spark 后变LongType没问题但转回 pandas 时溢出等等。在 Pandas 里正确做法是用pd.to_numeric(..., errorscoerce)或pd.to_datetime(..., format...)来强制转换不要裸用astype。这个习惯要带进 Spark。数据处理 pipeline 里经常能看到col(amount).cast(double)、col(order_time).cast(timestamp)凡是不确定类型的地方都要考虑转换失败的兜底逻辑比如先when(col(amount).cast(double).isNull(), default)再使用。大规模数据处理最忌讳的就是某个脏数据在最后阶段才炸出来全链路排查成本极高。4.4 集群搭建与脚本运行中的经典翻车现场最后一个章节聊聊集群搭建和 Spark 脚本运行时最容易踩的坑。看热词里“spark 集群搭建”“spark 搭建教程”“spark 数据分析案例”“spark 脚本示例”特别多说明这块是真的难倒不少人。本机装 PySpark 开发和真正搭一个集群跑生产任务完全是两码事。我见过最经典的几个问题Java 版本不匹配。Spark 3.x 对 Java 8/11 有不同支持装错版本开局就跪。节点间 hostname 无法解析。Standalone 模式下worker 注册不到 master报Failed to connect to master检查/etc/hosts是最优先动作。Python 环境不一致。每个节点上的 Python 版本、pandas/numpy 等依赖不一致任务分发到某个节点就报ModuleNotFoundError。生产环境强烈建议用 conda 或虚拟环境打包分发而不是逐台机器手工装。端口被防火墙挡住。Spark master 默认 7077Web UI 默认 8080YARN 的调度端口也有可能冲突。排查命令先跑jps看进程是否都在再用netstat查端口监听情况。还有一种资源抢占场景也值得专门说。现在不少团队会采购带 GPU 的 AI 工作站或一体机像 NVIDIA DGX Spark 这类设备就经常被装上一堆“一键安装脚本”。我见过有人在同一个机器上同时跑 ComfyUI、训练脚本、Spark结果 Spark 任务一跑就 OOM。后来发现是 ComfyUI 之类的服务常驻占掉了大量显存和内存。很多人在问“DGX Spark 怎么关机”其实重点不是关机而是关掉不用的服务、给 Spark 预留资源。如果你也在类似设备上跑 Spark先看free -h和nvidia-smi确认内存/显存还有多少余量再调整 Spark 的 executor 内存别指望一台机器同时扛下所有任务。spark-submit的常用参数也顺手整理一下spark-submit \ --master spark://your-master:7077 \ --deploy-mode cluster \ --driver-memory 4g \ --executor-memory 8g \ --executor-cores 4 \ --num-executors 10 \ --conf spark.sql.shuffle.partitions400 \ your_job.py这几个参数基本覆盖了资源分配的核心。每个参数不是越大越好要根据集群总资源反算比如 100GB 内存的机器executor-memory 开 32g那一个节点最多也就两三个 executor还要留给系统余量。总之先看机器能提供多少再决定参数并预留 20% 以上的余量给系统和 shuffle 缓存。4.5 常见问题速查表现象可能原因解决思路任务非常慢但没有报错分区数过多或过少、shuffle 量过大Spark UI 查看 stage 的 shuffle 数据量和 task 数调整分区、减少 join/groupByExecutor Lost / OOMexecutor 内存不足、数据倾斜、shuffle 溢写调整spark.executor.memory、spark.memory.fraction加随机盐做两阶段聚合read.json schema 推断超级慢文件多默认全量推断 schema用samplingRatio或显式指定 schema时间字段 parse 出来全是 null字符串格式与默认解析不匹配用to_timestamp(col, pattern)显式指定格式两个 DataFrame join 后数据翻倍关联键不唯一或类型不一致先对 key 去重/统一 key 类型确认粒度后再 joinCOULD NOT find a version that satisfies the requirement pandaspip 源没有对应版本或 Python 版本不匹配换北大/清华镜像源或用 venv/conda 安装worker 注册不到 masterhostname 解析问题或端口不通检查/etc/hosts、spark-env.sh的 master 地址、防火墙状态同一台机器又是 AI 又是 Spark任务总 OOM资源被常驻服务抢占关掉不用的一键服务进程重新规划 GPU 和内存分配从 Pandas 迁移到 Spark/Dask我踩过的最大的坑其实不是 API 记不熟而是思维没有转过来。单机 Pandas 里数据是“躺”在内存里的你想怎么拿就怎么拿分布式世界里数据是“流动”的你每做一个操作背后的网络和磁盘都要付出代价。所以再遇到慢任务先别急着加资源去 Spark UI 看看数据到底被搬动了多少次、每个分区扛了多少数据。把这些看透比记住一百个 API 都管用。
返回列表