Spark RDD持久化、广播变量和累加器

本文深入解析Spark中RDD持久化机制及共享变量的使用,包括缓存策略选择、性能优化技巧,以及广播变量和累加器的应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spark RDD持久化

RDD持久化工作原理

Spark非常重要的一个功能特性就是可以将RDD持久化在内存中。当对RDD执行持久化操作时,每个节点都会将自己操作的RDD的partition持久化到内存中,并且在之后对该RDD的反复使用中,直接使用内存缓存的partition。这样的话,对于针对一个RDD反复执行多个操作的场景,就只要对RDD计算一次即可,后面直接使用该RDD,而不需要反复计算多次该RDD。

巧妙使用RDD持久化,甚至在某些场景下,可以将spark应用程序的性能提升10倍。对于迭代式算法和快速交互式应用来说,RDD持久化,是非常重要的。

要持久化一个RDD,只要调用其cache()或者persist()方法即可。在该RDD第一次被计算出来时,就会直接缓存在每个节点中。而且Spark的持久化机制还是自动容错的,如果持久化的RDD的任何partition丢失了,那么Spark会自动通过其源RDD,使用transformation操作重新计算该partition。

cache()和persist()的区别在于,cache()是persist()的一种简化方式,cache()的底层就是调用的persist()的无参版本,同时就是调用persist(MEMORY_ONLY),将数据持久化到内存中。如果需要从内存中去除缓存,那么可以使用unpersist()方法。

RDD持久化使用场景

1、第一次加载大量的数据到RDD中

2、频繁的动态更新RDD Cache数据,不适合使用Spark Cache、Spark lineage

RDD持久化策略

Spark笔记整理(五):Spark RDD持久化、广播变量和累加器

持久化策略的选择

​ 默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。

​ 如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。

​ 如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。(这种最方便,不需要提前评估内存是否够用)

​ 通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。

测试案例

测试代码如下:

package cn.xpleaf.bigdata.spark.scala.core.p3

import org.apache.log4j.{Level, Logger}
import org.apache.spark.storage.StorageLevel
import org.apache.spark.{SparkConf, SparkContext}

/**
  * Spark RDD的持久化
  */
object _01SparkPersistOps {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setMaster("local[2]").setAppName(_01SparkPersistOps.getClass.getSimpleName())
        val sc = new SparkContext(conf)
        Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
        Logger.getLogger("org.apache.hadoop").setLevel(Level.OFF)

        var start = System.currentTimeMillis()
        val linesRDD = sc.textFile("D:/data/spark/sequences.txt")
        // linesRDD.cache()
        // linesRDD.persist(StorageLevel.MEMORY_ONLY)

        // 执行第一次RDD的计算
        val retRDD = linesRDD.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _)
        // retRDD.cache()
        // retRDD.persist(StorageLevel.DISK_ONLY)
        retRDD.count()
        println("第一次计算消耗的时间:" + (System.currentTimeMillis() - start) + "ms")

        // 执行第二次RDD的计算
        start = System.currentTimeMillis()
        // linesRDD.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_ + _).count()
        retRDD.count()
        println("第二次计算消耗的时间:" + (System.currentTimeMillis() - start) + "ms")

        // 持久化使用结束之后,要想卸载数据
        // linesRDD.unpersist()

        sc.stop()

    }
}

设置相关的持久化策略,再观察执行时间就可以有一个较为直观的理解。

共享变量

提供了两种有限类型的共享变量,广播变量和累加器。

广播变量是广播只读变量,着眼于读

累加器是全局写变量,着眼于写

介绍之前,先直接看下面一个例子:

package cn.xpleaf.bigdata.spark.scala.core.p3

import org.apache.log4j.{Level, Logger}
import org.apache.spark.{SparkConf, SparkContext}

/**
  * 共享变量
  *     我们在dirver中声明的这些局部变量或者成员变量,可以直接在transformation中使用,
  *     但是经过transformation操作之后,是不会将最终的结果重新赋值给dirver中的对应的变量。
  *     因为通过action,触发了transformation的操作,transformation的操作,都是通过
  *     DAGScheduler将代码打包 序列化 交由TaskScheduler传送到各个Worker节点中的Executor去执行,
  *     在transformation中执行的这些变量,是自己节点上的变量,不是dirver上最初的变量,我们只不过是将
  *     driver上的对应的变量拷贝了一份而已。
  *
  *
  *     这个案例也反映出,我们需要有一些操作对应的变量,在driver和executor上面共享
  *
  *     spark给我们提供了两种解决方案——两种共享变量
  *         广播变量
  *         累加器
  */
object _02SparkShareVariableOps {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setMaster("local[2]").setAppName(_01SparkPersistOps.getClass.getSimpleName())
        val sc = new SparkContext(conf)
        Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
        Logger.getLogger("org.apache.hadoop").setLevel(Level.OFF)

        val linesRDD = sc.textFile("D:/data/spark/hello.txt")
        val wordsRDD = linesRDD.flatMap(_.split(" "))
        var num = 0
        val parisRDD = wordsRDD.map(word => {
            num += 1
            println("map--->num = " + num)
            (word, 1)
        })
        val retRDD = parisRDD.reduceByKey(_ + _)

        println("num = " + num)
        retRDD.foreach(println)
        println("num = " + num)
        sc.stop()
    }
}

输出结果如下:

num = 0
map--->num = 1
map--->num = 1
map--->num = 2
map--->num = 2
map--->num = 3
map--->num = 4
(hello,3)
(you,1)
(me,1)
(he,1)
num = 0

广播变量

Spark的另一种共享变量是广播变量。通常情况下,当一个RDD的很多操作都需要使用driver中定义的变量时,每次操作,driver都要把变量发送给worker节点一次,如果这个变量中的数据很大的话,会产生很高的传输负载,导致执行效率降低。使用广播变量可以使程序高效地将一个很大的只读数据发送给多个worker节点,而且对每个worker节点只需要传输一次,每次操作时executor可以直接获取本地保存的数据副本,不需要多次传输。

这样理解, 一个worker中的executor,有5个task运行,假如5个task都需要这从份共享数据,就需要向5个task都传递这一份数据,那就十分浪费网络资源和内存资源了。使用了广播变量后,只需要向该worker传递一次就可以了。

创建并使用广播变量的过程如下:

在一个类型T的对象obj上使用SparkContext.brodcast(obj)方法,创建一个Broadcast[T]类型的广播变量,obj必须满足Serializable。 通过广播变量的.value()方法访问其值。 另外,广播过程可能由于变量的序列化时间过程或者序列化变量的传输过程过程而成为瓶颈,而Spark Scala中使用的默认的Java序列化方法通常是低效的,因此可以通过spark.serializer属性为不同的数据类型实现特定的序列化方法(如Kryo)来优化这一过程。

测试代码如下:

package cn.xpleaf.bigdata.spark.scala.core.p3

import org.apache.log4j.{Level, Logger}
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.{SparkConf, SparkContext}

/**
  * 使用Spark广播变量
  *
  * 需求:
  *     用户表:
  *         id name age gender(0|1)
  *
  *     要求,输出用户信息,gender必须为男或者女,不能为0,1
  */
object _03SparkBroadcastOps {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setMaster("local[2]").setAppName(_01SparkPersistOps.getClass.getSimpleName())
        val sc = new SparkContext(conf)
        Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
        Logger.getLogger("org.apache.hadoop").setLevel(Level.OFF)

        val userList = List(
            "001,刘向前,18,0",
            "002,冯  剑,28,1",
            "003,李志杰,38,0",
            "004,郭  鹏,48,2"
        )

        val genderMap = Map("0" -> "女", "1" -> "男")

        val genderMapBC:Broadcast[Map[String, String]] = sc.broadcast(genderMap)

        val userRDD = sc.parallelize(userList)
        val retRDD = userRDD.map(info => {
            val prefix = info.substring(0, info.lastIndexOf(","))   // "001,刘向前,18"
            val gender = info.substring(info.lastIndexOf(",") + 1)
            val genderMapValue = genderMapBC.value
            val newGender = genderMapValue.getOrElse(gender, "男")
            prefix + "," + newGender
        })
        retRDD.foreach(println)
        sc.stop()
    }
}

输出结果如下:

001,刘向前,18,女
003,李志杰,38,女
002,冯  剑,28,男
004,郭  鹏,48,男

当然这个案例只是演示一下代码的使用,并不能看出其运行的机制。
不过可以分析一下其原理,假如在执行map操作时,在某个Worker的一个Executor上有分配5个task来进行计算,在不使用广播变量的情况下,因为Driver会将我们的代码通过DAGScheduler划分会不同stage,交由taskScheduler,taskScheduler再将封装好的一个个task分发到Worker的Excutor中,也就是说,这个过程当中,我们的genderMap也会被封装到这个task中,显然这个过程的粒度是task级别的,每个task都会封装一个genderMap,在该变量数据量不大的情况下,是没有问题的,然后,当数据量很大时,同时向一个Excutor上传递5份这样相同的数据,这是很浪费网络中的带宽资源的;广播变量的使用可以避免这一问题的发生,将genderMap广播出去之后,其只需要发送给Excutor即可,它会保存在Excutor的BlockManager中,此时,Excutor下面的task就可以共享这个变量了,这显然可以带来一定性能的提升。
这里放上从网上找的一个图,就不自己画了,原理跟上面讲的是一样的:

Spark笔记整理(五):Spark RDD持久化、广播变量和累加器

累加器

Spark提供的Accumulator,主要用于多个节点对一个变量进行共享性的操作。Accumulator只提供了累加的功能。但是确给我们提供了多个task对一个变量并行操作的功能。但是task只能对Accumulator进行累加操作,不能读取它的值。只有Driver程序可以读取Accumulator的值。

非常类似于在MR中的一个Counter计数器,主要用于统计各个程序片段被调用的次数,和整体进行比较,来对数据进行一个评估。

测试代码如下:

package cn.xpleaf.bigdata.spark.scala.core.p3

import org.apache.log4j.{Level, Logger}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * Spark共享变量之累加器Accumulator
  *
  * 需要注意的是,累加器的执行必须需要Action触发
  */
object _04SparkAccumulatorOps {
    def main(args: Array[String]): Unit = {
        val conf = new SparkConf().setMaster("local[2]").setAppName(_01SparkPersistOps.getClass.getSimpleName())
        val sc = new SparkContext(conf)
        Logger.getLogger("org.apache.spark").setLevel(Level.OFF)
        Logger.getLogger("org.apache.hadoop").setLevel(Level.OFF)

        // 要对这些变量都*7,同时统计能够被3整除的数字的个数
        val list = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)

        val listRDD:RDD[Int] = sc.parallelize(list)
        var counter = 0
        val counterAcc = sc.accumulator[Int](0)
        val mapRDD = listRDD.map(num =>  {
            counter += 1
            if(num % 3 == 0) {
                counterAcc.add(1)
            }
            num * 7
        })
        // 下面这种操作又执行了一次RDD计算,所以可以考虑上面的方案,减少一次RDD的计算
        // val ret = mapRDD.filter(num => num % 3 == 0).count()
        mapRDD.foreach(println)
        println("counter===" + counter)
        println("counterAcc===" + counterAcc.value)
        sc.stop()
    }
}

输出结果如下:

49
56
7
63
14
70
21
77
28
84
35
91
42
counter===0
counterAcc===4

©著作权归作者所有:来自51CTO博客作者xpleaf的原创作品,如需转载,请注明出处,否则将追究法律责任

Rebuild started: Project: Project *** Using Compiler 'V6.22', folder: 'E:\Keil_v5\ARM\ARMCLANG\Bin' Rebuild target 'Target 1' assembling startup_stm32f10x_md.s... Start/core_cm3.c(445): error: non-ASM statement in naked function is not supported 445 | uint32_t result=0; | ^ Start/core_cm3.c(442): note: attribute is here 442 | uint32_t __get_PSP(void) __attribute__( ( naked ) ); | ^ Start/core_cm3.c(465): error: parameter references not allowed in naked functions 465 | "BX lr \n\t" : : "r" (topOfProcStack) ); | ^ Start/core_cm3.c(461): note: attribute is here 461 | void __set_PSP(uint32_t topOfProcStack) __attribute__( ( naked ) ); | ^ Start/core_cm3.c(479): error: non-ASM statement in naked function is not supported 479 | uint32_t result=0; | ^ Start/core_cm3.c(476): note: attribute is here 476 | uint32_t __get_MSP(void) __attribute__( ( naked ) ); | ^ Start/core_cm3.c(499): error: parameter references not allowed in naked functions 499 | "BX lr \n\t" : : "r" (topOfMainStack) ); | ^ Start/core_cm3.c(495): note: attribute is here 495 | void __set_MSP(uint32_t topOfMainStack) __attribute__( ( naked ) ); | ^ 4 errors generated. compiling core_cm3.c... compiling misc.c... compiling system_stm32f10x.c... compiling stm32f10x_adc.c... compiling stm32f10x_dac.c... compiling stm32f10x_exti.c... compiling stm32f10x_dbgmcu.c... compiling stm32f10x_dma.c... compiling stm32f10x_crc.c... compiling stm32f10x_cec.c... compiling stm32f10x_bkp.c... compiling stm32f10x_can.c... compiling stm32f10x_flash.c... compiling stm32f10x_pwr.c... compiling stm32f10x_fsmc.c... compiling stm32f10x_
最新发布
03-31
<think>嗯,用户遇到了Keil V6.22在编译STM32F10x项目时,core_cm3.c文件中出现裸函数中的非汇编语句参数引用错误。我需要仔细分析这个问题,并给出解决方案。 首先,根据用户提供的引用资料,问题似乎是由于Keil的编译器版本不兼容引起的。引用[3]提到,江科大的启动文件使用的是版本5的编译器,而新版的MDK默认使用版本6,导致不兼容。用户现在用的是V6.22,可能默认也是版本6编译器,而core_cm3.c中的裸函数可能包含非汇编语句,这在版本6中不被支持。 裸函数(naked function)通常用于直接编写汇编代码,不允许有C语句,否则编译器会报错。版本6可能对此检查更严格。引用[4]指出,将编译器版本切换回5可以解决这个问题。但用户可能不想降级,或者最新版MDK已经没有版本5选项,这时候可能需要修改项目设置或调整代码。 另一个解决方法是修改core_cm3.c文件中的相关函数。例如,检查报错的445行,是否在裸函数中使用了C语句。可能需要将其替换为纯汇编代码,或者将函数声明为非naked。不过,修改库文件需谨慎,可能影响其他部分。 用户可能需要指导他们如何切换编译器版本到5,如果安装包中没有,可能需要单独安装旧版本编译器,或者从Keil官网下载。同时,检查项目配置中的编译器选项是否正确设置为版本5。 另外,引用[2]提到可以不用替换文件,而是选择使用编译器版本5,这需要用户安装MDK版本5并配置项目使用它。但用户可能已经安装了较新版本,所以可能需要调整项目设置而非整个MDK版本。 总结可能的解决步骤:1. 检查并切换项目中的编译器版本到5;2. 修改core_cm3.c中的裸函数,移除非汇编语句;3. 如果无法切换编译器,考虑更新固件库到兼容版本6的版本。 需要确保回答结构清晰,分步骤说明,并提供相关引用支持。同时,生成相关问题,帮助用户进一步理解可能遇到的问题。</think>### 解决Keil V6.22编译core_cm3.c的裸函数错误 #### 问题分析 1. **根本原因**:Keil MDK默认使用Compiler V6,而STM32F10x的固件库(如`core_cm3.c`)可能仅兼容Compiler V5,导致裸函数(`naked function`)中出现非汇编语句或参数引用错误[^3][^4]。 - 裸函数要求仅包含汇编指令,V6编译器对此检查更严格。 - 编译器版本不兼容时,原有代码中的C语句或参数操作会触发报错。 --- #### 解决方案 ##### 方法1:切换为Compiler V5 1. **安装Compiler V5**(若未预装): - 下载并安装**Legacy Support for ARM Compiler 5**(需Keil官网账户)[^2]。 2. **配置项目使用V5**: - 在Keil中打开项目,进入`Project -> Options for Target -> Target`选项卡。 - 将`ARM Compiler`选项从`V6.22`改为`Use default compiler version 5`。 3. **重新编译**:验证是否解决报错。 ##### 方法2:修改core_cm3.c的裸函数(高风险) 1. **定位错误位置**: - 找到报错行(如`core_cm3.c`第445行),检查裸函数`__ASM void ...`中的非汇编语句。 2. **重写为纯汇编**: - 将C语句替换为内联汇编,例如: ```c __ASM void FunctionName(...) { // 替换为汇编指令,如MOV、BX LR等 } ``` 3. **注意**:此操作需熟悉ARM汇编,且可能影响库功能,建议优先切换编译器。 ##### 方法3:更新固件库(推荐) 1. **下载兼容V6的库**: - 从STM官网或CubeMX获取最新`CMSIS`包,确保支持Compiler V6。 2. **替换旧文件**: - 将项目中的`core_cm3.c`、`startup_stm32f10x_xx.s`等替换为新版本文件。 3. **验证兼容性**:重新编译项目,检查是否消除报错。 --- #### 扩展建议 - **兼容性检查**:若使用Keil V6.22,建议逐步迁移到官方支持的库版本,避免长期依赖旧编译器。 - **调试技巧**:若报错涉及参数引用,检查裸函数是否直接操作了C语言参数(如`int x`),需改用寄存器传递(如`R0`)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值