PySpark JDBC读取亿级表分批读取无OOM的内存机制问题
核心原因答疑
你从始至终都没有把全表数据同时存储在本地内存里,这是分批方式能跑通的根本原因。
1. 首先纠正对Spark DataFrame的认知误区
Spark的DataFrame本质是逻辑执行计划的抽象,不是实际存储的数据集合:
- 你往
df_effset列表里追加的不是1500万行实际数据,只是对应批次的JDBC查询规则、表结构等元数据,这部分内存占用只有几KB,完全可以忽略。 reduce(DataFrame.unionAll, df_effset)操作也没有做实际的数据合并,只是把多个分批查询的逻辑拼接成了一个统一的执行计划,全程没有加载任何实际数据。
2. 全量读取触发OOM的原因
默认情况下直接调用spark.read.jdbc读整张表时,Spark只会启动单个task拉取全表数据:
- JDBC驱动默认会把整个查询的结果集一次性全部加载到执行器内存中,1亿行数据的内存占用远超你配置的8G上限,直接触发OOM。
- 单task处理全量数据时,没有做内存分片,所有数据需要同时在内存中持有,没有释放空间的机会。
3. 分批读取能正常运行的核心逻辑
你写的分批逻辑全程峰值内存只会占用单批1500万行的大小,远低于8G上限:
- 每一次循环调用
df_new.count()时,才会实际触发一次MySQL查询,拉取当前批次的1500万行数据做行数统计,统计完成后这部分数据就会被Spark自动释放,不会在内存中留存。 - 最终
df_comp.count()执行时,Spark会按拼接好的执行计划,依次执行每个批次的JDBC查询,逐批统计行数再累加结果,全程同一时间只有一个批次的活跃数据在内存中,内存峰值永远控制在单批数据的大小,不会出现全量数据同时加载的情况。
额外优化建议
你目前用的LIMIT + OFFSET分批方式在OFFSET值较大时性能会非常差,比如OFFSET到9000万时,MySQL需要先扫描9000万行才能返回后续的1500万行,整体查询效率极低。更推荐直接用Spark JDBC原生的分区读取参数,不需要自己写循环逻辑,性能更高:
df = spark.read.jdbc( url=url, table=table, properties=properties, partitionColumn="你的主键列名", lowerBound=主键最小值, upperBound=主键最大值, numPartitions=7 # 1亿行分7个分区,每个分区约1400万行,和你之前的分批大小接近 )
内容的提问来源于stack exchange,提问作者Tingkang Wang
相关产品推荐
相关产品推荐

