1. 学Spark之前,先搞懂它到底解决了什么问题
网上Spark的教程一搜一大把,但大多数教程一上来就丢给你一堆抽象概念:RDD、DAG、Executor……看得人头皮发麻,关了页面还是不知道Spark到底是什么、学了能干嘛。这篇文章我换个思路,从一个实际项目出发,带你走一遍Spark的学习路径。先聊清楚它解决的是哪类问题,再讲核心概念,然后直接上手跑一个程序,最后把我在实际运维和开发中踩过的坑、调优经验一起整理出来。不管你是准备面试、要搭集群,还是刚入职接到一个实时数据处理的需求,这篇都能给你一条清晰的路。
我先说个结论:Spark是一个统一的、分布式的数据处理引擎。这句话拆开理解就是:第一,它处理的数据量大到单台机器扛不住;第二,它把计算任务分发到多台机器上并行执行;第三,你不仅可以用它做批处理,还能做SQL查询、流式计算、机器学习甚至图计算。很多人把它当成“Hadoop的升级版”,这个说法不完全准确,但方向上没有错。
1.1 大数据场景下的“算不动”从哪来
在没有Spark的时候,大数据处理的主流方案是Hadoop MapReduce。MapReduce的设计思想很简单:把数据切成小块,分给多台机器并行处理,然后再把结果合并起来。听起来很合理,但实际用起来有几个致命的痛点。
第一个痛点是中间结果落盘。MapReduce每个Map任务跑完,结果要写到磁盘上;Reduce任务再从磁盘读出来。如果业务逻辑复杂,需要多轮MapReduce串联,每轮都要读写磁盘。磁盘IO的速度和内存差着几个数量级,这就导致任务越复杂,慢得越离谱。我印象很深,之前用MapReduce跑一个用户行为分析任务,数据量大概几十TB,每轮要跑两三个小时,如果链路有七八个阶段,一个完整任务跑一天很正常。
第二个痛点是编程体验差。MapReduce用Java写,光一个WordCount就要写几十行代码,逻辑稍微复杂一点,类就堆得到处都是。业务人员想快速做数据探索和统计分析,这个门槛实在太高了。
第三个痛点是无法覆盖多样化场景。MapReduce本质上是批处理模型,做交互式查询很吃力,做流式计算几乎不行,做机器学习迭代计算更是噩梦。而真实业务场景里,你既需要每天凌晨跑批处理,也需要随时跑个SQL看数据,还需要对实时流入的数据做清洗和统计。用一套引擎统一搞定,是当时整个行业的期待。
Spark就是在这个背景下诞生的。2009年,加州大学伯克利分校的AMPLab实验室启动了这个项目,核心思想很简单:把中间结果尽量留在内存里,让计算引擎自己管理数据的分区、调度和容错。2010年开源,2014年成为Apache顶级项目,之后一路发展到今天,已经成为大数据领域事实上最主流的计算引擎。
1.2 Spark凭什么比MapReduce快
关于Spark比MapReduce快这件事,很多人有个误解,觉得是因为Spark用了内存计算,而MapReduce用磁盘。这个说法对了一半。内存计算确实是重要原因,但更准确地说,Spark赢在三个层面。
第一,DAG调度引擎。Spark会把一个复杂的计算任务拆成有向无环图,每个节点代表一个计算步骤。DAG调度器会在图里找最优的执行路径,能合并的步骤尽量合并,能并行执行的步骤就并行执行,中间数据优先保留在内存里,只有在内存不够的时候才落盘。MapReduce则不同,它的执行模型是固定的两阶段——Map和Reduce,每一轮都是一次完整的落盘和读取。打个比方,MapReduce像是每做完一道菜就把厨房收拾干净、所有食材重新放回冰箱,下一道菜再从冰箱拿出来;Spark则是整个做菜过程都在台面上流水线操作,材料随取随用,只在需要的时候才临时放一下。
第二,懒加载机制(Lazy Evaluation)。Spark的API分为转换操作(Transformation)和行动操作(Action)。转换操作只是记录执行计划,并不会真正计算;只有遇到行动操作时,才把整套执行计划优化后一次性提交执行。这个设计让引擎能全局优化,比如把多个读数据的步骤合并、把没用的字段裁剪掉。
第三,统一的执行引擎。Spark的底层执行引擎是统一的,SQL、流计算、机器学习、图计算,都跑在同一套调度和内存管理机制上。这意味着你不需要为不同场景搭建不同集群,一套集群解决所有问题,运维成本和管理成本也大幅下降。
我个人的建议是:学习Spark不要一上来就钻RDD源码或者调优参数,先带着“它在解决什么问题”的视角去用,用一段时间之后再回头补底层原理,效率会高很多。接下来我会按照这个思路,带你挨个拆解Spark的核心概念。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 入门必懂的核心概念:RDD、DataFrame和执行模型
很多Spark教程上来就把RDD、DataFrame、Dataset、Stage、Task这些名词全部铺开,读者看完大脑直接宕机。其实这些概念是有逻辑链条的,你只需要沿着一条线走:数据以什么形式存在 → 计算以什么方式执行 → 代码写完之后提交给谁去调度。一条线理清楚,整个Spark的骨架就搭起来了。
2.1 RDD:Spark的数据“积木”
RDD全称是Resilient Distributed Dataset,弹性分布式数据集。拆开看:Distributed,数据是跨多台机器分片存储的;Dataset,它是一个数据集合;Resilient,容错能力强,某个分片丢了或计算失败,可以从血统关系重新算出来。
用代码来理解最直观。
python复制from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("demo").master("local[*]").getOrCreate()
sc = spark.sparkContext
# 创建一个RDD
rdd = sc.parallelize([1, 2, 3, 4, 5])
# 转换操作:map是每个元素乘以2,返回的是一个新的RDD
doubled = rdd.map(lambda x: x * 2)
# 行动操作:真正触发计算,取出结果
print(doubled.collect()) # [2, 4, 6, 8, 10]
这里有两个关键点需要理解。
第一个是不可变。rdd.map之后得到的doubled是一个全新的RDD,原来的rdd不会变。这种不可变性让分布式环境下的数据安全得到了保障,多个任务并行处理时不会互相干扰。
第二个是分区。RDD的数据不是堆在一起的,而是按分区(Partition)存储。分区是Spark并行计算的单位,每个分区会放到一个Executor上执行。默认分区数跟集群的核数有关,也可以手动指定。分区太多会导致调度开销大,分区太少会导致并行度不够,怎么权衡后面在调优部分细说。
RDD的API分成两大类:
- 转换操作:map、filter、flatMap、distinct、reduceByKey、sortByKey等,都是惰性的,记录逻辑但不执行。
- 行动操作:collect、count、take、saveAsTextFile、reduce等,触发真正的计算并返回结果给Driver。
记住这条规则:转换操作只是“计划”,行动操作才是“执行”。我在带新人时经常提醒他们,不要在一个行动操作之后又接一堆转换操作然后再次行动,这样数据会被反复完整遍历,性能损耗极大。
2.2 DataFrame和Dataset:从函数式编程走向SQL化的进阶API
RDD虽然灵活,但有个明显的缺陷:它不知道每个元素内部的结构。比如一个RDD里装的是用户信息,Spark无法直接知道这个用户有几列、每列是什么类型。它只能把每条数据当成一个不透明的对象,靠开发者自己写逻辑去提取字段。这样就少了很多优化空间。
DataFrame的出现解决了这个问题。DataFrame可以理解成一张分布式的二维表,有列名,有Schema,有类型信息。Spark在这张表上可以做很多“作弊级”的优化。比如你只查两个字段,Spark的Catalyst优化器会自动做列裁剪,只读取那两列的数据;你在做过滤时,优化器会把过滤条件下推到数据源,减少读入的数据量。这些优化在RDD时代都是不可能的。
用代码对比一下两种风格的差异。
python复制# RDD风格
rdd = sc.textFile("users.txt") \
.map(lambda line: line.split(",")) \
.filter(lambda fields: int(fields[2]) > 18)
# DataFrame风格
df = spark.read.csv("users.txt", header=True)
adult_users = df.filter(df.age > 18)
DataFrame风格的代码不仅更简洁,执行效率往往也更高。因为Spark知道age这一列的类型和位置,可以做列式存储、谓词下推和更精细的过滤。此外,Spark还提供了SQL接口,你可以直接把DataFrame注册成临时表,然后写标准SQL。
python复制df.createOrReplaceTempView("users")
result = spark.sql("SELECT name, age FROM users WHERE age > 18")
对于熟悉SQL的开发者来说,这个门槛几乎为零。Dataset是RDD和DataFrame之间的一种强类型API,主要在Scala和Java中使用,Python中默认就用DataFrame,这里就不展开细讲了。
2.3 作业执行模型:Driver、Executor、Task和DAG
数据模型搞清楚了,可以看看Spark运行时是怎么组织的。整个过程分为四层。
Driver进程是你写的Spark程序运行的地方。它负责创建SparkContext、把代码转换成执行计划、调度任务、收集结果。你就把它当成项目负责人。
DAGScheduler是Driver内部的一个调度器,它把代码解析成DAG图,然后根据宽依赖(shuffle边界)把DAG切成多个Stage。阶段之间像接力赛一样,前一个阶段的数据要经过重新分区后才能交给后一个阶段。
TaskScheduler负责把Task分发给各个Executor。Stage内部又被拆成多个Task,每个Task处理一个分区的数据,放到一个Executor上执行。
Executor是真正干活的进程,它运行在集群的每台Worker节点上,里面可以并行执行多个Task。Executor负责执行数据计算,把结果写回内存或磁盘,并和Driver保持心跳通信。
很多人第一次看Spark UI时,看到Job、Stage、Task三层结构容易晕。打个比方:Job是一个完整的业务需求,比如“统计全站用户各年龄段的分布”;Stage是这个需求被拆分出的几个主要环节,比如“先过滤有效用户”是一个阶段,“再按年龄段聚合”是另一个阶段,两个阶段之间有数据洗牌;Task则是每个环节在每台机器上处理单个数据分片的动作。
这四层概念对应到具体的资源参数,是这么映射的:
| 层 | 概念 | 对应参数/组件 |
|---|---|---|
| 应用层 | Application | SparkContext / SparkSession |
| 作业层 | Job | 一个行动操作(Action)触发 |
| 阶段层 | Stage | 由宽依赖(Shuffle)分割 |
| 任务层 | Task | 每个分区一个Task,由Executor执行 |
理解了执行模型之后,再看集群部署、参数调优,就不再是碎片的死记硬背。每个参数改的是哪一层,分布式环境下工作是怎么被拆解和协同的,都有了一个清晰的坐标。
3. 从零跑通第一个Spark程序:装环境、写WordCount、配参数
理论知识聊完了,接下来进入动手环节。我建议你一开始不要追求直接搭一个多节点的集群,那样环境配置的坑会让你怀疑人生。先在本地把Spark跑起来,用一个小规模数据集验证代码逻辑,等你真正理解了任务提交、执行和资源分配之后,再考虑上集群。这个学习路径我验证过很多次,是最平滑的。
3.1 几种起步方式怎么选
Spark的部署模式主要有四种:本地模式(Local)、Standalone模式、YARN模式、Kubernetes模式。对于初学者,我强烈推荐先使用本地模式。
本地模式下,Spark的Driver和Executor都运行在同一台机器上,甚至同一个JVM进程里。你的代码逻辑和集群模式完全一致,只是没有网络通信和资源调度这一层。这就让学习曲线一下子缓和了很多,先专心搞懂API和解法,不用一上来面对一堆分布式环境问题。等代码逻辑验证通过,再配Standalone或YARN集群,心态和排查能力都完全不同。
安装上,最简单的路径是直接用pip安装PySpark。
bash复制pip install pyspark
装完没有Python环境的朋友,也可以直接下载Spark发行版,配上Java JDK 8/11/17,解压之后把SPARK_HOME和PATH配好即可。Windows用户需要注意,Spark在Windows本地模式下可能遇到文件权限问题,需要给SPARK_HOME/bin目录下的winutils.exe配置权限;我个人更建议初学者在Linux虚拟机或云主机上动手,能避开不少坑。
然后写第一个例子验证环境。
python复制from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("first_demo") \
.master("local[*]") \
.getOrCreate()
print(spark.version) # 能输出版本号说明环境OK
3.2 WordCount的两种写法:RDD和DataFrame
WordCount是大数据领域的“Hello World”,麻雀虽小五脏俱全。它能让你接触到输入、拆分、转换、聚合、排序、输出这一整套流程。先用RDD写法。
python复制from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("wordcount_rdd") \
.master("local[*]") \
.getOrCreate()
sc = spark.sparkContext
# 读取文件,每行变成RDD中的一个元素
lines = sc.textFile("data/words.txt")
# flatMap: 按空格拆分,并把多行结果拉平成一个单词流
words = lines.flatMap(lambda line: line.split(" "))
# map: 每个单词变成 (word, 1) 的键值对
pairs = words.map(lambda word: (word, 1))
# reduceByKey: 相同单词的计数累加
word_counts = pairs.reduceByKey(lambda a, b: a + b)
# sortBy: 按计数降序排列
sorted_counts = word_counts.sortBy(lambda x: x[1], ascending=False)
# collect: 行动操作,把结果拉回Driver端并打印
for word, count in sorted_counts.collect():
print(f"{word}: {count}")
sc.stop()
这里面最值得注意的细节是:为什么用flatMap而不是map?因为lines中的每个元素是一整行字符串,map操作会把每行拆成一个数组,得到的是数组的列表;而flatMap会把每个数组展平,得到所有单词拼起来的一个大列表。这是WordCount里最容易写错的地方,我第一次写就卡在这儿。
再来看DataFrame + SQL的写法。
python复制from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("wordcount_sql") \
.master("local[*]") \
.getOrCreate()
df = spark.read.text("data/words.txt")
# 使用SQL表达式拆分单词,并写入临时视图
df = df.selectExpr("explode(split(value, ' ')) AS word")
df.createOrReplaceTempView("words_view")
# 标准SQL聚合
result = spark.sql("SELECT word, COUNT(*) AS cnt FROM words_view GROUP BY word ORDER BY cnt DESC")
result.show()
spark.stop()
RDD写法能让你理解数据流和算子的作用,DataFrame写法能让你体会到Spark SQL在代码简洁性和性能优化上的优势。两种写法建议都跑一遍,感受一下差异。
3.3 集群搭建和提交任务时的资源参数
本地模式跑通了,就可以往集群方向走了。初学者想要搭建一个真正的多节点Spark集群,推荐先试Standalone模式,它不依赖于Hadoop和YARN,是Spark自带的独立资源调度器,结构清晰,适合学习。
集群搭建的大致步骤是:准备几台Linux机器,一台作为Master节点,其他作为Worker节点;每台机器都安装JDK和Spark,Master节点配置SPARK_MASTER_HOST;在Spark的conf目录下编辑spark-env.sh,设置内存和核数;启动Master和Worker进程,在Master的8080端口看到所有Worker注册成功,集群就建好了。整个过程并不复杂,但多台机器的网络互通、SSH免密、防火墙放行这些前置条件,经常会让人卡住。我的建议是先把两台机器跑通,再扩展到多台。
真正用到集群之后,你会频繁和spark-submit命令打交道。这个命令是Spark任务提交的入口,后面跟一堆资源参数。
bash复制spark-submit \
--master spark://your-master:7077 \
--deploy-mode cluster \
--executor-memory 4g \
--executor-cores 2 \
--num-executors 10 \
--driver-memory 2g \
--conf spark.default.parallelism=40 \
my_job.py
这些参数看起来很琐碎,但每个都对应着集群里的实际资源分配:
| 参数 | 作用 | 选择建议 |
|---|---|---|
| --master | 指定集群入口地址 | 本地模式用local[*],集群用spark://master:7077 |
| --executor-memory | 每个Executor可用的内存 | 根据数据量和单机资源定,常见2g-8g |
| --executor-cores | 每个Executor占用的CPU核数 | 一般2-4,太少浪费资源,太多容易争抢内存 |
| --num-executors | 总共启动多少个Executor | 总核数除以单Executor核数,留出余量 |
| --driver-memory | Driver进程的内存 | 如果collect的结果很大,需要加大 |
| --conf spark.default.parallelism | 默认分区/并行度 | 建议设为总核心数的2-3倍 |
切记一个原则:参数不是越大越好。Executor内存设置过大会导致GC停顿变长,Executor数量太多会带来网络和调度开销。我自己习惯按“总数据量/单Executor处理能力”先粗估一个值,跑一遍,再通过Spark UI观察每个Stage的耗时,逐步调整。
4. 实战翻车记录:OOM排查、外部数据源和各种经典坑
代码能跑起来只是第一步。实际项目中,你大概率会遇到性能问题、内存溢出、数据倾斜这些“拦路虎”。这一节我不讲理论,全部是实战中遇到的真实案例和排查思路。
4.1 OOM(内存溢出)的常见场景与解决方向
Spark任务报OOM,可能是最常见的故障了。很多人一看到OOM就panic,其实先别急,OOM也分几种场景,定位清楚再动手。
第一种是Driver端OOM。比如你调用了collect()把全量结果拉回Driver,结果集太大,直接把Driver内存打爆。这种情况错误日志里能看到Driver进程的堆栈。解决思路很简单:不要collect全量数据,先用take(n)拉取部分抽样查看;如果确实需要全量结果,考虑把结果写入外部存储(HDFS、对象存储、数据库);或者增加Driver内存。但根因是操作方式不对,不是内存不够。
第二种是Executor端OOM。它又分两种:一种是单个Executor处理的数据量超过Executor内存上限,另一种是内存中缓存太多中间结果。排查时先打开Spark UI,看失败的Stage里每个Task处理的数据量,再看Shuffle Read和Spill(溢写)的数值。如果某个Task处理的数据远大于平均值,很可能遇到了数据倾斜问题。
数据倾斜是Spark性能优化里绕不开的大山。它指的是某个Key对应的数据量特别大,导致绝大多数数据被分到一个Task上,其他Task空闲等待。我遇到过最夸张的一个案例:按用户ID聚合,其中有个“默认用户”的ID在数据里出现了几千万次,对应的Task跑了几十分钟都结束不了,其他Task几秒就跑完了。
解决数据倾斜的常用手段有几种:
- 过滤掉极端Key,比如明显无意义的默认值,先用filter过滤掉再单独处理。
- 给Key加随机前缀,把一个大Key拆成多个小Key,分散到不同Task处理,完了再合并。
- 使用广播变量,把小表广播到每个Executor,避免大表和小表Join时发生严重的数据倾斜。
这些方案没有银弹,得结合业务数据分布来选。我见过不少开发者把Spark调优的宝全押在增加内存上,实际上加大分区数、优化Join策略、合理使用广播变量,效果远比无脑加内存好得多,成本还低。
4.2 外部数据源读写:Redis和数据库适配
生产环境里,Spark经常需要和其他系统打交道。最常遇到的就是读Redis和读写关系型数据库。
先说读取Redis。Redis本身不是给大数据批量计算设计的,直接使用Redis客户端在Spark的map函数里逐条查询会产生严重的性能问题——每条记录都建立一次连接,网络开销直接拖垮任务。更合理的做法是用mapPartitions,在每个分区内复用连接,批量查询。
python复制import redis
def fetch_from_redis(partition):
# 每个分区只建一次连接
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
for key in partition:
value = r.get(key)
if value:
yield (key, value)
rdd.mapPartitions(fetch_from_redis).take(10)
再说关系型数据库。Spark通过JDBC读数据库时,注意两个细节:一是要用分区查询,不然全量数据只被一个Task拉取,速度极慢;二是尽量把过滤条件下推到数据库,只取需要的字段,减少数据传输量。Spark JDBC读写的标准写法如下:
python复制df = spark.read \
.format("jdbc") \
.option("url", "jdbc:mysql://localhost:3306/db") \
.option("dbtable", "users") \
.option("user", "root") \
.option("password", "xxx") \
.option("driver", "com.mysql.cj.jdbc.Driver") \
.option("partitionColumn", "id") \
.option("lowerBound", 1) \
.option("upperBound", 10000000) \
.option("numPartitions", 8) \
.load()
partitionColumn设为自增主键,配合lowerBound、upperBound和numPartitions,Spark就能把查询拆成多个子SQL并行执行。对于支持JDBC的数据库(包括很多国产数据库),适配思路完全一致:把对应的JDBC驱动Jar包放到SPARK_HOME/jars目录下,URL、用户名密码、驱动类名配置正确,就能正常读写。国产数据库通常还会提供适合自身特性的方言配置或适配层,大规模数据集成时需要注意测试。
4.3 Spark面试高频知识点速答
这个系列的面试题,几乎绕不开下面这些概念。我不建议死记硬背,把它们理解成一条逻辑链,面试时可以串起来讲。
为什么Spark比MapReduce快? 核心在于DAG调度减少了不必要的落盘,内存计算尽可能保留中间结果,加上任务级流水线执行和Catalyst优化器的作用。
什么是宽依赖和窄依赖? 窄依赖是父RDD的每个分区最多被子RDD的一个分区使用,比如map、filter;宽依赖是父RDD的每个分区可能被子RDD的多个分区使用,典型就是groupByKey、reduceByKey。宽依赖是Stage划分的边界,也是shuffle发生的时机。
什么是Spark的Shuffle? Shuffle是把数据按Key重新分区落盘的过程。它的代价极高,涉及序列化、磁盘IO和网络传输,是Spark性能优化的核心突破口。能用reduceByKey就不要用groupByKey,因为reduceByKey会在Map端做一次预聚合,减少Shuffle的数据量。
广播变量和累加器的区别是什么? 广播变量是只读的,会把一个变量分发到每个Executor上供多个Task共享,避免每个Task都传递一份大对象;累加器只能做累加操作,通常用于实现自定义的计数器,比如统计日志里某种错误的出现次数。
Spark中的内存管理是怎样的? Spark的内存分为执行内存和存储内存,执行内存用于计算和Shuffle,存储内存用于缓存RDD和DataFrame,两者可以互相借用。常见的参数spark.memory.fraction控制这两块总内存占Executor内存的比例,默认大约0.6。
这些知识不需要死记硬背,你用本地模式跑几个例子,然后打开Spark UI观察Job和Stage,再对照上面这些概念,记忆会非常牢固。
5. 学完基础之后,往哪个方向继续深入
Spark的基础脉络清楚了,下一步往哪里走,取决于你的业务场景和工作方向。我结合目前的行业现状,给你三条路径参考。
5.1 Spark SQL:学会用SQL处理大规模数据
我的建议是尽早把Spark SQL用熟练。不是因为RDD不重要,而是因为SQL这个工具的表达能力太强了,而且几乎不需要学习成本。你只要会写SQL,就能处理大数据。
Spark SQL的底层完全复用Spark的执行引擎,Catalyst优化器会帮你做谓词下推、列裁剪、常量折叠等一系列优化。很多团队的日常数据加工和报表任务,其实都是把Spark SQL跑在一个调度平台上,每天定时执行。如果你能在工作中熟练使用Spark SQL对接各种数据源,很快就能上手。
把DataFrame注册成临时表之后,就可以跑复杂的SQL了。
python复制spark.sql("""
SELECT region,
COUNT(DISTINCT user_id) AS uv,
SUM(order_amount) AS gmv
FROM orders
WHERE dt = '2024-06-01'
GROUP BY region
ORDER BY gmv DESC
""").show()
再往上走,可以学Hive对Spark的支持。现在很多企业的数仓架构是数据存在HDFS或数据湖里,表结构和元数据管理交给Hive Metastore,计算引擎用Spark。这种组合的好处是既能享受Hive成熟的元数据管理,又能用Spark的高性能计算。环境配置好之后,Spark SQL可以直接读取Hive表,跑完再写回数仓。
5.2 从批处理扩展到流式处理
如果你对实时数据处理感兴趣,Spark提供了Structured Streaming模块。它的设计理念非常优雅:把实时数据流当成一张无限增长的虚拟表,你可以用处理静态表的API和SQL去处理流数据。
Structured Streaming底层是基于微批处理的,也就是把持续不断的流数据切成一小批一小批的微批,每个微批本质上还是一个Spark批处理任务。这种设计的优势是能和批处理共用一套代码逻辑,容错机制也复用批处理的机制,保证exactly-once语义(每条数据精确处理一次)。对于大多数秒级到分钟级延迟的场景,Structured Streaming完全够用。
比如消费Kafka里的用户点击事件,做实时去重计数:
python复制df = spark.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "localhost:9092") \
.option("subscribe", "user_logs") \
.load()
query = df.selectExpr("CAST(value AS STRING) AS log") \
.writeStream \
.format("console") \
.outputMode("append") \
.start()
5.3 大模型时代的Spark新场景
最近一两年,AI大模型的热度很高,很多人以为传统的大数据技术是不是没有用武之地了。实际上恰恰相反,AI和大数据的结合越来越紧密。大模型需要海量的高质量训练数据进行清洗、去重、格式化,这一步的数据预处理工作,用Spark做再合适不过了。训练数据量动辄几十TB,单机处理不现实,Spark分布式数据管道的价值就体现出来了。
现在也有些GPU服务器厂商推出了面向AI场景的一体化大数据方案,比如DGX Spark把GPU底座和Spark生态结合起来,让数据预训练、微调、评估等环节能和大数据处理更流畅地衔接。在这些方案里,你是可以继续用Spark做特征工程、做数据管道的。
一个典型的AI数据管道是这样的:原始日志用Spark清洗,把文本清洗、格式转换、质量过滤都做成Spark任务;清洗好的数据写入特征库;特征库供训练框架读取,产出模型;模型推理结果再回流到在线系统。你看,这个闭环里Spark承担的就是“数据物流中心”的角色,这个角色会长期存在,而且越来越重要。
如果你的团队在做LLM相关的工程,可以研究一下Spark配合向量数据库做大规模文本预处理这条路径,很多场景下比传统脚本处理效率高出一个量级。
我自己的成长路径是:先在本地把小数据集跑通,理解RDD和DataFrame的用法;然后搭建一个小型集群,理解分布式调度和资源分配;再在工作中反复处理性能问题,一点点积累调优经验。这个过程中最有价值的习惯是勤看Spark UI,每个Job、每个Stage、每个Task的耗时和记录数都会告诉你问题的方向。希望这篇总结能帮你少走一些弯路,接下来就动手跑起来吧。
