Spark学习笔记
一、Spark核心基础
1.1 什么是Spark
Apache Spark 是一个快速、通用、可扩展的分布式计算引擎,基于Scala语言开发,由加州大学伯克利分校AMP实验室于2010年开源,2013年捐赠给Apache软件基金会,成为顶级开源项目。
核心定位:解决Hadoop MapReduce计算速度慢、迭代计算效率低的问题,提供内存计算能力,支持多种计算场景(批处理、流处理、机器学习、图计算等)。
1.2 Spark与Hadoop的对比
| 对比维度 | Spark | Hadoop MapReduce |
|---|---|---|
| 计算模型 | 内存计算,中间结果可缓存,支持迭代计算 | 磁盘计算,中间结果写入磁盘,不支持迭代优化 |
| 计算速度 | 比MapReduce快10-100倍(内存计算优势) | 速度较慢,受磁盘I/O限制 |
| 适用场景 | 批处理、流处理、机器学习、图计算、交互式查询 | 仅适用于大规模批处理,场景单一 |
| 依赖关系 | 可独立运行,也可依赖Hadoop的HDFS(存储)、YARN(调度) | 依赖Hadoop生态(HDFS存储、YARN调度) |
1.3 Spark核心优势
-
快速:基于内存计算,减少磁盘I/O开销,迭代计算无需重复读写数据。
-
通用:提供统一的API,支持批处理(Spark Core)、流处理(Spark Streaming/Structured Streaming)、机器学习(MLlib)、图计算(GraphX)四大核心模块。
-
可扩展:支持水平扩展,可部署在单机、集群(Standalone、YARN、Mesos、K8s),节点数量可动态调整。
-
易用:支持Scala、Java、Python、R、SQL等多种语言,API简洁,开发效率高。
1.4 Spark运行环境
1.4.1 运行模式分类
-
Local模式(本地模式):单机运行,适用于开发、测试、小规模数据处理(无需集群),核心参数为local[N](N为CPU核心数,local[*]表示使用全部核心)。
-
Standalone模式(独立集群模式):Spark自带的集群调度模式,无需依赖其他组件,部署简单,适用于中小规模集群。
-
YARN模式(Hadoop YARN模式):最常用的模式,依赖Hadoop的YARN作为资源调度器,HDFS作为存储,适用于大规模集群,与Hadoop生态无缝集成。
-
Mesos模式:依赖Apache Mesos作为资源调度器,适用于多框架(Spark、Hadoop、Storm等)共享集群资源的场景。
-
K8s模式(容器化模式):基于Kubernetes部署Spark集群,适用于容器化运维环境,可实现资源的动态调度和弹性伸缩。
1.4.2 核心运行环境配置(以YARN模式为例)
关键配置文件(spark/conf目录):
-
spark-env.sh:配置环境变量(如JAVA_HOME、HADOOP_HOME、YARN_CONF_DIR、Spark集群节点信息)。
-
spark-defaults.conf:配置Spark默认参数(如指定Master为yarn、指定Driver/Executor内存、指定日志存储路径)。
-
slaves(Standalone模式用):配置集群从节点(Worker)的主机名。
二、Spark核心组件与架构
2.1 Spark核心架构组件
Spark集群运行时,分为**Driver(驱动程序)和Executor(执行器)**两大核心角色,配合Cluster Manager(集群管理器)实现资源调度和任务执行。
2.1.1 Driver(驱动节点)
核心职责:
-
负责解析用户提交的代码(如Spark SQL、Scala/Java代码),生成抽象语法树(AST),转化为DAG(有向无环图)任务。
-
对DAG任务进行划分,拆分为多个Stage(阶段),每个Stage包含多个Task(任务)。
-
向Cluster Manager申请集群资源(CPU、内存),并将Task分配给Executor执行。
-
监控Task的执行状态,处理Task失败重试、任务中断等异常情况。
-
收集Executor执行后的结果,汇总并返回给用户。
2.1.2 Executor(执行节点)
核心职责:
-
接收Driver分配的Task,执行具体的计算逻辑(如Map、Reduce、Filter等操作)。
-
负责本地数据的存储和缓存(使用内存或磁盘缓存中间结果,提升迭代计算效率)。
-
将Task执行的中间结果或最终结果反馈给Driver。
-
每个Executor是一个独立的JVM进程,在集群的Worker节点上运行,可并行执行多个Task(Task数量由CPU核心数决定)。
2.1.3 Cluster Manager(集群管理器)
核心职责:负责集群资源(CPU、内存)的分配和管理,协调Driver和Executor之间的通信,不同运行模式对应不同的Cluster Manager:
-
Local模式:内置简单的Cluster Manager,无需单独部署。
-
Standalone模式:Spark自带的Cluster Manager(Master+Worker)。
-
YARN模式:Hadoop YARN的ResourceManager(资源管理)+ NodeManager(节点管理)。
-
K8s模式:Kubernetes的API Server、Scheduler等组件。
2.2 DAG(有向无环图)与Stage划分
2.2.1 DAG概念
Spark将用户提交的计算任务转化为DAG(有向无环图),其中每个节点代表一个RDD(弹性分布式数据集)的转换操作,边代表RDD之间的依赖关系(血缘关系),无循环依赖,确保任务可有序执行。
2.2.2 Stage划分规则
Driver会根据DAG中RDD的依赖关系,将任务划分为多个Stage(阶段),Stage之间是串行执行,Stage内部的Task是并行执行。
划分依据:宽依赖(Shuffle依赖)和窄依赖(Narrow依赖)。
-
窄依赖:一个父RDD的分区只对应一个子RDD的分区(如Map、Filter、MapPartitions),无需Shuffle,可在同一个Stage内完成。
-
宽依赖:一个父RDD的分区对应多个子RDD的分区(如GroupByKey、ReduceByKey、Join),需要进行Shuffle(数据重新分区、网络传输),宽依赖是Stage划分的边界,每个宽依赖对应一个新的Stage。
2.2.3 Shuffle(洗牌)机制
Shuffle是Spark中最耗时的操作,核心是将Executor之间的数据进行重新分区和网络传输,分为两个阶段:
-
Map阶段:Executor执行Task,将计算结果按照指定的分区规则(Hash分区、Range分区等)写入本地磁盘的临时文件。
-
Reduce阶段:每个Reduce Task从多个Map Task的临时文件中读取对应分区的数据,进行合并、计算,最终输出结果。
优化要点:减少Shuffle操作(如用ReduceByKey替代GroupByKey)、调整Shuffle分区数、开启Shuffle缓存等。
2.3 RDD(弹性分布式数据集)核心概念
2.3.1 RDD定义
RDD(Resilient Distributed Dataset)是Spark的核心抽象,代表一个不可变、可分区、可并行计算的分布式数据集,具备以下特性:
-
不可变:RDD创建后无法修改,只能通过转换操作(Transformation)生成新的RDD。
-
可分区:RDD的数据被划分为多个分区(Partition),分布在集群的不同Executor上,分区数量可配置(默认与HDFS块大小一致,128M/256M)。
-
弹性:支持数据的容错(基于血缘关系自动恢复丢失的分区)、动态调整分区数量。
-
可并行:每个分区的计算可独立并行执行,互不干扰。
2.3.2 RDD的创建方式
-
从本地集合创建(适用于Local模式测试):
-
parallelize():将本地集合转化为RDD,可指定分区数。
-
makeRDD():与parallelize()功能类似,支持指定分区的偏好位置(Preferred Locations)。
-
-
从外部存储创建(最常用):
-
textFile():读取文本文件(本地文件、HDFS、S3等),每行作为一个元素,返回RDD[String]。
-
read.csv()/read.json():通过Spark SQL的API读取结构化文件,返回DataFrame(可转换为RDD)。
-
hadoopFile():读取Hadoop支持的文件格式(如SequenceFile、MapFile)。
-
-
从其他RDD转换创建:通过Transformation操作(如Map、Filter)从已有的RDD生成新的RDD。
2.3.3 RDD的两大操作类型
Spark中RDD的操作分为Transformation(转换操作)和Action(行动操作),核心区别:是否触发任务执行(惰性求值)。
(1)Transformation(转换操作)
特性:惰性求值,仅记录RDD之间的依赖关系(血缘关系),不立即执行计算,只有当Action操作被调用时,才会触发整个DAG的执行。
常用转换操作:
-
Map(func):对RDD中的每个元素应用func函数,返回一个新的RDD(一对一映射)。
-
Filter(func):对RDD中的每个元素应用func函数,筛选出返回值为true的元素,返回新的RDD。
-
FlatMap(func):对RDD中的每个元素应用func函数(返回一个迭代器),将迭代器中的元素扁平化,返回新的RDD(一对多映射)。
-
MapPartitions(func):对RDD的每个分区应用func函数(输入是整个分区的迭代器),效率比Map高(减少函数调用次数)。
-
GroupByKey():按照元素的key进行分组,返回(key, 迭代器)形式的RDD(宽依赖,有Shuffle)。
-
ReduceByKey(func):按照元素的key进行分组,对每个组内的元素应用func函数进行聚合(宽依赖,有Shuffle,比GroupByKey高效,因为会在本地先聚合)。
-
SortByKey(ascending=true):按照元素的key进行排序,ascending指定升序/降序。
-
Join(otherRDD, how="inner"):将两个RDD按照key进行连接(inner、left、right、full outer join),宽依赖,有Shuffle。
-
Union(otherRDD):将两个RDD合并,返回一个包含所有元素的新RDD(分区数相加,不去重)。
-
Intersection(otherRDD):求两个RDD的交集,返回新的RDD(去重,有Shuffle)。
(2)Action(行动操作)
特性:立即触发计算,执行整个DAG任务,将结果返回给Driver或写入外部存储。
常用行动操作:
-
count():返回RDD中元素的个数。
-
first():返回RDD中的第一个元素。
-
take(n):返回RDD中的前n个元素。
-
collect():将RDD中的所有元素收集到Driver端(注意:数据量不能太大,否则会导致Driver内存溢出)。
-
reduce(func):对RDD中的元素应用func函数进行聚合,返回一个单一值(如求和、求最大值)。
-
foreach(func):对RDD中的每个元素应用func函数,无返回值(如写入外部存储)。
-
saveAsTextFile(path):将RDD中的元素写入文本文件(本地路径、HDFS等),每个分区对应一个文件。
-
saveAsSequenceFile(path):将RDD中的元素写入SequenceFile(Hadoop支持的二进制文件格式)。
2.3.4 RDD的缓存与持久化
核心目的:当RDD需要被多次使用(如迭代计算、多次Action操作)时,将其缓存到内存或磁盘,避免重复计算,提升效率。
(1)缓存API
-
cache():默认将RDD缓存到内存(等价于persist(StorageLevel.MEMORY_ONLY))。
-
persist(storageLevel):手动指定存储级别,支持多种存储策略:
-
MEMORY_ONLY:仅缓存到内存,内存不足时,不缓存,下次使用重新计算(默认)。
-
MEMORY_AND_DISK:优先缓存到内存,内存不足时,将剩余数据缓存到磁盘。
-
DISK_ONLY:仅缓存到磁盘。
-
MEMORY_ONLY_SER:将RDD元素序列化后缓存到内存(节省内存,序列化有开销)。
-
MEMORY_AND_DISK_SER:序列化后缓存到内存和磁盘。
-
-
unpersist():清除RDD的缓存(释放内存/磁盘资源)。
(2)注意事项
-
缓存是惰性的,只有当第一个Action操作触发时,才会将RDD缓存到指定存储介质。
-
缓存的RDD会保留血缘关系,当缓存数据丢失(如内存溢出、节点故障)时,可通过血缘关系重新计算恢复。
-
避免缓存不必要的RDD,否则会浪费资源;对于一次性使用的RDD,无需缓存。
2.3.5 RDD的血缘关系与容错机制
-
血缘关系(Lineage):RDD之间的依赖关系构成血缘关系,每个RDD都记录了自己的父RDD和转换操作,形成一个血缘链条。
-
容错机制:基于血缘关系实现容错,当某个RDD的分区丢失(如Executor节点故障)时,Spark无需重新计算整个RDD,只需根据血缘关系,重新计算丢失的分区即可,大大提升容错效率。
三、Spark SQL核心知识
3.1 Spark SQL简介
Spark SQL是Spark用于处理结构化数据的模块,基于Spark Core构建,支持SQL查询和DataFrame/Dataset API,可与Spark的其他模块(如Spark Streaming、MLlib)无缝集成。
核心优势:
-
统一的API:支持SQL和DataFrame/Dataset两种方式操作结构化数据,灵活易用。
-
优化的执行计划:内置Catalyst优化器,可对SQL语句和DataFrame操作进行优化(如谓词下推、列裁剪、Join优化),提升执行效率。
-
兼容多种数据源:支持读取HDFS、Hive、MySQL、PostgreSQL、JSON、CSV等多种结构化数据源,也可写入多种存储。
-
与Hive兼容:可直接访问Hive的元数据(Metastore),执行Hive SQL语句,无需修改现有Hive数据。
3.2 DataFrame与Dataset
3.2.1 DataFrame
DataFrame是一个分布式的表格型数据结构,包含行和列,每个列有名称和数据类型(Schema),类似于关系型数据库的表,也类似于Pandas的DataFrame,但分布式存储在集群中。
特点:
-
有Schema(结构信息):明确每个列的名称和数据类型(如String、Int、Double),便于优化和类型检查。
-
不可变:与RDD一样,DataFrame创建后无法修改,只能通过转换操作生成新的DataFrame。
-
支持多种操作:可通过SQL语句操作,也可通过DataFrame API(如select、filter、groupBy)操作。
-
底层基于RDD:DataFrame本质上是一个包含Schema信息的RDD[Row](Row表示一行数据)。
3.2.2 Dataset
Dataset是Spark 1.6版本引入的新抽象,结合了RDD的强类型 和DataFrame的结构化优势,支持编译时类型检查,比DataFrame更安全、更灵活。
特点:
-
强类型:Dataset中的元素是特定的Java/Scala对象(如Case Class),编译时可检查类型错误,避免运行时类型异常。
-
有Schema:与DataFrame一样,包含Schema信息,支持结构化查询。
-
兼容RDD和DataFrame:可与RDD、DataFrame相互转换(Dataset <-> DataFrame <-> RDD)。
-
支持两种API:Typed API(针对强类型对象的操作,如map、filter)和Untyped API(与DataFrame API一致,如select、groupBy)。
3.2.3 RDD、DataFrame、Dataset对比
| 对比维度 | RDD | DataFrame | Dataset |
|---|---|---|---|
| 类型安全 | 强类型(编译时检查) | 弱类型(运行时检查) | 强类型(编译时检查) |
| Schema信息 | 无Schema(仅存储数据) | 有Schema(结构化) | 有Schema(结构化) |
| 优化支持 | 无优化(依赖用户手动优化) | 支持Catalyst优化 | 支持Catalyst优化 |
| API灵活性 | 高(支持任意复杂操作) | 中(结构化操作为主) | 高(结合RDD和DataFrame优势) |
| 适用场景 | 非结构化/半结构化数据、复杂计算 | 结构化数据、SQL查询、快速分析 | 结构化数据、强类型需求、复杂业务逻辑 |
3.2.4 三者相互转换
-
RDD → DataFrame:
-
通过toDF()方法(需导入spark.implicits._),自动推断Schema(或手动指定Schema)。
-
示例:rdd.toDF("name", "age")
-
-
DataFrame → RDD:
-
通过rdd属性,返回RDD[Row]。
-
示例:df.rdd
-
-
DataFrame → Dataset:
-
通过as[CaseClass]方法,指定强类型对象。
-
示例:df.as[Person](Person为Case Class)
-
-
Dataset → DataFrame:
-
通过toDF()方法,或使用selectExpr指定列名。
-
示例:ds.toDF()
-
-
Dataset → RDD:
-
通过rdd属性,返回RDD[CaseClass]。
-
示例:ds.rdd
-
-
RDD → Dataset:
-
先将RDD转换为DataFrame,再转换为Dataset;或直接通过toDS()方法(需导入spark.implicits._)。
-
示例:rdd.map(x => Person(x._1, x._2)).toDS()
-
3.3 Spark SQL实操要点
3.3.1 SparkSession(入口对象)
Spark SQL的入口对象是SparkSession,替代了Spark 1.x版本中的SQLContext和HiveContext,负责创建DataFrame、执行SQL查询、访问数据源等。
创建SparkSession示例(Scala):
scala
import org.apache.spark.sql.SparkSession
object SparkSQLDemo {
def main(args: Array[String]): Unit = {
// 创建SparkSession
val spark = SparkSession.builder()
.appName("SparkSQLDemo") // 应用名称
.master("local[*]") // 运行模式(Local模式)
.enableHiveSupport() // 开启Hive支持(可选)
.getOrCreate() // 获取或创建SparkSession
// 导入隐式转换(用于RDD→DataFrame等操作)
import spark.implicits._
// 执行SQL查询、操作DataFrame/Dataset...
// 关闭SparkSession
spark.stop()
}
}
3.3.2 读取结构化数据源
Spark SQL支持读取多种结构化数据源,常用API:
-
读取CSV文件:spark.read.csv("path").toDF("col1", "col2"),可指定分隔符、是否有表头(header=true)、数据类型(inferSchema=true)。
-
读取JSON文件:spark.read.json("path"),自动推断Schema。
-
读取Hive表:spark.sql("select * from hive_db.hive_table"),需开启enableHiveSupport()。
-
读取JDBC数据库(MySQL/PostgreSQL):
spark.read.format("jdbc") .option("url", "jdbc:mysql://host:port/dbname") .option("dbtable", "table_name") .option("user", "username") .option("password", "password") .load()
3.3.3 执行SQL查询
Spark SQL支持两种SQL查询方式:
-
直接执行SQL语句:通过spark.sql("sql语句"),返回DataFrame。
- 示例:val df = spark.sql("select name, age from user where age > 18")
-
创建临时视图(Temp View)后查询:
-
createTempView("view_name"):创建会话级临时视图,仅当前SparkSession有效,会话关闭后视图消失。
-
createGlobalTempView("view_name"):创建全局临时视图,所有SparkSession共享,查询时需加global_temp前缀(如select * from global_temp.view_name)。
-
示例:df.createTempView("user_view"); spark.sql("select * from user_view")
-
3.3.4 写入结构化数据源
常用写入API:df.write.format("格式").option("参数").save("路径")
-
写入CSV/JSON文件:df.write.csv("path")、df.write.json("path"),可指定mode(overwrite覆盖、append追加、ignore忽略、errorifexists报错)。
-
写入Hive表:df.write.mode("overwrite").saveAsTable("hive_db.hive_table")。
-
写入JDBC数据库:
df.write.format("jdbc") .option("url", "jdbc:mysql://host:port/dbname") .option("dbtable", "table_name") .option("user", "username") .option("password", "password") .mode("append") .save()
四、Spark Streaming核心知识
4.1 Spark Streaming简介
Spark Streaming是Spark用于处理实时流数据的模块,基于Spark Core构建,采用"微批处理"(Micro-Batching)机制,将实时流数据切分为一系列小的批处理任务,批量处理数据,实现近实时计算(延迟通常在秒级)。
核心优势:
-
易用性:API与Spark Core/RDD高度一致,熟悉RDD操作即可快速上手。
-
容错性:基于RDD的血缘关系实现容错,流数据的每个微批都对应一个RDD,故障时可重新计算。
-
可扩展性:与Spark集群无缝集成,支持水平扩展,可处理大规模流数据。
-
集成性:可与Spark SQL、MLlib、GraphX等模块集成,实现流数据的实时分析、机器学习等场景。
注意:Spark Streaming是"微批处理",并非真正的实时流处理(如Flink的流式处理),适用于延迟要求不高(秒级)的场景;若需要毫秒级延迟,建议使用Flink。
4.2 Spark Streaming核心概念
4.2.1 DStream(离散流)
DStream(Discretized Stream)是Spark Streaming的核心抽象,代表一个连续的流数据序列,本质上是一系列连续的RDD(每个RDD对应一个微批的数据)。
特性:
-
不可变:与RDD一样,DStream创建后无法修改,只能通过转换操作生成新的DStream。
-
微批构成:每个DStream由多个微批的RDD组成,每个RDD包含一个时间间隔(Batch Interval)内的数据。
-
操作类型:支持转换操作(Transformation)和输出操作(Output Operation),与RDD的操作类似。
4.2.2 Batch Interval(批处理间隔)
核心参数,指定将流数据切分为微批的时间间隔(如1秒、5秒),决定了Spark Streaming的延迟时间(延迟 ≈ 批处理间隔)。
注意:批处理间隔的设置需结合集群性能和业务需求,间隔太小(如100ms)会导致任务过多,集群压力大;间隔太大(如1分钟)会导致延迟过高。
4.2.3 StreamingContext(流处理入口)
StreamingContext是Spark Streaming的入口对象,负责创建DStream、启动流处理、停止流处理,基于SparkContext创建。
创建StreamingContext示例(Scala):
scala
import org.apache.spark.streaming.{StreamingContext, Seconds}
import org.apache.spark.SparkConf
object SparkStreamingDemo {
def main(args: Array[String]): Unit = {
// 1. 创建SparkConf
val conf = new SparkConf().setAppName("SparkStreamingDemo").setMaster("local[*]")
// 2. 创建StreamingContext,指定批处理间隔为1秒
val ssc = new StreamingContext(conf, Seconds(1))
// 3. 创建DStream、执行操作...
// 4. 启动流处理(开始接收数据并处理)
ssc.start()
// 5. 等待流处理停止(阻塞当前线程,直到手动停止或发生异常)
ssc.awaitTermination()
// 6. 停止流处理(可选,手动停止)
ssc.stop()
}
}
4.3 DStream的操作
4.3.1 转换操作(Transformation)
与RDD的转换操作类似,分为无状态转换 和有状态转换。
(1)无状态转换
每个微批的处理独立于其他微批,不依赖历史数据,常用操作:
-
map(func):对DStream中的每个元素应用func函数,返回新的DStream。
-
filter(func):筛选出符合条件的元素,返回新的DStream。
-
flatMap(func):扁平化处理,与RDD的flatMap一致。
-
reduceByKey(func):按照key聚合,每个微批内独立聚合(不依赖历史微批的数据)。
-
join(otherDStream):将两个DStream按照key连接,每个微批内独立连接。
(2)有状态转换
处理当前微批时,依赖历史微批的数据(如累计计数、滑动窗口统计),常用操作:
-
updateStateByKey(func):维护每个key的状态,根据当前微批的数据和历史状态,更新key的新状态(如累计求和、计数)。
-
window(windowDuration, slideDuration):创建滑动窗口DStream,窗口时长(windowDuration)指定窗口包含的微批数量,滑动步长(slideDuration)指定窗口滑动的微批数量(如窗口时长10秒,滑动步长5秒,即每5秒统计一次过去10秒的数据)。
-
reduceByKeyAndWindow(func, windowDuration, slideDuration):在滑动窗口内,按照key聚合数据。
注意:使用有状态转换时,需设置检查点(Checkpoint),用于保存历史状态数据,防止节点故障导致状态丢失。
4.3.2 输出操作(Output Operation)
触发DStream的计算,将处理结果输出到外部存储或控制台,常用操作:
-
print():将每个微批的前10条数据打印到控制台(适用于测试)。
-
foreachRDD(func):对DStream中的每个RDD应用func函数,可将数据写入外部存储(如HDFS、MySQL、Kafka)。
-
saveAsTextFiles(prefix, suffix):将每个微批的RDD写入文本文件,文件名由前缀、时间戳、后缀组成。
4.4 数据源接入
Spark Streaming支持接入多种实时数据源,分为基础数据源 和高级数据源。
4.4.1 基础数据源
-
文件数据源:监控指定目录,当有新文件写入时,读取文件数据作为微批(支持HDFS、本地文件等),示例:ssc.textFileStream("hdfs://path")。
-
Socket数据源:从指定的Socket端口接收数据(适用于测试),示例:ssc.socketTextStream("host", port)。
-
RDD队列数据源:将本地RDD队列作为数据源,适用于测试,示例:ssc.queueStream(rddQueue)。
4.4.2 高级数据源
需导入对应的依赖包(如Kafka、Flume),常用:
-
Kafka数据源(最常用):通过Spark Streaming的Kafka集成包,接收Kafka主题中的数据,支持两种集成方式:Receiver-based(旧版本,效率低)和Direct Stream(新版本,效率高,推荐)。
-
Flume数据源:接收Flume采集的日志数据,支持推模式(Flume push to Spark)和拉模式(Spark pull from Flume)。
-
Kinesis数据源:接收Amazon Kinesis的流数据。
4.5 检查点(Checkpoint)机制
4.5.1 核心作用
-
保存状态数据:用于有状态转换(如updateStateByKey、window),保存历史状态,防止节点故障导致状态丢失。
-
恢复Driver:当Driver节点故障时,可从检查点中恢复Driver的状态,重启流处理任务,无需重新启动。
4.5.2 配置检查点
通过StreamingContext的checkpoint()方法指定检查点存储路径(本地路径或HDFS路径),示例:
scala
// 设置检查点路径(HDFS路径,推荐)
ssc.checkpoint("hdfs://path/to/checkpoint")
注意:检查点路径需是可靠的存储(如HDFS),本地路径仅适用于Local模式测试,集群模式下会导致节点故障后无法恢复。
五、Spark常见问题与优化
5.1 常见问题及解决方案
5.1.1 Driver内存溢出(OOM)
原因:Driver内存不足,常见于collect()操作读取大量数据、广播变量过大、任务过多导致Driver负载过高。
解决方案:
-
避免使用collect()操作读取大量数据,改用take()、saveAsTextFile()等操作。
-
调整Driver内存:通过--driver-memory参数指定(如spark-submit --driver-memory 4g)。
-
优化广播变量:减少广播变量的大小,避免广播大量数据。
5.1.2 Executor内存溢出(OOM)
原因:Executor内存不足,常见于缓存大量RDD、Shuffle数据量过大、单个Task处理的数据过多。
解决方案:
-
调整Executor内存:通过--executor-memory参数指定(如spark-submit --executor-memory 8g)。
-
优化缓存策略:减少不必要的RDD缓存,使用MEMORY_AND_DISK存储级别,避免仅缓存到内存。
-
调整Shuffle分区数:增加Shuffle分区数(--conf spark.sql.shuffle.partitions=200),减少单个分区的数据量。
-
优化Task粒度:增加Task数量,使每个Task处理的数据量适中(建议每个Task处理128M-256M数据)。
5.1.3 Shuffle性能低下
原因:Shuffle操作涉及网络传输和磁盘I/O,是Spark中最耗时的操作,常见于分区数不合理、数据倾斜、序列化效率低。
解决方案:
-
调整Shuffle分区数:根据集群CPU核心数和数据量调整,一般设置为CPU核心数的2-3倍(如100-200个分区)。
-
避免不必要的Shuffle:用ReduceByKey替代GroupByKey,用Broadcast Join替代普通Join(小表Join大表时)。
-
解决数据倾斜:
-
对倾斜的key进行拆分(如加盐),分散到多个分区。
-
使用随机前缀,将倾斜的key分散到不同的Reduce Task。
-
过滤掉异常倾斜的key(如无效数据)。
-
-
优化序列化:使用Kyro序列化替代Java序列化(--conf spark.serializer=org.apache.spark.serializer.KryoSerializer),减少数据传输量和内存占用。
5.1.4 任务执行缓慢
原因:CPU核心不足、内存分配不合理、数据倾斜、Task数量过少。
解决方案:
-
增加集群节点或CPU核心数,提升并行计算能力。
-
合理分配内存:区分Executor的堆内存和堆外内存,避免内存浪费。
-
解决数据倾斜(参考上面的Shuffle优化)。
-
增加Task数量,提升并行度(Task数量建议大于等于集群总CPU核心数)。
5.2 核心优化策略
5.2.1 资源优化
-
Driver优化:合理设置Driver内存(根据任务复杂度),避免Driver成为瓶颈。
-
Executor优化:
-
设置合适的Executor数量(--num-executors),一般为集群节点数的2-3倍。
-
设置合适的Executor内存(--executor-memory)和CPU核心数(--executor-cores),避免单Executor占用过多资源。
-
-
内存优化:开启堆外内存(--conf spark.executor.memoryOverhead),用于存储序列化数据、Shuffle临时数据,避免堆内存溢出。
5.2.2 计算优化
-
减少Shuffle操作:优先使用窄依赖操作(如Map、Filter),避免不必要的宽依赖(如GroupByKey、Join),从源头减少网络传输和磁盘I/O开销,这是计算优化的核心前提(与5.1.3 Shuffle性能低下的优化逻辑呼应)。
-
优化Task粒度:合理设置Task数量,一般建议Task数量为集群总CPU核心数的2-3倍,确保每个Task处理的数据量控制在128M-256M之间。既避免Task过大导致单个Executor内存压力激增,也防止Task过小造成集群任务调度开销过高、资源利用率不足。
-
使用高效的算子:优先选择高性能算子替代低效算子,降低计算开销。例如用ReduceByKey替代GroupByKey(ReduceByKey支持本地预聚合,可大幅减少Shuffle阶段的数据量);简单筛选后映射场景,可用filter+map替代flatMap提升效率;去重操作可通过groupByKey+first()自定义实现,减少distinct算子的冗余计算。
-
优化数据格式:采用高效存储和序列化格式,进一步降低I/O和内存消耗。存储层面,用Parquet、ORC等列式存储格式替代文本格式,可减少不必要的字段读取,提升磁盘读取效率;序列化层面,推荐使用Kyro序列化替代默认的Java序列化,序列化效率可提升30%-50%,同时大幅减少内存占用和网络传输的数据量。
-
避免数据倾斜:从计算层面进一步规避倾斜问题。可通过数据预处理过滤无效倾斜key、大表与大表Join时采用分桶Join减少Shuffle压力、根据数据实时分布动态调整分区数等方式,确保计算任务均匀分配到各个Executor,避免单个Task长期阻塞拖慢整体任务进度。
-
利用惰性求值特性:充分发挥Spark惰性求值的优势,合理规划Transformation和Action操作的执行顺序。将过滤、列裁剪等轻量级操作前置,提前筛选无效数据、剔除不必要字段,减少后续计算的数据量;同时避免频繁调用Action操作,尽量将多个关联的Action操作合并执行,减少DAG的重复构建和计算,提升整体执行效率。