Spark知识点总结

  • Spark快于mapreduce
  1. Spark基于内存,Spark多个任务之间数据通信是基于内存,而Hadoop是基于磁盘
  2. 传统的mapreduce虽然具有自动容错、平衡负载的功能,但是他采用的是非循环式的数据流模型(每次流程都需要读取数据),这使得迭代计算需要大量的磁盘IORDD的一个重要特征是,分布式数据集可以在不同的并行环境当中被重复使用
  3. 高效的算法(基于DAGDAG有向无环图在此过程中减少了shuffle以及落地磁盘的次数如果计算不涉及与其他节点进行数据交换,Spark 可以在内存中一次性完成这些操作,也就是中间结果无须落盘,减少了磁盘 IO 的操作
  4. JVM优化。Spark Task的启动时间快。Spark采用fork线程的方式,Spark每次MapReduce操作是基于线程的,只在启动。而Hadoop采用创建新的进程的方式,启动一个Task便会启动一次JVM
  • RDD属性 1.可分区,数据集的基本组成单位 2.计算每个分区的函数,RDD的计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的。computer函数会对迭代器进行复合,不需要保存每次的计算结果3.RDD的依赖关系 4.分区器,针对key-value对 5.优先位置,移动数据不如移动计算,spark在任务调度时会尽可能的将计算任务分配到其所要处理的数据块的存储位置
  • checkpoint cache保证了需要重复访问数据的运用可以运行的更快。spark的DAG某些场景可能会很庞大,task中的computing chain可能会很长,计算某些RDD也可能会很耗时,这时如果task运行失败,那么task整个ccomputing chain都会重新计算,代价太高,因此,有必要将计算代价较大的RDD进行checkpoint,当下游的RDD出错时,直接从checkpoint获取数据。在checkpoint操作也可以优化,因为在调用checkpoint的时候需要单独启动job写数据到hdfs,所以在调用之前进行cache,这样可以避免RDD被重复计算,重新启动job只需要将内存中的数据拷贝到hdfs即可
  • 广播变量 在每个executor中只保留一份变量副本,而不是每个task,省去了很多网络传输,而且会通过高效的广播算法来减少传输过程中的消耗。广播变量是不能被修改,RDD不能广播。中间数据,应用数据,规则数据,数据量不大,会被频繁使用的数据可以广播,task如果使用到driver program中比较大的对象,可以广播
  • SparkSql
  • DataFrame的目的就是为了让大型的数据集处理更加方便。

       RDD,dataFrame,dataSet三者比较。RDD编译时类型安全,面向对象编程风格,序列化代价高,gc性能开销;dataFrame引入schema和off-heap。schema:每一行数据结构都是一样的,存储在schema中,spark能够通过schema读懂数据,因此通信和IO只需要序列化和反序列化数据。off-heap是直接收操作系统管理的,spark能够以二进制的方式序列化数据到off-heap(不包含结构),当要操作数据时直接在off-heap中去取,不受JVM限制;dataSet则结合了这些二者优点,并引入了Encoder,当序列化数据时,Encoder产生字节码与off-heap进行交互,能够达到按需访问数据的效果,不必序列化整个对象

  • Spark调优

 1.采用kryo的序列化方式使用序列化的地方<1>算子使用到外部变量的时候<2>将自定义的类型作为rdd的泛型类型时<3>使用可序列化的持久化时<4>shuffle) 2.确定内存消耗。可以创建RDD,将其放入缓存,然后观察web页面。估计特定对象的内存消耗,可以使用SizeEstimator方法,这对于尝试使用不同数据布局来调整内存以及确定广播变量在每个执行程序堆中占用空间量比较有用3.数据结构优化尽量使用字符串替代对象,使用原始类型(比如IntLong)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能 4.对象还是很大的情况下使用序列化存储RDD,影响就是降低了访问速度5.对computing chain过长的RDD持久化 6.广播变量的使用,避免shuffle 7.使用map-side预聚合shuffle操作(类似mapreduce的combiner)8.使用高性能算子repartitionAndSortWithinPartitions<需要传入自定义分区器,还需要定义隐式变量,实现排序规则>代替repartitionsort操作)9.jvm调优。可以配置参数观察log,查看内存回收信息。Spark内存回收优化是确保只有长时间存活的RDD才能保存到老生代区。同时新生代足够大以保证生命较短的对象,避免full gc。优化步骤:大量full gc则表明执行内存不足可通过参数spark.storage.memoryFraction减小缓存。大量minor gc,需要为eden分配更多的内存 (spark.executor.extraJavaOptions)10.并行度 spark.default.parallelism设置,通常一个cpucore分配2~3个任务11.设置数据本地化(PROCESS_LOCAL<同一jvm>,NODE_LOCAL<同一节点>,NO_PREF,RACK_LOCAL<同一机架>,ANY)。12.参数设置(https://www.cnblogs.com/feiyudemeng/p/9057749.html--文章原处,特此mx)补充shuffle相关参数调优:spark.shuffle.file.buffer(shufflewrite缓存区大小)spark.reduce.maxSizeInFlight(shuffleread缓存区大小)spark.shuffle.io.maxRetries(拉取数据失败重试次数,还有拉取等待时间,这里不做说明了)spark.shuffle.memoryFraction(如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘),还有一些shufflemanage类型选取(三种shuffle机制)

  • SparkString调优
    • 首次处理减压策略 首次启动job时,由于冷启动会造成内存使用较大,限制首次启动的数据量
    • 合理设置批处理时间
    • 合理设置kafka拉取量 spark.streaming.kafka.maxRatePerPartition 默认没有上限
    • 缓存反复使用的DStream
    • 其他  设置合理的并行度,cpu,高性能算子,kryo等
  •  RDD的弹性表现
    • 自动进行内存磁盘切换
    • 基于linage的高效容错
    • task如果失败会自动进行特定次数的重试
    • stage如果失败也会自动重试,而且只会计算失败的分片
    • checkpoint和persist,数据计算之后持久化缓存
    • 数据调度弹性,DAG Task调度和资源无关
    • 数据分片高度弹性。 可以很多碎片合成大的,也可partitioner
  • RDD通过lineage为什么高效
  1. Lazy执行,每次都是新的rdd,降低控制难度
  2. 记录原数据,是每次修改都记录,代价很大如果修改一个集合,代价就很小,官方说rdd是粗粒度的操作,是为了效率,为了简化,每次都是操作数据集合
  3. 面向大规模数据分析,数据检查点操作成本很高,需要通过数据中心的网络连接在机器之间复制庞大的数据集,而网络带宽往往比内存带宽低得多,同时还需要消耗更多的存储资源
  •  Spark为什么要进行持久化,在什么场景下使用

    为了方便容错,如果不持久化父RDD的话,就得重新计算

    以下场景适用persist

1)某个步骤计算非常耗时,需要进行persist持久化
2)计算链条非常长,重新恢复要算很多步骤,很好使,persist
3checkpoint所在的rdd要持久化persist
lazy级别,框架发现有checnkpointcheckpoint时单独触发一个job,需要重算一遍,checkpoint
要持久化,写个rdd.cache或者rdd.persist,将结果保存起来,再写checkpoint操作,这样执行起来会非常快,不需要重新计算rdd链条了。checkpoint之前一定会进行persist
4shuffle之后为什么要persistshuffle要进性网络传输,风险很大,数据丢失重来,恢复代价很大
5shuffle之前进行persist,框架默认将数据持久化到磁盘,这个是框架自动做的。

  •  parquet文件优势  1.列式存储,跳过不符合条件的列,降低IO 2.高效压缩,减少磁盘消耗 3.只需要读需要的列,支持向量运算  4.据观察,一定程度上能优化spark调度执行,可以减少stage执行消耗,同时可以优化执行路径
  • RangePartitioner实现原理  RangePartitioner分区则尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,也就是说一个分区中的元素肯定都是比另一个分区内的元素小或者大;但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。其原理是水塘抽样(水塘抽样适合不确定数据范围,无法加入到内存的数据,随机选取一个数)
  • spark内存管理方案       首先相比较于spark1.x的静态内存管理机制,1.6x版本作了优化,采用统一内存管理。分为可用内存和系统预留内存(300m),可用内存又分为统一内存和其他,统一内存占60%,其中中storage和execution各占50%,other占40%,用于保留用户数据结构,spark中的内部数据结构,并且在稀疏和异常大的记录的情况下保护OOM错误,还作为误差缓冲,因为统一内存很多是估算的,如果超过限制,可以起到保护作用。该部分内存不受memorymanager管理

300m系统预留内存之前默认是占25%,有可能造成内存溢出

动态占用计算内存不会被归还,具体原因有如下

    1. 数据清除的开销 : 驱逐storage内存的开销取决于 storage levelMEMORY_ONLY 可能是最昂贵的,因为需要重新计算,MEMORY_AND_DISK_SER 正好相反,只涉及到磁盘IO。溢写 execution 内存到磁盘的开销并不昂贵,因为 execution 存储的数据格式紧凑(compact format),序列化开销低。并且,清除的 storage 内存可能不会被用到,但是,可以预见的是,驱逐的 execution 内存是必然会再被读到内存的,频繁的驱除重读 execution 内存将导致昂贵的开销。
    2. 实现的复杂度 : storage 内存的驱逐是容易实现的,只需要使用已有的方法,drop blockexecution 则复杂的多,首先,execution page 为单位管理这部分内存,并且确保相应的操作至少有 one page ,如果把这 one page 内存驱逐了,对应的操作就会处于饥饿状态。此外,还需要考虑 execution 内存被驱逐的情况下,等待 cache block 如何处理。

注意:Runtime.getRuntime.maxMemory实际比分配的内存要小,这是因为内存分配池的堆部分划分为 EdenSurvivor Tenured 三部分空间,而这里面一共包含了两个 Survivor 区域,而这两个 Survivor 区域在任何时候我们只能用到其中一个。

Spark 任务内存管理中,使用 HashMap 存储任务与其消耗内存的映射关系,其中 n 为当前 Executor 中活跃的任务树, Execution 内存大小为 10GB,当前 Executor 内正在运行的 Task 个数为5,则该 Task 可以申请的内存范围为 10 / (2 * 5) ~ 10 / 5,也就是 1GB ~ 2GB 的范围

  • Sparkoom一般解决方案

 

Task 需要更多的内存,而又得不到足够的内存而导致。因此,解决方案要从增加每个 Task 的内存使用量,满足任务需求 或 降低单个 Task 的内存消耗量,从而使现有内存可以满足任务运行需求两个角度出发。

 

  1. 增加单个task的内存使用量

 

降低 Executor 的可用 Core 的数量 N , 使 Executor 中同时运行的任务数减少,在总资源不变的情况下,使每个 Task 获得的内存相对增加。当然,这会使得 Executor 的并行度下降。可以通过调高 spark.executor.instances 参数来申请更多的 executor 实例(或者通过spark.dynamicAllocation.enabled 启动动态分配),提高job的总并行度

        2. 降低单个task内存消耗量

1) 配置

P = spark.default.parallism (非SQL应用)

P = spark.sql.shuffle.partition (SQL 应用)

       2) 调整应用逻辑包括一般常用的代码优化

  • Sparkonyarn优点
    • 和其他计算框架并行,共享资源
    • 资源分配更加细致
    • Application部署简化,客户端提交后由yarn负责资源的管理调度,利用container作为资源的隔离单位。
    • 通过队列的方式,管理同时运行在yarn上的多个服务,根据不同类型的服务,调整资源的使用量实现资源的弹性管理
  • Exexutor产生FULL GC 的原因,影响

可能导致Executor僵死问题,海量数据的shuffle和数据倾斜等都可能导致full gc。以shuffle为例,伴随着大量的Shuffle写操作,JVM的新生代不断GCEden Space写满了就往Survivor Space写,同时超过一定大小的数据会直接写到老生代,当新生代写满了之后,也会把老的数据搞到老生代,如果老生代空间不足了,就触发FULL GC,还是空间不够,那就OOM错误了,此时线程被Blocked,导致整个Executor处理数据的进程被卡住

  •  SparkSQL三种join

(1) Broadcast Join

使用场景:当一张表比较小一张表比较大的时候可以使用,使用的条件有以下几个:

1. 被广播的表需要小于spark.sql.autoBroadcastJoinThreshold所配置的值,默认是10M (或者加了broadcast join的hint)

2. 基表不能被广播,比如left outer join时,只能广播右表

缺点:只能广播小表,如果出现频繁广播的情况对driver端的内存是个考验。

(2) Shuffle Hash Join

将两个表中要join的key进行hash,并创建n个分区,根据key的hash算法将key相同的数据放入到同一个分区里进行join,采用的是对join进行分而治之的思想,适合两张表的数据量比较大,不太好对一张表进行广播的场景。在一定程度上减少了driver广播一侧表的压力,也减少了executor端取整张被广播表的内存消耗。

(3) Sort Merge Join

对要join的数据进行排序,然后进行join。具体做法是进行一次shuffle,map端根据join的条件确定每条记录的key,基于key做shuffle write,在shuffle read阶段对数据进行merge sort。

适合两张表的数据都比价大,hash join无法满足的情况。

  • Spark数据倾斜解决
    首先出现数据倾斜的场景可能是某先task执行很快,而少数task执行很慢,或者是之前运行健康的流程突然oom

出现问题首先会去定位问题,task执行很慢的话可以通过web ui判断当前运行到第几个stage,其实根据stage的划分原理,就可以定位到具体是哪部分出问题;oom通常可以查看log中的异常栈,一般来说,异常栈就会定位到代码中那部分发生问题,当然,不是所有的oom都是数据倾斜导致的,还得综合web ui去查看task处理的数据量再做判断。

定位到导致数据倾斜的算子后,通常有如下四种解决方案

1.过滤掉少数导致倾斜的key 2.提高shuffle的并行度 可以让原本分配给一个task的多个key分配给多个task 3.双重聚合 进行两阶段聚合,先给每个key打上随机数,进行局部聚合,再去掉进行全局聚合 4.reduce join转换为map join 规避掉join这种算子,避免shuffle操作,用broadcast 和map实现join

  • 谈谈对RDD的理解

        RDD是一个只读的,弹性的,分布式,可分区的数据集合。RDD有五大属性,详细见上文,是Spark中最重要的抽象概念。它分布于集群节点,以函数式编程的方式进行各种并行运算,基于内存,实质是一种更加通用的迭代计算框架,这也是spark解决了mapreduce的什么问题(在hadoop中一片文章中提到过),之所以说是弹性的(上文也提到过)RDD基于lineage实现高效的容错。一般来说,分布式数据集容错方式可分为两种,数据检查点和记录数据更新。面向大规模的数据分析,记录检查点成本相对较高,spark选择了后者,并支持为粗粒度转换,针对每一个块,记录RDD的一系列转换操作,这种操作在spark中被称为血统。本质上类似数据库中的重做日志,只不过重做日志粒度大,针对全局数据。之所以说lineage是高效的,上文也有提到过,这里不再赘述。

  • Spark高阶函数总结一笔

action:take,top,takeOrdered,aggregate(求平均值)

transformation:mapPartitionWithIndex,repartitionAndSortWithinPartitions,aggregateByKey,combineByKey(分组球平均值)

SparkStreaming中的高阶:窗口操作,updateStateByKey(DStream中的数据进行按keyreduce操作,然后对各个批次的数据进行累加。注意这种累加需要事先设置checkpoint。因为累加的rdd依赖关系会很长。

  • Spark的运行流程 
  • Spark on yarn的运行流程

1. action类算子

take用于获取RDD中从0num-1下标的元素,不排序。

top函数用于从RDD中,按照默认(降序)或者指定的排序规则,返回前num个元素。

takeOrderedtop类似,只不过以和top相反的顺序返回元素。

aggregate可以用来求平均值

def aggregate[U](zeroValue: U)(seqOp: (U, T) ⇒ U, combOp: (U, U) ⇒ U)(implicit arg0: ClassTag[U]): U

 

seqOp操作会聚合各分区中的元素,然后combOp操作把所有分区的聚合结果再次聚合,两个操作的初始值都是zeroValue

 

2. Transformation算子

mapPartitionsWithIndex取分区中对应的数据时,还可以将分区的编号取出来,这样就可以知道数据是属于哪个分区的

aggregateByKey

combineByKeySpark中一个比较核心的高级函数,其他一些高阶键值对函数底层都是用它实现的,求平均值就是用它实现的3个重要的函数参数:

u createCombiner: V => C ,这个函数把当前的值作为参数,此时我们可以对其做些附加操作(类型转换)并把它返回 (这一步类似于初始化操作)

u mergeValue: (C, V) => C,该函数把元素V合并到之前的元素C(createCombiner)(这个操作在每个分区内进行)

u mergeCombiners: (C, C) => C,该函数把2个元素C合并 (这个操作在不同分区间进行)

求平均值代码实现

@Test

def testAvg(): Unit = {

    val data = sc.parallelize(Seq(("a", 2), ("a", 6), ("b", 7), ("b", 3)))

    data.combineByKey(

      (v) => (v, 1),

      (acc:(Int, Int), v) => (acc._1 + v, acc._2 + 1),

      (acc1:(Int, Int), acc2:(Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)

    ).map{

      case(k, v) => (k, v._1/ v._2.toDouble)

    }.foreach(println)

}

 

 

1. Spark streaming高级算子介绍及使用

updateStateByKey: DStream中的数据进行按keyreduce操作,然后对各个批次的数据进行累加。注意这种累加需要事先设置checkpoint。因为累加的rdd依赖关系会很长。

实例代码:

 

 

 

 

 

 

 

 

 

 

 

posted @ 2020-04-02 16:12  嘿呦  阅读(973)  评论(0)    收藏  举报