ARTICLE DETAIL

资讯详情

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

Spark入门到实战:从RDD到DataFrame的分布式计算与性能优化

Spark入门到实战:从RDD到DataFrame的分布式计算与性能优化 1. 先把概念捋清楚Spark到底是个什么东西1.1 一句话说清Spark的定位Spark是一个分布式计算引擎专门用来处理“单台机器搞不定”的数据计算问题。什么叫做单台机器搞不定两个典型场景一是数据量大到内存放不下二是计算逻辑复杂到单机跑完要好几个小时甚至直接卡死。你可能听说过Hadoop MapReduceSpark和它的定位很像都是做分布式计算但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.x3.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非常适合学习和初步开发。准备步骤非常简单第一步安装JDK8/11/17均可Spark是基于JVM的无论你用Python还是ScalaJDK都是必须的。官方要求JDK 8/11/17我用的是JDK 11稳定没毛病。装完记得配置JAVA_HOME.第二步安装Python3.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、Dataset3.1 RDDSpark最底层的抽象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我的建议维度RDDDataFrameDataset适用人群框架开发者、底层功能需求数据分析、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.fraction0.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.MainSPARK_HOME或JAVA_HOME没配置好检查环境变量确保Spark解压路径和JDK路径正确读取文件后数据都是nullCSV文件没设置headerTrue或者分隔符不对确认表头参数和文件实际格式必要时指定option(delimiter, \t)PySpark提示Python worker failed to connect backPython路径找不到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内存不够导致频繁GC7.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 教程” 这类泛泛的标题试着直接把报错原文贴到搜索引擎里往往一搜就中。很多牛人已经把这些坑的记录发布在各个社区了你看到的报错大概率别人早就踩过、修过了。
返回列表