spark整理知识点

架构与运维:

概述:

Spark是一个分布式计算引擎,由 Scala 语言编写的计算框架,基于内存的快速、通用、可扩展的大数据分析引擎;
Spark的计算模式也属于MapReduce;Spark框架是对MR框架的优化;

比较:

MapReduce                                                     Spark
数据存储结构:磁盘HDFS文件系统的split                     使用内存构建弹性分布式数据集RDD对数据进行运算和cache
编程范式:Map + Reduce,仅提供两个操作,表达力欠缺        提供了丰富的操作,使数据处理逻辑的代码非常简短
计算中间结果落到磁盘,IO及序列化、反序列化代价大        计算中间结果在内存中,维护存取速度比磁盘高几个数量级
Task以进程的方式维护,需要数秒时间才能启动任务             Task以线程的方式维护对于小数据集读取,能够达到亚秒级的延迟

与hadoop比较:

相对于Hadoop的MapReduce会在运行完工作后将中间数据存放到磁盘中,Spark使用了内存运算技术,能在数据尚未写入硬盘时即在内存分析运算。
Spark在存储器内运行程序的运算速度能做到比Hadoop MapReduce的运算速度快上100倍,即便是运行程序于硬盘时,Spark也能快上10倍速度。
总结:
    spark积极使用内存
    mapreduce的reduce函数一次可以拿到所有的该key对应的value列表,所以shuffle需要排序,等待所有的数据。而spark的map每写入一点数据ResultTask可以拉取进行聚合(groupbykey除外)。

Spark允许用户将数据加载至集群存储器,并多次对其进行查询,非常适合用于机器学习算法。
支持一组丰富的高级工具,包括使用 SQL 处理结构化数据处理的 Spark SQL,用于机器学习的 MLlib,用于图计算的 GraphX,以及 Spark Streaming
提出了用一个统一的引擎支持批处理,流处理,交互式查询,机器学习等常见的数据处理场景。

特点:

1、Spark积极使用内存。    
    Spark框架可以把多个map reduce task组合在一起连续执行,中间的计算结果不需要落地
2、多进程模型(MR) vs 多线程模型(Spark)。
    MR框架中的的Map Task和Reduce Task是进程级别的,而Spark Task是基于线程模型的。MR框架中的 map task、reduce task都是 jvm 进程,每次启动都需要重新申请资源,消耗了不必要的时间。
    Spark则是通过复用线程池中的线程来减少启动、关闭task所需要的系统开销。

架构:

Cluster Manager集群资源的管理者。
Worker Node工作节点,管理本地资源;
Driver 运行应用的 main() 方法并且创建了 SparkContext
    负责向集群申请资源,向master注册信息,负责作业的调度,作业的解析,生成stage并调度Task到Executor上。
Executor在工作节点上运行,执行 Driver 发送的 Task,并向 Driver 汇报计算结果;

多种部署模式:

本地模式。最简单的运行模式,Spark所有进程都运行在一台机器的 JVM 中
    spark-shell --master local
伪分布式模式。在一台机器中模拟集群运行,相关的进程在同一台机器上(用的非常少)
    spark-shell --master local-cluster[Nodes,cores,memory]
集群部署模式

简单安装:

wget https://archive.apache.org/dist/spark/spark-2.4.3/spark-2.4.3-bin-hadoop2.7.tgz
tar zxf spark-2.4.3-bin-hadoop2.7.tgz
cd spark-2.4.3-bin-hadoop2.7/
配置环境变量/etc/profile
修改集群配置slaves、spark-defaults.conf、spark-env.sh、log4j.properties
分发配置
启动集群./start-all.sh(注意hadoop的脚本也叫这个,可以重命名)

常见env:
    spark.local.dir    spark存放临时数据的目录,默认/tmp,开机或定时清理,默认为内存的一半大小。

集群部署模式:

Standalone(开发测试环境):

独立模式,自带完整的服务,可单独部署到一个集群中,无需依赖任何其他资源管理系统
sbin/start-master.sh / sbin/stop-master.sh
sbin/start-slaves.sh / sbin/stop-slave.sh
sbin/start-slave.sh / sbin/stop-slaves.sh    启动节点上的worker进程,调试中较为常用
sbin/start-all.sh / sbin/stop-all.sh    
提交作业:
    ./bin/spark-submit --master spark://172.22.9.181:7077 --jars ./spark-sql-kafka-0-10_2.11-2.4.3.jar,./kafka-clients-0.10.2.0.jar 
    --class chenzl.kafka_test.SparkTest kafka-test-0.0.1-SNAPSHOT.jar

Yarn(生产环境):

配置spark-env.sh,添加hadoop conf路径(主要是这步)
    export HADOOP_CONF_DIR=/opt/lagou/servers/hadoop-2.9.2/etc/hadoop
优化spark-default.conf
    spark.yarn.historyServer.address linux121:18080 与 hadoop historyserver 10020集成
    spark.yarn.jars hdfs:///spark-yarn/jars/*.jar  将jar包上传,以后每次job不用再上传。hdfs dfs -put $SPARK_HOME/jars/* /spark-yarn/jars/
启动即可
整合HistoryServer服务
    spark-defaults.conf配置spark.yarn.historyServer.address linux121即可
提交任务: 
    spark-submit --master yarn
Mesos

运行模式:

Client模式(缺省)。Driver运行在提交任务的Client,此时可以在Client模式下,看见应用的返回结果,适合交互、调试
    命令:spark-submit 
Cluster模式。Driver运行在Spark集群中,看不见程序的返回结果,合适生产环境
    命令:spark-submit --deploy-mode cluster

HistoryServer服务:

配置:

spark-defaults.conf
spark-env.sh

启动:

$SPARK_HOME/sbin/start-history-server.sh

访问:

http://worker1:18080/

HA高可用:

基于zookeeperStandby Master,生产环境使用
    1、安装ZooKeeper,并启动
    2、修改 spark-env.sh 文件,并分发到集群中
    3、启动 Spark 集群(linux121
    4、在 linux122 上启动master
基于文件系统的单点恢复

相关术语:

Application     用户提交的spark应用程序,由集群中的一个driver 和 许多executor 组成
Application jar 一个包含spark应用程序的jar,jar不应该包含 Spark 或 Hadoop的 jar,这些jar应该在运行时添加
Driver program     运行应用程序的main(),并创建SparkContext(Spark应用程序)
Cluster manager 管理集群资源的服务,如standalone,Mesos,Yarn
Deploy mode     区分 driver 进程在何处运行。在 Cluster 模式下,在集群内部运行 Driver。 在 Client 模式下,Driver 在集群外部运行
Worker node     运行应用程序的工作节点
Executor         运行应用程序 Task 和保存数据,每个应用程序都有自己的executors,并且各个executor相互独立
Task             executors应用程序的最小运行单元
Job             在用户程序中,每次调用Action函数都会产生一个新的job,也就是说每个
Action             生成一个job
Stage             一个 job 被分解为多个 stage,每个 stage 是一系列 Task 的集合

RDD编程:

概述:

RDD是 Spark 的基石,是实现 Spark 数据处理的核心抽象。
RDD 是一个抽象类,它代表一个不可变、可分区、里面的元素可并行计算的集合。

5个特征:

1.有一个分区的列表

即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值;
RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute 函数得到每个分区的数据。
如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。

2.有一个计算函数compute,对每个分区进行计算

一个对分区数据进行计算的函数。Spark中RDD的计算是以分片为单位的,每个RDD都会实现 compute 函数以达到该目的。compute函数会对迭代器进行组合,不需要保存每次计算的结果;

3.对其他RDDs的依赖(宽依赖、窄依赖)列表

RDD之间存在依赖关系。RDD的每次转换都会生成一个新的RDD,RDD之间形成类似于流水线一样的前后依赖关系(lineage)。
在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算;

4.对key-value RDDs来说,存在一个分区器(Partitioner)【可选的】

对于 key-value 的RDD而言,可能存在分区器(Partitioner)。Spark 实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。
只有 key-value 的RDD,才可能有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量;

5.对每个分区有一个优先位置的列表【可选的】

一个列表,存储存储每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。
按照 "移动计算不移动数据" 的理念,Spark在任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。

特点:

1、分区

2、只读

RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD;一个RDD转换为另一个RDD,通过丰富的操作算子(map、filter、union、join、reduceByKey… …)实现

3、依赖

RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护着这种血缘关系(lineage),也称之为依赖。

4、缓存

可以控制存储级别(内存、磁盘等)来进行缓存。
后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。

5、checkpoint

RDD支持 checkpoint 将数据保存到持久化的存储中,这样就可以切断之前的血缘关系,因为checkpoint后的RDD不需要知道它的父RDDs了,它可以从 checkpoint 处拿到数据。

适合场景:

1.对数据集进行低级转换、操作和控制;

2.数据是非结构化的,例如媒体流或文本流;

3.想使用函数式编程结构而不是特定领域的表达式来操作数据;

4.在按名称或列处理或访问数据属性时,不关心强加模式,例如列格式;

5.可以放弃 DataFrames 和 Datasets 提供的一些优化和性能优势。

编程模型:

数据源 -> RDD -> transformation -> action

RDD的本质:
    一个RDD 本质上是一个函数,而RDD的变换不过是函数的嵌套。
    NewHadoopRDD是数据来源,每个parition负责获取数据,获得过程是通过iterator.next 获得一条一条记录的。

处理模型:
    每次计算都只从源头取一条数据,直到stage划分,节省内存。

OOM原因:
    发生oom是因为shuffle不立刻写,不然写文件一条条太慢。

创建RDD:

从集合创建RDD
    从集合中创建RDD,主要用于测试。Spark 提供了以下函数:parallelize、makeRDD、range
    def parallelize[T](seq: Seq[T], numSlices: Int = defaultParallelism)
    def makeRDD[T](seq : scala.Seq[T], numSlices : scala.Int = { /* compiled code */ })
    range(start, end=None, step=1, numPartitions=None)[source]
从文件系统创建RDD
    1.本地文件系统
        val lines = sc.textFile("file:///root/data/wc.txt")
        使用本地文件系统要注意:该文件是不是在所有的节点存在(在Standalone模式下)
    2.分布式文件系统HDFS的地址
        val lines = sc.textFile("hdfs://linux121:9000/user/root/data/uaction.dat")
    3.Amazon S3的地址
从RDD创建RDD
    Transformation操作

Transformation操作:

map(func):对数据集中的每个元素都使用func,然后返回一个新的RDD
filter(func):对数据集中的每个元素都使用func,然后返回一个包含使functrue的元素构成的RDD
flatMap(func):与 map 类似,每个输入元素被映射为0或多个输出元素
mapPartitions(func):和map很像,但是map是将func作用在每个元素上,而mapPartitionsfunc作用在整个分区上。假设一个RDDN个元素,M个分区(N>> M),
    那么map的函数将被调用N次,而mapPartitions中的函数仅被调用M次,一次处理一个分区中的所有元素。当内存资源充足时,建议使用mapPartitions,以提高处理效率。
mapPartitionsWithIndex(func):与 mapPartitions 类似,多了分区索引值信息。
groupBy(func):按照传入函数的返回值进行分组。将key相同的值放入一个迭代器
glom():将每一个分区形成一个数组,形成新的RDD类型 RDD[Array[T]]
sample(withReplacement, fraction, seed):采样算子。以指定的随机种子(seed)随机抽样出数量为fraction的数据,withReplacement表示是抽出的数据是否放回,true为有放回的抽样
distinct([numTasks])):对RDD元素去重后,返回一个新的RDD。可传入numTasks参数改变RDD分区数
coalesce(numPartitions):缩减分区数,无shuffle
repartition(numPartitions):增加或减少分区数,有shuffle,底层是coalesce
sortBy(func, [ascending], [numTasks]):使用 func 对数据进行处理,对处理后的结果进行排序
--------------------------------------
intersection(otherRDD)
union(otherRDD)
subtract (otherRDD)
cartesian(otherRDD):笛卡尔积
zip(otherRDD):将两个RDD组合成 key-value 形式的RDD,默认两个RDDpartition数量以及元素数量都相同,否则会抛出异常。

Action操作:

collect() / collectAsMap()
stats / count / mean / stdev / max / min
reduce(func) / fold(func) / aggregate(func)        比较:foldaggregate都可以定义初始值,aggregate更灵活,可以自定义局部和全局函数。
first()Return the first element in this RDD
take(n)Take the first num elements of the RDD
top(n):按照默认(降序)或者指定的排序规则,返回前num个元素。
takeSample(withReplacement, num, [seed]):返回采样的数据
foreach(func) / foreachPartition(func)
    概述:
        与mapmapPartitions类似,区别是foreach 是 Action
    示例:
        对于dataframe.foreachPartition,参数为IteratorWrapper(non-empty iterator),但不能用增强 for 循环for(Row x:list)
{}
        可以用普通的for循环或while循环:
            (Iterator<Row> x)->{for(;x.hasNext();) {System.out.println("Hello " + x.next().getAs("value"));};}  # while循环也可以
            (Iterator<Row> x)->{while(x.hasNext()) {System.out.println("Hello " + x.next().getAs("value"));};}
saveAsTextFile(path) / saveAsSequenceFile(path) / saveAsObjectFile(path)

Key-Value RDD:

Transformation操作:

-----------------map操作-------------
mapValues / flatMapValues / keys / values                
---------------聚合操作-------------------
groupByKey / reduceByKey / foldByKey / aggregateByKey    
    底层实现:
        combineByKey(OLD) / combineByKeyWithClassTag
    比较: 
        aggregateByKey灵活,分区内的合并与分区间的合并,可以采用不同的方式;这种方式是低效的!(没有combine)
        foldByKey和aggregateByKey可以定义初始值,而且aggregateByKey定义的初始值类型可以不同
        groupByKey在一般情况下效率低,尽量少用,因为Shuffle过程中传输的数据量大,效率低。而reduceByKey预先进行combine.
            不一定有shuffle,看数据是否已经做过分区hashPartitioner
    优化场景:TopN
        用aggregateByKey替代groupByKey,分区内先预排序,取分区的TopN,然后分区间再合并,减小了shuffle的数据量。
-----------------RDD之间操作--------------
subtractByKey:类似于subtract,删掉 RDD 中键与 other RDD 中的键相同的元素
-----------------排序操作-----------------
sortByKey:sortByKey函数作用于PairRDD,对Key进行排序。在org.apache.spark.rdd.OrderedRDDFunctions 中实现
-----------------join操作-----------------
cogroup(全连接) / join / leftOuterJoin / rightOuterJoin / fullOuterJoin(等同fullOuterJoin)
    示例:
        join根据key来,然后得到(key,tuple元组)的结果
            rdd1.fullOuterJoin(rdd2)
                              .map {case (udIdMd5, (issuedOpt, rowOpt)) => if (rowOpt.isDefined) {val row = rowOpt.get } else {}

Action操作:

collectAsMap / countByKey
lookup(key)高效的查找方法,只查找对应分区的数据(如果RDD有分区器的话)

输入与输出:

1、文本文件

数据读取:
textFile(String)。可指定单个文件,支持通配符。
对于大量的小文件用 wholeTextFiles,返回值RDD[(StringString)],其中Key是文件的名称,Value是文件的内容
对于目录,包含多个子路径,可以textFile("/某个路径/*")
数据保存:
saveAsTextFile(String)。指定的输出目录。

2、csv文件

读取 CSV(Comma-Separated Values)/TSV(Tab-Separated Values)数据和读取 JSON 数据相似,都需要先把文件当作普通文本文件来读取数据,然后通过将每一行进行解析实现对CSV的读取。
CSV/TSV 数据的输出也是需要将结构化RDD通过相关的库转换成字符串RDD,然后使用 Spark 的文本文件 API 写出去。

3、json文件

如果 JSON 文件中每一行就是一个JSON记录,那么可以通过将JSON文件当做文本文件来读取,然后利用相关的JSON库对每一条数据进行JSON解析。
JSON数据的输出主要是通过在输出之前将由结构化数据组成的 RDD 转为字符串RDD,然后使用 Spark 的文本文件 API 写出去。
json文件的处理使用SparkSQL最为简洁。

4、SequenceFile

SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。 Spark 有专门用来读取 SequenceFile 的接口。
在 SparkContext中,可以调用:sequenceFile[keyClass, valueClass];
调用 saveAsSequenceFile(path) 保存PairRDD,系统将键和值能够自动转为Writable类型。

5、对象文件

对象文件是将对象序列化后保存的文件,采用Java的序列化机制。
通过objectFile[k,v](path) 接收一个路径,读取对象文件,返回对应的 RDD,也可以通过调用saveAsObjectFile() 实现对对象文件的输出。因为是序列化所以要指定类型。

6、JDBC

类似JAVA的使用方法,创建conn和prepareStatement
示例: 
    result.foreachPartition(saveAsMySQL(_))避免一个foreach创建一个连接,还可以保存前repartition优化。
注意:
    输入只有一个分区

7.orc文件:

sparkSession.read.orc(shipmentInfoPath)
小文件处理:
    orc本身适合存储256M以上,看看源头
    coalesce减小分区数

序列化:

概述:

初始化工作是在Driver端进行的,实际运行程序是在Executor端进行的,变量的传递涉及到了进程通信,是需要序列化的。

自定义类型:

使用 case class
实现 Serializable 接口

依赖关系:

概述:

依赖可以将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。

作用:

1.记录Lineage
2.用来划分stage

分类:

窄依赖。RDDs之间分区是一一对应的(1:1 或 n:1
    算子:unioncartesian、分好区的join
宽依赖。子RDD每个分区与父RDD的每个分区都有关,是多对多的关系(即n:m)。有shuffle发生。依据:一个父RDD是否被多个子RDDpartition使用。
    算子:groupBydistinctrepartitionsortByintersectionsubtract(一些byKey的操作,大部分的join
        (bykey但不一定,要看rdd是否已经做过分区hashpartition,可以看源码,有判断partitioner是否相等)

区分:

1.可以通过页面web查看Job的图例来判断宽窄依赖。
2.可以查看rdd.dependencies
3.查看血缘关系rdd.toDebugString

划分Stage:

DAG(Directed Acyclic Graph) 有向无环图。原始的RDD通过一系列的转换就就形成了DAG,根据RDD之间的依赖关系的不同将DAG划分成不同的Stage:
对于窄依赖,partition的转换处理在Stage中完成计算
对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算。划分Stage的依据。
RDD任务切分为:Driver programe、Job、Stage(TaskSet)和Task
Driver program:初始化一个SparkContext即生成一个Spark应用
Job:一个Action算子就会生成一个Job
Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage
Task:Stage是一个TaskSet,将Stage划分的结果发送到不同的Executor执行即为一个Task
    Task是Spark中任务调度的最小单位;每个Stage包含许多Task,这些Task执行的计算逻辑相同的,计算的数据是不同的(分区)
注意:
Driver programe->Job->Stage-> Task每一层都是1对n的关系。

缓存:

概述:

涉及到的算子:persist、cache、unpersist;都是 Transformation
缓存是将计算结果写入不同的介质,用户定义可定义存储级别(存储级别定义了缓存存储的介质,目前支持内存、堆外内存、磁盘);
通过缓存,Spark避免了RDD上的重复计算,能够极大地提升计算速度;

场景:

如果多个动作需要用到某个 RDD,而它的计算代价又很高,那么就应该把这个 RDD 缓存起来;

注意:

缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除。
RDD的缓存的容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列的转换,丢失的数据会被重算。
RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。
以 分区为单位;程序执行完毕后,系统会清理cache数据;

使用:

persist()可以指定持久化级别参数
cache()会调用persist(MEMORY_ONLY)
unpersist()方法手动地把持久化的RDD从缓存中移除

常见级别:

MEMORY_ONLY        将RDD作为反序列化的对象存储JVM 中。如果RDD不能被内存装下,一些分区将不会被缓存,并且在需要的时候被重新计算。 默认的缓存级别
MEMORY_AND_DISK    将RDD 作为反序列化的的对象存储在JVM 中。如果RDD不能被与内存装下,超出的分区将被保存在硬盘上,并且在需要时被读取
MEMORY_ONLY_SER    将RDD 作为序列化的的对象进行存储(每一分区一个字节数组)。 通常来说,这比将对象反序列化的空间利用率更高,读取时会比较占用CPU
MEMORY_AND_DISK_SER与MEMORY_ONLY_SER 相似,但是把超出内存的分区将存储在硬盘上而不是在每次需要的时候重新计算
DISK_ONLY         只将RDD 分区存储在硬盘上
DISK_ONLY_2等    带2的与上述的存储级别一样,但是将每一个分区都复制到集群的两个结点上

容错机制Checkpoint:

概述:

涉及到的算子:checkpoint;也是 Transformation
检查点本质是通过将RDD写入高可靠的磁盘,主要目的是为了容错。检查点通过将数据写入到HDFS文件系统实现了RDD的检查点功能。
Lineage过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的RDD开始重做Lineage,就会减少开销。

与cache的区别:

缓存把 RDD 计算出来然后放在内存中,但是 RDD 的依赖链不能丢掉, 当某个点某个 executor 宕了,上面 cache 的RDD就会丢掉, 需要通过依赖链重放计算。
checkpoint 是把 RDD 保存在 HDFS中,是多副本可靠存储,此时依赖链可以丢掉,所以斩断了依赖链。

场景:

1) DAG中的Lineage过长,如果重算,则开销太大
2) 在宽依赖上做 Checkpoint 获得的收益更大,因为窄依赖重算当前分区即可,而宽依赖需要将之前所有的窄依赖都算一次。

使用:

rdd2.checkpoint
rdd2.isCheckpointed

分区与并行度:

默认分区规则:

spark.default.parallelism的默认值
    1、本地模式
        spark-shell --master local[N] spark.default.parallelism = N
        spark-shell --master local spark.default.parallelism = 1
    2、伪分布式  
        spark-shell --master local-cluster[x,y,z] spark.default.parallelism = x * y
    3、分布式模式(yarn & standalone)
        spark.default.parallelism = max(应用程序持有executor的core总数, 2)
RDD的分区数
    1、通过集合创建
        sc.defaultParallelism = spark.default.parallelism
    2、通过textFile创建
        sc.defaultMinPartitions = min(spark.default.parallelism, 2)
        本地文件:分区数 = max(本地文件分片数, sc.defaultMinPartitions)本地文件分片数 = 本地文件大小 / 32M
        HDFS文件:分区数 = max(hdfs文件 block 数, sc.defaultMinPartitions)指定的分区数 < hdfs文件的block数,指定的数不生效。

reduceTask分区数量的确定:

对于shuffle操作如reduceByKeyjoin为父RDD最大的分区数。
如果没有父rddparallelize,且非localmesos集群下,则默认是2或者所有executorscores数量。如果配置了spark.default.parallelism,则为该值。
注意:
    spark.default.parallelism参数只对HashPartitioner有效,所以如果是别的Partitioner或者自己实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。
    如果是别的partitioner导致的shuffle内存溢出,就需要从partitioner的代码增加partitions的数量。

spark-sql中用spark.sql.shuffle.partitions

RDD分区器:

概述:

只有Key-Value类型的RDD才可能有分区器,Value类型的RDD分区器的值是None。
分类
HashPartitioner:
    最简单、最常用,也是默认提供的分区器。对于给定的key,计算其hashCode,并除以分区的个数取余。
    如果余数小于0,则用 余数+分区的个数,最后返回的值就是这个key所属的分区ID。该分区方法可以保证key相同的数据出现在同一个分区中。
    主动使用:
        val rdd2 = rdd1.partitionBy(new org.apache.spark.HashPartitioner(10))
    算子: 
        一些ByKey的算子
RangePartitioner:
    简单的说就是将一定范围内的数映射到某一个分区内。sortByKey会使用RangePartitioner。
    在执行分区之前其实并不知道数据的分布情况,如果想知道数据分区就需要对数据进行采样;水塘抽样算法。
    水塘采样:从包含n个项目的集合S中选取k个样本,其中n为一很大或未知的数量,尤其适用于不能把所有n个项目都存放到主内存的情况;
    在采样的过程中执行了collect()操作,引发了Action操作。

自定义分区器:

Spark允许用户通过自定义的Partitioner对象,灵活的来控制RDD的分区方式。
实现: 
    继承Partitioner接口,重写getPartition方法和numPartitions变量
    自己定义分区数量,需要自己去统计数据集大小。然后根据key决定发送哪些数据到不同的partition。

广播变量:

概述:

有时候需要在多个任务之间共享变量,或者在任务(Task)和Driver Program之间共享变量。
为了满足这种需求,Spark提供了两种类型的变量:
    广播变量(broadcast variables)
    累加器(accumulators)
广播变量将变量在节点的 Executor 之间进行共享(由Driver广播出去);
广播变量用来高效分发较大的对象。向所有工作节点(Executor)发送一个较大的只读值,以供一个或多个操作使用。

解决的问题:

在算子函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC,都会极大地影响性能。

过程如下:

1.对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T]对象。 任何可序列化的类型都可以这么实现(在 Driver 端)
2.通过 value 属性访问该对象的值(在 Executor 中)
3.变量只会被发到各个 Executor 一次,作为只读值处理

相关参数:

spark.broadcast.blockSize(缺省值:4m
spark.broadcast.checksum(缺省值:true
spark.broadcast.compress(缺省值:true

运用:

将普通的Join变为Map Side Join,去掉了shuffle过程。
val productBC = sc.broadcast(productRDD.collectAsMap())
val resultRDD = orderRDD.map{case (pid, orderInfo) =>
    val productInfo = productBC.value
    (pid, (orderInfo, productInfo.getOrElse(pid, null)))        //直接在map里获取广播变量的值
}

累加器:

概述:

累加器的作用:可以实现一个变量在不同的 Executor 端能保持状态的累加;
累计器在 Driver 端定义,读取;在 Executor 中完成累加;
累加器也是 lazy 的,需要 Action 触发;Action触发一次,执行一次,触发多次,执行多次;

场景:

记录某些事件的数量;

分类:

LongAccumulator 用来累加整数型
DoubleAccumulator 用来累加浮点型
CollectionAccumulator 用来累加集合元素

Standalone作业提交原理:

集群的组成部分:

Driver:用户编写的 Spark 应用程序就运行在 Driver 上,由Driver 进程执行
Master:主要负责资源的调度和分配,并进行集群的监控等职责
Worker:Worker 运行在集群中的一台服务器上。负责管理该节点上的资源,负责启动启动节点上的 Executor
Executor:一个 Worker 上可以运行多个 Executor,Executor通过启动多个线程(task)对 RDD 的分区进行并行计算

SparkContext 中的三大组件:

DAGScheduler:负责将DAG划分成若干个Stage
TaskScheduler:将DAGScheduler提交的 Stage(Taskset)进行优先级排序,再将task 发送到 Executor
SchedulerBackend:
    定义了许多与Executor事件相关的处理,包括:新的executor注册进来的时候记录executor的信息,增加全局的资源量(核数);
    executor更新状态,若任务完成的话,回收core;其他停止executor、remove executor等事件 

提交步骤:

1、启动应用程序,完成SparkContext的初始化
2、Driver向Master注册,申请资源
3、Master检查集群资源状况。若集群资源满足,通知Worker启动Executor
4、Executor启动后向Driver注册(称为反向注册)
5、Driver完成DAG的解析,得到Tasks,然后向Executor发送Task
6、Executor 向Driver汇总任务的执行情况
7、应用程序执行完毕,回收资源

Shuffle原理:

概述:

Shuffle是MapReduce计算框架中的一个特殊的阶段,介于Map 和 Reduce 之间。
Map的输出结果要被Reduce使用时,输出结果需要按key排列,并且分发到Reducer上去,这个过程就是shuffle。
shuffle涉及到了本地磁盘(非hdfs)的读写和网络的传输,大多数Spark作业的性能主要就是消耗在了shuffle环节。因此shuffle性能的高低直接影响到了整个程序的运行效率

算法发展过程:

在Spark Shuffle的实现上,经历了Hash、Sort、Tungsten-Sort(堆外内存)三阶段

现阶段实现:

类似Hadoop Shuffle,使用排序,避免了hash产生海量小文件的问题。

RDD编程优化:

1、RDD复用

避免创建重复的RDD。在开发过程中要注意:对于同一份数据,只应该创建一个RDD,不要创建多个RDD来代表同一份数据。

2、RDD缓存/持久化

3、巧用 filter

filter过滤掉较多数据后,使用 coalesce 对数据进行重分区

4、使用高性能算子

1、避免使用groupByKey,根据场景选择使用高性能的聚合算子 reduceByKey、aggregateByKey
2、coalesce、repartition,在可能的情况下优先选择没有shuffle的操作
3、foreachPartition 优化输出操作
4、map、mapPartitions,选择合理的选择算子mapPartitions性能更好,但数据量大时容易导致OOM
5、用 repartitionAndSortWithinPartitions 替代 repartition + sort 操作
6、减少对数据源的扫描(算法复杂了,比如rangePartitioner之前没用水塘算法,必须要遍历两次)

5、设置合理的并行度

Spark作业中的并行度指各个stage的task的数量,设置合理的并行度,让并行度与资源相匹配。简单来说就是在资源允许的前提下,并行度要设置的尽可能大,达到可以充分利用集群资源。
合理的设置并行度,可以提升整个Spark作业的性能和运行速度

6、广播大变量

使用广播变量,只会在每个Executor保存一个副本,Executor的所有task共用此广播变量,这样就节约了网络及内存资源

Spark SQL:

概述:

Spark SQL自从面世以来不仅接过了shark的接力棒,为spark用户提供高性能的SQL on hadoop的解决方案,还为spark带来了通用的高效的,多元一体的结构化的数据处理能力。

优势:

写更少的代码
读更少的数据(SparkSQL的表数据在内存中存储不使用原生态的JVM对象存储方式,而是采用内存列存储)
提供更好的性能(字节码生成技术、SQL优化)

数据抽象:

概述:

SparkSQL提供了两个新的抽象,分别是DataFrameDataSet
DataFrameDataset进行操作许多操作都需要这个包进行支持, import spark.implicits._
同样的数据都给到这三个数据结构,经过系统的计算逻辑,都得到相同的结果。不同是它们的执行效率和执行方式;
在后期的Spark版本中,DataSet会逐步取代 RDD 和 DataFrame 成为唯一的API接口。

DataFrame:

概述:
DataFrame可以看做分布式 Row 对象的集合,提供了由列组成的详细模式信息,使其可以得到优化。DataFrame 不仅有比RDD更多的算子,还可以进行执行计划的优化
DataFrame更像传统数据库的二维表格,除了数据以外,还记录数据的结构信息,即schema
DataFrame也支持嵌套数据类型(struct、array和map
DataFrame API提供的是一套高层的关系操作,比函数式的RDD API要更加友好,门槛更低
Dataframe的劣势在于在编译期缺少类型安全检查,导致运行时出错。
与RDD和Dataset不同,DataFrame每一行的类型固定为Row,只有通过解析才能获取各个字段的值
与RDD的关系:
DataFrame(即带有Schema信息的RDD,但不是强类型,执行才判断),Spark通过Schema就能够读懂数据。
DataFrame = RDD[Row] + Schema
创建:
1、由集合生成
val lst = List(("Jack"28184), ("Tom"10144), ("Andy",16165))
val df1 = spark.createDataFrame(lst).withColumnRenamed("_1""name1").withColumnRenamed("_2""age1").withColumnRenamed("_3""height1")
2、由RDD生成
val rddToDF = spark.createDataFrame(rdd1, schema)
或者通过case class
val rdd2: RDD[Person] = spark.sparkContext.makeRDD(arr2).map(f=>Person(f._1, f._2,f._3))
rdd.toDF()
或者
rdd.toDF("col1","col2")
3、从文件创建
val df3 = spark.read.options(Map(("delimiter"";"), ("header""true"))).schema(schema).csv("data/people2.csv")
4、由ds生成
val testDF = testDS.toDF

DataSet:

概述:
RDD相比,保存了更多的描述信息,概念上等同于关系型数据库中的二维表;
DataFrame相比,保存了类型信息,是强类型的,提供了编译时类型检查;(Dataset[Row],每一行的类型是Row,不解析,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用上面提到的getAS方法)
调用Dataset的方法先会生成逻辑计划,然后Spark的优化器进行优化,最终生成物理计划,然后提交到集群中运行。
DataSet包含了DataFrame的功能,在Spark2.0中两者得到了统一:DataFrame表示为DataSet[Row],即DataSet的子集。
优点:
df/ds都基于spark sql引擎,使用Catalyst分析执行计划。
Dataset[T] 类型化 API 针对数据工程任务进行了优化,而非类型化 Dataset[Row](DataFrame 的别名)甚至更快,适合交互式分析。
与RDD的关系:
Dataset(Dataset = RDD[case class].toDS)
与DataFrame的关系
1、Dataset和DataFrame拥有完全相同的成员函数,区别只是每一行的数据类型不同;
2、DataFrame 定义为 Dataset[Row]。每一行的类型是Row,每一行究竟有哪些字段,各个字段又是什么类型都无从得知,只能用前面提到的getAS方法或者模式匹配拿出特定字段;
3、Dataset每一行的类型都是一个case class,在自定义了case class之后可以很自由的获得每一行的信息;
创建:
1、由range生成
val numDS = spark.range(51005)
2、由集合生成
case class Person(name:String, age:Int, height:Int)
val seq1 = Seq(Person("Jack"28184), Person("Tom"10,144), Person("Andy"16165))
val ds1 = spark.createDataset(seq1)
3、由RDD生成
val rdd2: RDD[Person] = spark.sparkContext.makeRDD(arr2).map(f=>Person(f._1, f._2,f._3))
val ds3 = spark.createDataset(rdd2)
或者
val ds2 = rdd2.toDS()
4.由df生成:
case class Coltest(col1:String,col2:Int)extends Serializable
val testDS = testDF.as[Coltest]
5.通过spark.read生成:
val people = spark.read.parquet("...").as[Person]  // Scala
Dataset<Person> people = spark.read().parquet("...").as(Encoders.bean(Person.class)); // Java
适合场景:
1.想要丰富的语义、高级抽象和特定领域的 API,请使用 DataFrame 或 Dataset
    rdd写自定义操作比sql简单,但需要注意gc的情况,防止数据量过大。
    而df只需要定义好spark.sql.shuffle.partitionsshuffle的过程一般gc时间少。
2.处理需要高级表达式、过滤器、映射、聚合、平均值、求和、SQL 查询、列式访问以及对半结构化数据使用 lambda 函数,请使用 DataFrame 或 Dataset。 
3.想在编译时获得更高程度的类型安全性、需要类型化的 JVM 对象、利用 Catalyst 优化并从 Tungsten 的高效代码生成中受益,请使用 Dataset。 
4.希望跨 Spark 库统一和简化 API,请使用 DataFrame 或 Dataset。 
5.如果是 R 用户,请使用 DataFrames。 
5.如果是 Python 用户,请使用 DataFrames 并在需要更多控制时使用 RDD
选择dataset还是dataframe:
dataset经过map、groupBy、Join操作后,经常变为dataframe,而且dataset要写case class
从Spark 2.0开始,DataFrame和DataSet的API合并在一起,实现了跨库统一成为一套API。
官方建议使用Dataset
df没有字段引用,只能通过row.getString(int)索引来引用,ds有。

Row:

Row是一个泛化的无类型 JVM object

Schema:

DataFrame中提供了详细的数据结构信息,从而使得SparkSQL可以清楚地知道该数据集中包含哪些列,每列的名称和类型各是什么,DataFrame中的数据结构信息,即为schema。
创建:
    val schema1 = StructType( StructField("name", StringType,false) ::StructField("age", IntegerType,false) ::StructField("height", IntegerType,false) :: Nil)
    val schema2 = StructType( Seq(StructField("name", StringType,false),StructField("age", IntegerType,false),StructField("height",IntegerType, false)))
    val schema4 = (new StructType).add(StructField("name", StringType, false)).add(StructField("age", IntegerType, false)).add(StructField("height", IntegerType, false))

SparkSession:

概述:

SparkSession,SparkSession 封装了 SqlContext 及HiveContext;实现了 SQLContext 及 HiveContext 所有功能;
通过SparkSession可以获取到SparkConetxt;

创建:

val spark = SparkSession.builder().appName("Spark SQL basic example").config("spark.some.config.option""some-value").getOrCreate()

Action操作:

与RDD类似的操作:

showcollect、collectAsList、headfirstcount、take、takeAsList、reduce

与结构相关:

printSchema、explaincolumns、dtypes、col

Transformation 操作:

与RDD类似的操作:

map(得到无命名的字段)、filter、flatMap、mapPartitions、sample、 randomSplit、 limit、distinct、dropDuplicates、describe
示例:
    df.filter("col!=0")

存储相关:

cacheTable、persist、checkpoint、unpersist、cache
备注:Dataset 默认的存储级别是 MEMORY_AND_DISK

select相关:

select、selectExpr、drop、withColumn添加新字段、withColumnRenamed、cast(内置函数)
列的多种表示方法。使用""、$""、'、col()、ds("")
示例: 
    df1.select($"
ename", $"hiredate", $"sal").show
    df1.select("
ename", "hiredate", "sal").show
    df1.select('ename, 'hiredate, 'sal).show
    df1.select(col("
ename"), col("hiredate"), col("sal")).show
    df1.select(df1("
ename"), df1("hiredate"), df1("sal")).show
    df1.select($"
ename", $"hiredate", $"sal"+100).show
    df1.select(expr("
comm+100"), expr("sal+100"),expr("ename")).show  //expr里面只能使用引
增加一列:
    df.withColumn("
Country", lit("USA")) 
复杂select:
    selectExpr("
udid_md5", "if(partition_id is not null, concat(type, '-siToken'), type) as type, if(partition_id is null,0,partition_id) as token_id")

where相关:

where == filter

groupBy相关:

groupBy、agg、maxmin、avg、sum、count(后面5个为内置函数)
实现reductByKey操作:
    ds.groupBy($"key").agg(sum($"value").alias("value")) 得到df

orderBy相关:

orderBy == sort

join相关:

crossJoin
join        
    df1.join(df1, "empno").count
    df1.join(df1, Seq("empno""ename")).show               # 可以避免写多个。可以重名。之后接的select,如果非join键还得用df()的方式,join键默认是主表(看join类型)。
    df1.join(df2, $"name"===$"sname""left").show

    ds1.join(ds2, ds1($"name")===ds2($"name"), "left").show  可以避免重名的情况。后面的select类似。
    ds1.join(ds2, $"name"===$"sname""left_outer").show
    ds1.join(ds2, $"name"===$"sname""right").show
    ds1.join(ds2, $"name"===$"sname""right_outer").show
    ds1.join(ds2, $"name"===$"sname""outer").show
    ds1.join(ds2, $"name"===$"sname""full").show
    ds1.join(ds2, $"name"===$"sname""full_outer").show
    ds1.alias("ta").join(ds2,$"ta.name"=ds2("name")).show()

集合相关:

union==unionAll(过期)、intersect、except

空值处理:

na.fill、na.drop、na.replace、ds.filter($"comm".isNull)

窗口函数:

val w1 = Window.partitionBy("cookieid").orderBy("createtime")
val w2 = Window.partitionBy("cookieid").orderBy("pv")
val w3 = w1.rowsBetween(Window.unboundedPreceding,Window.currentRow)
val w4 = w1.rowsBetween(-11)
使用: 
    .over(w2)

其他函数:

lit()        用法df.withColumn("key4", lit("new_str_col"))

SQL语句:

概述:

SparkSQL与HQL语法兼容;与HQL相比,SparkSQL更简洁。

创建表:

ds.createTempViewcreateOrReplaceTempView
执行sql
spark.sql("SQL")

并行度设置:

spark.sql.shuffle.partitions 默认200,sql中发生join或者聚合时,使用该配置。

输入与输出:

概述:

SparkSQL内建支持的数据源包括:Parquet、JSON、CSV、Avro、Images、BinaryFiles(Spark 3.0)。其中Parquet是默认的数据源。
CSV
输入val df3 = spark.read.format("csv").option("inferSchema""true").option("header""true").load("data/people1.csv")
输出DataFrame.write.format(args).option(args).bucketBy(args).partitionBy(args).save(path)

Parquet文件:

读取:
sql形式:
spark.sql(
    """
    |CREATE OR REPLACE TEMPORARY VIEW users
    |USING parquet
    |OPTIONS (path "data/users.parquet")
    |"""
.stripMargin
    )
普通形式:
spark.read.parquet(path)得到df
写入:
df.write.format("parquet").mode("overwrite").option("compression""snappy").save("data/parquet")

json文件:

输入val df6 = spark.read.format("json").load(fileJson),或者sql语法也可以
输出spark.sql("SELECT * FROM emp").write.format("json").mode("overwrite").save("data/json")
JDBC
val jdbcDF = spark.read.format("jdbc")
    .option("url""jdbc:mysql://linux123:3306/ebiz?useSSL=false")
    .option("driver""com.mysql.jdbc.Driver")
    .option("dbtable""lagou_product_info")
    .option("user""hive").option("password""12345678")
    .load()
    读取优化:
        jdbc默认读取是一个分区数,这个时候spark UI面板上可以看到GC频繁。
        思路1:
            .option("partitionColumn""date")可以设置分区数。注意select要包含声明的date
            .option("numPartitions"30)
            .option("lowerBound", startDay)
            .option("upperBound", endDay)
        思路2:
            foreach每个日期执行一个分区sql拉取数据。
jdbcDF.write.format("jdbc").option("url""jdbc:mysql://linux123:3306/ebiz?useSSL=false&characterEncoding=utf8")
    .option("user""hive").option("password""12345678")
    .option("driver""com.mysql.jdbc.Driver")
    .mode("append").save
    输出优化:
        1.useServerPrepStmts=false&rewriteBatchedStatements=true
            参考https://stackoverflow.com/questions/36912442/low-jdbc-write-speed-from-spark-to-mysql,开启预编译和批量
        2.write前使用repartition控制分区数量
        3.format("jdbc").option("batchsize",1000)默认大小为1000
            其他方式.jdbc(url,tableName, connectionProperties)需要放到properties里面

关于mode说明:
    SaveMode.ErrorIfExists(默认)。若表存在,则会直接报异常,数据不能存入数据库
    SaveMode.Append。若表存在,则追加在该表中(唯一索引会报错);若该表不存在,则会先创建表,再插入数据
    SaveMode.Overwrite。先将已有的表及其数据全都删除,再重新创建该表,最后插入新的数据
    SaveMode.Ignore。若表不存在,则创建表并存入数据;若表存在,直接跳过数据的存储,不会报错

UDF:

概述:

UDF(User Defined Function),自定义函数。函数的输入、输出都是一条数据记录,类似于Spark SQL中普通的数学或字符串函数。实现上看就是普通的Scala函数;

创建:

def len(bookTitle: String):Int = bookTitle.length
spark.udf.register("len", len _)

使用:

对于sql
    val booksWithLongTitle = spark.sql("select title, author from books where len(title) > 10")
对于DF
    val booksWithLongTitle = dataFrame.filter("longLength(title,10)")
    或者 
    val longLength = udf((bookTitle: String, length: Int) => bookTitle.length > length) //这种函数无需注册
    val booksWithLongTitle = dataFrame.filter(longLength($"title",lit(10)))    //可以直接使用,而不是在字符串内。

UDAF:

概述:

UDAF(User Defined Aggregation Funcation),用户自定义聚合函数。函数本身作用于数据集合,能够在聚合操作的基础上进行自定义操作(多条数据输入,一条数据输出);
类似于在group by之后使用的sum、avg等函数;
普通的UDF不支持数据的聚合运算。

创建:

类型不安全:
继承UserDefinedAggregateFunction,要实现父类的几个抽象方法。
通过input.getAs[Double](0)获取输入的值,涉及到类型转换
类型安全:
继承Aggregator[泛型]类,泛型写上case class

访问Hive:

在 pom 文件中增加依赖:

在 resources中增加hive-site.xml文件
    最好使用 metastore service 连接Hive;使用直连 metastore 的方式时,SparkSQL程序会修改 Hive 的版本信息;

示例:

val spark = SparkSession.builder().appName("Demo1").master("local[*]").enableHiveSupport()
    .config("spark.sql.parquet.writeLegacyFormat"true)    // 设为true时,Spark使用与Hive相同的约定来编写Parquet数据
    .getOrCreate()
spark.sql("show databases").show
spark.sql("select * from ods.ods_trade_product_info").show
保存df.write.mode(SaveMode.Append).saveAsTable("ods.ods_trade_pro duct_info_back")

Join原理:

概述:

在 Spark 的物理计划阶段,Spark的Join Selection类会根据Join hints策略、Join 表的大小、Join是等值Join还是不等值以及参与Joinkey是否可以排序等条件来选择最终的 Join 策略,
最后Spark会利用选择好的Join策略执行最终的计算。

Join 策略:

Broadcast hash join (BHJ)(优化,避免了 Shuffle 操作)
    流程:
        利用 collect 算子将小表的数据从 Executor 端拉到 Driver 端
        在 Driver 端调用 sparkContext.broadcast 广播到所有 Executor 端
        在 Executor 端使用广播的数据与大表进行 Join 操作(实际上是执行map操作)
    条件:
        1.小表的数据必须很小,可以通过 spark.sql.autoBroadcastJoinThreshold 参数来配置,默认是 10MB。如果内存比较大,可以将阈值适当加大
        2.将 spark.sql.autoBroadcastJoinThreshold 参数设置为 -1,可以关闭这种连接方式
        3.只能用于等值 Join,不要求参与 Join 的 keys 可排序
Shuffle hash join(SHJ)
    把大表和小表按照相同的分区算法和分区数进行分区(根据参与 Join 的keys 进行分区),这样就保证了 hash 值一样的数据都分发到同一个分区中,
    然后在同一个 Executor 中两张表 hash 值一样的分区就可以在本地进行 hash Join 了
    条件: 
        仅支持等值 Join,不要求参与 Join 的 Keys 可排序
        spark.sql.join.preferSortMergeJoin 参数必须设置为 false,参数是从 Spark2.0.0 版本引入的,默认值为 true,也就是默认情况下选择 Sort Merge Join
        小表的大小(plan.stats.sizeInBytes)必须小于spark.sql.autoBroadcastJoinThreshold * spark.sql.shuffle.partitions(默认值200
        而且小表大小(stats.sizeInBytes)的三倍必须小于等于大表的大小(stats.sizeInBytes),也就是 a.stats.sizeInBytes * 3 < = b.stats.sizeInBytes
Shuffle sort merge join (SMJ)(普通join)
    思想: 
        将两张表按照 join key 进行shuffle,保证join key值相同的记录会被分在相应的分区
        对每个分区内的数据进行排序,排序后再对相应的分区内的记录进行连接
    条件: 
        仅支持等值 Join,并且要求参与 Join 的 Keys 可排序
Shuffle-and-replicate nested loop join,又称笛卡尔积(Cartesian product join)
Broadcast nested loop join (BNLJ)(非等值join
    支持等值和不等值 Join,支持所有的 Join 类型。

SQL解析过程:

概述:

Spark SQL对SQL语句的处理和关系型数据库类似,即词法/语法解析、绑定、优化、执行。
Spark SQL会先将SQL语句解析成一棵树,然后使用规则(Rule)对Tree进行绑定、优化等处理过程。

Sql构成:

Core: 负责处理数据的输入和输出,如获取数据,查询结果输出成DataFrame等
Catalyst: 负责处理整个查询过程,包括解析、绑定、优化等
Hive: 负责对Hive数据进行处理
Hive-ThriftServer: 主要用于对Hive的访问

查看执行计划:

df.queryExecution
顺序包含了Parsed Logical Plan(解析sql)、Analyzed Logical Plan(分析逻辑计划)、Optimized Logical Plan(优化)、Physical Plan

常见优化:

谓词下推(Push Down Predicate)、常量折叠(Constant Folding)、字段裁剪(Columning Pruning)
做完逻辑优化,还需要先转换为物理执行计划,将逻辑上可行的执行计划变为 Spark可以真正执行的计划。
SparkSQL 把逻辑节点转换为了相应的物理节点, 比如 Join 算子,Spark 根据不同场景为该算子制定了不同的算法策略。

Spark Streaming:

概述:

Spark Streaming具有有高吞吐量和容错能力强等特点;
Spark Streaming支持的数据输入源很多,例如:Kafka(最重要的数据源)、Flume、Twitter 和 TCP 套接字等;
数据输入后可用高度抽象API,如:map、reduce、join、window等进行运算;
处理结果能保存在很多地方,如HDFS、数据库等;    
Spark Streaming使用离散化流(Discretized Stream)作为抽象表示,称为DStream。
DStream 可以从各种输入源创建,比如 Flume、Kafka 或者 HDFS。创建出来的
DStream 支持两种操作:
    转化操作,会生成一个新的DStream
    输出操作(output operation),把数据写入外部系统中
DStream 提供了许多与 RDD 所支持的操作相类似的操作支持,还增加了与时间相关的新操作,比如滑动窗口。

架构:

概述:

Spark Streaming使用 mini-batch 的架构,把流式计算当作一系列连续的小规模批处理来对待。
Spark Streaming从各种输入源中读取数据,并把数据分组为小的批次。新的批次按均匀的时间间隔创建出来。
在每个时间区间开始的时候,一个新的批次就创建出来,在该区间内收到的数据都会被添加到这个批次中。在时间区间结束时,批次停止增长。
时间区间的大小是由批次间隔这个参数决定的。批次间隔一般设在500毫秒到几秒之间,由开发者配置。
DStream是一个 RDD 序列,每个RDD代表数据流中一个时间片内的数据。

运行流程:

1、客户端提交Spark Streaming作业后启动Driver,Driver启动Receiver,Receiver接收数据源的数据
2、每个作业包含多个Executor,每个Executor以线程的方式运行task,Spark Streaming至少包含一个receiver task(一般情况下)
3、Receiver接收数据后生成Block,并把BlockId汇报给Driver,然后备份到另外一个 Executor 上
4、ReceiverTracker维护 Reciver 汇报的BlockId
5、Driver定时启动JobGenerator,根据Dstream的关系生成逻辑RDD,然后创建Jobset,交给JobScheduler。!!!
6、JobScheduler负责调度Jobset,交给DAGScheduler,DAGScheduler根据逻辑RDD,生成相应的Stages,每个stage包含一到多个Task,将TaskSet提交给TaskSchedule
7、TaskScheduler负责把 Task 调度到 Executor 上,并维护 Task 的运行状态

优缺点:

优点:

Spark Streaming 内部的实现和调度方式高度依赖 Spark 的 DAG 调度器和RDD,这就决定了 Spark Streaming 的设计初衷必须是粗粒度方式的。
同时,由于 Spark 内部调度器足够快速和高效,可以快速地处理小批量数据,这就获得准实时的特性
由于 DStream 是在 RDD 上的抽象,那么也就更容易与 RDD 进行交互操作,在需要将流式数据和批处理数据结合进行分析的情况下,将会变得非常方便

缺点:

Spark Streaming 的粗粒度处理方式也造成了不可避免的延迟。

与Structured Streaming比较:

spark streaming的缺点:

框架自身只能根据 Batch Time 单元进行数据处理,很难处理基于event time(即时间戳)的数据,很难处理延迟,乱序的数据
端到端的数据容错保障逻辑需要用户自己构建,难以处理增量更新和持久化存储等一致性问题

Structure Streaming的思想:

将数据源映射为一张无界长度的表,通过表的计算,输出结果映射为另一张表。
以结构化的方式去操作流式数据,简化了实时计算过程,同时还复用了 Catalyst 引擎来优化SQL操作。此外还能支持增量计算和基于event time的计算。

基础数据源:

文件数据流:

特点:
不支持嵌套目录
文件需要有相同的数据格式
文件进入 directory 的方式需要通过移动或者重命名来实现
一旦文件移动进目录,则不能再修改,即便修改了也不会读取新数据
文件流不需要接收器(receiver),不需要单独分配CPU核
创建:
val lines = ssc.textFileStream("data/log/")

Socket数据流:

特点:
如果给虚拟机配置的cpu数为1,使用local[*]也只会启动一个线程,该线程用于receiver task,此时没有资源处理接收达到的数据。
DStream的 StorageLevel 是 MEMORY_AND_DISK_SER_2;
创建:
val lines = ssc.socketTextStream("linux122"9999)

RDD队列流:

创建:
val rddQueue = new Queue[RDD[Int]]()
val queueStream = ssc.queueStream(rddQueue)
特点:
参数oneAtATime:缺省为true,一次处理一个RDD;设为false,一次处理全部RDD
RDD队列流可以使用local[1]
涉及到同时出队和入队操作,所以要做同步

DStream操作:

无状态转换:

map(func)        将源DStream中的每个元素通过一个函数func从而得到新的DStreams
flatMap(func)    和map类似,但是每个输入的项可以被映射为0或更多项
filter(func)    选择源DStream中函数func判为true的记录作为新DStreams
repartition(numPartitions)通过创建更多或者更少的partition来改变此DStream的并行级别
union(otherStream)联合源DStreams和其他DStreams来得到新DStream
count()            统计源DStreams中每个RDD所含元素的个数得到单元素RDD的新DStreams
reduce(func)    通过函数func(两个参数一个输出)来整合源DStreams中每个RDD元素得到单元素RDDDStreams。这个函数需要关联从而可以被并行计算
countByValue()    对于DStreams中元素类型为K调用此函数,得到包含(K,Long)对的新DStream,其中Long值表明相应的K在源DStream中每个RDD出现的频率
reduceByKey(func,[numTasks])(K,V)对的DStream调用此函数,返回同样(K,V)的新DStream,新DStream中的对应V为使用reduce函数整合而来。
    默认情况下,这个操作使用Spark默认数量的并行任务(本地模式为2,集群模式中的数量取决于配置参数spark.default.parallelism)。也可以传入可选的参数numTasks来设置不同数量的任务
join(otherStream,[numTasks])DStream分别为(K,V)(K,W)对,返回(K,(V,W))对的新DStream
cogroup(otherStream,[numTasks])DStream分别为(K,V)(K,W)对,返回(K,(Seq[V],Seq[W])对新DStreams
** transform(func)RDDRDD映射的函数func作用于源DStream中每个RDD上得到新DStream。这个可用于在DStreamRDD上做任意操作。

有状态转换:

窗口操作
    概述: 
        基于窗口的操作会在一个比 StreamingContext 的 batchDuration(批次间隔)更长的时间范围内,通过整合多个批次的结果,计算出整个窗口的结果。
    参数:
        窗口长度(windowDuration)。控制每次计算最近的多少个批次的数据
        滑动间隔(slideDuration)。用来控制对新的 DStream 进行计算的间隔。两者都必须是 StreamingContext 中批次间隔(batchDuration)的整数倍。
    函数:
        reduceByWindow(_+_, Seconds(20), Seconds(10))
        window(Seconds(20),Seconds(10))
        reduceByKeyAndWindow((a: Int, b: Int) => a + b,Seconds(20),Seconds(10))
状态追踪操作:
    UpdateStateByKey
        为Streaming中每一个Key维护一份state状态,state类型可以是任意类型的,可以是自定义对象;更新函数也可以是自定义的
        通过更新函数对该key的状态不断更新,对于每个新的batch而言,Spark Streaming会在使用updateStateByKey 的时候为已经存在的key进行state的状态更新
        使用 updateStateByKey 时要开启 checkpoint 功能
        在每一个批次的时候返回之前所有的key的状态,可能会有内存问题
    mapWithState
        用于全局统计key的状态。如果没有数据输入,便不会返回之前的key的状态,有一点增量的感觉。
        checkpoint也不会像updateStateByKey那样,占用太多的存储。

DStream输出操作:

概述:

与 RDD 中的惰性求值类似,如果一个 DStream 及其派生出的 DStream 都没有被执行输出操作,那么这些 DStream 就都不会被求值。
如果 StreamingContext 中没有设定输出操作,整个流式作业不会启动。

操作:

print()在运行流程序的Driver上,输出DStream中每一批次数据的最开始10个元素。用于开发和调试
saveAsTextFiles(prefix,[suffix])以text文件形式存储 DStream 的内容。每一批次的存储文件名基于参数中的prefixsuffix
saveAsObjectFiles(prefix,[suffix])以 Java 对象序列化的方式将Stream中的数据保存为 Sequence Files。每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"
saveAsHadoopFiles(prefix,[suffix])将Stream中的数据保存为 Hadoop files。每一批次的存储文件名基于参数中的为"prefix-TIME_IN_MS[.suffix]"
foreachRDD(func)最通用的输出操作。将函数 func 应用于DStream 的每一个RDD

foreachRDD:

通用的输出操作 foreachRDD,用来对 DStream 中的 RDD 进行任意计算。在foreachRDD中,可以重用 Spark RDD 中所有的 Action 操作。
需要注意的:
    连接不要定义在 Driver 中
    连接定义在 RDD的 foreach 算子中,则遍历 RDD 的每个元素时都创建连接,得不偿失
    应该在 RDD的 foreachPartition 中定义连接,每个分区创建一个连接
    可以考虑使用连接池

与Kafka整合:

概述:

对Kafka的支持分为两个版本08(在高版本中将被废弃)、010,两个版本不兼容。
针对不同的spark、kafka版本,集成处理数据的方式分为两种:Receiver Approach和Direct Approach,不同集成版本处理方式的支持

Receiver based Approach:

基于 Receiver 的方式使用 Kafka 旧版消费者高阶API实现。
对于所有的 Receiver,通过 Kafka 接收的数据被存储于 Spark 的 Executors上,底层是写入BlockManager中,默认200ms生成一个blockspark.streaming.blockInterval)。
然后由 Spark Streaming 提交的 job 构建BlockRDD,最终以 Spark Core任务的形式运行。
注意:
    Receiver 作为一个常驻线程调度到 Executor上运行,占用一个cpu
    Receiver 个数由KafkaUtils.createStream调用次数决定,一次一个 Receiver
    receiver默认200ms生成一个block,可根据数据量大小调整block生成周期。一个block对应RDD一个分区。
    receiver接收的数据会放入到BlockManager,每个 Executor 都会有一个BlockManager实例,由于数据本地性,那些存在 Receiver 的 Executor 会被调度执行更多的 Task
        就会导致某些executor比较空闲
    默认情况下,Receiver是可能丢失数据的。可以通过设置spark.streaming.receiver.writeAheadLog.enabletrue开启预写日志机制,将数据先写入一个可靠地分布式文件系统(如HDFS),
    确保数据不丢失,但会损失一定性能    

Direct Approach:

概述:
不使用 Receiver。减少不必要的CPU占用;减少了 Receiver接收数据写入BlockManager,然后运行时再通过blockId、网络传输、磁盘读取等来获取数据的整个过程,提升了效率;
    无需WAL,进一步减少磁盘IO;
Direct方式生的RDD是KafkaRDD,它的分区数与 Kafka 分区数保持一致,便于把控并行度
    注意:在 Shuffle 或 Repartition 操作后生成的RDD,这种对应关系会失效
可以手动维护offset,实现 Exactly Once 语义
1.0示例
val dstream: InputDStream[ConsumerRecord[String, String]] =
    KafkaUtils.createDirectStream(ssc,LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String, String](topics,kafkaParams))
kafka配置:
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG
ConsumerConfig.GROUP_ID_CONFIG
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG        //"earliest"
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG    //false
LocationStrategies(本地策略):
LocationStrategies.PreferBrokers:如果 Executor 在 kafka 集群中的某些节点上,可以使用这种策略。此时Executor 中的数据会来自当前broker节点
LocationStrategies.PreferConsistent:大多数情况下使用的策略,将Kafka分区均匀的分布在Spark集群的 Executor
LocationStrategies.PreferFixed:如果节点之间的分区有明显的分布不均,使用这种策略。通过一个map指定将 topic 分区分布在哪些节点中
ConsumerStrategies(消费策略)
ConsumerStrategies.Subscribe,用来订阅一组固定topic
ConsumerStrategies.SubscribePattern,使用正则来指定感兴趣的topic
ConsumerStrategies.Assign,指定固定分区的集合
这三种策略都有重载构造函数,允许指定特定分区的起始偏移量;使用 Subscribe或 SubscribePattern 在运行时能实现分区自动发现。                

Offset管理:

1、获取偏移量(Obtaining Offsets)
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd.foreachPartition { iter => val o: OffsetRange = offsetRanges(TaskContext.get.partitionId)
                                    println(s"${o.topic} ${o.partition} ${o.fromOffset}${o.untilOffset}")
}
注意:对HasOffsetRanges的类型转换只有在对 createDirectStream 调用的第一个方法中完成时才会成功,而不是在随后的方法链中。
RDD分区和Kafka分区之间的对应关系在 shuffle 或 重分区后会丧失,如reduceByKey 或 window
2、存储偏移量(Storing Offsets)
在Streaming程序失败的情况下,Kafka交付语义取决于如何以及何时存储偏移量。
Spark输出操作的语义为 at-least-once。
如果要实现EOS语义(Exactly Once Semantics),必须在幂等的输出之后存储偏移量或者 将存储偏移量与输出放在一个事务中。
可以按照增加可靠性(和代码复杂度)的顺序使用以下选项来存储偏移量:
    Checkpoint
        对Spark Streaming运行过程中的元数据和每RDDs的数据状态保存到一个持久化系统中如HDFS
        如果Streaming程序的代码变了,重新打包执行就会出现反序列化异常的问题。这是因为Checkpoint首次持久化时会将整个 jar 包序列化,以便重启时恢复。
        重新打包之后,新旧代码逻辑不同,就会报错或仍然执行旧版代码。
        要解决这个问题,只能将HDFS上的checkpoint文件删除,但这样也会同时删除Kafka 的offset信息。   
    Kafka   
        自动提交:
            默认情况下,消费者定期自动提交偏移量,它将偏移量存储在一个特殊的Kafka主题中(__consumer_offsets)。
            但在某些情况下,这将导致问题,因为消息可能已经被消费者从Kafka拉去出来,但是还没被处理。
        手动提交
            一般处理并输出之后执行手动提交,确保至少消费一次。
            stream.foreachRDD { rdd =>
                val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
                stream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
            }
    自定义存储   
        步骤:
            1.在 DStream 初始化的时候,需要指定每个分区的offset用于从指定位置读取数据
            2.读取并处理消息
            3.处理完之后存储结果数据
            4.用虚线圈存储和提交offset,强调用户可能会执行一系列操作来满足他们更加严格的语义要求。这包括幂等操作和通过原子操作的方式存储offset
            5.将 offsets 保存在外部持久化数据库如 HBase、Kafka、HDFS、ZooKeeper、Redis、MySQL ... ...
        比较: 
            HDFS延迟有点高,小文件
            ZK不适合频繁的读写操作
Redis管理的Offset:
存储格式:
hash,key为s"$prefix:$topic:$groupId",field为partition,value为offset
读取:
val offsets: Map[TopicPartition, Long] = OffsetsRedisUtils.getOffsetFromRedis(topics, groupId)
val dstream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(ssc,
    LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String, String](topics,kafkaParams, offsets))
保存:
val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
offsetRanges.map(range => (range.topic, range.partition -> range.untilOffset))
.groupBy(_._1)
.map { case (topic, buffer) => (topic, buffer.map(_._2)) }
.foreach { case (topic, partitionAndOffset) =>
    val offsets: Array[(StringString)] = partitionAndOffset.map(elem => (elem._1.toString,elem._2.toString))
    import scala.collection.JavaConverters._
    jedis.hmset(getKey(topic, groupId), offsets.toMap.asJava)
}    

Structured Streaming:

概述:

spark streaming这种构建在微批处理上的流计算引擎,比较突出的问题就是处理延时较高(无法优化到秒以下的数量级),以及无法支持基于event_time的时间窗口做聚合逻辑。
Structured Streaming是一个基于Spark SQL引擎的可扩展、容错的流处理引擎。统一了流、批的编程模型,你可以使用静态数据批处理一样的方式来编写流式计算操作。
并且支持基于event_time的时间窗口的处理逻辑。

Dataflow模型:

1、核心思想

对无边界,无序的数据源,允许按数据本身的特征进行窗口计算,得到基于事件发生时间的有序结果,并能在准确性、延迟程度和处理成本之间调整。

2、四个维度

抽象出四个相关的维度,通过灵活地组合来构建数据处理管道,以应对数据处理过程中的各种复杂的场景
what 需要计算什么
where 需要基于什么时间(事件发生时间)窗口做计算
when 在什么时间(系统处理时间)真正地触发计算
how 如何修正之前的计算结果
论文的大部分内容都是在说明如何通过这四个维度来应对各种数据处理场景。

3、相关概念

事件时间和处理时间
    event_time,事件的实际发生时间
    process_time,处理时间,是指一个事件被数据处理系统观察/接收到的时间
窗口
    除了一些无状态的计算逻辑(如过滤,映射等),经常需要把无边界的数据集切分成有限的数据片以便于后续聚合处理(比如统计最近5分钟的XX等),窗口就应用于这类逻辑中
    常见的窗口包括:
    sliding window,滑动窗口,除了窗口大小,还需要一个滑动周期,比如小时窗口,每5分钟滑动一次。
    fixed window,固定窗口,按固定的窗口大小定义,比如每小时、天的统计逻辑。固定窗口可以看做是滑动窗口的特例,即窗口大小和滑动周期相等。
    sessions,会话窗口,以某一事件作为窗口起始,通常以时间定义窗口大小(也有可能是事件次数),发生在超时时间以内的事件都属于同一会话,比如统计用户启动APP之后一段时间的浏览信息等。

引擎:

默认情况下,结构化流式查询使用微批处理引擎进行处理,该引擎将数据流作为一系列小批处理作业进行处理,从而实现端到端的延迟,最短可达100毫秒,并且完全可以保证一次容错。
Spark 2.3以来,引入了一种新的低延迟处理模式,称为连续处理,它可以在至少一次保证的情况下实现低至1毫秒的端到端延迟。
也就是类似于 Flink 那样的实时流,而不是小批量处理。实际开发可以根据应用程序要求选择处理模式。

数据结构:

DataSet/DataFrame

编程模型:

一个流的数据源从逻辑上来说就是一个不断增长的动态表格,随着时间的推移,新数据被持续不断地添加到表格的末尾。
用户可以使用 Dataset/DataFrame 函数式API或者 SQL 来对这个动态数据源进行实时查询。每次查询在逻辑上就是对当前的表格内容执行一次 SQL 查询。
什么时候执行查询则是由用户通过触发器(Trigger)来设定时间(毫秒级)。用户既可以设定执行周期让查询尽可能快地执行,从而达到实时的效果也可以使用默认的触发。
示例: 
    result.writeStream
        .format("console")
        .outputMode("complete")
        .trigger(Trigger.ProcessingTime(0))//触发时间间隔
        .start()
        .awaitTermination()
一个流的输出有多种模式,
    可以是基于整个输入执行查询后的完整结果,complete
    也可以选择只输出与上次查询相比的差异,update
    或者就是简单地追加最新的结果。append

输入:

Socket source (for testing): 从socket连接中读取文本内容。
Kafka source: 从Kafka中拉取数据,与0.10或以上的版本兼容,后面单独整合Kafka
示例: 
    val df = spark
        .read
        .format("kafka")
        .option("kafka.bootstrap.servers""host1:port1,host2:port2")
        .option("subscribe""topic1")
        .load()

输出:

1.output mode:以哪种方式将result table的数据写入sink

1.Append mode:默认模式,新增的行才输出,每次更新结果集时,只将新添加到结果集的结果行输出到接收器。仅支持那些添加到结果表中的行永远不会更改的查询。
    因此,此模式保证每行仅输出一次。例如,仅查询selectwheremap,flatMap,filter,join等会支持追加模式。不支持聚合
2.Complete mode: 所有内容都输出,每次触发后,整个结果表将输出到接收器。聚合查询支持此功能。仅适用于包含聚合操作的查询。
3.Update mode:更新的行才输出,每次更新结果集时,仅将被更新的结果行输出到接收器(自Spark2.1.1起可用),不支持排序
2.format/output sink的一些细节:数据格式、位置等。
支持kafka、foreach、console

3.query name:指定查询的标识。类似tempview的名字

4.trigger interval:触发间隔,如果不指定,默认会尽可能快速地处理数据

5.checkpoint地址:一般是hdfs上的目录。注意:Socket不支持数据恢复,如果设置了,第二次启动会报错 ,Kafka支持

自定义writer使用foreach示例:

class RedisWriter extends ForeachWriter[BusInfo{
    var jedisPool: JedisPool = null
    var jedis: Jedis = null
    override def open(partitionId: Long, epochId: Long): Boolean = {
        jedis = RedisWriter.getConnection()
        true;
    }
    override def process(value: BusInfo): Unit = {
        val lglat: String = value.lglat
        val deployNum: String = value.deployNum
        jedis.set(deployNum, lglat);
    }
    override def close(errorOrNull: Throwable): Unit = {
        jedis.close()
    }
}

Spark GraphX:

概述:

GraphX 是 Spark 一个组件,专门用来表示图以及进行图的并行计算。GraphX 通过重新定义了图的抽象概念来拓展了 RDD: 定向多图,其属性附加到每个顶点和边。
GraphX最大的贡献是,在Spark之上提供一栈式数据解决方案,可以方便且高效地完成图计算的一整套流水作业。

图的相关术语:

图是一种较线性表和树更为复杂的数据结构,图表达的是多对多的关系。
顶点(Vertex)
任意两个顶点之间的通路被称为边(Edge),根据是否有序分为有向图和无向图
与顶点相关联的边的数量被称为顶点的度(Degree)。以顶点为起点的边的数量被称为该顶点的出度(OutDegree),以顶点为终点的边的数量被称为该顶点的入度(InDegree)。
如果任意两个顶点之间是连通的,则称为连通图。
强连通图:任意的两个顶点都存在通路。

图数据库:

Neo4j 是一个比较老牌的开源图数据库,目前在业界的使用也较为广泛,它提供了一种简单易学的查询语言 Cypher。
适合交互式查询,查询效率很高
Neo4j 查询与插入速度较快,没有分布式版本,容量有限,而且一旦图变得非常大,如数十亿顶点,数百亿边,查询速度将变得缓慢。

图计算Pregel:

基于 BSP 模型(Bulk Synchronous Parallel,整体同步并行计算模型),将计算分为若干个超步(super step),在超步内,通过消息来传播顶点之间的状态。
Pregel 可以看成是同步计算,即等所有顶点完成处理后再进行下一轮的超步,Spark 基于 Pregel 论文实现的海量并行图挖掘框架 GraphX。
每一个超步包含三部分内容:
    计算compute:每一个processor利用上一个超步传过来的消息和本地的数据进行本地计算
    消息传递:每一个processor计算完毕后,将消息传递个与之关联的其它processors
    整体同步点:用于整体同步,确定所有的计算和消息传递都进行完毕后,进入下一个超步

GraphX 架构:

算法层。基于 Pregel 接口实现了常用的图算法。包括 PageRankSVDPlusPlusTriangleCount、 ConnectedComponentsStronglyConnectedConponents 等算法
接口层。在底层 RDD 的基础之上实现了 Pregel 模型 BSP 模式的计算接口
底层。图计算的核心类,包含:VertexRDDEdgeRDDRDD[EdgeTriplet]

存储模式:

边分割(Edge-Cut):每个顶点都存储一次,但有的边会被打断分到两台机器上。
    这样做的好处是节省存储空间;坏处是对图进行基于边的计算时,对于一条两个顶点被分到不同机器上的边来说,要跨机器通信传输数据,内网通信流量大
点分割(Vertex-Cut):每条边只存储一次,都只会出现在一台机器上。邻居多的点会被复制到多台机器上,增加了存储开销,同时会引发数据同步问题。好处是可以大幅减少内网通信量

核心数据结构:

概述:

包括graphverticesedgestriplets
Graph
GraphX 用属性图的方式表示图,顶点有属性,边有属性。存储结构采用边集数组的形式,即一个顶点表,一个边表
顶点 ID 是非常重要的字段,它不光是顶点的唯一标识符,也是描述边的唯一手段。
顶点表与边表实际上就是 RDD,它们分别为 VertexRDD 与 EdgeRDD
组成:
    1.vertices 为顶点表,VD 为顶点属性类型,顶点 RDD 类型为 VerticeRDD,继承自 RDD[(VertexId, VD)]
    2.edges 为边表,ED 为边属性类型,边 RDD 类型为 EdgeRDD,继承自 RDD[Edge[ED]],可以通过 Graph 的 vertices 与 edges 成员直接得到顶点 RDD 与边 RDD
vertices        
vertices对应着名为 VertexRDD 的RDD。这个RDD由顶点id和顶点属性两个成员变量。
VertexRDD继承自 RDD[(VertexId, VD)],这里VertexId表示顶点idVD表示顶点所带的属性的类别。    
edges
edges对应着EdgeRDD。这个RDD拥有三个成员变量,分别是源顶点id、目标顶点id以及边属性。
triplets
通过 triplets 成员,用户可以直接获取到起点顶点、起点顶点属性、终点顶点、终点顶点属性、边与边属性信息。
triplets 的生成可以由边表与顶点表通过 ScrId 与DstId 连接而成。
triplets对应着EdgeTriplet。它是一个三元组视图,这个视图逻辑上将顶点和边的属性保存为一个RDD[EdgeTriplet[VD, ED]]。    

基本操作:

创建Graph:

val vertexRDD: RDD[(Long, (String, Int))] = sc.makeRDD(vertexArray)
val edgeRDD: RDD[Edge[Int]] = sc.makeRDD(edgeArray)    
val graph: Graph[(String, Int), Int] = Graph(vertexRDD,edgeRDD)
获取vertices
graph.vertices.foreach(println)
获取edges    
graph.edges.foreach(println)
获取triplets
graph.triplets.foreach(println)
转换操作
mapVertices
mapEdges
结构操作
subgraph(过滤条件)得到子图
连接操作
joinVertices内连接
outerJoinVertices左连接
聚合操作
pregel
示例:
    找出5到各顶点的最短距离
    val sssp: Graph[DoubleInt] = initialGraph.pregel(Double.PositiveInfinity)(
        (id, dist, newDist) => math.min(dist, newDist), // 两个消息来的时候,取它们当中路径的最小值
        // Send Message函数
        // 比较 triplet.srcAttr + triplet.attr和 triplet.dstAttr。如果小于,则发送消息到目的顶点
        triplet => { // 计算权重
            if (triplet.srcAttr + triplet.attr < triplet.dstAttr) {
                Iterator((triplet.dstId, triplet.srcAttr + triplet.attr))
            } else {
            Iterator.empty
            }
        },
        // mergeMsg
        (a, b) => math.min(a, b) // 最短距离
        )

强连通体算法:

使用:

val graph: Graph[IntInt] = GraphLoader.edgeListFile(sc,"data/graph.dat")
graph.connectedComponents()生成连通图,每个中心的id为最小的顶点id。

应用场景:

1.寻找相同的用户,合并信息
2.关注相似推荐

Spark原理:

任务提交流程:

Using spark-submit, the user submits an application.
1.用户提交application
In spark-submit, we invoke the main() method that the user specifies. It also launches the driver program.
2.调用main方法,创建driver
The driver program asks for the resources to the cluster manager that we need to launch executors.
3.driver向master申请资源,创建executors
The cluster manager launches executors on behalf of the driver program.
4.master创建executors
The driver process runs with the help of user application. Based on the actions and transformation on RDDs, the driver sends work to executors in the form of tasks.
5.driver解析任务,发送tasks到worker上的executors
The executors process the task and the result sends back to the driver through the cluster manager.
6.executors执行task并将结果发送给driver
    每个task记录执行逻辑,通过迭代器一条条读取数据,执行相应的逻辑。

核心组件:

Master(Cluster Manager):集群中的管理节点,管理集群资源,通知 Worker 启动Executor 或 Driver。
Worker :集群中的工作节点,负责管理本节点的资源,定期向Master汇报心跳,接收Master的命令,启动Driver 或 Executor。
Driver:执行 Spark 应用中的 main 方法,负责实际代码的执行工作。其主要任务:
    负责向集群申请资源,向master注册信息
    Executor启动后向 Driver 反向注册
    负责作业的解析、生成Stage并调度Task到Executor上
    监控Task的执行情况,执行完毕后释放资源
    通知 Master 注销应用程序
Executor:是一个 JVM 进程,负责执行具体的Task。Spark 应用启动时,Executor节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。
    如果有 Executor 节点发生了故障或崩溃, 会将出错节点上的任务调度到其他Executor 节点上继续运行。
    Executor 核心功能:
        负责运行组成 Spark 应用的任务,并将结果返回给 Driver 进程
        通过自身的 Block Manage 为应用程序缓存RDD

Yarn模式运行机制:

cluster模式
    1.ClientRM提交请求,并上传jarHDFS
    2.RM在集群中选择一个NM,在其上启动AppMaster,在AppMaster中实例化SparkContext(Driver)
    3.AppMasterRM注册应用程序,注册的目的是申请资源。RM监控App的运行状态直到结束
    4.AppMaster申请到资源后,与NM通信,在Container中启动Executor进程
    5.ExecutorDriver注册,申请任务
    6.Driver对应用进行解析,最后将Task发送到Executor
    7.Executor中执行Task,并将执行结果或状态汇报给Driver
    8.应用执行完毕,AppMaster通知RM注销应用,回收资源
Client模式 
    启动应用程序实例化SparkContext,向RM申请启动AppMaster
    RM在集群中选择一个NM,在其上启动AppMaster
    AppMasterRM注册应用程序,注册的目的是申请资源。RM监控App的运行状态直到结束
    AppMaster申请到资源后,与NM通信,在Container中启动Executor进程
    ExecutorDriver注册,申请任务
    Driver对应用进行解析,最后将Task发送到Executor
    Executor中执行Task,并将执行结果或状态汇报给Driver
    应用执行完毕,AppMaster通知RM注销应用,回收资源

RPC框架:

概述:

RPC(Remote Procedure Call)远程过程调用。
Spark 2.X 的 RPC 框架是基于优秀的网络通信框架 Netty 开发的。

RpcEndpoint:

表示一个消息通信体,可以接收、发送、处理消息。可能是master或者worker
生命周期:
    constructor -> onStart -> receive* -> onStop
onStart在接收任务消息前调用主要用来执行初始化
receive 和 receiveAndReply 分别用来接收RpcEndpoint send 或 ask 过来的消息    
    receive方法,接收由RpcEndpointRef.send方法发送的消息,该类消息不需要进行响应消息(Reply),而只是在RpcEndpoint端进行处理
    receiveAndReply方法,接收由RpcEndpointRef.ask发送的消息,RpcEndpoint端处理完消息后,需要给调用RpcEndpointRef.ask的通信端响应消息
    send发送的消息不需要立即处理,ask发送的消息需要立即处理

RpcEndPointRef:

对远程 RpcEndpoint 的一个引用。当需要向一个具体的RpcEndpoint发送消息时,需要获取到该RpcEndpoint的引用,然后通过该引用发送消息。
比如worker本地有master的引用,可以往master发送消息。

master启动流程:

Master 的 onStart 方法中最重要的事情是:执行恢复

Worker 启动流程:

onStart方法中最重要的事情是:向master注册

SparkContext内部组件:

SparkEnv
DAGScheduler。DAG调度器,调度系统中最重要的组件之一,负责创建job,将DAG的RDD划分为不同的stage,提交stage
TaskScheduler。任务调度器,调度系统中最重要的组件之一,按照调度算法对集群管理器已经分配给应用程序的资源进行二次调度后分配任务,TaskScheduler调度的 Task是 DAGScheduler创建的,
    因此DAGScheduler是TaskScheduler的前置调度器
SchedulerBackend。用于对接不同的资源管理系统

SparkEnv内部组件:

MapOutPutTracker
ShuffleManager
BlockManager
MemoryManager

SparkContext启动流程:

1. 初始设置

2. 创建 SparkEnv

3. 创建 SparkUI

4. Hadoop 相关配置

5. Executor 环境变量

6. 注册 HeartbeatReceiver 心跳接收器

7. 创建 TaskScheduler、SchedulerBackend

8. 创建和启动 DAGScheduler

9. 启动TaskScheduler、SchedulerBackend

10. 启动测量系统 MetricsSystem

11. 创建事件日志监听器

12. 创建和启动 ExecutorAllocationManager

13. ContextCleaner 的创建与启动

14. 自定义 SparkListener 与启动事件

15. Spark 环境更新

16. 投递应用程序启动事件

17. 测量系统添加Source

18. 将 SparkContext 标记为激活

三大组件启动流程:

1、创建SchedulerBackend、TaskScheduler

TaskScheduler的实现只有一个,但是不同模式传入的参数不同
SchedulerBackend的实现有多个
    在Standalone模式下,创建的分别是:StandaloneSchedulerBackend、TaskSchedulerImpl
    创建TaskSchedulerImpl后,构建了任务调度池 FIFOSchedulableBuilder /FairSchedulableBuilder

2、创建 DAGScheduler

3、执行 TaskScheduler.start

4、CoarseGrainedSchedulerBackend.start

5、创建 StandaloneAppClient

6、Master处理注册信息

作业执行原理:

1.Action 操作后会触发 Job 的计算,并交给 DAGScheduler 来提交。
2.dagScheduler.runJob 提交job,等待执行结果, job 是串行执行的。
3.Stage划分
    DAGScheduler 根据 RDD 的血缘关系构成的 DAG 进行切分,将一个Job划分为若干Stages,具体划分策略是:
    从最后一个RDD开始,通过回溯依赖判断父依赖是否是宽依赖(即以Shuffle为界),划分Stage;窄依赖的RDD之间被划分到同一个Stage中,可以进行 pipeline 式的计算
4、提交ResultStage
5、提交 Task
6、Task调度
    DAGScheduler 将 Stage 打包到 TaskSet 交给TaskScheduler,TaskScheduler 会将TaskSet 封装为 TaskSetManager 加入到调度队列中

调度策略:
    FIFO(默认调度策略)、FAIR

本地化调度:
    PROCESS_LOCAL data is in the same JVM as the running code. This is the best locality possible
    NODE_LOCAL data is on the same node. Examples might be in HDFS on the same node, or in another executor on the same node. This is a little slower than PROCESS_LOCAL because the data has to travel between processes
    NO_PREF data is accessed equally quickly from anywhere and has no locality preference
    RACK_LOCAL data is on the same rack of servers. Data is on a different server on the same rack so needs to be sent over the network, typically through a single switch
    ANY data is elsewhere on the network and not in the same rack

返回结果:
失败重试与黑名单机制
    配置重试次数:
        --conf spark.yarn.maxAppAttempts=1   am的重试次数

shuffle详解:

流程:

Shuffle Write
    Map side combine (if needed)
    Write to local output file
Shuffle Read
    Block fetch
    Reduce side combine
    Sort (if needed)

发展历史:

Hash Shuffle V1
    在 Map Task 过程按照 Hash 的方式重组 Partition 的数据,不进行排序。每个 Map Task 为每个 Reduce Task 生成一个文件,通常会产生大量的文件
Hash Shuffle V2 -- File Consolidation
    所有的 MapTask 相同的分区文件合并,这样每个 Executor 上最多只生成 N 个分区文件。
Sort Shuffle V1
    参考了 MapReduce 中 Shuffle 的处理方式,引入基于排序的 Shuffle 写操作机制。
    将所有对结果写入同一个文件。该文件中的记录首先是按照 Partition Id 排序,每个 Partition 内部再按照Key 进行排序
Tungsten-Sort Based Shuffle / Unsafe Shuffle(钨丝计划)
    将数据记录用二进制的方式存储,直接在序列化的二进制数据上 Sort 而不是在 Java 对象上,这样一方面可以减少内存的使用和 GC 的开销,
    另一方面避免Shuffle 过程中频繁的序列化以及反序列化。
    在排序过程中,它提供 cache-efficient sorter,使用一个 8 bytes 的指针,把排序转化成了一个指针数组的排序,极大的优化了排序性能。
    限制条件:
        Shuffle 阶段不能有aggregate 操作,所以像 reduceByKey 这类有 aggregate 操作的算子是不能使用 Tungsten-Sort Based Shuffle
        分区数不能超过一定大小(2^24-1,这是可编码的最大 Parition Id
Sort Shuffle V2
    把 Sort Shuffle 和 Tungsten-Sort Based Shuffle 全部统一到Sort Shuffle 中,自动决定采取哪种排序。

Shuffle Writer:

ShuffleWriter(抽象类),有3个具体的实现:(不同的场景用不同的shuflle,提高速度。)
    SortShuffleWriter(baseShuffle)。sortShulleWriter 需要在 Map 排序
        适用性最强。
    UnsafeShuffleWriter(serialShuffle)。使用 Java Unsafe 直接操作内存,避免Java对象多余的开销和GC 延迟,效率高
        使用条件:
            没有map端聚合,
            RDD的partitions分区数小于16,777,216
            Serializer支持relocation 【Serializer 可以对已经序列化的对象进行排序,这种排序起到的效果和先对数据排序再序列化一致】,不使用java序列化方式,而kryo
    BypassMergeSortShuffleWriter。和Hash Shuffle的实现基本相同,区别在于map task输出汇总一个文件,同时还会产生一个index file
        概述:
            bypass意为忽略,忽略了merge和sort
        使用条件:
            没有map端聚合操作 且 RDD的partition分区数小于spark.shuffle.sort.bypassMergeThreshold (缺省200),使用BypassMergerSortShuffleWriter
            不是聚合类的shuffle算子
写入流程:
    1.数据先写入一个内存数据结构中。
    2.检查是否达到内存阈值。
    3.数据排序。
    4.数据写入缓冲区。
    5.重复写多个临时文件。
    6.临时文件合并。
    7.写索引文件。

与hadoop shuffle的比较:

相同:
功能类似
区别:
1.Hadoop中有一个Map完成,Reduce便可以去fetch数据了,不必等到所有Map任务完成;
而Spark的必须等到父stage完成,也就是父stage的 map 操作全部完成才能去fetch数据。这是因为spark必须等到父stage执行完,才能执行子stage,主要是为了迎合stage规则
2.Hadoop的Shuffle是sort-base的,那么不管是Map的输出,还是Reduce的输出,都是partition内有序的,而spark不要求这一点
3.Hadoop的Reduce要等到fetch完全部数据,才将数据传入reduce函数进行聚合(需要排序,这样可以一个reduceTask处理该key所有的values),
而 Spark默认没有排序的情况下,可以一边fetch一边聚合。有排序的情况下(groupByKey)+ MapPartitionsRDD才可以达到mapreduce的效果。
4.spark追求高效,有些场景sort不是必须的。

数据倾斜:

原因:

数据异常
    参与计算的 key 有大量空值(null),这些空值被分配到同一分区
Map Task数据倾斜,主要是数据源导致的数据倾斜:
    数据文件压缩格式(压缩格式不可切分,所包含的实际数据量也可能差别很多)
        可通过在数据生成端将不可切分文件存储为可切分文件,或者保证各文件包含数据量相同的方式避免数据倾斜。
    Kafka数据分区不均匀(除非Producer端的Partition使用随机Partitioner,不然Kafka的每一个Partition对应Spark的一个Task(Partition))
Reduce task数据倾斜(重灾区,最常见):
    Shuffle (外因)。Shuffle操作涉及到大量的磁盘、网络IO,对作业性能影响极大,map从数据源获取的key通过shuffle产生了倾斜。
    Key分布不均 (内因)
    数据倾斜产生的主要原因:Shuffle问题 + key分布不均

处理:

做好数据预处理:

过滤key中的空值
消除数据源带来的数据倾斜(文件采用可切分的压缩方式)

处理数据倾斜的基本思路:

找出哪些key分布比较多(UI可以查看,或者数据抽样)df.select(“key”).sample(false,0.1).(k=>(k,1)).Bykey(+).map(k=>(k._2,k._1)).sortByKey(false).take(10)
hive etl预处理,预先join(hive还是会发生数据倾斜)
过滤导致倾斜的少量的key,将导致数据倾斜的key给过滤掉
消除shuffle
减少shuffle过程中传输的数据(使用高性能算子,避免使用groupByKey,用reduceByKey)
选择新的可用于聚合或joinKey(结合业务)
重新定义分区数(变更 reduce 的并行度。理论上分区数从 N 变为 N-1 有可能解决或改善数据倾斜)
    调整并行度。一般是增大并行度,但有时如本例减小并行度也可达到效果。
自定义分区器partitioner
    将原本被分配到同一个Task的不同Key分配到不同Task。只适合大量不同的key
加盐强行打散Key,两阶段聚合,注意不适合joinjoin的话一个表随机前缀后,需要另一个表膨胀N倍)

spark优化:

编码的优化:

1、RDD复用

2、RDD缓存/持久化

每次你对一个RDD执行一个算子操作时,都会重新从源头处计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。

3、巧用 filter

4、使用高性能算子

1、避免使用groupByKey,根据场景选择使用高性能的聚合算子 reduceByKey、aggregateByKey
2、coalesce、repartition,选择没有shuffle的操作
控制每个分区的大小,避免太多小文件或者spill memory太多。
3、foreachPartition 优化输出操作
4、map、mapPartitions,选择合理的选择算子
mapPartitions性能更好,但数据量大时容易导致OOM
5、用 repartitionAndSortWithinPartitions 替代 repartition + sort 操作
6、合理使用 cache、persist、checkpoint,选择合理的数据存储级别
7、filter的使用
8、减少对数据源的扫描(算法复杂了)

5、设置合理的并行度,充分利用集群资源

内存不够(sparkUI看到spill memory以及GC time很长)的情况下:
可以缩小每个task的数据量,repartition增加分区数

6、广播大变量

7、Kryo序列化

8、多使用Spark SQL

9、优化数据结构

尽量使用字符串替代对象,使用原始类型(比如IntLong)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。

10、使用高性能库

11、避免小文件

参数优化:

1、Shuffle 调优

减少Shuffle过程中的数据量
避免Shuffle
    spark默认自动开启广播变量,spark.sql.autoBroadcastJoinThreshold=10M,只有cache的表小于10M的才被广播到Executor上去执行map side join

2、内存调优

静态内存管理:
堆内内存(包含执行内存(执行 Shuffle 时占用的内存)、存储内存、对象内存)
    spark.storage.memoryFraction
        参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。
        参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。
    spark.shuffle.memoryFraction(execution内存)
        参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。
        参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。
    other(0.2
        用于用户定义的数据结构,或者spark内部元数据。

堆外内存(默认不启用)
    概述:
        可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。
        另外,堆外内存可以被精确地申请和释放,而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。
        缺点是必须自己编写内存申请和释放的逻辑。
    组成:
        动态按照0.5分配给storage、execution
    配置:
        spark.memory.offHeap.size 真正作用于spark executor的堆外内存
memoryOverhead内存(集群模式下):
    概述:
        主要用于JVM overHead,字符串, NIO Buffer等native开销。
    注意:
        注意-Xmx只是堆区的内存,不包含JVM本身运行内存。所以还要考虑分配给程序的内存大于-Xmx。
        如果是容器,可以考虑-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap让jvm决定heap的大小。
    配置:
        spark.executor.memoryOverhead 默认值executorMemory * 0.10, 最小值384
        在生产环境下,384m往往不够用,限制死经常出现
            Container killed by YARN for exceeding physical memory limits. 3.0 GB of 3 GB physical memory used. Consider boosting spark.executor.memoryOverhead.
内存计算公式:
    源码公式:
        (systemMemory(JVM可用内存) - reservedMemory(默认300))* MEMORY_FRACTION * MEMORY_STORAGE_FRACTION
        systemMemory要比-Xmx(即指定的spark.executor.memory)小一点,整个可用的堆区空间需要减去垃圾收集器的幸存者空间s1的大小。(s0和s1大小相同)
        控制参数:
            -XX:NewRatio=2 -XX:SurvivorRatio=8默认
        查看gc日志:
            --conf spark.executor.extraJavaOptions="-XX:+PrintGCDetails -Xloggc:/tmp/gc.log"
        查看gc分配空间:
            jinfo -flags (jps看到的进程号)查看启动参数
            jmap -heap 965782查看内存占用情况
        验证:
            jmap查看的幸存区大小 + systemMemory不等于Xmx
            println("Runtime.getRuntime().maxMemory()=" + Runtime.getRuntime.maxMemory) 打印的maxMemory各个节点不相同,有些等于Xmx
            应该是JVM分配的问题,导致Xmx可用的比例约为90%
        参考:
            https://stackoverflow.com/questions/23701207/why-do-xmx-and-runtime-maxmemory-not-agree
            http://www.openkb.info/2021/02/spark-code-unified-memory-manager.html
            https://stackoverflow.com/questions/43801062/how-does-web-ui-calculate-storage-memory-in-executors-tab
    spark UI计算公式:
        bytes/1000/1000(3.0版本已经fix,调整rootlogger level查看ui executor的stderr即可知道)
        注意展示的storage memory为usable memory,包含分配给storage和execution的内存。
            即systemMemory(JVM可用内存) - reservedMemory(默认300))* MEMORY_FRACTION
    每个executor的申请内存为:
        spark.executor.memoryOverhead + spark.executor.memory + spark.memory.offHeap.size + spark.executor.pyspark.memory向上取整
        (调整log info可以看到driver也是申请了)

统一内存管理
概述:
    与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域
Storage内存:
    spark.memory.fraction(默认0.6) * spark.memory.storageFraction(默认0.5
Execution内存:
    与Storage内存共享spark.memory.fraction
    用于缓存shuffle过程中产生的中间数据
Other:
    默认0.4,用于用户定义的数据结构,或者spark内部元数据。
Reserved Memory:
    预留内存,作用与other相同,可保障留出足够的空间。
    默认300M。
    (heap space - 300MB)剩下的内存用于分配。
Off-Heap堆外内存:
    动态按照0.5分配给storage、execution。
    存储内存的空间被对方占用后,无法让对方”归还”,因为需要考虑 Shuffle 过程中的很多因素,实现起来较为复杂

3、资源优化

Driver。需要占用一定的资源,缺省情况下系统给Driver分配1个Core,1G内存;内存可以适当调整
Executor。每个Executor进程都占有一定数量的内存和core。分配多少个core,意味着允许有多少个 Task 可以并行执,这些Task共享内存资源
如何确定多少executor数量?
    看数据文件大小500G
    官方推荐2-3个tasks每个cpu core,取4
    那么需要core:500G/128M = 4000个数据块,4000/ 4 = 1000core
    每个节点假设16core/48G,确定一个executor资源5core/15G(可以4core/8g,那么剩下16G作为堆外内存executor之间共享)
    需要的executor数量:
        1000core/5 = 200

4、动态资源分配

这意味着,不使用的资源,应用程序会将资源返回给集群,并在稍后需要时再次请求资源。
    spark.dynamicAllocation.enabled = true
    Standalone模式:spark.shuffle.service.enabled = true
    Yarn模式:《Running Spark on YARN》-- Configuring the External Shuffle Service

5、调节本地等待时长

6、调节连接等待时长

生产优化经验:

1.一个spark任务只有一个action,多个stage,tasks数量很多(在DF.rdd这一步,之前是sparkSession.read.orc,hdfs查看orc小文件特别多)。并且很多重复的stage(看UI)。
    进行措施:
        1.控制分区数。tasks数量因为orc没有内置配置小文件合并,自己通过hdfs client读取目录大小,然后按照指定size对partition使用coalesce减小。
        2.缓存。对于重复的stage,分析了代码,的确存在一个val变量被重复引用的现象,对val变量加上.persist(StorageLevel.MEMORY_AND_DISK)缓存。
            注意情况:
                spark会自动开启变量broadcast,默认300s超时,可以选择关闭-c spark.sql.autoBroadcastJoinThreshold=-1(在当前场景这个更快)
                或者增加broadcast时长-c spark.sql.broadcastTimeout=3600
            SPARK UI:
                启用了缓存后,页面上还是能看到两个broadcast job DAG相同,但是其实会并发执行。(看running的task数量,以及目前succeed的数量都可以验证)
                除此之外,同一个job里面不同的stage可能也会重复,前后可能是一些Exchange的操作。但其实会执行很快。(不会拖慢很多时间)
        3.如果GC time比较多,说明coalesce不是小文件太多,而是内存不够了。(web UI中的executors或者点击具体的stage,可以查看)
            解决:
                repartition增加分区,在Spark UI可以看到ShuffledRowRDD的过程(spill memory可能会大,而spill disk是序列化后的数据)
                如果是DF,或者DS,则不用通过repartition,直接调整spark.sql.shuffle.partitions,省去了repartition的时间。


2.newHadoopApi读取HFile,一个region一个partition,提示GC overhead limit exceeded或java heap space
    解决:
        newHadoopApi之后用repartition,(如果是HbaseRDD先map转化再repartition,会极大增加GC time,可能直接repartition有优化,边查询边shuffle write(更多空间,因为map去掉了部分),避免内存存太多数据)(对比速度有提升,在内存一定的情况下)
        System.setProperty("spark.serializer""org.apache.spark.serializer.KryoSerializer")
        其他可能有用的配置:
            spark.sql.files.maxPartitionBytes(default128 MB)
            spark.sql.shuffle.partitions则是对Spark SQL专用的设置,而spark.default.parallelism只有在处理RDD时才会起作用,对Spark SQL的无效。
    优化:
        可以conf指定哪个column,可以减少一部分读取量以及shuffle write的数据量。
3.出现spark java.lang.OutOfMemoryError: Java heap space或者spark java.lang.OutOfMemoryError: GC overhead limit exceeded
    解决:
        可能是driver内存不足,看看是否有collect操作
3.executor被remove,查看faile的task,提示
    ExecutorLostFailure (executor 6 exited unrelated to the running tasks) Reason: Container marked as failed: container_e15_1624872993898_14966_01_000012 on host: 10.0.0.5Exit status: -100. Diagnostics: Container released on a *lost* node.
    定位过程:
        1.web ui查看log,但是不全。
        1.查看nodemanager的log路径下的log(不同的log信息)。
        2.再搜索错误信息
        3.对比错误节点的磁盘使用情况。
    原因:
        节点磁盘利用率超过 90% 时,该磁盘将被视为运行不正常。核心节点或任务节点被终止。
        任务数据超过内存,stage划分阶段需要shuffle到磁盘上。
    解决:
        修改所有节点上 yarn-default.xml 中的 yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage 属性。然后重新启动 hadoop-yarn-nodemanager 服务。
        扩容磁盘。
        不使用磁盘(比如扩大内存,控制数据量)。

python api使用心得:

1. local[8]要比standalone快很多,单个节点的所有核数虽然能获取到,*963.但估计并行度不够,核数不能完全用到(但web ui显示16 used)

parallelize指定分区数量!!!不要让spark根据集群数量自动设置,不灵活。
设置比cpu核数多的分区数好处是完成快的分区可以继续接任务!

2.standalone mode下

用/sbin/start-slave.sh spark://172.22.9.181:7077启动的节点不用带--py-files 也能访问节点路径下的自定义模块
    如果slave是localhost,能正常运行脚本(无论是python还是spark-submit)
    如果slave是其他节点时,python启动会报Initial job has not accepted any resources(.set()配置后就报import错误了)
                           spark-submit启动,会读取spark.driver.host,能找到资源,但报import自定义模块的错误
        解决:
            其他节点的服务器下,配置须相同,spark启动的python解释器会根据path寻找模块
        现在可以两台机器跑一个实时脚本了。
用/sbin/start-slaves.sh启动的脚本
    from feature.user.user_feature import UserFeature是一个类,类.方法会报错import no module的,可以通过
        userfeature_get_features = UserFeature.get_features解决

    --py-files feature.zip报各种worker pickle.loads()导致的import error(在spark的worker目录下没有cp到文件)
        py3下根本没传到zip文件,在spark的worker目录下也没有zip文件
        py2下根本没传到zip文件,在spark的worker目录下也没有zip文件
    代码里addPyFile则可以,但遇上os.listdir无法解析zip import问题
        # 对于py2和py3都有用,但zip文件对于os.listdir无解决方法,后来通过zipfile模块列举和importlib导入解决
        # 对于open文件类操作,用zipfile读取的字节内容无法通过io.BytesIO封装为文件对象再打开,因为open要求输入路径,
            # 可以不打zip包,而是--py-files直接上传
    解决: 
        py3的脚本不添加zip文件了,而是设置属性:
            .set("spark.executorEnv.PYTHONPATH",'/home/ubuntu/data/code/feature-server'),使得worker import时自动从path导入
            # conf需要用spark-submit提交
        相同的方法去执行py2脚本,后来也可以了。

3.log和print输出在spark目录下的work文件夹stderr stdout

如果没有要看zip文件是否更新了

4.对于yarn集群:

yarn的container的cpu和内存的最小数量,实际上就是spark的executor能获取到的数量,所以一开始要加大最小的数量
如果启动nodemanager失败,查看logs/hadoop-...-nodemanager.log日志

或者: 
    每个app手动设置spark或flink获取的cpu和内存数量

5.对于依赖第三方模块,可以通过virtualenv创建一个独立的Python运行环境,然后在这个Python运行环境里安装各种第三方包。
然后将安装的第三方包打包成*.zip或者*.egg。    

如果您的软件包依赖于已编译的代码,并且您的集群中的计算机具有与您编译egg的代码不同的CPU体系结构,则这将不起作用。
比如Numpy,主要原因是Python不允许动态导入.so文件,而Numpy由于是C编译的,存在*.so文件。

6.standalone模式下

cpu可用核数通过spark.cores.max配置(平均到每个节点),没设置会采取spark.deploy.defaultCores

python xxx.py启动,在py代码里面配置集群资源参数
也可spark-submit启动,需要配置PYSPARK_PYTHON,不然默认是2.7,然后使用--archivesyarn)和--py-files(之前只能addPyFile,后来又可以了)上传依赖zip
该模式下默认是client mode,需要配置PYSPARK_PYTHONnoset的2.7

7.yarn模式下

1.python版本driver和worker必须一致

2.依赖的第三方库可以打包成ZIP文件

3.不同的python解释器版本,使用pyvenv或virtualenv打包解释器和依赖包为zip文件,上传至服务器

①安装虚拟环境和依赖的第三方库
    virtualenv venv-python --python=python3.6       # python3自带的venv不能将路径指定为使用相对路径,以便于迁移运行
    source your_env_path/bin/activate               
    cat requirement.txt|xargs -n 1 pip install
②打包zip
    zip -r  your_env_name.zip your_env_name/
    1.虚拟环境的解释器不能依赖本地的库如libpython3.6m.so.1.0,需要手动添加到zip包内并设置环境变量LD_LIBRARY_PATH找到该共享库
        --conf spark.yarn.appMasterEnv.LD_LIBRARY_PATH=./spark-python/lib/python3.6/lib-dynload     cluster模式
      或--conf spark.executorEnv.LD_LIBRARY_PATH=./spark-python/lib/python3.6/lib-dynload            client模式
        # work节点的/etc/ld.so.conf无法编辑,不然可以直接配置
    2.如果worker节点上的解释器import contextlib/runpy等某个包失败,手动搜索cp到zip包./spark-python/lib/python3.6/内
        也可将源码包./Lib/下所有文件复制到虚拟环境lib/python3.6/内

    # 建个ubuntu从未安装过python的docker,会发现比集群缺少更多的c库依赖,就算将提示的c库复制到docker了也不行

③提交任务spark-submit,指定--master yarn,--archives传虚拟环境和依赖包  --py-files传项目zip包
    worker的环境变量通过--conf spark.yarn.executorEnv.来设置
    driver的环境变量通过--conf spark.yarn.appMasterEnv.来设置,注意xxx.py要放在最后

    1.如果是deploy-modeclient,则需要本地已经安装好项目依赖的第三方库并能跑通(能使用--archives)
        优点:本地client输出错误日志,方便调试,cluster无日志输出,通过yarn log -applicationId xxx 查看

    2.如果是cluster,则不需,打包好东西即可。
        配置--conf spark.yarn.appMasterEnv.PYSPARK_PYTHON=./venv4num/venv4num/bin/python2 设置解释器的路径
        代码里面用到本地绝对路径的,都要改为相对路径,一般是open\read文件类操作:
            a.项目代码zip包通过--py-files上传
                判断当前os.path.dirname(base_path).endswith(".zip"),将yy/xxx.zip/a.txt变为yy/a.txt
                    再--py-files a.txt上传需要的单个文件或--archives上传整个zip项目包(不能同zip名字)
                也可以直接"./level.csv"直接引用,只要文件已经上传到工作目录下
            b.项目代码zip包通过--archives上传,解压的目录还没加入classpath(无法import feature-server),解决是python虚拟环境解释器加入pth
                --archives上传的同路径的zip,不会再通过--py-files上传(估计是都上传后才解压,这时--py-files就找不到了)
                多次--archives会覆盖掉前面的
                如果能解决path的问题,线上的项目代码只需改为相对路径,两者就能共存:
                    - 目前通过sys.path.insert(0,"./feature-server/")可以解决import问题,但文件名前面得加上"feature-server/xxx"
                    - 或者找到方法,让当前脚本所在目录cd到./feature-server目录下,这样文件可以兼容了
                        os.chdir("./feature-server/")
                        sys.path.insert(0,"./") ,以cd后的路径为跟目录的相对路径'.',这样import能起作用,而且文件名也能兼容了
                                                                                                    (以cd后的路径来寻找文件)                                                                          
                        # 要想和原来兼容:
                            # 只需判断spark运行时才cd目录(或将venv放在项目zip包里,指定解释器路径为里一层)
                            # 添加sys.path.insert(0,"./")(pth可以通过../来添加工作目录所在路径)
                        # 文件兼容仅针对那些相对路径"./xxx.csv',os.path.abspath(__file__)获取绝对路径需要分main脚本和import脚本
                # --archives不能解压到当前工作路径,不然也不用费劲了,如xx.zip#"",xx.zip#" ",xx.zip#"./"都不行,必须指定一个文件夹名字
                # driver的pythonpath配置好了后,worker节点就不用像client那样需要单独配置了
            c.可打包上传至 pypi ,安装至本 venv ,在入口代码里使用绝对路径导入使用(推荐)
        注意: 
            主脚本会放在driver主目录下执行(启动解释器),路径是main.py(__file__),main脚本内的"./level.py"的路径也是./level.py,
                包括import的其他文件内也是这样(不同于import路径或__file__,openread时都是以运行脚本时所在目录为原点的)
                # import的模块下仅能用相对路径"./xxx.csv',不能用os.path.abspath(__file__)获取绝对路径再自己拼接,会获取到cache的路径
                    # 而不是工作路径。这个问题后来解决了,缓存消失就正常了(还是推荐相对路径,但对根路径有要求)
                # __file__是import时的相对路径,如./system_config/system_config.py,相对于..../container_e53_1563796357061_18361_01_000001/feature_server
                # 总结:import的脚本不能用os.path.abspath方法
                # driver的main脚本不仅能用相对路径,也能用os.path.abspath(__file__),获取是的工作路径
                #如: 
                    #/usr/local/datadisk/yarn/nm/usercache/chenzl/filecache/68/feature3.zip/system_config/system_config.py
                #而不是:
                    #/usr/local/datadisk/yarn/nm/usercache/chenzl/appcache/application_1563796357061_18361/
                        #container_e53_1563796357061_18361_01_000001/feature_server/system_config/system_config.py
                
            work节点执行函数时,当前import路径是./xxx.zip/module/a.py

4.尝试公司的yarn集群上,运行standalone模式已经跑通的脚本,成功,而自己搭建的yarn集群失败,估计问题是yarn的配置

level、特征与模型都能正常运行,模型依赖的xgboost、numpy、pandas都能打包

示例:

client模式:
/home/ubuntu/data/spark-python/bin/spark-submit --master yarn --deploy-mode client --archives ./spark-python.zip#spark-python \
/home/ubuntu/data/code/feature-server/level/user_level/ph/ph_user_pre_level_spark.py
# 需要通过--py-files上传项目的zip包,代码里面作相应的修改,--archives不能兼容driver和worker
cluster模式:
/home/ubuntu/data/spark-python/bin/spark-submit --master yarn --deploy-mode cluster 
--archives ./spark-python.zip#spark-python, 
--conf spark.yarn.appMasterEnv.PYSPARK_PYTHON=./spark-python/bin/python         # work节点不需配置
--conf spark.yarn.appMasterEnv.LD_LIBRARY_PATH=./spark-python/lib/python3.6/lib-dynload # driver和worker都需要
--conf spark.executorEnv.LD_LIBRARY_PATH=./spark-python/lib/python3.6/lib-dynload         # 
--py-files /home/ubuntu/data/code/feature3.zip,
    /home/ubuntu/data/code/feature-server/system_config/system.conf,    # 上传需要的单个文件夹,能够在工作目录下直接open
    /home/ubuntu/data/code/feature-server/level/user_level/ph/ph_user_pre_level.csv,
    /home/ubuntu/data/code/feature-server/level/user_level/ph/ph_user_pre_level_0.csv,
    /home/ubuntu/data/code/feature-server/level/user_level/ph/ph_user_pre_level_1.csv,
/home/ubuntu/data/code/feature-server/level/user_level/ph/ph_user_pre_level_spark.py

# 需要main脚本cd目录和main脚本sys.path.insert(0,"./"),worker继承clusster driver的环境变量,不用单独设置
# 文件使用相对路径,使用os.path.dirname()而不是os.path.abspath()即可(后来又可以了,估计是缓存的问题)
/home/ubuntu/data/spark-python/bin/spark-submit --master yarn --deploy-mode cluster \
--archives ./spark-python.zip#spark-python,/home/ubuntu/data/code/feature3.zip#feature_server \
--conf spark.yarn.appMasterEnv.PYSPARK_PYTHON=./spark-python/bin/python \                    # 这个不填默认从当前环境变量获取
--conf spark.yarn.appMasterEnv.LD_LIBRARY_PATH=./spark-python/lib/python3.6/lib-dynload \
--conf spark.executorEnv.LD_LIBRARY_PATH=./spark-python/lib/python3.6/lib-dynload \
/home/ubuntu/data/code/feature-server/level/user_level/ph/ph_user_pre_level_spark.py

8.流式处理kafka python

# 少量分区的批处理,map到reduce到最后输出阶段,如果map的数据立刻给reduce的话(flink正是如此),可以实现流式处理
# spark是每一个间隔后才发送一个batch数据RDD

kafka有两种方法接受数据:
    1.老方法,使用Receivers和kafka的high-level API
    2.新方法,不使用Receivers

Receiver-based的方法:
    概述: 
        这个方法使用Receiver来接收data。Receiver通过Kafka high-level consumer API来实例化。
        Kafka发送的record会被Receiver存储在Spark executors,然后Spark Streaming创建jobs来处理data.
        默认配置下,节点故障会导致数据丢失,为了zero-data loss, 启用Write-Ahead Logs。
    步骤:
        1.Linking
            python需下载kafka依赖jar包
        2.Programming
            from pyspark.streaming.kafka import KafkaUtils
            kafkaStream = KafkaUtils.createStream(streamingContext, [ZK quorum], [consumer group id],{number of partitions:topic})
            默认会解码Kafka record为utf-8字符串
            注意要点:
                - Kafka Topic partitions与spark partitions of RDDs无关,所以增加KafkaUtils.createStream()中的topic-specific partitions仅仅增加了
                    多个线程,每个线程有一个receiver来接收数据,不增加处理data的并行度。
                - 可以创建多个不同groups、topics的Kafka input DStreams来并行接收数据
                - 如果启用了Write-Ahead Logs,input stream的storage level改为StorageLevel.MEMORY_AND_DISK_SER
                    KafkaUtils.createStream(..., StorageLevel.MEMORY_AND_DISK_SER)
        3.Deploying
            使用--packages添加依赖包
                ./bin/spark-submit --packages org.apache.spark:spark-streaming-kafka-0-8_2.12:2.4.3 ...     # 报错了
            或者手动下载:
                wget https://repo1.maven.org/maven2/org/apache/spark/spark-streaming-kafka-0-8-assembly_2.11/2.4.3/spark-streaming-kafka-0-8-assembly_2.11-2.4.3.jar
                然后通过--jars提交
直接方法(No Receivers):
    新的无receiver的“direct”方法自从Spark 1.3后被引入以确保更强的end-to-end保证。
    这个方法周期性地查询Kafka获取每个topic+partition最新的offsets, 定义offset ranges在每个batch处理。
    当处理数据的jobs被创建后, Kafka’s simple consumer API被调用以从kafka读取offsets的自定义范围(类似从file system读取文件)。
    与有receiver的方法相比的优点:
        1.Simplified Parallelism: 
            无需创建Kafka streams然后union。通过directStream, Spark Streaming会创建和Kafka partitions一样多的RDD partitions来消费, 
            会从kafka并行读取数据。# 没有占用到core,后续的转换操作仍可使用同一个核,如需并行处理转换,设置nums或修改配置
        2.Efficiency:
            第一个方法要想zero-data loss要求data存储在Write-Ahead Log, 在这里还会继续复制备份数据。
            这种做法实际上是效率低的,因为data实际上复制了两次,一次是Kafka,第二次是Write-Ahead Log。
            第二种方法因为没有receiver,因此无需Write-Ahead Logs。只要有足够的Kafka retention, messages可以从Kafka恢复。
        3.Exactly-once semantics: 
            第一种方法使用Kafka’s high-level API来在Zookeeper储存消费的offsets。这是传统从Kafka消费的方法。
                结合write-ahead logs可以确保zero data loss (i.e. at-least once semantics), 有小概率会由于故障而二次消费部分records。
                因为Spark Streaming接收到的数据与Zookeeper追踪的offsets之间的不一致。
            第二种方法使用simple Kafka API不使用Zookeeper。Offsets通过Spark Streaming内置checkpoints进行保存追踪。
            消除了Spark Streaming与Zookeeper/Kafka的不一致,每一个record只会被Spark Streaming有效接收exactly once不管故障。
            为了实现输出结果exactly-once semantics, 保存数据的输出操作需是idempotent, 或者原子性事务的保存结果和offsets。

    该方法的缺点是不会更新offset到Zookeeper, 因此Zookeeper-based的Kafka monitoring tools没有进展。
    可以获取每个batch的offsets然后自己更新到Zookeeper

    步骤: 
        1.Linking
        2.Programming
            from pyspark.streaming.kafka import KafkaUtils
            directKafkaStream = KafkaUtils.createDirectStream(ssc, [topic], {"metadata.broker.list": brokers})  # 没有group_id

            可以传递messageHandler给createDirectStream来获取KafkaMessageAndMetadata,包含了关于当前record的metadata,然后转换为任意类型。
            默认Python API获取到的record是解码Kafka数据后的(UTF8 encoded strings。可以自定义decode函数。

            Kafka parameters必须指定metadata.broker.list或bootstrap.servers。默认从每个partition的最新offset开始消费。
            可以通过配置Kafka parameters的auto.offset.reset为smallest以从0开始消费。

            还可以从任意offset开始消费,使用KafkaUtils.createDirectStream的其他参数。此外每个batch的Kafka offsets可以获取。
                offsetRanges = []
                def storeOffsetRanges(rdd):
                    global offsetRanges
                    offsetRanges = rdd.offsetRanges()       # 
                    return rdd
                def printOffsetRanges(rdd):
                    for o in offsetRanges:
                        print "%s %s %s %s" % (o.topic, o.partition, o.fromOffset, o.untilOffset) # 该batch的该RDD
                directKafkaStream.transform(storeOffsetRanges).foreachRDD(printOffsetRanges)
            # 可以通过这个方法来更新Zookeeper,如果需要Zookeeper-based Kafka monitoring tools来展示streaming application的进度。

            注意offsetRanges仅当directKafkaStream的第一个方法内调用时才成功。
            可以transform()而不是foreachRDD()作为第一个方法调用以获取offsets, 然后再执行Spark methods。
            需注意RDD partition和Kafka partition的one-to-one关系不会在shuffle或repartition(如reduceByKey() or window())方法后保留。

            另一个需要注意的地方是这个方法没有用到receiver-related的配置,spark.streaming.receiver.*将不会应用到这个方法创建的input DStreams。
            可以使用spark.streaming.kafka.*来配置,其中一个重要的是spark.streaming.kafka.maxRatePerPartition,配置了 每个Kafka partition读取消息的maximum rate (in messages per second)。
        3.Deploying
            与第一种方法相同。
            ./bin/spark-submit --jars ./spark-streaming-kafka-0-8-assembly_2.11-2.4.3.jar ~/data/code/feature-server/model/spark_model/model_event_spark.py

无论哪种方法,都只使用多个线程来并行接收kafka为多个RDD(partition),然后再集合为一个RDD,算作一个block,多个block合作一个batch的blocks
然后进行transform(只执行一次),blocks的数量已经由batch和block的interval决定,map的并行度与之无关。
map的print不完全显示(可能是只显示其中一个Partition,官方说是打印每个RDD的首个元素),可以通过count计数或mapPartitions。

checkpoint恢复后的kafka正常消费,但map函数(worker节点)获取到的全局变量就不是实时更新的了

standalone模式 
    ./bin/spark-submit --jars ./spark-streaming-kafka-0-8-assembly_2.11-2.4.3.jar 
    ~/data/code/feature-server/model/spark_model/model_event_spark.py
    # 需要设置pythonpath

yarn集群下client mode提交作业
    /home/ubuntu/data/spark-python/bin/spark-submit 
    --master yarn --deploy-mode client 
    --jars ./spark-streaming-kafka-0-8-assembly_2.11-2.4.3.jar 
    --archives ./spark-python.zip#spark-python,/home/ubuntu/data/code/feature3.zip#feature_server
    --conf spark.executorEnv.LD_LIBRARY_PATH=./spark-python/lib/python3.6/lib-dynload 
    --conf spark.executorEnv.PYTHONPATH=./feature_server                                
    /home/ubuntu/data/code/feature-server/model/spark_model/model_event_spark.py

    # client模式下,driver的pythonpath在代码设置,worker的通过--archivers和--conf找到代码的模块
    # 如果不行看看代码更新但checkpoint还没清空

cluster模式
    /home/ubuntu/data/spark-python/bin/spark-submit --master yarn --deploy-mode cluster 
    --jars ./spark-streaming-kafka-0-8-assembly_2.11-2.4.3.jar 
    --archives ./spark-python.zip#spark-python,/home/ubuntu/data/code/feature3.zip#feature_server 
    --conf spark.yarn.appMasterEnv.PYSPARK_PYTHON=./spark-python/bin/python
    --conf spark.yarn.appMasterEnv.LD_LIBRARY_PATH=./spark-python/lib/python3.6/lib-dynload         # libpython3.6m.so.1.0解释器启动时需要,这时还没chdir
    --conf spark.executorEnv.LD_LIBRARY_PATH=./spark-python/lib/python3.6/lib-dynload 
    /home/ubuntu/data/code/feature-server/model/spark_model/model_event_spark.py

    # 只需将代码从sys.path.insert(0"/home/ubuntu/data/code/feature-server")变为os.chdir("./feature_server/")即可 
    # 提交后会一直运行,需要手动kill掉

stream流式跑模型总结:
    1.设置spark.streaming.backpressure.enabled和spark.streaming.kafka.maxRatePerPartition,先限制最大速率
        然后基于从小开始,如maxRatePerPartition=1,观看log输出的Total delay是否持续增长,从而调整最大速率
        # 增减worker节点的数量会影响处理速率,repartition或修改block interval来修改rdd的数量(partition数量已由kafka接收器固定)即可

    2.task的数量如果用的是foreachPartition,那么默认有10个tasks,对应kafka的10个partition
        foreachRDD里面使用foreachPartition,那么并行度是10,会将该batch的全部blocks对应的partition的record一次分发给一个tasks
        而mapPartitions的话,试了一下,效率不高,创建task的数量也少(只有一个,后来数量随机增加,rate不能太高),还是推荐用foreachRDD
        # mapPartitions的task数量随机,不能根据task完成数量来sleep等待

    3.如何让kafka等待process完才poll下一批
        先定义cnt = getDroppedWordsCounter(ssc._sc),在tranform函数中while判断0<=task_processing_cnt.value<16时一直sleep #16为partition数量
        其中的16是task数量,默认为10对应kafka10个partition,可以repartition
        开始值为-1,跳过while,然后task_processing_cnt.value = 0,等待cnt增加到16后下批即可跳出循环

        spark.streaming.backpressure.enabled作用不大,offset还是持续增长 # 过了几次batch才会真正起作用

    4.python2 yarn集群
    ·   client mode提交
        /home/ubuntu/data/spark-python/bin/spark-submit --master yarn --deploy-mode client 
        --jars ./spark-streaming-kafka-0-8-assembly_2.11-2.4.3.jar
        --archives ./spark-model.zip#spark-model,/home/ubuntu/data/code/feature4.zip#feature_data_service
        --conf spark.executorEnv.LD_LIBRARY_PATH=./spark-model/lib/python2.7/lib-dynload
        /home/ubuntu/data/code/feature-data-service/model/new_order_model/model_spark_run.py
        # 需要的c库复制放在/usr/lib/x86_64-linux-gnu/(后来xgboost因为libm.so.6版本报错)或pip install
        # 对于libBLT.2.5.so.8.6(python-tk依赖),在代码里手动load
        # worker的python path在venv里面添加pth文件
        # 第三方库的so文件与c库导入路径不同,c库得LD_LIBRARY_PATH

        cluster mode提交:(兼容client模式,client添加的/home/ubuntu/data/code/feature-server不影响worker)
            /home/ubuntu/data/spark-python/bin/spark-submit --master yarn --deploy-mode cluster 
            --jars ./spark-streaming-kafka-0-8-assembly_2.11-2.4.3.jar 
            --archives ./spark-model.zip#spark-model,/home/ubuntu/data/code/feature4.zip#feature_data_service 
            --conf spark.yarn.appMasterEnv.LD_LIBRARY_PATH=./spark-model/lib/python2.7/lib-dynload      # 要求不能代码里chdir,否则找不到
            --conf spark.yarn.appMasterEnv.PYSPARK_PYTHON=./spark-model/bin/python      
            --conf spark.executorEnv.LD_LIBRARY_PATH=./spark-model/lib/python2.7/lib-dynload 
            /home/ubuntu/data/code/feature-data-service/model/new_order_model/model_spark_run.py
            # 不能使用os.chdir("./feature_data_service/"),这是因为依赖的第三方库的so文件在import时才会导入,chdir后就找不到了,使用..无效
            # 这时候要求代码里面的file需要通过os.path.dirname()获取路径前缀或者使用相对路径
            # import还是使用../../../../feature_data_service的pth文件即可
            # 对于空文件夹,--archives不会解压,需要随便放个文件

        发现模型保存结果数量变少,kafka消费25000,结果仅有7800左右的数量
            1).排除了conn在同一个worker使用相同socket问题:foreachPartition的函数使用conn={}
            2).在foreachPartition内的for循环写入每个record到mysql,cache也写入mysql,发现数量是正确的,问题出在模型产出结果上面。
            3).因为该模型为还清期数012的模型,不是每个record都满足条件
            4).记录输入模型的uid列表len大于模型结果数量,模型结果数量=获取完特征的uid数量,get_model_self_define_features_batch函数问题
                报错了部分,改正后记录输入模型的uid列表=模型结果数量=获取完特征的uid数量

        如何查看running应用的状态
            1.通过submit时给出的tracking URL或cdh管理工具Cloudera Manager中通过yarn app链接找到tracking URL,即可查看跳转到spark web ui
                点击streaming选项卡查看Input Rate,Scheduling Delay,Processing Time,Total Delay
                点击具体的job链接可查看stderrstdout
            2.

    5.print如果driver没显示,说明分发给worker了,在worker的日志可看到

    6.虚拟环境下尝试复制不同ubuntu版本的python解释器和依赖包,其中xgboost install时与/lib/x86_64-linux-gnu/libm.so.6的版本绑定了,不能复制

9.structured stream使用心得

1.foreachBatch适合不做处理后提交该batch所有数据的df,1个并发度。
    得到的dataFrame(key、value、topic、partition、offset、timestamp等),包含多个record,循环(foreach不能用,df.foreachPartition可以)来处理。
    类似streaming的dstream.foreachRDD(lambda rdd: rdd.foreachPartition(sendPartition)),多个RDD的同一个partition会合并一起发送,
    这等于dataFrame的一个partition。
    foreachPartition拿到的是row可迭代对象,需处理一下:

    dataFrame.value拿到的是Column类实例,

    foreach才利用到多个partition,默认10个partition,foreach会执行10个tasks。缺点是多个row调用多次函数。

2.foreachBatch的输出在local,foreach的输出要看是哪个worker在执行,查看对应机器的stderr即可
3.如果出现Retrying to fetch latest offsets because of incorrect offsets错误,导致startOffsets变为0的,是kafka-client的版本问题
    目前已知0.10.1.0有这个问题,0和2都没有
4.如何增加并行度?
    接收只有10个partition,产生的dataFrame可以通过df.repartition(num).writeStream来增加并行度。
5.对于event-time,由于kafka可能由于迁移而导致消息的timestamp发生变化,所以dataFrame的这个字段没有什么意义,而是消息里面去存产生的时间。
    至于如何提取dataFrame里面的create-time出来:
        1.new_rdd = dataFrame.rdd.map(lambda Row:提取row的信息,转为新的ROW),函数内再通过spark.createDataFrame(new_rdd)即可创建
            在foreachBatch方法内:
                new_rdd = df.rdd.map(lambda rowRow(uid=json.loads(row.value.decode())["uid"]))
                new_df = spark.createDataFrame(new_rdd)
            缺点:
                不能在writeStream前面selectwhere,不过影响不大。
        2.dataFrame方法withColumn + from_json
            df.withColumn('age2', from_json(col("value").cast("string"), schema))       # 第二个参数是column表达式
                表达式:
                    df.selectExpr("CAST(value AS STRING)")  value转为字符串的dataFrame,只有value这个column
                    df.value.cast("string")                 返回Column<CAST(value AS STRING)>,等于
                        # “string”, “boolean”, “integer”
                    df.select(from_json(col("value").cast("string"), schema).alias("value")).value  # 获取json这个column
                    df.select(from_json(col("value").cast("string"), schema)).columns[0]            # 两者等价
                转json
                    df.select(from_json(df.value.cast("string"), schema))       # from_json
                    df.select(from_json(col("value").cast("string"), schema))   # 两者等价

        3.使用from_json和select
            schema = StructType().add("uid", IntegerType()).add("country_id", IntegerType()).add("ref_id", IntegerType()).add(
                        "createTime", IntegerType()).add("event_type", IntegerType())
            或schema = StructType([StructField(filename,LongType(),Falsefor filename in ["uid","country_id","event_type","ref_id","createTime"]])
            df.select(from_json(col("value").cast("string"), schema).alias("value")).select("value.uid")  
                from_json将JSON string转为MapType(jsontostructs,Spark SQL struct,这样就嵌套了StructType,外面的StructType
                    的StructField是一个StructType)
                select使用该表达式后得到的dataframe可以通过select("value.uid")提取值
            如何转为类型:
                通过select("value.uid")提取值后,再使用withColumn转换:
                    new = df.withColumn("value",from_json(col("value").cast("string"), schema)).select("value.uid")
                    new.withColumn("uid",new.uid.cast("string")).collect()
                    # 一般不需转换,from_json使用自定义的schema,得到的字段已经是预期的了
                或直接通过select
                    new = df.withColumn("value",from_json(col("value").cast("string"), schema))
                    new.select(new.value.uid.cast("string")).collect()
                    # df.withColumn("value",from_json(col("value").cast("string"), schema)).select(col("value.uid").cast("string")).collect()
                    # 这种写法更加简洁。
                # select和withColumn
        4.使用get_json_object方法
            # get_json_object返回的是Column实例,后面可接cast,path解析到的json string类型
            df.withColumn("uid",get_json_object(col("value").cast("string"),"$.uid").cast("integer")).collect()
6.如何保存offset
    message = query.lastProgress        # 第一次为none,因为当前batch还没处理完
    tmp = []
    if message:
        data = message["sources"][0]["endOffset"]
        topics = list(data.keys())
        for one in topics:
            for part,offset in data[one].items():
                tmp.append([one,part,offset,offset])
        with db4MysqlConn() as conn:
            conn.insert_many("""insert into tmp.r_test_offset (topic,`partition`,`fromOffset`,`untilOffset`) values(%s,%s,%s,%s)
                                    on duplicate key update fromOffset=values(fromOffset),untilOffset=values(untilOffset)"""
,
                             tmp)
            conn.commit()
7.消费遗漏问题
    可能是消费时的条件问题(但已排查),或者spark kafka的问题
    手动测试test topic消费,没有遗漏,生产topic生产太快,来不及看,找了个慢的topic,没有遗漏

standalone模式提交:
    ./bin/spark-submit --master spark://172.22.9.181:7077 --jars ./spark-sql-kafka-0-10_2.11-2.4.3.jar,./kafka-clients-0.10.2.0.jar 
    /home/ubuntu/data/code/feature-data-service/model/level_model/spark_run.py

yarn client模式:
    /home/ubuntu/data/spark-python/bin/spark-submit --master yarn --deploy-mode client 
        --jars ./spark-sql-kafka-0-10_2.11-2.4.3.jar,./kafka-clients-0.10.2.0.jar 
        --archives ./spark-xgboost.zip#spark-xgboost,/home/ubuntu/data/code/feature4.zip#feature_data_service 
        --conf spark.executorEnv.LD_LIBRARY_PATH=./spark-xgboost/lib/python2.7/lib-dynload 
        /home/ubuntu/data/code/feature-data-service/model/level_model/spark_run.py

cluster模式:
    /home/ubuntu/data/spark-python/bin/spark-submit --master yarn --deploy-mode cluster
    --jars ./spark-sql-kafka-0-10_2.11-2.4.3.jar,./kafka-clients-0.10.2.0.jar 
    --archives ./spark-xgboost.zip#spark-xgboost,/home/ubuntu/data/code/feature4.zip#feature_data_service
    --conf spark.executorEnv.LD_LIBRARY_PATH=./spark-xgboost/lib/python2.7/lib-dynload
    --conf spark.yarn.appMasterEnv.LD_LIBRARY_PATH=./spark-xgboost/lib/python2.7/lib-dynload 
    --conf spark.yarn.appMasterEnv.PYSPARK_PYTHON=./spark-xgboost/bin/python
    /home/ubuntu/data/code/feature-data-service/model/level_model/spark_run.py

    单个模型触发:
        /home/ubuntu/data/spark-python/bin/spark-submit --master yarn --deploy-mode client 
        --jars ./spark-sql-kafka-0-10_2.11-2.4.3.jar,./kafka-clients-0.10.2.0.jar 
        --archives ./spark-model.zip#spark-model,/home/ubuntu/data/code/feature4.zip#feature_data_service 
        --conf spark.executorEnv.LD_LIBRARY_PATH=./spark-model/lib/python2.7/lib-dynload 
        --conf spark.yarn.appMasterEnv.PYSPARK_PYTHON=./spark-model/bin/python 
        --conf spark.yarn.appMasterEnv.LD_LIBRARY_PATH=./spark-model/lib/python2.7/lib-dynload 
        --name ins_repay_model /home/ubuntu/data/code/feature-data-service/model/user_classifier_model/spark_run.py

10.对于Continuous Processing模式:

1.不能使用foreachBatch sink,所以通过dataFrame.rdd.map不能解析kafka消息内容,streaming dataFrame后一定要接writeStream.start()

有两个选择:
- 简单的select和withColumn方法
- 自定义函数udf,select()会对每个row调用udf,print测试即可(先利用foreachBatch和work路径)
    如何调试:
        @udf(returnType=BinaryType())   # 会自动转换字符串为bytearray,对于字符串'null'转为bytearray(b'null'))
        def message_process(value,topic):
            data = asyncio.get_event_loop().run_until_complete(kafka_producer(value,topic))
            return json.dumps(data)
        df.select("key","partition",message_process("value","topic").alias("value")).filter(col("value")!="null").show()
        # 注意return为None时,json.dumps()转换为字符串'null',不是数据库的null了,df.dropna()或df.na.drop()都没用了
        # 对于bytearray(b'null')),也适用filter(col("value")!="null")
    或:
        return json.dumps(dataif data else None
        df.select("key""partition", message_process("value""topic").alias("value")).filter("value is not null").show()

2.对于测试,console输出没显示,memory也不好用。
可以先用micro-batch模式,调试好udf函数,再改用Continuous Processing
# console没输出是因为一开始kafka-client的版本问题

3.关于offset,目前没找到接口来存放offset

只能存放在checkpoint,修改代码时手动修改初始offset

4.foreachBatch在boostrap错误的情况下,还能获取消息?

原来是risk和installment集群都有这两个topic
risk好用(offset没更新,超过就不好用了),installment不好用是因为topic初始offset的问题

5.错误Lost task 16.0 in stage 0.0 (TID 16, 172.22.9.179, executor 1): java.util.NoSuchElementException: None.get

console + trigger(processingTime="1 second")测试没问题    # 1 seconds也一样
kafka + trigger(processingTime="1 second")测试没问题
console + trigger(continuous="1 second")出现错误,原因是Continuous processing mode与python udf的问题
    spark 2.4.3使用ThreadLocal,需要使用InheritableThreadLocal,将在spark 3.0修复
    pandas_udf也不行
    # Python UDF使用线程从Python processes读取数据、写入数据,如果使用ThreadLocal,获取不到之前的epoch

spark-2.4.4版本更新,console模式print能正常输出(client输出类似cluster,而work输出太多使用grep捕捉)
    kafka模式下,输出到test topic有输出。

6.发送kafka sink的消息格式:

可包含partition,但不解析(kafka默认的partitioner有这功能,估计是spark不发送),试过最新2.3.x版本的client也不行
如何指定uid对应的partition:
    用java自定义partitioner
    关于key\value本身,是dataframe的binary类型,[722 63 72 65 6...的形式
        1.Python api:
            python中对应为bytearray(b'8308643'),通过python decode后是str类型的uid,不是int
            如果不decode,而是通过returnType=StringType()来将binary转为String,结果是在python打印为"[B@4f023edb",df.show()也为"[B@4f023edb"
            如果decode了,则为df.show()或console模式和print打印都为"8308643"  
            如果returnType=BinaryType(),则df.show()为[32 30 33 31 39 3...,而print则自动转为python的bytearray(b'8308643')。
            # key就算在spark内dataframe通过cast转为string后,传给Java partitioner的还是[7B 22 63 72 65 6...的形式
            # 所以不要让spark自己转binary为string,而是python通过decode或者java通过new String(bytearray, "utf-8")
        2.Java api:
            在java中对应为byte[](具体是什么java的paritioner没打印,用java编写脚本后take(5)显示为"[Lorg.apache.spark.sql.Row;@6202698f"
            first()显示为[[B@2dae35da,[B@17b788c1,businessEvent,0,1845180,2019-08-18 04:42:32.211,0],
            df.first().getString(2)能获取StringType的内容,getByte()获取binary会报错byte[],getAs("key")可以获取对应字段的内容"[B@2de2c005"
            通过udf函数print出binary对应的column,内容也是"[B@2de2c005"这种)
            通过Utils.utf8(keyBytes)转为string,Integer.valueOf转为数字。
            # Array.toString只会变为"[32 30 33 31 39 3..."这种格式的字符串,这样用Integer.valueOf()转不了
            # 得用String a = new String(bytearray, "utf-8")
            # Base64.getEncoder().encodeToString(bytes)得到的是"OTE3NzQxNg=="

standalone模式:

./bin/spark-submit --master spark://172.22.9.181:7077 
--jars ./spark-sql-kafka-0-10_2.11-2.4.3.jar,./kafka-clients-0.10.2.0.jar 
/home/ubuntu/data/code/feature-server/model/model_event_spark.py
posted @ 2021-12-29 20:46  心平万物顺  阅读(308)  评论(0编辑  收藏  举报