Spark入门基础教程

from: http://www.linuxidc.com/Linux/2016-03/129506.htm

背景

  目前按照大数据处理类型来分大致可以分为:批量数据处理、交互式数据查询、实时数据流处理,这三种数据处理方式对应的业务场景也都不一样;
  关注大数据处理的应该都知道Hadoop,而Hadoop的核心为HDFSMapReduce,HDFS分布式文件系统在Hadop中是用来存储数据的;MapReduce为Hadoop处理数据的核心,接触过函数式编程的都知道函数式语言中也存在着Map、Reduce函数其实这两者的思想是一致的;也正是因为Hadoop数据处理核心为MapReduce奠定了它注定不是适用场景广泛的大数据框架;
  可以这么说Hadoop适用于Map、Reduce存在的任何场景,具体场景比如:WordCount、排序、PageRank、用户行为分析、数据统计等,而这些场景都算是批量数据处理,而Hadoop并不适用于交互式数据查询、实时数据流处理;
  这时候就出现了各种数据处理模型下的专用框架如:Storm、Impala、GraphLab等;
  1、Storm:针对实时数据流处理的分布式框架;
  2、Impala:适用于交互式大数据查询的分布式框架;
  3、GraphLab:基于图模型的机器学习框架;


            1、MapReduce简单模型
  
  这时候如果一个团队或一个公司中同时都有设计到大数据批量处理、交互式查询、实时数据流处理这三个场景;这时候就会有一些问题:
  1、学习成本很高,每个框架都是不同的实现语言、不同的团队开发的;
  2、各个场景组合起来代价必然会很大;
  3、各个框架中共享的中间数据共享与移动成本高;
  

Spark

  就在这时候UC Berkeley AMP推出了全新的大数据处理框架:Spark提供了全面、统一适用与不同场景的大数据处理需求(批量数据处理、交互式数据查询、实时数据流处理、机器学习);Spark不仅性能远胜于Hadoop而却还兼容Hadoop生态系统,Spark可以运行在Hadoop HDFS之上提供争强 功能,可以说Spark替代了Hadoop MapReduce,但Spark依然兼容Hadoop中的YARN与Apache Mesos组件,现有Hadoop用户可以很容易就迁移到Spark;
  Spark提出了RDD(Resilient Distributed Datasets)这么一个全新的概念,RDD弹性分布式数据集是并行、容错的分布式数据结构;RDD可以持久化到硬盘或内存当中,为一个分区的数据集,分区的多少决定了并行计算的粒度;并且提供了一系列的操作RDD中的数据:
  1、创建操作(Creation Operation):RDD由SparkContext通过内存数据或外部文件系统创建;
  2、转换操作(Transformation Operation):将RDD通过转换操作变为另一个RDD,Spark提供了map、flatMap、filter等一系列的转换操作;
  3、控制操作(Control Operation):将RDD持久化到内存或硬盘当中,如cache将filterRDD缓存到内存;
  4、行动操作:(Action Operation):Spark采用了惰性计算,对于任何行动操作都会产生Spark Job运行产生最终结果;提供了join、groupBy、count等操作,Spark中存在两种操作产生的结果为Scala集合或者标量与RDD保存到文件或数据库;


             1、Spark结构图

  Spark RDD:Spark RDD提供了一系列的操作接口,为不变的数据存储结构并存储与内存中使用DAG进行任务规划使更好的处理MapReduce类似的批处理;
  Shark/Spark SQL:分布式SQL引擎,兼容Hive性能远比Hive高很多;
  Spark Streaming:将数据流分解为一系列批处理作业使用Spark调度框架更好的支持数据流操作,支持的数据输入源有:Kafka、Flume等;
  GraphX:兼容Pregel、GraphLab接口为基于Spark的图计算框架;
  MLlib:为Spark的机器学习算法库,支持常用的算法有:分类算法、推荐算法、聚类算法等等;

  性能卓越、支持多种大数据处理模型、支持多种编程语言接口:Java、Scala、Python,许多大公司如IBM等大力支持推广Spark的发展;

Spark运行模式与Standalone模式部署

前面简单的介绍了Spark的一些概念还有Spark生态圈的一些情况,这里主要是介绍Spark运行模式与Spark Standalone模式的部署;

Spark运行模式

  在Spark中存在着多种运行模式,可使用本地模式运行、可使用伪分布式模式运行、使用分布式模式也存在多种模式如:Spark Mesos模式、Spark YARN模式;

Spark Mesos模式:官方推荐模式,通用集群管理,有两种调度模式:粗粒度模式(Coarse-grained Mode)与细粒度模式(Fine-grained Mode);
Spark YARN模式:Hadoop YARN资源管理模式;
Standalone模式: 简单模式或称独立模式,可以单独部署到一个集群中,无依赖任何其他资源管理系统。不使用其他调度工具时会存在单点故障,使用Zookeeper等可以解决;
Local模式:本地模式,可以启动本地一个线程来运行job,可以启动N个线程或者使用系统所有核运行job;

Standalone模式部署实践

  Standalone模式需要将Spark复制到集群中的每个节点,然后分别启动每个节点即可;Spark Standalone模式的集群由Master与Worker节点组成,程序通过与Master节点交互申请资源,Worker节点启动Executor运行;
  这里使用了两节点部署Spark集群:192.168.2.131、192.168.2.133,下面简称为:133与131节点;其中133节点既是Master节点同时又是Worker节点,131节点为Worker节点;


节点结构图

部署步骤:
  一、
首先在133节点上下载Java、Scala与Spark并解压到/usr/local目录下,这里使用的Spark是带有Hadoop的版本

  下载解压到local

  二、配置Java、Scala与Spark环境变量,这里把环境变量配置到/etc/profile文件中,请忽略Hadoop环境变量;

  环境变量配置

  三、测试Java、Scala是否配置成功,在终端输入:java -version与scala -version

  四、配置Spark环境变量,进入Spark目录下的conf目录把slaves.template重命名为slaves,接着把spark-env.sh.template重命名为:spark-env.sh;

    重命名
    修改spark-env.sh文件,添加环境变量;

    spark-env修改
  五、
在133节点使用scp把下载好的Java、Scala、Spark发送到131节点,并在131节点上重复以上所有步骤;
  六、在两个节点都完成以上所有步骤后开始启动Spark,133节点既是Master又是Worker;
    1、首先在133启动Spark,进入Spark目录的sbin目录执行./start-all.sh:

    Master启动
    使用jps命令发现存在Master与Worker进程,说明Spark已启动成功;

    2、启动131节点的Spark,进入Spark目录的sbin目录执行:./start-slave.sh spark://192.168.2.133:7077
    start-slave.sh后面的地址为Master节点的通信地址,指定当前slave节点连接到的Master;

    slave启动:
    使用jps命令,存在Worker进程则说明当前的Spark Worker节点启动成功;
  七、 Spark Web页面
    可以通过http://192.168.2.133:8080/ 地址查看到当前Spark集群的信息,这地址为Master节点的地址;

SparkWeb:

参考资料:
http://spark.apache.org/docs/latest/spark-standalone.html

Spark中最核心的概念为RDD(Resilient Distributed DataSets)中文为:弹性分布式数据集,RDD为对分布式内存对象的 抽象它表示一个被分区不可变且能并行操作的数据集;RDD为可序列化的、可缓存到内存对RDD进行操作过后还可以存到内存中,下次操作直接把内存中RDD作为输入,避免了Hadoop MapReduce的大IO操作;

RDD生成

  Spark所要处理的任何数据都是存储在RDD之中,目前两种方式可以生成一个RDD:
  1、从RDD进行转换操作
  2、使用外部存储系统创建,如:HDFS;

RDD操作

  RDD支持两种操作:
    转换(transformation operation)
    转换操作将一个RDD经过操作后返回一个全新的RDD,转换操是lazy(惰性)的这期间不会产生任何数据的计算;
    转换函数有:distinct、filter、map、flatMap、union、groupByKey等;
    行动(action operation)
    每一个行动操作都会触发Spark Job进行计算并返回最终的结果,行动操作有这么几类:返回标量,count返回元素的个数;返回Scala集合,task(n)返回0到n-1组成的集合;写入外部存储,saveAsHadoopFile(path)存储到HDFS;
    行动函数有:count、top、task、saveAsHadoopFile等;
  RDD为不可变的数据集,可以使用转换操作“修改”一个RDD,但这操作过后返回的是一个全新的RDD 原本RDD并没有改变;


          RDD状态转换图

Lineage

  Spark RDD只支持粗粒度的操作,对一个RDD的操作都会被作用于该RDD的所有数据;为了保证RDD的高可用性RDD通过使用Lineage(血统)记录了RDD演变流程(从其他RDD到当前RDD所做的操作) 当RDD分区数据丢失时可以通过Lineage的信息重新计算与恢复分区数据,或进行RDD的重建;
  RDD的依赖关系(dependencies)
  由于对RDD的操作都是粗粒度的一个转换操作过后都会产生一个新的RDD,RDD之间会形成一个前后依赖关系;Spark中存在两种依赖:窄依赖(Narrow Dependencies)、宽依赖(Wide Dependencies);
  窄依赖(Narrow Dependencies):一个父RDD的分区只能被一个子RDD的一个分区使用;
  宽依赖(Wide Dependencies):多个子RDD的分区依赖于一个父RDD的同一个分区;
  窄依赖的节点(RDD)关系如果流水一般,所以当节点失败后只需重新计算父节点的分区即可,宽依赖需要重新计算父节点的多个分区代价是非常昂贵的;


          窄依赖Narrow


          宽依赖Wide

参考资料:
http://www.cs.berkeley.edu/~matei/papers/2012/nsdi_spark.pdf
http://spark.apache.org/docs/latest/programming-guide.html

编译打包

  Spark支持Maven与SBT两种编译工具,这里使用了Maven进行编译打包;
  在执行make-distribution脚本时它会检查本地是否已经存在Maven还有当前Spark所依赖的Scala版本,如果不存在它会自动帮你下载到build目录中并解压使用;Maven源最好配置成OSChina的中央库,这下载依赖包比较快;
  耐心等待,我编译过多次所以没有下载依赖包,大概半个小时左右编译完成;注意:如果使用的是Java 1.8需要给JVM配置堆与非堆内存,如:export MAVEN_OPTS="-Xmx1.5g -XX:MaxPermSize=512M -XX:ReservedCodeCacheSize=512m";

  进入Spark根目录下,执行:

./make-distribution.sh --tgz

 --tgz 参数是指编译后生成tgz包 
 - PHadoop 支持Hadoop
 -Pyarn :支持yarn
 -Phive :支持hive
 --with-tachyon:支持tachyon内存文件系统
 -name:与--tgz一起用时,name代替Hadoop版本号

 ./make-distribution.sh --tgz --name 2.6.0 -Pyarn -Phadoop-2.6 -Phive 

   开始编译检查本地环境,如不存在合适的Scala与Maven就在后台下载;

编译中:

编译完成并打包生成tgz:


  

编译完成后把生成的文件拷贝到当前Spark的dist目录中并且打包生成spark-1.5.3-SNAPSHOT-bin-2.2.0.tgz文件;

Spark——共享变量

Spark执行不少操作时都依赖于闭包函数的调用,此时如果闭包函数使用到了外部变量驱动程序在使用行动操作时传递到集群中各worker节点任务时就会进行一系列操作:
  1、驱动程序使将闭包中使用变量封装成对象,驱动程序序列化对象,传给worker节点任务;
  2、worker节点任务接收到对象,执行闭包函数;
由于使用外部变量势必会通过网络、序列化、反序列化,如外部变量过大或过多使用外部变量将会影响Spark程序的性能;
  Spark提供了两种类型的共享变量(Shared Variables):广播变量(Broadcast Variables)、累加器(Accumulators );
  
广播变量(Broadcast Variables)
  Spark提供的广播变量可以解决闭包函数引用外部大变量引起的性能问题;广播变量将只读变量缓存在每个worker节点中,Spark使用了高效广播算法分发变量从而提高通信性能;如直接在闭包函数中使用外部 变量该变量会缓存在每个任务(jobTask)中如果多个任务同时使用了一个大变量势必会影响到程序性能;
  广播变量:每个worker节点中缓存一个副本,通过高效广播算法提高传输效率,广播变量是只读的;
  Spark Scala Api与Java Api默认使用了Jdk自带序列化库,通过使用第三方或使用自定义的序列化库还可以进一步提高广播变量的性能;

广播变量使用示例:

val sc = SparkContext("");
val eigenValue = sc.bradcast(loadEigenValue())
val eigen = computer.map{x => 
    val temp = eigenValue.value
    ...
    ...
}

      左节点不使用广播变量,右使用广播变量
累加器(Accumulators)

  累加器可以使得worker节点中指定的值聚合到驱动程序中,如统计Spark程序执行过程中的事件总数等;

val sc = new SparkContext(...)
val file = sc.textFile("xxx.txt")
val eventCount = sc.accumulator(0,"EventAccumulator")  //累加器初始值为0

val formatEvent = file.flatMap(line => {
     if(line.contains("error")){
         eventCount +=1
     }
    })
formatEvent.saveAsTextFile("eventData.txt")
println("error event count : " + eventCount);

  在使用累加器(Accumulators)时需要注意,只有在行动操作中才会触发累加器,也就是说上述代码中由于flatMap()为转换操作因为Spark惰性特征所以只用当saveAsTextFile() 执行时累加器才会被触发;累加器只有在驱动程序中才可访问,worker节点中的任务不可访问累加器中的值;
  Spark原生支持了数字类型的的累加器如:Int、Double、Long、Float等;此外Spark还支持自定义累加器用户可以通过继承AccumulableParam特征来实现自定义的累加器此外Spark还提供了accumulableCollection()累加集合用于;创建累加器时可以使用名字也可以不是用名字,当使用了名字时在Spark UI中可看到当中程序中定义的累加器, 广播变量存储级别为MEMORY_AND_DISK;

Spark作业调度阶段分析

Spark作为分布式的大数据处理框架必然或涉及到大量的作业调度,如果能够理解Spark中的调度对我们编写或优化Spark程序都是有很大帮助的;
  在Spark中存在转换操作(Transformation Operation)行动操作(Action Operation)两种;而转换操作只是会从一个RDD中生成另一个RDD且是lazy的,Spark中只有行动操作(Action Operation)才会触发作业的提交,从而引发作业调度;在一个计算任务中可能会多次调用 转换操作这些操作生成的RDD可能存在着依赖关系,而由于转换都是lazy所以当行动操作(Action Operation )触发时才会有真正的RDD生成,这一系列的RDD中就存在着依赖关系形成一个DAG(Directed Acyclc Graph),在Spark中DAGScheuler是基于DAG的顶层调度模块;

相关名词

  Application:使用Spark编写的应用程序,通常需要提交一个或多个作业;
  Job:在触发RDD Action操作时产生的计算作业
  Task:一个分区数据集中最小处理单元也就是真正执行作业的地方
  TaskSet:由多个Task所组成没有Shuffle依赖关系的任务集
  Stage:一个任务集对应的调度阶段 ,每个Job会被拆分成诺干个Stage

    
          1.1 作业调度关系图

RDD Action作业提交流程

  这里根据Spark源码跟踪触发Action操作时触发的Job提交流程,Count()是RDD中的一个Action操作所以调用Count时会触发Job提交;
  在RDD源码count()调用SparkContext的runJob,在runJob方法中根据partitions(分区)大小创建Arrays存放返回结果;

RDD.scala

/** * Return the number of elements in the RDD. */
def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum

SparkContext.scala

def runJob[T, U: ClassTag](
  rdd: RDD[T],
  func: (TaskContext, Iterator[T]) => U,
  partitions: Seq[Int],
  resultHandler: (Int, U) => Unit): Unit = {

  val callSite = getCallSite
  val cleanedFunc = clean(func) logInfo("Starting job: " + callSite.shortForm)
  if (conf.getBoolean("spark.logLineage", false)) {
    logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
  }
  dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
}

  在SparkContext中将调用DAGScheduler的runJob方法提交作业,DAGScheduler主要任务是计算作业与任务依赖关系,处理调用逻辑;DAGScheduler提供了submitJob与runJob方法用于 提交作业,runJob方法会一直等待作业完成,submitJob则返回JobWaiter对象可以用于判断作业执行结果;
  在runJob方法中将调用submitJob,在submitJob中把提交操作放入到事件循环队列(DAGSchedulerEventProcessLoop)中;

def submitJob[T, U](
 rdd: RDD[T],
 func: (TaskContext, Iterator[T]) => U,
 partitions: Seq[Int],
 callSite: CallSite,
 resultHandler: (Int, U) => Unit,
 properties: Properties): JobWaiter[U] = {
      ......  
      eventProcessLoop.post(JobSubmitted(
      jobId, rdd, func2, partitions.toArray, callSite, waiter,
      SerializationUtils.clone(properties)))
      ......
  }  

  在事件循环队列中将调用eventprocessLoop的onReceive方法;

Stage拆分

  提交作业时DAGScheduler会从RDD依赖链尾部开始,遍历整个依赖链划分调度阶段;划分阶段以ShuffleDependency为依据,当没有ShuffleDependency时整个Job 只会有一个Stage;在事件循环队列中将会调用DAGScheduler的handleJobSubmitted方法,此方法会拆分Stage、提交Stage;

 private[scheduler] def handleJobSubmitted(jobId: Int,
  finalRDD: RDD[_],
  func: (TaskContext, Iterator[_]) => _,
  partitions: Array[Int],
  callSite: CallSite,
  listener: JobListener,
  properties: Properties) {
var finalStage: ResultStage = null
......
  finalStage = newResultStage(finalRDD, func, partitions, jobId, callSite) ...... val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
......
val jobSubmissionTime = clock.getTimeMillis()
jobIdToActiveJob(jobId) = job
activeJobs += job
finalStage.setActiveJob(job)
val stageIds = jobIdToStageIds(jobId).toArray
val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))
listenerBus.post(
  SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))
submitStage(finalStage)

submitWaitingStages()
}

调度阶段提交

  在提交Stage时会先调用getMissingParentStages获取父阶段Stage,迭代该阶段所依赖的父调度阶段如果存在则先提交该父阶段的Stage 当不存在父Stage或父Stage执行完成时会对当前Stage进行提交;

 private def submitStage(stage: Stage) {
  val jobId = activeJobForStage(stage)
  if (jobId.isDefined) {
    if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
      val missing = getMissingParentStages(stage).sortBy(_.id)
      if (missing.isEmpty) {
        submitMissingTasks(stage, jobId.get)
      } else {
        for (parent <- missing) {
          submitStage(parent)
        }
        waitingStages += stage
      }
    }
  }
  ......
}

参考资料:
http://spark.apache.org/docs/latest/

Scala 的详细介绍:请点这里
Scala 的下载地址:请点这里



你可能感兴趣的:(spark,入门)