2.Spark Core

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_15014327/article/details/84074817

一.Spark编程模型

1.术语定义

  • 应用程序(Application): 基于Spark的用户程序,包含了一个Driver Program 和集群中多个的Executor。
  • 驱动程序(Driver Program):运行Application的main()函数并且创建SparkContext,通常用SparkContext代表Driver Program。
  • 执行单元(Executor): 是为某Application运行在Worker Node上的一个进程,该进程负责运行Task,并且负责将数据存在内存或者磁盘上,每个Application都有各自独立的Executors。
  • 集群管理程序(Cluster Manager): 在集群上获取资源的外部服务(例如:Standalone、Mesos或Yarn)。
  • 操作(Operation):作用于RDD的各种操作分为Transformation和Action。

2.模型组成

Spark程序分为两部分:Driver部分和Executor部分。

clip_image002

2.1 Driver部分

主要是对SparkContext进行配置、初始化以及关闭。初始化SparkContext是为了构建Spark应用程序的运行环境,在初始化SparkContext时,要先导入一些Spark的类和隐式转换;在Executor部分运行完毕后,需要将SparkContext关闭。

2.2 Executor部分

主要是对数据的处理,数据主要分为三种:

  • 原生数据

1.原生输入

  • Scala集合数据:如,Array(1,2,3,4),Spark使用parallelize()转换成RDD。
  • Hadoop数据:Spark支持存储在Hadoop上的文件和Hadoop支持的其他文件系统,如本地文件、HBase、SequenceFile及Hadoop输入格式。Spark使用textFile()可以将本地文件或HDFS文件转换成RDD。

2.原生输出

除了支持以上两种数据,Spark还支持Scala标量。

  • Scala标量数据:如count、reduce、aggregate等。
  • Scala集合数据:如collect、lookup等。
  • Hadoop数据:如saveAsTextFile、saveAsSequenceFile。
  • RDD

四种算子:

  • 输入算子:将原生数据 -> RDD,如parallelize、txtFile等。
  • 缓存算子:对于要多次使用的RDD,可以缓冲加快运行速度,对重要数据采用多备份缓存。
  • 转换算子:最重要的算子,是Spark生成DAG图的对象,转换算子并不立即执行,在触发Action算子后再提交给Driver处理,生成DAG图 -> Stage -> Task -> Worker执行。
  • 行动算子:将运算结果RDD转换成原生数据,如count、collect、reduce、saveAsTextFile等。

二.RDD

1.术语定义

  • 弹性分布式数据集(RDD):Spark基本计算单元,可以通过一系列算子进行计算。
  • 有向无环图(DAG):反映RDD之间的依赖关系。
  • 有向无环图调度器(DAG Scheduler):根据Job构建基于Stage的DAG,并提交Stage给TaskScheduler。
  • 任务调度器(Task Scheduler):将TaskSet提交给worker运行并汇报结果。
  • 窄依赖(Narrow Dependency):子RDD依赖于父RDD中固定的data partition。
  • 宽依赖(Wide Dependency):子RDD对父RDD中的所有data partition都有依赖。

2.RDD概念

RDD是Spark最基本抽象,是对分布式内存的抽象使用,实现了以操作本地集合的方式来操作分布式数据集的抽象实现。RDD是Spark最核心的东西,它表示已被分区,不可变的并能够被并行操作的数据集合,不同的数据集格式对应不同的RDD实现。RDD必须是可序列化的。RDD可以cache到内存中,每次对RDD数据集的操作之后的结果,都可以存放到内存中,下一个操作可以直接从内存中输入,省去了MapReduce大量的磁盘IO操作。对于迭代运算比较常见的机器学习算法和交互式数据挖掘,效率提升非常大。

RDD 最适合那种在数据集上的所有元素都执行相同操作的批处理式应用。在这种情况下, RDD 只需记录血统中每个转换就能还原丢失的数据分区,而无需记录大量的数据操作日志。所以 RDD 不适合那些需要异步、细粒度更新状态的应用 ,比如 Web 应用的存储系统,或增量式的 Web 爬虫等。对于这些应用,使用具有事务更新日志和数据检查点的数据库系统更为高效。

2.1 RDD特点

  • 开源:一种是从持久存储获取数据,另一种是从其他RDD生成

  • 只读:状态不可变,不能修改

  • 分区:支持元素根据Key来分区,保存到多个节点上,还原时只会重新计算丢失分区的数据,而不会影响整个系统

  • 路径:在RDD中称为血统,即:RDD有充足的信息关于它是如何从其他RDD产生而来的

  • 持久化:可以控制存储级别(内存、磁盘)来进行持久化

  • 操作:丰富的Action,如:count、reduce、collect和save等

2.2 RDD基础数据类型

目前有两种类型的基础RDD,这两种类型的RDD可以通过同学的方式操作,从而获得子RDD等一系列扩展,形成lineage血统关系图:

1. 并行集合(Parallelized Collections):接收一个已经存在的Scala集合,然后进行各种并行计算。

2. Hadoop数据集(Hadoop Datasets):在一个文件的每条记录上运行函数。只要文件系统是HDFS,或者hadoop支持的存储系统即可。

2.2.1 并行集合

并行集合是可以通过SparkContext的parallelize(),在一个已经存在的Scala集合上创建的。创建的对象将会被拷贝,创建出一个可以被并行操作的分布式数据集。例如,下面演示了如何从一个数组创建一个并行集合。

val rdd = sc.parallelize(Array(1 to 10), 5) //指定了partition的数量,可指定可不指定

2.2.2 Hadoop数据集

Spark可以将任何Hadoop所支持的存储资源转换成RDD,如:本地文件、HDFS、HBase等。Spark支持文本文件、SequenceFile和任何Hadoop InputFormat格式。

(1) 使用textFile()方法可以将本地文件或HDFS文件转换成RDD

支持整个文件目录读取,文件可以是文本或压缩文件(读取文件目录时,支持通配符)。

# 读取文本文件,获取只有两个字段的行数
val rdd1 = sc.textFile("file:///home/hdfs/*.tsv")
val rdd2 = rdd1.map(_.split("\t")).filter(_.length == 2)
rdd2.count()

(2) 使用wholeTextFiles()读取目录里面的小文件,返回元组 (用户名、内容)

(3) 使用SequenceFile[K,V]()方法可以将SequenceFile转换成RDD。SequenceFile文件是Hadoop用来存储二进制形式K-V而设计的一种平面文件

(4) 使用SparkContext.hadoopRDD()方法可以将其他任何Hadoop输入类型转化成RDD使用方法

此外,通过Transformation可以将HadoopRDD等转换成FilterRDD(依赖一个父RDD产生)和JoinedRDD(依赖所有父RDD)等。

2.3 转换与操作

对于RDD可以有两种计算方式:转换(返回值还是一个RDD) 与 操作(返回值不是一个RDD)

  1. 转换(Transformations):如,map,filter,groupBy,join等。Transformations操作是Lazy的,也就是说从一个RDD到另一个RDD并不会马上执行,只会记录需要这样的操作,直到有真正的Actions操作时才真正运行计算。
  2. 操作(Actions):如,count,collect,save等。Actions操作会返回结果或把RDD数据写到存储系统中。Actions是触发Spark启动计算的动因。

2.3.1 转换

map(func)

返回一个新的分布式数据集,由每个原元素经过func函数转换后组成

filter(func)

返回一个新的数据集,由经过func函数后返回值为true的原元素组成

flatMap(func)

类似于map,但是每一个输入元素,会被映射为0到多个输出元素(因此,func函数的返回值是一个Seq,而不是单一元素)

flatMap(func)

类似于map,但是每一个输入元素,会被映射为0到多个输出元素(因此,func函数的返回值是一个Seq,而不是单一元素)

sample(withReplacement,  frac, seed)

根据给定的随机种子seed,随机抽样出数量为frac的数据

union(otherDataset)

返回一个新的数据集,由原数据集和参数联合而成

groupByKey([numTasks])

在一个由(K,V)对组成的数据集上调用,返回一个(K,Seq[V])对的数据集。注意:默认情况下,使用8个并行任务进行分组,你可以传入numTask可选参数,根据数据量设置不同数目的Task

reduceByKey(func,  [numTasks])

在一个(K,V)对的数据集上使用,返回一个(K,V)对的数据集,key相同的值,都被使用指定的reduce函数聚合到一起。和groupbykey类似,任务的个数是可以通过第二个可选参数来配置的。

join(otherDataset,  [numTasks])

在类型为(K,V)和(K,W)类型的数据集上调用,返回一个(K,(V,W))对,每个key中的所有元素都在一起的数据集

groupWith(otherDataset,  [numTasks])

在类型为(K,V)和(K,W)类型的数据集上调用,返回一个数据集,组成元素为(K, Seq[V], Seq[W]) Tuples。这个操作在其它框架,称为CoGroup

cartesian(otherDataset)

笛卡尔积。但在数据集T和U上调用时,返回一个(T,U)对的数据集,所有元素交互进行笛卡尔积。

flatMap(func)

类似于map,但是每一个输入元素,会被映射为0到多个输出元素(因此,func函数的返回值是一个Seq,而不是单一元素)

2.3.2 操作

reduce(func)

通过函数func聚集数据集中的所有元素。Func函数接受2个参数,返回一个值。这个函数必须是关联性的,确保可以被正确的并发执行

collect()

在Driver的程序中,以数组的形式,返回数据集的所有元素。这通常会在使用filter或者其它操作后,返回一个足够小的数据子集再使用,直接将整个RDD集Collect返回,很可能会让Driver程序OOM

count()

返回数据集的元素个数

take(n)

返回一个数组,由数据集的前n个元素组成。注意,这个操作目前并非在多个节点上,并行执行,而是Driver程序所在机器,单机计算所有的元素(Gateway的内存压力会增大,需要谨慎使用)

first()

返回数据集的第一个元素(类似于take(1)

saveAsTextFile(path)

将数据集的元素,以textfile的形式,保存到本地文件系统,hdfs或者任何其它hadoop支持的文件系统。Spark将会调用每个元素的toString方法,并将它转换为文件中的一行文本

saveAsSequenceFile(path)

将数据集的元素,以sequencefile的格式,保存到指定的目录下,本地系统,hdfs或者任何其它hadoop支持的文件系统。RDD的元素必须由key-value对组成,并都实现了Hadoop的Writable接口,或隐式可以转换为Writable(Spark包括了基本类型的转换,例如Int,Double,String等等)

foreach(func)

在数据集的每一个元素上,运行函数func。这通常用于更新一个累加器变量,或者和外部存储系统做交互

2.4 依赖类型

在RDD中将依赖分为两种类型:窄依赖(Narrow Dependencies)和宽依赖(Wide Dependencies)。窄依赖是指父RDD的每个分区都只被子RDD的一个分区使用。如:Map。宽依赖是指父RDD的分区被多个子RDD的分区依赖。如:Join。

clip_image009

  1. 窄依赖
    • 子RDD的每个分区依赖于常数个父分区(即于数据规模无关)
    • 输入输出一对一的算子,且结果RDD的分区结构不变,主要是map,flatMap
    • 输入输出一对一,但结果RDD的分区结构发生了变化,如union,coalesce
    • 从输入中选择部分元素的算子,如filter,distinct,subtract,sample
  2. 宽依赖
    • 子RDD的每个分区依赖所有父RDD分区
    • 对单个RDD基于Key进行重组和reduce,如groupByKey、reduceByKey
    • 对两个RDD基于Key进行join和重组,如join

2.5 RDD缓存

Spark可以使用persist和cache方法将任意RDD缓存到内存、磁盘文件系统中。缓存是容错的,如果一个RDD分片丢失,可以通过构建它的transformation自动重构。缓存RDD,存取速度增加。一般的executor内存60%做cache,剩下的40%做task。

Spark中,RDD类可以使用cache()和persist()来缓存。cache()是persist()的特例,将该RDD缓存到内存中。而persist()可以指定一个StorageLevel。StorageLevel的列表可以在StorageLevel 伴生单例对象中找到:

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

}

Spark的不同StorageLevel ,目的满足内存使用和CPU效率权衡上的不同需求。我们建议通过以下的步骤来进行选择:

  1. 如果你的RDDs可以很好的与默认的存储级别(MEMORY_ONLY)契合,就不需要做任何修改了。这已经是CPU使用效率最高的选项,它使得RDDs的操作尽可能的快;
  2. 如果不行,试着使用MEMORY_ONLY_SER并且选择一个快速序列化的库使得对象在有比较高的空间使用率的情况下,依然可以较快被访问;
  3. 尽可能不要存储到硬盘上,除非计算数据集的函数,计算量特别大,或者它们过滤了大量的数据。否则,重新计算一个分区的速度,和与从硬盘中读取基本差不多快;
  4. 如果你想有快速故障恢复能力,使用复制存储级别(例如:用Spark来响应web应用的请求)。所有的存储级别都有通过重新计算丢失数据恢复错误的容错机制,但是复制存储级别可以让你在RDD上持续的运行任务,而不需要等待丢失的分区被重新计算;
  5. 如果你想要定义你自己的存储级别(比如复制因子为3而不是2),可以使用StorageLevel 单例对象的apply()方法;
  6. 在不使用cached RDD的时候,及时使用unpersist方法来释放它。

三.RDD动手实战

1. spark-shell编程

案例一 — RDD基础应用

我们可以看到RDD的LineAge演变,通过paralelize方法建立了一个ParallelCollectionRDD,使用map()方法后该RDD为MapPartitionsRDD,接着使用filter()方法后转变为MapPartitionsRDD。

使用collect方法时触发运行作业,通过任务计算出结果:

案例二 — 使用Spark实现wordcount

scala> val text = sc.textFile("file:///home/hdfs/lw/helloworld.txt")
scala> val words = text.flatMap(_.split(" ")).map(x => {if(x.endsWith(",") || x.endsWith(".")) x.substring(0,x.length-1) else x})
scala> val wc = words.map((_,1)).sortByKey().reduceByKey(_ + _)
scala> wc.toDebugString
res8: String = 
(2) MapPartitionsRDD[30] at reduceByKey at <console>:31 []
 |  ShuffledRDD[29] at sortByKey at <console>:31 []
 +-(2) MapPartitionsRDD[26] at map at <console>:31 []
    |  MapPartitionsRDD[25] at map at <console>:29 []
    |  MapPartitionsRDD[24] at flatMap at <console>:29 []
    |  file:///home/hdfs/liuwei/helloworld.txt MapPartitionsRDD[23] at textFile at <console>:27 []
    |  file:///home/hdfs/liuwei/helloworld.txt HadoopRDD[22] at textFile at <console>:27 []
scala> wc.collect
res9: Array[(String, Int)] = Array((Convention,1), (health,1), (21,1), (commenting,1), 
(China,3), (big,1), (National,1), (at,2), (food,1), (Beijing,1), (Center,1), (Nov,1), 
(force,1), (for,1), (and,1), (ANUFOOD,1), (from,1), (International,1), (23,1), (next,1), 
(this,1), (is,1), (that,1), (many,1), (with,1), (to,1), (market,1), (organic,1), (in,2), 
(sellers,2), (industry,1), (year’s,1), (held,1), (were,1), (out,1), (the,3))

2. IDEA Spark编程

1.下载安装IDEA

https://www.jetbrains.com/idea/download/download-thanks.html?platform=windows&code=IIC

2.安装Scala和SBT插件

3.新建Scala项目

File -> New -> Project,选择Scala -> sbt创建项目。

创建完项目后,项目目录下有build.sbt和build.properties。

4.添加Spark SBT依赖

本地运行的Spark环境是Spark2.2.1。去Spark官网上查找对应的SBT Spark依赖,添加到build.sbt文件中,稍等片刻下载完成。

5.编程:统计搜狗用户日志文件中排名为1的条数

将准备好的搜狗实验室测试数据放到项目的data目录下:

在scala开发的root目录下新建Scala Class —> HelloWorld.scala:

import org.apache.spark.{SparkConf, SparkContext}

object HelloWorld {
  case class SogouQ(visitTime:String,userId:String,keyWord:String,rank:Int,seqId:Int,url:String)

  def main(args: Array[String]): Unit = {
    // 数据文件路径
    val dataFile = "data/SogouQ.sample"
    //spark配置
    val conf = new SparkConf().setAppName("SogouResult").setMaster("local")
    //spark context对象
    val sc = new SparkContext(conf)

    //文件 -> RDD
    val text = sc.textFile(dataFile)
    //每行按照\t切分
    val rdd1 = text.map(_.split("\t"))
    //每行封装成SogouQ对象
    val rdd2 = rdd1.map(r => SogouQ(r(0),r(1),r(2),r(3).split(" ")(0).toInt,r(3).split(" ")(1).toInt,r(4)))
    //过滤rank = 1
    val rdd3 = rdd2.filter(_.rank == 1)
    //统计排名=1的个数
    val rdd4 = rdd3.count()
    //打印
    println("result:" + rdd4) 
    //运行结果:2701
  }
}

转自:http://www.cnblogs.com/shishanyuan/p/4721102.html

猜你喜欢

转载自blog.csdn.net/qq_15014327/article/details/84074817