介绍
Spark看到MRv1的问题,对MapReduce做了大量优化,总结如下:
-
快速处理能力。随着实时大数据应用越来越多,Hadoop作为离线的高吞吐、低响应框架已不能满足这类需求。Hadoop MapReduce的Job将中间输出和结果存储在HDFS中,读写HDFS造成磁盘IO成为瓶颈。Spark允许将中间输出和结果存储在内存中,节省了大量的磁盘IO。同时Spark自身的DAG执行引擎也支持数据在内存中的计算。Spark官网声称性能比Hadoop快100倍,如图2-3所示。即便是内存不足需要磁盘IO,其速度也是Hadoop的10倍以上。
-
易于使用。Spark现在支持Java、Scala、Python和R等语言编写应用程序,大大降低了使用者的门槛。自带了80多个高等级操作符,允许在Scala,Python,R的shell中进行交互式查询。
-
支持查询。Spark支持SQL及Hive SQL对数据查询。
-
支持流式计算。与MapReduce只能处理离线数据相比,Spark还支持实时的流计算。Spark依赖Spark Streaming对数据进行实时的处理,其流式处理能力还要强于Storm。
-
可用性高。Spark自身实现了Standalone部署模式,此模式下的Master可以有多个,解决了单点故障问题。此模式完全可以使用其他集群管理器替换,比如YARN、Mesos、EC2等。
-
丰富的数据源支持。Spark除了可以访问操作系统自身的文件系统和HDFS,还可以访问Cassandra, HBase, Hive, Tachyon以及任何Hadoop的数据源。这极大地方便了已经使用HDFS、Hbase的用户顺利迁移到Spark。
-
Driver:表示main()函数,创建SparkContext。由SparkContext负责与ClusterManager通信,进行资源的申请,任务的分配和监控等。程序执行完毕后关闭SparkContext
-
Worker:集群中可以运行Application代码的节点。在Standalone模式中指的是通过slave文件配置的worker节点,在Spark on Yarn模式中指的就是NodeManager节点。
-
Executor:某个Application运行在Worker节点上的一个***进程***,该进程负责运行某些task,并且负责将数据存在内存或者磁盘上。在Spark on Yarn模式下,其进程名称为CoarseGrainedExecutor Backend,一个CoarseGrainedExecutor Backend进程有且仅有一个executor对象,它负责将Task包装成taskRunner,并从线程池中抽取出一个空闲线程运行Task,这样,每个CoarseGrainedExecutorBackend能并行运行Task的数据就取决于分配给它的CPU的个数。
1) 每个节点上可以运行一个或多个Executor服务;
2) 每个Executor配有一定数量的slot,表示该Executor中可以同时运行多少个ShuffleMapTask或者ReduceTask;
3) 每个Executor单独运行在一个JVM进程中,每个Task则是运行在Executor中的一个线程;application之间不共享Executor。
4) 同一个Executor内部的Task可共享内存,比如通过函数SparkContext#broadcast广播的文件或者数据结构只会在每个Executor中加载一次,而不会像MapReduce那样,每个Task加载一次;
5) Executor一旦启动后,将一直运行,且它的资源可以一直被Task复用,直到Spark程序运行完成后才释放退出。
总体上看,Spark采用的是经典的scheduler/workers模式,每个Spark应用程序运行的第一步是构建一个可重用的资源池,然后 在这个资源池里运行所有的ShuffleMapTask和ReduceTask(注意,尽管Spark编程方式十分灵活,不再局限于编写Mapper和 Reducer,但是在Spark引擎内部只用两类Task便可表示出一个复杂的应用程序,即ShuffleMapTask和ReduceTask),而 MapReduce应用程序则不同,它不会构建一个可重用的资源池,而是让每个Task动态申请资源,且运行完后马上释放资源。
对比以下,MR采用的是多进程模型
-
DAGScheduler,依据RDD之间的对应关系创建DAG,包含stage的划分等
-
TaskScheduler,将具体的task分配给worker
-
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,
-
一个JOB会被拆分为多个stage(stage之间按顺序执行),每个Stage包含多个task(task之间并行,RDD有多少个partion就有多少task)。前面的stage包含的是ShuffleMapTasks,最后一个stage包含的是ResultTask。下面我们将详细讲解这个问题。
SPARK ON YARN
spark on yarn提交任务有两种模式,一种是cluster模式,一种是client模式。
a.执行命令“./spark-shell --master yarn”默认运行的是client模式。
b.执行"./spark-shell --master yarn-client"或者"./spark-shell --master yarn --deploy-mode client"运行的也是client。
c.执行"./spark-shell --master yarn-cluster"或者"./spark-shell --master yarn --deploy-mode cluster"运行的是cluster模式。但是,这两个启动cluster模式的命令是不可用的,这就牵涉到这两种模式的区别。
d.执行 ./bin/spark-submit --class org.apache.spark.examples.SparkPi --master yarn-cluster --num-executors 3 --driver-memory 1g --executor-memory 1g --executor-cores 1 --queue thequeue lib/spark-examples-1.6.1-hadoop2.6.0.jar 10 实际上spark yarn模式。
client和cluster模式的主要区别在于,前者的driver是运行在客户端进程中,后者的driver是运行在NodeManager的ApplicationMaster之中。我们知道,driver是负责分发作业的,运行在ApplicationMaster里的driver是无法直接与客户端进行通信的,故而c中所提到的两条命令不可用。对应的spark-sql中的一些命令也是不可用的。
生产上我们通常使用spark on yarn-cluster模式,由于client模式的driver在客户端上,客户端可能与executor不在同一局域网,他们之间的通信会很慢。影响工作效率。
Stage的划分
窄依赖指父RDD的每一个分区最多被一个子RDD的分区所用,表现为
一个父RDD的分区对应于一个子RDD的分区
两个父RDD的分区对应于一个子RDD 的分区。
宽依赖指子RDD的每个分区都要依赖于父RDD的所有分区,这是shuffle类操作。
因此spark划分stage的整体思路是:从后往前推,遇到宽依赖就断开,划分为一个stage;遇到窄依赖就将这个RDD加入该stage中。
在spark中,Task的类型分为2种:ShuffleMapTask和ResultTask;简单来说,DAG的最后一个阶段会为每个结果的partition生成一个ResultTask。而其余所有阶段都会生成ShuffleMapTask。
每个Stage里面的Task的数量是由该Stage中最后一个RDD的Partition的数量所决定的。之所以称之为ShuffleMapTask是因为它需要将自己的计算结果通过shuffle到下一个stage中。
安装与部署
注意,SPARK可以在HADOOP体系中(YARN)进行安装,也可以单独安装(但是需要HDFS)。
单独安装无非是设置SSH无密码登陆,下载解压代码,修改环境变量和相关配置,分别启动master和slave(嗅到一丝政治不正确的味道)。然后部署client。这里不再赘述。
基本概念
共享变量
Spark两种共享变量:广播变量(broadcast variable)与累加器(accumulator)。
广播变量一般用于大对象的分发。
广播变量的使用如下
object BroadcastTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local").setAppName("broadcast")
val sc = new SparkContext(conf)
val list = List("hello java")
val broadcast = sc.broadcast(list)//如果不使用广播变量,代码仍然能运行,但是会为每一个task复制一份,导致不必要的网络传输和内存存储
val linesRDD = sc.textFile("./word")
linesRDD.filter(line => {
broadcast.value.contains(line)
}).foreach(println)
sc.stop()
}
}
累加器的使用如下
object AccumulatorTest {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local").setAppName("accumulator")
val sc = new SparkContext(conf)
val accumulator = sc.accumulator(0); //创建accumulator并初始化为0
val linesRDD = sc.textFile("./word")
val result = linesRDD.map(s => {
accumulator.add(1) //有一条数据就增加1
s
})
result.collect();
println("words lines is :" + accumulator.value)
sc.stop()
}
}
具体流程
standalone client模式
- 用户在一个worker上提交任务给master。
- 该worker启动一个driver,即SchedulerBackend。Worker创建一个DriverRunner线程,DriverRunner启动SchedulerBackend线程。driver向master申请资源。
- Master还让其余Worker启动Executor,即ExecutorBackend。Worker创建一个ExecutorRunner线程,ExecutorRunner会启动ExecutorBackend进程。
- ExecutorBackend启动后会向Driver的SchedulerBackend注册,SchedulerBackend进程中包含DAGScheduler,ExecutorBackend向SchedulerBackend汇报的时候把TaskScheduler中的Task调度到ExecutorBackend执行。
- 所有stage都完成后作业结束
standalone cluster模式
- 用户启动客户端,客户端提交应用程序给Master。
- Master调度应用,针对每个应用分发给指定的一个Worker启动Driver,即Scheduler-Backend。 Worker接收到Master命令后创建DriverRunner线程,在DriverRunner线程内创建SchedulerBackend进程。Driver充当整个作业的主控进程。Master会指定其他Worker启动Exeuctor,即ExecutorBackend进程,提供计算资源。流程和上面很相似
- Worker创建ExecutorRunner线程,ExecutorRunner会启动ExecutorBackend进程。
- ExecutorBackend启动后,向Driver的SchedulerBackend注册,这样Driver获取了计算资源就可以调度和将任务分发到计算节点执行。SchedulerBackend进程中包含DAGScheduler,它会根据RDD的DAG切分Stage,生成TaskSet,并调度和分发Task到Executor。对于每个Stage的TaskSet,都会被存放到TaskScheduler中。TaskScheduler将任务分发到Executor,执行多线程并行任务。
YARN CLINET模式
- 客户端生成作业信息提交给ResourceManager(RM)
- RM在本地NodeManager启动container并将Application Master(AM)分配给该NodeManager(NM)
- NM接收到RM的分配,启动Application Master并初始化作业,此时这个NM就称为Driver
- Application向RM申请资源,分配资源同时通知其他NodeManager启动相应的Executor
- Executor向本地启动的Application Master注册汇报并完成相应的任务
YARN CLUSTER模式
- 客户端生成作业信息提交给ResourceManager(RM)
- RM在某一个NodeManager(由Yarn决定)启动container并将Application Master(AM)分配给该NodeManager(NM)
- NM接收到RM的分配,启动Application Master并初始化作业,此时这个NM就称为Driver
- Application向RM申请资源,分配资源同时通知其他NodeManager启动相应的Executor
- Executor向NM上的Application Master注册汇报并完成相应的任务
原理
Spark在进行transformation计算的时候,不会触发Job ,只有执行action操作的时候,才会触发Job,在Driver中SparkContext根据RDD之间的依赖关系创建出DAG有向无环图,DAGScheduler负责解析这个图,解析时是以Shuffle为边界,反向解析,构建stage。将多个任务根据依赖关系划分为不同的Stage,将每个Stage的Taste Set 提交给TaskScheduler去执行,任务会在Executor进程的多个Task线程上执行,完成Task任务后 将结果信息提交到ExecutorBackend中 他会将信息提交给TaskScheduler。
TaskScheduler接到消息后通知TaskManager,移除该Task任务,开始执行下一个任务。TaskScheduler同时会将信息同步到TaskSet Manager中一份,全部任务执行完毕后TaskSet Manager将结果反馈给DAGScheduler,如果属于ResultTask 会交给JobListener。否则话全部任务执行完毕后写入数据。
BlockManger负责管理整个集群的读写。
Mapreduce的过程整体上分为四个阶段:InputFormat MapTask ReduceTask OutPutFormat 当然中间还有shuffle阶段。
SPARK相关的API
RDD
RDD有一种非常特殊也是非常实用的format——pair RDD,即RDD的每一行是(key, value)的格式。这种格式很像Python的字典类型,便于针对key进行一些处理。一份待处理的原始数据会被按照相应的逻辑(例如jdbc和hdfs的split逻辑)切分成n份,每份数据对应到RDD中的一个Partition,Partition的数量决定了task的数量,影响着程序的并行度。
常用transformation操作
RDD的transformation,又分为宽依赖和窄依赖,区别是是否发生洗牌操作。shuffle 操作是 spark 中最耗时的操作,应尽量避免不必要的 shuffle.
- map
- filter
- gropuByKey 根据key进行分组,每个key对应一个Iterable
- reduceByKey 对每个key对应的value进行reduce操作
rdd.groupByKey().mapValues(_ .sum) 与 rdd.reduceByKey(_ + _) 执行的结果是一样的,但是前者需要把全部的数据通过网络传递一遍,而后者只需要根据每个 key 局部的 partition 累积结果,在 shuffle 的之后把局部的累积值相加后得到结果.
常用action操作
- Reduce
val c = sc.parallelize(1 to 10)
c.reduce((x, y) => x + y)//结果55
- collect
- count
cache操作
cache()方法表示:使用非序列化的方式将RDD的数据全部尝试持久化到内存中,cache()只是一个transformtion,是lazy的,必须通过一个action触发,才能真正的将该RDD cache到内存中。
##Shuffle的实现
Shuffle是MapReduce框架中的一个特定的phase,介于Map phase和Reduce phase之间,当Map的输出结果要被Reduce使用时,输出结果需要按key哈希,并且分发到每一个Reducer上去,这个过程就是shuffle。由于shuffle涉及到了磁盘的读写和网络的传输,因此shuffle性能的高低直接影响到了整个程序的运行效率。
Spark sql
//===========================================1 spark SQL===================
//数据导入方式
Dataset<Row> df = spark.read().json("..\\sparkTestData\\people.json");
//查看表
df.show();
//查看表结构
df.printSchema();
//查看某一列 类似于MySQL: select name from people
df.select("name").show();
//查看多列并作计算 类似于MySQL: select name ,age+1 from people
df.select(col("name"), col("age").plus(1)).show();
//设置过滤条件 类似于MySQL:select * from people where age>21
df.filter(col("age").gt(21)).show();
//做聚合操作 类似于MySQL:select age,count(*) from people group by age
df.groupBy("age").count().show();
//上述多个条件进行组合 select ta.age,count(*) from (select name,age+1 as "age" from people) as ta where ta.age>21 group by ta.age
df.select(col("name"), col("age").plus(1).alias("age")).filter(col("age").gt(21)).groupBy("age").count().show();
//直接使用spark SQL进行查询
//先注册为临时表
df.createOrReplaceTempView("people");
Dataset<Row> sqlDF = spark.sql("SELECT * FROM people");
sqlDF.show();
如何理解SPARK UI上展示的信息
org.apache.spark.streaming.api.java.JavaStreamingContext.start(JavaStreamingContext.scala:554)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
py4j.reflection.MethodInvoker.invoke(MethodInvoker.java:237)
py4j.reflection.ReflectionEngine.invoke(ReflectionEngine.java:357)
py4j.Gateway.invoke(Gateway.java:280)
py4j.commands.AbstractCommand.invokeMethod(AbstractCommand.java:132)
py4j.commands.CallCommand.execute(CallCommand.java:79)
py4j.GatewayConnection.run(GatewayConnection.java:214)
java.lang.Thread.run(Thread.java:745)
可以看到,Spark实际上是使用py4j操作操作自己的内部的java对象。
应用
实时处理数据的平台
携程使用SparkStreaming构建了自己的实时处理数据的平台
MuiseSpark Core是我们基于Spark Streaming实现的二次封装,用于支持携程多种消息队列,其中HermesKafka与源生的Kafka基于Direct Approach的方式消费数据,Hermes Mysql与Qmq基于Receiver的方式消费数据。接下来将要讲的诸多特性主要是针对Kafka类型的数据源。
Muisespark core主要包含了以下特性:
Kafka Offset自动管理
支持Exactly Once与At Least Once语义
提供Metric注册系统,用户可注册自定义metric
基于系统与用户自定义metric进行预警
Long running on Yarn,提供容错机制
http://bigdata.51cto.com/art/201709/552647.htm
FAQ
- 如何引入第三方依赖?
参考https://www.cnblogs.com/dinghong-jo/p/7873646.html。
思路是要么在所有机器上安装该库。要不将库打包在jar包中。其实和Storm的方式相同。