Spark 批处理引擎

479 阅读8分钟

Spark核心组件

  1. Driver 驱动器节点,用于执行Spark任务的main方法,负责实际代码的执行
  • 将用户程序转化为作业(Job)
  • 在Executor之间调度任务(Task)
  • 跟踪Executor的执行情况
  • 通过UI展示查询运行情况

2.Executor 负责Spark作业中具体任务的执行

  • 负责运行组成Spark应用的任务,并将结果返回给驱动器(Driver)
  • 通过自身的块管理器(Block Manager)为用户程序中要求缓存的RDD提供内存式。RDD式直接缓存在Executor进程内,因此任务可以在运行时充分利用缓存数据加速运算。

Spark部署模式

  1. Local 运行于本机
  2. Standalone 构建Master+Slaves资源调度集群,Spark任务提交给Master运行,是Spark自身的调度系统
  3. Yarn:Spark客户端直连Yarn,不需要额外构建Spark集群。有yarn-client和yarn-cluster两种。区别:Driver运行节点。
Yarn Client

Yarn Cluster

提交作业
bin/spark-submit \
--master local[*] \
--driver-cores 2 \      # driver使用的内核数,默认为1
--driver-memory 8g \    # driver内存大小,默认512M
--executor-cores 4 \    # executor使用的核数,默认为1,建议2-5个
--num-executors 10 \    # 启动executors的数量,默认为2
--executor-memory 8g \  # executor内存大小,默认1G
--class PackageName.ClassName.xxx.jar \
--name "Spark Job Name" \
InputPath \
OutputPath

Spark任务调度

Lineage

RDD只支持粗粒度转换,即对RDD数据集的单算子操作。通过一系列算子操作得到新的RDD,算子记录称为lineage(血缘)。

依赖

narrow depedency:每个父RDD的Partition仅最多被一个子RDD的partition使用 Wide dependency:多个字RDD的Partition会依赖同一个父RDD的Partition。

DAG

Directed Acyclic Graph(有向无环图),RDD的一系列转换操作形成了DAG

任务划分
  • Application:初始化SparkContext即生成Application
  • Job:一个Action算子会生成一个Job
  • Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖这划分一个Stage
  • Task:Stage是一个TaskSet,将Stage根据分区数划分成一个个Task。
RDD缓存

RDD通过persist或cache可以将前面的计算结果缓存。使用计算过程中的数据缓存,不会截断血缘关系。

def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
def cache(): this.type = persist()

object StorageLevel {
    val NONE = new StorageLevel(false, false, false, false)
    val DISK_ONLY = new StorageLevel(true, false, false, false)
    val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
    val MEMORY_ONLY = new StorageLevel(false, true, false, true)
    val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
    val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
    val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
    val MEMORY_AND_DISK = new StorageLevel(false, true, false, true)
    val MEMORY_AND_DISK_2 = new StorageLevel(false, true, false, true, 2)
    val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
    val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
    val OFF_HEAP = new StorageLevel(false, false, true, false)
}
RDD CheckPoint

通过将RDD写入Disk做检查点,截断血缘关系,checkpoint会额外提交一次任务。lineage过长会造成容错成本高。

Task级调度

DAGScheduler将Stage打包到TaskSet交给TaskScheduler,TaskScheduler将TaskSet封装为TaskManager加入到调度队列。

  1. FIFO
  2. Fair(runningTasks,minShare,weight)
    (a.runningTasks > a.minShare && b.runningTasks < b.minShare) => [b, a]
    (a.runningTasks < a.minShare && b.runningTasks < b.minShare && a.runningTasks / a.minShare > b.runingTasks / b.minShare) => [b, a]
    (a.runningTasks > a.minShare && b.runningTasks > b.minShare && a.runningTasks / a.weight > b.runingTasks / b.weight) => [b, a]
    

HashShuffle & SortShuffle

未经优化的HashShuffle

优化后的HashShuffle

普通的SortShuffle

bypadd运行机制的SortShuffleManager

算子

transformation
  1. map(func):返回新的RDD,该RDD由每一个输入元素经func函数转换后组成
  2. mapPartition(func):以分区作为数据单元,进行map。分区元素过大时,可能会OOM
  3. reduceByKey(fun, [numTask]):在(K,V)的RDD上调用,使用func将相同的key聚合到一起
  4. groupByKey(),按照key进行分组,直接进行shuffle;reduceByKey:按照key进行聚合,在shuffle前有combine(预聚合)。
  5. aggregateByKey(zeroValue:U, [partitioner:Partitioner])(seqOp:(U, V)=>U, combOp:(U, U)=>U)
  6. combineByKey(createCombiner: V=>C, mergeValue:(C, V)=>C, mergeCombiners:(C, C)=>C)
  7. coalesce & repartition,repartition底层调用:coalesce(numPartitions, shuffle=true),所以repartition一定会发生shuffle
  8. join(otherDataset, [numTasks]),(k,v).join((k.w)) => (k,(v, w))
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object Practice {
    def main(args: Array[String]): Unit = {
        val sparkConf = new SparkConf().setMaster("local[*]").setAppName("Test")
        val sc = new SparkContext(sparkConf)
        
        val line = sc.textFile("..../ageng.log")
        val provienceAdAndOne = line.map{x=>
            val fields: Array[String] = x.split(" ")
            ((fields(1), fields(3)), 1)
        }
        
        val provinceAdToSum = provinceAdAndOne.reduceByKey(_+_)
        val provienceToAdSum = provienceAdToSum.map(x=>(x._1._1, (x._1._2, x._2)))
        
        val provienceGroup = provienceToAdSum.groupByKey()
        
        val provienceAdTop3 = provienceGroup.mapValues { x=>
            x.toList.sortWith((x,y)=>x._2>y._2).take(3)
        }
        
        provienceAdTop3.collect().foreach(prientln)
        
        sc.stop()
    }
}
Action
  1. reduce(func):通过func函数,先聚合分区内的元素,再聚合分区间
  2. collect():以数组的形式返回所有元素
  3. count(): 返回元素个数
  4. first(): 返回第一个元素
  5. take(n): 返回前n个元素组成的数组
  6. aggregate()
    var rdd = sc.makeRDD(1 to 10, 2)
    rdd.aggregate(0)(_+_, _+_)
    
  7. fold() aggregate简化操作
    var rdd = sc.makeRDD(1 to 10, 2)
    rdd.aggregate(0)(_+_)
    
  8. saveAsTextFile(path)/saveAsSequenceFile(path)/saveAsObjectFile(path)

RDD、DataFrame、DataSet

RDD

RDD(Resilient Distributed Dataset),弹性分布式数据集

* - A list of partitions
* - A function for computing each split
* - A list of dependencies on other RDDs
* - Optionally, a Partitioner for key-value RDDs
* - Optionally, a list of prefered locations to compute each split on

优点:

编译时类型安全,面向对象的编程风格

缺点:

无论是集群间的通讯,还是IO操作都需要对对象的结构和数据进行序列化和反序列化,GC性能开销,频繁创建和销毁对象,势必增加GC

DataFrame

DataFrame引入了schema和off-heap schema:数据结构的信息。Spark通过schema就能够读懂数据,在通信与IO是就只需序列化和反序列化数据,而结构部分就可以省略了。

DataSet

DataSet结合了RDD和DataFrame的优点,并新增了Encoder 当序列化数据时,Encoder产生字节码与off-heap进行交互,能够达到按需访问数据的效果,而不用反序列化整个对象。

转换

kryo序列化

kryo序列化比java序列化更快更紧凑,但spark默认的序列化是java序列化,应为kryo并不支持所有序列化类型,RDD类型序列化是要进行注册。DataFrame和DataSet中自动实现了kryo序列化。

spark 小知识点

BroadCaset join

原理:先将小表数据查询出来聚合到driver端,在广播到各个executor端,使表与表join是进行本地join,避免进行网络传输产生shuffle。

Spark Reduce缓存

spark.reducer.maxSizeInFlight,reduce task单次拉取数据量,增大次参数可以减少reduce拉去次数。默认值为48MB,一般设为96MB

spark.shuffle.file.buffer,每个shuffle文件输出流的内存缓冲区大小,调大此参数可以减少在创建shuffle文件时进行磁盘搜索和系统调用的次数,默认为32K,一般设为64K。

注册UDF函数

SparkSession.udf.register

join和left join区别

join:返回左右集合中匹配成功的,类似于inner join left join:以左表为主,匹配不上的记录置空,类似于left outer join

Spark Streaming

DStream

离散化数据流(discretized stream),DStream:代表一段时间所产生的的RDD

架构图

  • SparkStreamingContext:对作业进行一些配置,如:DStream切分的批次间隔(duration)、与其他模块如DStreamGraphJobScheduler等交互
  • DStreamGraph:维护了输入DStream和输出DStream的实例,还会通过generateJobs生成作业集合(RDD DAG),由JobScheduler调度启动执行任务
  • JobScheduler:作业调度器,所有任务都是JobScheduler调度Executor完成的
  • CheckPointSpark Streaming容错机制的核心,会定时对已算好的中间结果及其他中间状态进行存储,避免依赖链过长的问题
  • Receiver Tracker:通过Executor上的ReceiverSupervisor来管理所有的receiver。主要功能是吧需要计算的数据发送给Executor,当Executor接收完毕后,将数据块的元数据上报给ReceiverTracker
  • ExecutorReceiverTracker会和Executor通信,启动ReceiverSupervisor实例,ReceiverSupervisor启动Receiver接收数据。收到数据后,用ReceiverBlockHandler以块的形式写到Executor的磁盘或内存,对应的实现是BlockManagerBasedBlockHandlerWriteAheadLongBasedBlockManager。前者根据ExecutorStorageLevel写到相应的存储层,后者先进行预写日志(Write Ahead Log).

转换算子

无状态转换算子

作用于DStream的每一个RDD

map
mapPartitions
reduceByKey
reduce
flatmap
glom
filter
repartition
union
tranform RDD独有
有状态转换算子

对DStream中的某几个算子进行操作,或向保存中间结果

slice
window
countByWindow
reduceByWindow
reduceByKeyAndWindow
countByValueAndWindow
Example
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.{Seconds, StreamingContext, Time}

case class Record(window: String)

object SqlNetworkWordCount {
    def main (args: Array[String]) {
        val sparkConf = new SparkConf().setAppName("SqlNetworkWordCount")
        val ssc = new StreamingContext(sparkConf, Seconds(2))
        
        val lines = ssc.socketTextStream("localhost", 9999, StorageLevel.MEMORY_AND_DISK_SER)
        val words = lines.flatMap(_.split(" "))
        
        words.forreachRDD { (rdd: RDD[String], time: Time) =>
            val spark = SparkSessionSingletom.getInstance(rdd.sparkContext.getConf)
            import spark.implicitis._
            
            val wordDataFrame = rdd.map(w => Record(w)).toDF()
            
            wordsDataFrame.createOrReplaceTempView("words")
            
            spark.sql("select word, count(*) as total from words group by word"),show()
        }
        
        ssc.start()
        ssc.awaitTermination()
    }
}

object SparkSessionSingleton {
    @transient private var instance: SparkSession = {
        if (instance == null) {
            instance = SparkSession
            .builder
            .configure(sparkConf)
            .getOrCreate()
        }
        instance
    }
}
优化
  1. 批次间隔,一般不低于0.5s
  2. 当性能低下是,减小窗口大小,增加滑动步长
  3. mapWithState,延迟表现和同时维护的key的数量由于updateStateByKey
  4. 可以增大数据处理的并行度提高性能,spark.default.parallelism,遇到数据倾斜可以使用reparation
  5. filter后会产生大量零碎分区,使用coalesce或reparation进行合并或重分。
  6. 资源调度,Hadoop2.6引入的基于标签的调度服务(Label based scheduling),使流计算得到稳定且独立的计算资源
  7. 缓存数据与数据清除,需要重复计算的数据用cache算子进行缓存,设置spark.streaming.unpersist=true,让Spark决定哪些数据需要缓存。
  8. JVM GC
    --conf "spark.executor.extraJavaOption=-XX:+UseG1GC"
    --conf "spark.executor.extraJavaOption=-XX:+UseParallelGC"
    
  9. spark.streaming.blockInterval=200
  10. 反压:当每批数据处理时间大于批次间隔时间。Spark 1.5引入反压机制,spark.streaming.backpressure.enabled=true,本质:限制Receiver接收数据的速度。上限由spark.streaming.receiver.maxRate配置,如果是Kafka Direct方式接收数据,上限由spark.streaming.kafka.maxRatePerPartition配置

Structured Streaming

增量微批