大数据-90 Spark RDD容错机制:Checkpoint原理、场景与最佳实践 容错机制详解

点一下关注吧!!!非常感谢!!持续更新!!!

🚀 AI篇持续更新中!(长期更新)

AI炼丹日志-31- 千呼万唤始出来 GPT-5 发布!"快的模型 + 深度思考模型 + 实时路由",持续打造实用AI工具指南!📐🤖

💻 Java篇正式开启!(300篇)

目前2025年09月01日更新到: Java-113 深入浅出 MySQL 扩容全攻略:触发条件、迁移方案与性能优化 MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务正在更新!深入浅出助你打牢基础!

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈! 大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解

AI 辅助调查报告

章节内容

上节完成的内容如下:

  • Spark RDD的依赖关系
  • 重回 WordCount
  • RDD 持久化
  • RDD 缓存

RDD容错机制

基本概念

检查点(Checkpoint)机制详解

检查点是Spark中一种重要的容错机制,它通过将RDD数据持久化到可靠的分布式文件系统(如HDFS)来实现容错功能。与普通的持久化操作相比,检查点具有以下特点:

工作原理
  1. 执行过程:

    • 当对RDD调用checkpoint()方法时,Spark会向该RDD注册检查点标记
    • 在后续的Action操作触发时,Spark会启动专门的作业来执行检查点操作
    • 检查点数据会被写入配置的检查点目录(通常是HDFS路径)
  2. 与持久化的区别:

    • 持久化(Persist/Cache):
      • 存储级别:内存/磁盘
      • 数据位置:Executor本地
      • 生命周期:应用程序结束后自动清除
      • 依赖链:保留完整Lineage
    • 检查点(Checkpoint):
      • 存储级别:HDFS等可靠存储
      • 数据位置:分布式文件系统
      • 生命周期:需要手动清理
      • 依赖链:会被截断
典型应用场景
  1. 迭代计算:在机器学习算法中,迭代次数可能很多,使用检查点可以定期保存中间结果
    • 示例:在PageRank算法中,每迭代10次做一次checkpoint
  2. 复杂转换:当RDD经过大量转换操作导致Lineage过长时
    • 示例:一个RDD经过100次map操作后,建议添加checkpoint
  3. 关键数据:对计算结果可靠性要求高的场景
配置和使用
  1. 设置检查点目录:
scala 复制代码
   sc.setCheckpointDir("hdfs://namenode:8020/checkpoint")
  1. 执行检查点:
scala 复制代码
   val rdd = sc.textFile("hdfs://...")
   rdd.checkpoint()
   rdd.count() // 触发检查点执行
性能考量
  1. 优点:
    • 提供可靠的故障恢复能力
    • 可以截断过长的Lineage
    • 适用于长期运行的Spark应用
  2. 缺点:
    • 写入HDFS会带来额外的I/O开销
    • 需要额外的存储空间
    • 不是实时生效,需要等待作业完成
最佳实践
  1. 对于需要重复使用的RDD,建议先persist()再checkpoint()
  2. 在宽依赖(shuffle)之后添加checkpoint效果更好
  3. 合理设置检查点间隔,避免过于频繁的检查点操作影响性能

通过合理使用检查点机制,可以在保证数据可靠性的同时,提高Spark应用的容错能力和执行效率。

适合场景

  • DAG中的Lineage过长,如果重新计算,开销会很大
  • 在宽依赖上做checkpoint获得的收益更大

启动Shell

shell 复制代码
# 启动 spark-shell
spark-shell --master local[*]

checkpoint

scala 复制代码
// 设置检查点目录
sc.setCheckpointDir("/tmp/checkpoint")

val rdd1 = sc.parallelize(1 to 1000)
val rdd2 = rdd1.map(_*2)
rdd2.checkpoint
// checkpoint是lazy操作
rdd2.isCheckpointed

可以发现,返回结果是False

RDD 依赖关系1

checkpoint之前的rdd依赖关系

  • rdd2.dependencies(0).rdd
  • rdd2.dependencies(0).rdd.collect

我们可以观察到,依赖关系是有的,关系到之前的 rdd1 的数据了:

触发checkpoint

我们可以通过执行 Action 的方式,来触发 checkpoint 执行一次action,触发checkpoint的执行

  • rdd2.count
  • rdd2.isCheckpointed

此时观察,可以发现 checkpoint 已经是 True 了:

RDD依赖关系2

我们再次观察RDD的依赖关系: 再次查看RDD的依赖关系。可以看到checkpoint后,RDD的lineage被截断,变成从checkpointRDD开始

  • rdd2.dependencies(0).rdd
  • rdd2.dependencies(0).rdd.collect

此时观察到,已经不是最开始的 rdd1 了:

查看checkpoint

我们可以查看对应的保存的文件,查看RDD所依赖的checkpoint文件

  • rdd2.getCheckpointFile 运行的结果如下图:

RDD的分区

基本概念

shell 复制代码
spark.default.paralleism: 默认的并发数 2

本地模式

shell 复制代码
# 此时 spark.default.paralleism 为 N
spark-shell --master local[N]
# 此时 spark.default.paralleism 为 1
spark-shell --master local

伪分布式

  • x为本机上启动的Executor数
  • y为每个Executor使用的core数
  • z为每个Executor使用的内存
  • spark.default.paralleism 为 x * y
shell 复制代码
spark-shell --master local-cluster[x,y,z]

分布式模式

shell 复制代码
spark.default.paralleism = max(应用程序持有Executor的core总数, 2)

创建RDD方式

集合创建

简单的说,RDD分区数等于cores总数

scala 复制代码
val rdd1 = sc.paralleize(1 to 100)
rdd.getNumPartitions

textFile创建

如果没有指定分区数:

  • 本地文件: rdd的分区数 = max(本地文件分片数,sc.defaultMinPartitions)
  • HDFS文件:rdd的分区数 = max(HDFS文件block数,sc.defaultMinPartitions)

需要额外注意的是:

  • 本地文件分片数 = 本地文件大小 / 32M
  • 读取 HDFS 文件,同时指定了分区数 < HDFS文件的Block数,指定的数将不会生效
scala 复制代码
val rdd = sc.textFile("data/1.txt")
rdd.getNumPartitions

RDD分区器

判断分区器

以下RDD分别是否有分区器,是什么类型的分区器

scala 复制代码
val rdd1 = sc.textFile("/wcinput/wc.txt")
rdd1.partitioner

val rdd2 = sc.flatMap(_.split("\\s+"))
rdd2.partitioner

val rdd3 = rdd2.map((_, 1))
rdd3.partitioner

val rdd4 = rdd3.reduceByKey(_ + _)
rdd4.partitioner

val rdd5 = rdd4.sortByKey()
rdd5.partitioner

分区器作用与分类

在PairRDD(key,value)中,很多操作都是基于Key的,系统会按照Key对数据进行重组,如 GroupByKey 数据重组需要规则,最常见的就是基于Hash的分区,此外还有一种复杂的基于抽样Range分区方法:

HashPartitioner

最简单、最常用,也是默认提供的分区器。 对于给定的Key,计算HashCode,并除以分区的个数取余,如果余数小于0,则用余数+分区的个数,最后返回的值就是这个Key所属的分区ID。 该分区方法可以保证Key相同的数据出现在同一个分区中。 用户可以通过 partitionBy主动使用分区器,通过 partitions参数指定想要分区的数量。

默认情况下的分区情况是:

scala 复制代码
val rdd1 = sc.makeRDD(1 to 100).map((_, 1))
rdd1.getNumPartitions

执行结果如下图所示: 执行结果如下图所示,分区已经让我们手动控制成10个了:

scala 复制代码
val rdd2 = rdd1.partitionBy(new org.apache.spark.HashPartitioner(10))
rdd2.getNumPartitions
rdd2.glom.collect.foreach(x => println(x.toBuffer))

RangePartitioner

简单来说就是将一定范围内的数映射到某个分区内,在实现中,分界的算法尤为重要,用到了水塘抽样算法。sortByKey会使用RangePartitioner。 进行代码的测试:

scala 复制代码
val rdd3 = rdd1.partitionBy(new org.apache.spark.RangePartitioner(10, rdd1))
rdd3.glom.collect.foreach(x => println(x.toBuffer))

执行结果如下图所示: 但是现在的问题是:在执行分区之前其实并不知道数据的分布情况,如果想知道数据的分区就需要对数据进行采样。

  • Spark中的RangePartitioner在对数据采样的过程中使用了 "水塘采样法"
  • 水塘采样法是:在包含N个项目的集合S中选取K个样本,其中N为1或者很大的未知的数量,尤其适用于不能把所有N个项目都存放到主内存的情况。
  • 在采样过程中执行了 collect() 操作,引发了 Action 操作。

自定义分区器

Spark允许用户通过自定义的Partitioner对象,灵活的来控制RDD的分区方式。 我们需要实现自定义分区器,按照以下的规则进行分区:

  • 分区 0 < 100
  • 100 <= 分区1 < 200
  • 200 <= 分区2 < 300
  • 300 <= 分区3 < 400
  • .......
  • 900 <= 分区9 < 1000

编写代码

scala 复制代码
package icu.wzk

import org.apache.spark.rdd.RDD
import org.apache.spark.{Partitioner, SparkConf, SparkContext}

import scala.collection.immutable


class MyPartitioner(n: Int) extends Partitioner {

  override def numPartitions: Int = n

  override def getPartition(key: Any): Int = {
    val k = key.toString.toInt
    k / 100
  }
}

object UserDefinedPartitioner {

  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
      .setAppName("UserDefinedPartitioner")
      .setMaster("local[*]")
    val sc = new SparkContext(conf)
    sc.setLogLevel("WARN")

    val random = scala.util.Random
    val arr: immutable.IndexedSeq[Int] = (1 to  100)
      .map(idx => random.nextInt(1000))

    val rdd1: RDD[(Int, Int)] = sc.makeRDD(arr).map((_, 1))
    rdd1.glom.collect.foreach(x => println(x.toBuffer))

    println("=========================================")

    val rdd2 = rdd1.partitionBy(new MyPartitioner(10))
    rdd2.glom.collect().foreach(x => println(x.toBuffer))
    
    sc.stop()
    
  }

}

打包上传

这里之前已经重复过多次,就跳过了

shell 复制代码
mvn clean package

运行测试

shell 复制代码
spark-submit --master local[*] --class icu.wzk.UserDefinedPartitioner spark-wordcount-1.0-SNAPSHOT.jar

可以看到如下的运行结果:

相关推荐
花花无缺6 小时前
python自动化-pytest-标记
后端·python
恒州博智QYResearch咨询6 小时前
全球汽车氮化镓技术市场规模将于2031年增长至180.5亿美元,2025-2031年复合增长率达94.3%,由Infineon和Navitas驱动
大数据·汽车
Villiam_AY6 小时前
使用 chromedp 高效爬取 Bing 搜索结果
后端·爬虫·golang
CryptoPP6 小时前
跨境金融数据对接实践:印度NSE/BSE股票行情API集成指南
开发语言·后端·金融
陈敬雷-充电了么-CEO兼CTO6 小时前
具身智能模拟器:解决机器人实机训练场景局限与成本问题的创新方案
大数据·人工智能·机器学习·chatgpt·机器人·具身智能
程序员爱钓鱼6 小时前
Go语言实战案例-实现简易定时提醒程序
后端·google·go
TDengine (老段)7 小时前
TDengine 时间函数 TIMETRUNCATE 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
堕落年代7 小时前
Spring Boot HTTP状态码详解
spring boot·后端·http
Victor3567 小时前
Redis(49)Redis哨兵如何实现故障检测和转移?
后端