spark机器学习

第一章 mesos spark shell

  1. SPARK-shell
    (1)修改spark/conf/spark-env.sh ,增加以下内容
    export MESOS_NATIVE_JAVA_LIBRARY=/usr/local/mesos/lib/libmesos.so
    export SPARK_EXECUTOR_URI=<上传spark-1.6.0.tar.gz对应的hdfs URL, 如果已经把spark的jar包放在了mesos agent机器上,不用这个配置>
    

(2)运行命令:
shell ./bin/spark-shell --master mesos://host:5050
(3)代码
```
scala> val lines=sc.textFile("/root/README.md")
lines: org.apache.spark.rdd.RDD[String] = /root/README.md MapPartitionsRDD[3] at textFile at :27

scala> lines.count
res1: Long = 3 
```

(4)web页面:4040端口,可以看到上面执行的count操作

  1. SPARK的核心概念
    (1)spark-shell其实是一个driver programme(驱动器程序)
    (2)driver programme包含应用的main函数,定义了集群上的分布式数据集,用来发起集群上的各种并行操作
    (3)由于spark-shell在开启时,制定了master,因此driver programe提交这些任务到集群上操作

  2. spark文件操作

第二章 RDD编程

  1. 操作过程
    (1)RDD包含两类操作:transaction和action,只有action会对RDD计算出一个结果
    (2)RDD会在每次action操作时重新计算。可以通过presist对RDD持久化,在第一次对持久化的RDD计算后,spark会把RDD内容保存到内存中(以分区方式存储在集群的每台机器上),这样之后的action操作就可以重用这些RDD数据。默认不会把RDD缓存在内存中是因为,大量数据不应该仅仅以不同的形式多份存在内存中。

    val lines=sc.textFile("/root/README.md")
    val rdd2 = lines.filter(line=>line.contains("aa")).count
    rdd2.presist()
    rdd2.count
    
  2. 创建RDD的两种途径
    (1)把已有集合传给SparkContext的parallelize()方法
    (2)从外部数据创建rdd

    sc.parallelize(List("aaa","bbb"))
    sc.textFile("/root/README.md")
    
  3. RDD操作
    (1)transaction:
        a. 转化操作只能产生新的RDD,而不能改版原先RDD的数据

    errorsRDD = inputRDD.filter(lambda x: "error" in x)
    warningsRDD = inputRDD.filter(lambda x: "warning" in x)
    badLinesRDD = errorsRDD.union(warningsRDD)
    

    b. main函数中,所有的RDD会用谱系图记录产生依赖关系
![Screenshot from 2017-05-04 22-45-27.png-15kB][1]
(2)action :
    a. 获取操作:take(int n) 获取RDD的前n行数据, collect()获取RDD中的所有数据
    b. 当调用一个新action操作时,整个RDD都会重新计算,导致行为低效,用户可以将中间结果持久化

  1. 常见算子
    (1)transaction
    val rdd1 = sc.parallelize(List(1,2,3,3))    // (1,2,3,3)
    
    scala> rdd1.map(x=>x+1).collect             // Array(2, 3, 4, 4)
    scala> rdd1.flatMap(x=>x.to(3)).collect     // Array(1, 2, 3, 2, 3, 3, 3)
    scala> rdd1.filter(x => x!=1).collect       // Array(2, 3, 3)
    scala> rdd1.distinct.collect                // Array(2, 1, 3)   
    scala> rdd1.sample(false,0.5).collect       // 随机取样,个数和数值每次都不一样(是否替换)
    
    val rdd1 = sc.parallelize(List(1,2,3))      // (1,2,3)
    val rdd2 = sc.parallelize(List(3,4,5))      // (3,4,5)
    
    scala> rdd1.union(rdd2).collect             // Array(1, 2, 3, 3, 4, 5)
    scala> rdd1.intersection(rdd2).collect      // Array(3)  求两个RDD共同的元素
    scala> rdd1.subtract(rdd2).collect          // Array(2, 1) 求两个RDD不同的部分
    scala> rdd1.cartesian(rdd2).collect         // 两个RDD求笛卡尔积 Array((1,3), (1,4), (1,5), (2,3), (3,3), (2,4), (2,5), (3,4), (3,5))
    

(2)action
```scala
val rdd1 = sc.parallelize(List(1,2,3,3)) /home/lj/Documents // (1,2,3,3)

scala> rdd1.collect                        // Array(1, 2, 3, 3) 返回RDD所有元素
scala> rdd1.count                          // 4   返回RDD的元素个数
scala> rdd1.countByValue                   // Map(2 -> 1, 1 -> 1, 3 -> 2) 返回键值对(元素值 -> 元素个数)
scala> rdd1.take(2)                        // Array(1, 2)   返回RDD中的n个元素
scala> rdd1.top(2)                         // Array(3, 3)   返回RDD中的前2个元素


```

![action.png-118kB][2]

  1. 持久化
    (1)RDD持久化时,计算出RDD的节点会分别保存他们所求出的分区数据
    (2)如果一个有持久化数据的节点发生故障,spark会在用到缓存数据时重算丢失的数据分区。如果想在节点故障时不拖累执行速度,也可以把数据备份到多个节点上。
    (3)persist()默认把数据以序列化的形式缓存在jvm堆(内存)中,同时,也可以通过调整持久化级别把数据缓存到磁盘或堆外缓存上。
    scala> rdd2.persist(StorageLevel.DISK_ONLY)         // persist不会立刻触发缓存,而是等到第一次action操作后,自动缓存这个RDD结果
    res4: rdd2.type = MapPartitionsRDD[1] at filter at <console>:30
    
    scala> rdd2.count                                  
    res5: Long = 1
    

(4)如果缓存的RDD数据在节点上的内存放不下了,spark会通过LRU(最近最少被使用)原则吧老数据移除内存,存放新数据。因此,不论只缓存到内存还是同时缓存到内存和硬盘, 都不会因为缓存而使得作业停止,但是缓存过多不必要的数据,会带来更多分区重算时间

第三章 键值对RDD

一. 普通RDD转换成pair RDD

  1. 初始化

    scala> val rdd1 = sc.parallelize(List(1->2,3->4,3->9))
    rdd1: org.apache.spark.rdd.RDD[(Int, Int)] = ParallelCollectionRDD[0] at parallelize at <console>:24
    

二. pair RDD转化操作

  1. 聚合操作:组合RDD中相同key的value
    (1)reduceByKey:接受一个函数,为数据集中每个键进行规约操作,每个规约操作会将键相同的值合并起来
    (2)foldByKey:将key相同的分在一组,再对组内的value进行fold操作.使用一个零值初始进行折叠(零值与另一个元素合并结果仍为该元素)
    // mapValues与reduceByKey计算每个键对应的均值
    scala> val rdd1 = sc.parallelize(Array(("panda",3),("pink",1),("panda",6),("pink",3)))
    scala> val rdd2 = rdd1.mapValues(x=>(x,1)).reduceByKey((v1,v2)=>(v1._1+v2._1,v1._2+v2._2))   // Array((panda,(9,2)), (pink,(4,2))) 
    scala> val rdd3 = rdd2.mapValues(v=>v._1*1.0/v._2)  // Array((panda,4.5), (pink,2.0))
     
    

(3)combineByKey(createCombiner,mergeValue,mergeCombiners):combineByKey方法的三个参数分别对应聚合的几个阶段。在遍历所有元素时,每个元素的key,要么没有遇到过,要么与之前的某个元素的key相同。第一个参数createCombiner:将每个元素的value映射成新的value,相当于mapvalue方法。第二个参数mergeValue是说,当发现该元素的key与之前已经映射成新value的元素的key相同时,这个新形势的value与新遍历到的元素的旧形式的value如何组合。第三个参数mergeCombiners:当每个分区的元素都已经形成了新形势的k,v,此时如何对相同k的value进行组合
```scala
// combineValues计算每个key的平均值
scala> val rdd1 = sc.parallelize(Array(("panda",3),("pink",1),("panda",6)))
scala> val rdd2 = rdd1.combineByKey(
| (v)=>(v,1),
| (nValue:(Int,Int),oValue)=>(nValue._1+oValue,nValue._2+1),
| (nValue1:(Int,Int),nValue2:(Int,Int))=>(nValue1._1+nValue2._1,nValue1._2+nValue2._2)
| )
scala> val rdd3 = rdd2.mapValues(v=>v._1*1.0/v._2) // Array((panda,4.5), (pink,1.0))

```

(4)并行度优化:在执行分组和聚合时,可以指定spark的分区数
scala sc.parallelize(data).reduceByKey((x,y)=>x+y,10) // 指定10个分区

  1. 数据分组
    (1)groupByKey:把相同键值的RDD[K,V]经过聚合变成RDD[K,Iterator(V)]. 因此,

    rdd.reduceByKey(func)  =  rdd.groupByKey().mapValues(v=>v.reduce(func))
    
  2. 连接
    (1)cogroup:将两个pair rdd合并成一个rdd,形式为RDD[k,Iterator[v],Iterator[w]]
    (2)leftOuterJoin和rightOuterJoin:分别表示左右连接

    scala>  val rdd1 = sc.parallelize(Array((1, 30), (2, 29), (4, 21)))  
    scala> val rdd2 = sc.parallelize(Array((1, "zhangsan"), (2, "lisi"), (3, "wangwu")))
    
    scala> rdd1.cogroup(rdd2).collect                 // cogroup
    res0: Array[(Int, (Iterable[Int], Iterable[String]))] =
    Array((4,(CompactBuffer(21),CompactBuffer())), 
          (2,(CompactBuffer(29),CompactBuffer(lisi))), 
          (1,(CompactBuffer(30),CompactBuffer(zhangsan))), 
          (3,(CompactBuffer(),CompactBuffer(wangwu))))
          
    
    scala> rdd1.leftOuterJoin(rdd2).collect           // leftOuterJoin
    res1: Array[(Int, (Int, Option[String]))] = Array((4,(21,None)), (2,(29,Some(lisi))), (1,(30,Some(zhangsan))))
    
    scala> rdd1.rightOuterJoin(rdd2).collect          // rightOuterJoin
    res2: Array[(Int, (Option[Int], String))] = Array((2,(Some(29),lisi)), (1,(Some(30),zhangsan)), (3,(None,wangwu)))
    
  3. 数据排序
    (1)sortByKey:默认按照升序排列相同key的value,让rdd有顺序的save到磁盘或展示出来

    scala> val rdd1 = sc.parallelize(Array(("panda",3),("pink",1),("panda",6)))
    
    scala> def sortInt = new Ordering[Int]{
         |   override def compare(a:Int,b:Int) = a.toString.compare(b.toString)
         | }
    
    scala> rdd1.sortByKey().collect()  // Array((panda,3), (panda,6), (pink,1))
    
  4. Pair RDD的action操作
    (1)countByKey:统计每个key出现的个数
    (2)collectAsMap:把RDD输出Map
    (3)lookup(key):返回key对应的所有value

    scala> val rdd1 = sc.parallelize(List((1,2),(3,4),(3,6)))
    
    scala> rdd1.countByKey()
    res5: scala.collection.Map[Int,Long] = Map(1 -> 1, 3 -> 2)
    
    scala> rdd1.collectAsMap
    res6: scala.collection.Map[Int,Int] = Map(1 -> 2, 3 -> 6)
    
    scala> rdd1.lookup(3)
    res7: Seq[Int] = WrappedArray(4, 6)
    
  5. 数据分区
    (1)分布式程序中,通信的代价很大,因此控制数据分布来减少网络传输可以极大提升整体性能。但分区并非对所有的应用都是好的,比如如果RDD只需被扫描一次就完全不必预先对其分区。只有当数据集多次在诸如连接这样的基于key的操作时,分区才会有用。

    (2)所有的Pair RDD都能进行分区,系统会根据一个针对key的函数对元素进行分组。spark没有给出显示控制每个key具体落在哪个工作节点上的方法(其中一个原因是节点失败时让然可以在其他节点进行工作)。但是spark可以确保同一组key出现在同一个节点上。eg:通过哈希分区将一个RDD分成100个分区,此时key的哈希值对100取模结果相同的记录都会被放在同一个节点上。

    (3)应用举例:内存中保存着一份由(UserID,UserInfo)对组成的RDD表,其中UserInfo包含用户订阅的url。这张表会周期性的与一个小文件进行组合,小文件存储着过去五分钟用户所访问的url。因此现在要每五分钟对用户访问其未订阅的url做统计。

    // userid和userinfo这个表一般不变
    val userData = sc.sequenceFile[UserId,UserInfo]("hdfs://...").persist()
    
    // 周期性的调用该方法,处理过去5分钟产生的事件日志
    def processNewLogs(logFileName:String){
        val events = sc.sequenceFile[UserId,UserInfo](logFileName) // 用户点击事件日志
        val joined = userData.join(events)                         // Pair RDD of (UserId,(UserInfo,LinkInfo))
        val offTopicVisits = joined.filter({
            case (userid,(userinfo,linkinfo)) => !userinfo.topics.contains(linkinfo.topic)
        }).count()
    }
    
    

      上面的代码可以运行,但是效率不高。原因在于。默认情况下,join操作会把两个pair RDD中的所有key的哈希值都求出来,再将key哈希值相同的记录通过网络传到一个机器上,然后在那台机器上进行连接操作。因为userData这张用户订阅uel表,远远比没五分钟出现的小表大,所以每五分钟都要对userData表进行哈希取值,然后跨节点混洗。
      因此,我们的改进方法就是先对userData表进行哈希分区,之后持久化到内存中,让每五分钟出现的操作只对小表进行hash取值
scala val userData = sc.sequenceFile[UserId,UserInfo]("hdfs://...") .partitionBy(new HashPartitioner(100)) //分成100个分区,分区个数至少和集群中机器的个数相同 .persist() // partitionBy只是转化操作,需要持久化才能避免每次引用该rdd重新分区
(4)scala可以通过RDD的partitioner属性获取分区信息
```scala
scala> val pairs = sc.parallelize(List((1,1),(2,2),(3,3)))
scala> pairs.partitioner // 无分区
res8: Option[org.apache.spark.Partitioner] = None

scala> val partitioned = pairs.partitionBy(new org.apache.spark.HashPartitioner(2))
scala> partitioned.partitioner   // 有分区
res9: Option[org.apache.spark.Partitioner] = Some(org.apache.spark.HashPartitioner@2)
```

(5)合适手动设置分区有效益:
用partitioner方法对RDD分区会对很多操作产生益处:reduceBy , cogroup , groupwith , join , leftOuterJoin , rightOuterJoin , groupByKey , combineByKey , lookup。但是对于像reduceByKey这样操作单个RDD的方法,他们只会把每个key对应的value在本地机器上进行计算,最终把所有机器上的结果进行规约,这种操作本身就不会产生数据跨节点混洗。但是想cogroup和join这样操作两个RDD的方法,如果对这两个RDD采用相同的办法手动分区,那么相同key的项都在同一台机器上,这样就避免了产生数据的跨界点混洗

(6)自定义分区方式
    继承org.apache.spark.Partitioner类,实现下列三个方法:
    (a) numPartitions:创建的总分区数
    (b) getPartition(key:Any):根据key,返回分区编号 ( 编号从0到numPartitions-1 )
    (c) equals():该方法来判断你的分区器对象是否和其他分区器实例相同
scala /** 创建一个基于域名的分区器,这个分区器只对url中的域名部分求哈希 */ class DomainNumPartitioner(numParts:Int) extends Partitioner{ override def numPartitions:Int = numParts override def getPartition(key:Any):Int = { val domain = new java.net.Url(key.toString).getHost() val code = (domain.hashCode % numPartitions) if(code <0) code + numPartitions else code } override def equals(other:Any):Boolean = other match{ case dnp:DomainNumPartitioner => dnp.numPartitions == numPartitions case _ => false } }

第四章 spark的共享变量

  1. 累加器
    (1)由于spark的任务再多个节点上跑,驱动节点上的普通变量不能再多个节点上共享,因此为了解决共享变量的问题,提出了累加器( 结果聚合 )和广播变量 ( 广播 )
    // 计算文件中空行的行数
    scala> val file = sc.textFile("/root/README.md")
    scala> val count = sc.accumulator(0)
    count: org.apache.spark.Accumulator[Int] = 0
    scala> val call = file.map(line => { if(line=="") count += 1})
    scala> println(count.value)
    5
    

这个例子中,使用了累加器在数据读取时进行错误统计,而没有对rdd进行filter和reduce实现
(2)累加器是一种只写变量,操作节点不能访问累加器的值,必须要对每次更新操作进行复杂的通信
(3)通过value变量获取可累加器的值
(4)累加器操作应该写在action的动作中:比如写在forEach算子。因为在转化算子中,比如如果有一个分区执行map操作失败了,spark会在另一个节点重新运行该任务,即使该节点没有崩溃,只是处理速度比别的节点慢很多。spark也可以抢占式的再另一个节点上启动一个任务副本,谁先结束任务就取谁的副本。因此,这种情况会导致累加器操作重复执行多次

  1. 广播变量
    (1)广播变量是一种只读变量
    (2)虽然spark会把闭包中的变量发送到每个工作结点,但这种方法比广播变量低效得多。原因有二:
            a ) 广播变量再变量的发送上对大对象有网络优化
            b ) 如果这个变量来自于读取文件,不适用广播变量会导致这个文件会被不同工作节点读取多次。
    (3)使用value获取广播变量的值
    scala> val words = sc.broadcast(List("fuck","shit"))
    scala> words.value
    res1: List[String] = List(fuck, shit)
    

(4)当广播变量的数据很大时,应当选择一种合适的序列化机制

  1. 分区共享连接池等资源
    (1)当map等转换操作中包含访问数据库等操作时,就需要通过数据库连接池的方式重用连接。而分布式的代码在不同分区中运行,简单的复用连接池对象无法正常工作。
    (2)scala提供了mapPartitions(function)算子,这个function中的变量会在分区之间a共享(这个function输入为每个分区的元素迭代器,返回一个执行结果的序列迭代器)

    object Test1 extends App{
      def sumofeveryPartition(in:Iterator[Int]):Int = {
        var sum = 0
        in.reduce(_+_)
      }
      val conf = new SparkConf().setAppName("test111").setMaster("mesos://base1:5050")
      val sc = new SparkContext(conf)
      val input = sc.parallelize(List(1,2,3,4))
      val result = input.mapPartitions(   // partVal : Iterator[Int],RDD中的元素是Int的
        partVal => Iterator(sumofeveryPartition(partVal))
      )
      result.collect().foreach(print(_))   // 2个结果:3,7
      sc.stop
    }
    
    
  2. 数值操作
    (1)spark对包含数值数据的RDD提供了统计学方法

    方法 含义
    count(long value) RDD中元素个数
    mean() 元素平均值
    vaiance 元素方差
    samoleVariance() 从采样中计算出方差
    stdev() 标准差
    sampleStdev() 采样的标准差

(2)通过RDD的stats()方法,返回org.apache.spark.util.StatCounter对象,该对象包含mean()平均值,stdev()标准差等数值方法
scala scala> val rdd1 = sc.parallelize(List(1,2,3,4)) scala> val stats = rdd1.stats() scala> stats.mean

第五章 submit提交集群

  1. 驱动器节点2个职责
    (1)把用户程序转换成分布式任务:
    所有的spark程序遵从同一个流程:把输入数据创建一系列RDD,通过转化操作派生出新的RDD,最后使用行动操作收集或存储结果RDD中的数据
    spark程序会隐式地创建出一个由操作组成的有向无环图,当驱动器程序执行时会把这个逻辑图转换为物理执行计划
    (2)为执行器节点调度任务
    驱动器程序必须在各执行器进程间协调任务调度。执行器进程启动后,会向驱动器进程注册自己

  2. 执行器节点2个作用:
    spark应用启动时,执行器节点就被同时启动
    (1)执行组成spark应用的任务,并将结果返回给驱动器程序
    (1)通过BlockManager为用户程序中要求缓存的RDD提供内存式存储

  3. 集群管理器
    spark依赖集群管理器启动驱动器节点。集群管理器是spark的可插拔式组件。集群管理器用于启动执行器节点。而驱动器可以被集群管理器也可以不被集群管理器启动

  4. spark-submit
    (1)基本格式:

    bin/spark-submit [options] <app jar | python file> [app options]
    
  5. 构建程序包
    (1)build.sbt:

    import AssemblyKeys._
    name := "Simple Project"
    version := "1.0"
    organization := "com.databricks"
    scalaVersion := "2.11.8"
    libraryDependencies ++= Seq(
    	 // Spark依赖
    	 "org.apache.spark" % "spark-core_2.10" % "1.2.0" % "provided",
    	 // 第三方库
    	 "net.sf.jopt-simple" % "jopt-simple" % "4.3",
    	 "joda-time" % "joda-time" % "2.0"
    )
    // 这条语句打开了assembly插件的功能
    assemblySettings
    // 配置assembly插件所使用的JAR
    jarName in assembly := "my-project-assembly.jar"
    // 一个用来把Scala本身排除在组合JAR包之外的特殊选项,因为Spark
    // 已经包含了Scala
    assemblyOption in assembly :=
     (assemblyOption in assembly).value.copy(includeScala = false)
    
    

(2)project/assembly.sbt
```shell
# 显示project/assembly.sbt的内容
$ cat project/assembly.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2")

$ sbt assembly
```
  1. submit的部署模式
    (1)客户端模式
    客户端模式下,驱动器程序会在执行spark-submit的机器上,此时终端可以看到驱动器程序的输出,但要保持终端始终连接。且该机器与执行节点需要有很快速的网络交换
    (2)集群模式:--deploy-mode cluster
    该模式下,驱动器程序本身也会在集群中申请资源运行自己的进程。这样,可以在程序运行时关闭电脑。
    (3)yarn管理的spark集群既有客户端模式,又有集群模式。但是mesos管理的spark集群,只有客户端模式,但是mesos管理下的任务,可以动态分配CPU(即执行器进程占用的cpu个数会在他们执行的过程中动态变化)。这种默认的方式成为细粒度模式。mesos也支持粗粒度模式,一开始分配固定的cpu,内存(Spark 应用的 spark.mesos.coarse 设置为 true)
    (4)yarn集群和mesos集群的选择:
    Mesos 相对于 YARN 和独立模式的一大优点在于其细粒度共享的选项,该选项可以将类似 Spark shell这样的交互式应用中的不同命令分配到不同的 CPU 上。因此这对于多用户同时运行交互式 shell 的用例更有用处。除此之外,选择使用yarn模式更为合适

第六章 Spark-SQL

  1. DataSet与DataFrame
    (1)DataSet是Spark1.6以后新加的分布式数据集,比RDD有诸多好处,比如强类型和提供更有力的表达式方法,适应sql执行引擎。
    (2)DataFrame是包含列名的DataSet

    // 1. 构建对象的分布式数据集
    scala> val spark = SparkSession.builder().appName("test-sql").config("p1", "v1").getOrCreate()
    scala> case class People(name:String,age:Int)
    scala> val ds1 = Seq(People("Andy",32)).toDS
    scala> ds1.show()
    +----+---+
    |name|age|
    +----+---+
    |Andy| 32|
    +----+---+
    scala> ds1.collect
    res2: Array[People] = Array(People(Andy,32))
    
    // 2. 构建一般数据类型的分布式数据集
    scala> val primitiveDS = Seq(1, 2, 3,5,7,9).toDS()
    primitiveDS: org.apache.spark.sql.Dataset[Int] = [value: int]
    
    scala> primitiveDS.map(_+1).collect
    res3: Array[Int] = Array(2, 3, 4, 6, 8, 10)
    
    //3. 把文件读成对象的分布式数据集
    scala> case class Person(name: String, age: BigInt)
    defined class Person
    
    scala> val peopleDS = spark.read.json("examples/src/main/resources/people.json").as[Person]   
    // 不加as[Person],只会读成res6: Array[org.apache.spark.sql.Row] = Array([null,Michael], [30,Andy], [19,Justin])
    peopleDS: org.apache.spark.sql.Dataset[Person] = [age: bigint, name: string]
    
    scala> peopleDS.collect
    res5: Array[Person] = Array(Person(Michael,null), Person(Andy,30), Person(Justin,19))
    
    
    
  2. 解析json文件
    (1)文件格式:文件的每一行都是一个json串,每一行会被转化为一个Row对象
    (2)Spark-sql读取文件后,把整个形成一个DataFrame,带有列名的表

    scala> import org.apache.spark.sql.SparkSession
    scala> import spark.implicits._
    
    scala> val spark = SparkSession.builder().appName("test-sql").config("p1", "v1").getOrCreate()
    【WARN SparkSession$Builder: Using an existing SparkSession; some configuration may not take effect.】
    【spark: org.apache.spark.sql.SparkSession = org.apache.spark.sql.SparkSession@399ef33f】
    
    scala> val df = spark.read.json("examples/src/main/resources/people.json")
    df: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
    
    scala> df.select("name").show()
    +-------+
    |   name|
    +-------+
    |Michael|
    |   Andy|
    | Justin|
    +-------+
    
    scala> df.select($"name", $"age" + 1).show
    +-------+---------+
    |   name|(age + 1)|
    +-------+---------+
    |Michael|     null|
    |   Andy|       31|
    | Justin|       20|
    +-------+---------+
    
    
  3. 用sql语句查询session中的视图
    session中的视图只能存在于这个session中,一旦session结束,视图消失。如果想在所有session中共享,就要使用全局视图

    scala> df.createOrReplaceTempView("people")
    
    scala> val sqlDF = spark.sql("select * from people")
    sqlDF: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
    
    scala> sqlDF.collect
    res8: Array[org.apache.spark.sql.Row] = Array([null,Michael], [30,Andy], [19,Justin])
    ```
    
4. **全局视图**
全局视图保存在系统数据库`global_temp`中,使用全局视图时,必须加上数据库的名字
    ```scala
    scala> df.createGlobalTempView("people")

    scala> spark.sql("SELECT * FROM global_temp.people").collect
    res11: Array[org.apache.spark.sql.Row] = Array([null,Michael], [30,Andy], [19,Justin])
    
    scala> spark.newSession.sql("SELECT * FROM global_temp.people").collect
    res12: Array[org.apache.spark.sql.Row] = Array([null,Michael], [30,Andy], [19,Justin])

    ```
5. **spark-sql支持的文件类型**
spark-sql支持的文件类型:`json, parquet, jdbc, orc, libsvm, csv, text`
    ```scala
    scala> val peopleDF = spark.read.format("json").load("examples/src/main/resources/people.json")
    peopleDF: org.apache.spark.sql.DataFrame = [age: bigint, name: string]
    
    scala> peopleDF.select("name", "age").write.format("parquet").save("namesAndAges.parquet")
    SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
    SLF4J: Defaulting to no-operation (NOP) logger implementation
    SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

    ```
    
## **第七章  spark streaming**
#### **一. 入门例子**
1. 要求:
(1)从一台服务器的7777端口上接受一个以换行符分割的多行文本,从中筛选出包含error的行并打印出来
(2)使用命令模拟向端口7777发送消息
    ```shell
    $ nc -lk localhost 7777
    <在此输入文本>
    ```
    
2. streaming代码:
    ```scala
    import org.apache.spark._
    import org.apache.spark.streaming._
    
    object TestMain {
      def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
        val ssc = new StreamingContext(conf,Seconds(1))
        val lines = ssc.socketTextStream("localhost",9999)
        val words = lines.flatMap(_.split(""))
        val pairs = words.map(x=>(x,1))
        val wordcounts = pairs.reduceByKey(_+_)
        wordcounts.print()
        ssc.start()
        ssc.awaitTermination()
      }
    }

    ```
    ```scala
    libraryDependencies ++= Seq(
      "org.apache.spark" % "spark-streaming_2.11" % "2.1.0" ,
      "org.apache.spark" % "spark-core_2.11" % "2.1.0"
    )
    ```
3. 提交代码
(1)submit提交命令
    ```shell
    spark-submit --class "Test" --master local[4] sparkdemo_2.11-1.0.jar
    ```
(2)ide中配置的提交jvm参数

    ```java
    -Dspark.master=local[4] -Dspark.app.name=mystreamingtest
    ```

#### **二. 架构与抽象**
1. 离散化流的概念
(1)spark-streaming使用微批次架构,把流式数据当做一系列小规模批处理对待。新批次按照均匀时间间隔创建出来
(2)streaming的编程模型是离散化流DStream,他是一个RDD序列,每个RDD代表数据流中一个时间片内的数据

2. straming在驱动器和执行节点的执行过程
(1)spark streamng为每个输入源启动接收器,接收器以任务的形式运行在执行器中。
(2)接收器从输入源收集数据并保存为RDD。他们在收到输入数据后会把数据复制到另一个执行器进程来保障容错性。
(3)数据被保存在执行器进程的内存中,和缓存RDD的方式一样。
(4)streamingcontext周期性的运行spark任务来处理这些数据,把数据和之前区间的RDD整合。

3. spark-streaming的容错性
(1)streaming对DStream提供的容错性,和spark为RDD提供的容错性一致。只要数据还在,就能根据RDD谱系图重算出任意状态的数据集。
(2)默认情况下,数据分别存在于两个节点上,这样可以保证数据容错性,但是只根据谱系图重算所有从程序启动就接收到的数据可能会花很长时间。因此streaming提供检查点来保存数据到hdfs中。一般情况下,每处理5-10次就保存一次
    ```scala
    ssc.checkpoint("hdfs:// ... ")    // 本地开发时,可以使用本地路径
    ```

### **三. streaming的转化操作**
1. DStream无状态转化
(1)无状态转化操作是应用到每个时间片的RDD上的
&nbsp;&nbsp;&nbsp;&nbsp;eg:`map,flatMap,filter,repartition,reduceByKey,groupByKey`
(2)无状态转化操作也可用于把两个同时间片内的DStream连接起来

2. 有状态转化操作 - 滑动窗口(有状态转化操作需要打开检查点机制来确保容错性)
(1)基于窗口的操作在一个比streamingcontext批次更长的时间范围内,通过整合更多个批次的结果,计算整个窗口的结果。所以通过window产生的DStream中每个RDD会包含多个批次的数据,可以对这些数据进行count() , transform()操作。
    ```scala
    object TestMain {
      def main(args: Array[String]): Unit = {
        val sparkConf = new SparkConf()
        val ssc = new StreamingContext(sparkConf, Seconds(1))
        ssc.checkpoint("./checkpoints")   // 设置检查点
    
        // 初始消息RDD
        val initialRDD = ssc.sparkContext.parallelize(List(("hello", 1), ("world", 1)))
    
        // 创建sparkstreaming环境
        val lines = ssc.socketTextStream("localhost",9999)
        val words = lines.flatMap(_.split(" "))
        val pairs = words.map(x => (x, 1))  // pairRDD
    
        /**
          * @param reduceFunc : reduce function
          * @param windowDuration:窗口宽度,一次批处理的时间长短
          * @param slideDuration:两次窗口滑动间隔
          */
        pairs.reduceByKeyAndWindow((a:Int,b:Int)=>a+b,Seconds(15),Seconds(1))
        pairs.print()
        ssc.start()
        ssc.awaitTermination()
      }
    }
    ```
3. 有状态转化操作 - updateStatesByKey()与mapWithState
(1)这两个方法都是操作PairRdd的,他们要求新消息以只读的形式到来,key是新消息,value是新消息对应的状态
(2)mapWithState需要传入mappingfunc来计算消息的新状态: `(KeyType, Option[ValueType], State[StateType]) => MappedType`
(3)updateStateByKey需要传入updateFunc来更新消息状态,输入参数:` (Seq[V], Option[S]) => Option[S]`
(4)用这两个方法实现持续统计单词技术:
    ```scala
    import org.apache.spark._
    import org.apache.spark.streaming._
    
    object TestMain {
      def main(args: Array[String]): Unit = {
        val sparkConf = new SparkConf()
        val ssc = new StreamingContext(sparkConf, Seconds(5))
        ssc.checkpoint(".")   // 设置检查点
    
        // 初始消息RDD
        val initialRDD = ssc.sparkContext.parallelize(List(("hello", 1), ("world", 1)))
    
        // 创建sparkstreaming环境
        val lines = ssc.socketTextStream("localhost",9999)
        val words = lines.flatMap(_.split(" "))
        val wordDstream = words.map(x => (x, 1))  // pairRDD
    
        // 定义状态更新函数:输入key,新到的pairRDD中value值,已经保存的key的状态值,返回一个键值对(key,State)
        val mappingFunc = (word: String, one: Option[Int], state: State[Int]) => {
          val sum = one.getOrElse(0) + state.getOption.getOrElse(0)
          val output = (word, sum)
          state.update(sum)
          output
        }
    
        // values是新消息pairRDD的value值,state是以保存的状态值
        def updateFunc (values:Seq[Int],state:Option[Int]): Option[Int] ={
          val newcount = state.getOrElse(0)+values.size
          Some(newcount)
        }
    
        val stateDstream = wordDstream.mapWithState(StateSpec.function(mappingFunc).initialState(initialRDD))  // 通过mapping方法累积所有消息状态
        val stateDstream = wordDstream.updateStateByKey[Int](updateFunc _)   // 通过update方法累积所有消息状态
        stateDstream.print()
        ssc.start()
        ssc.awaitTermination()
      }
    }
    ```

### **四. 输出操作**
1. print()
(1)Dstream如果没有被执行输出操作,则这些DStream不会被求值。若StreamingContext中没有定义输出操作,整个context就不会启动

2. 保存文件
(1)saveAsTextFile
    ```scala
    // output-1497685765000.txt文件,根据streamingcontext设置的时间间隔执行一次
    wordcounts.saveAsTextFiles("output","txt"); 
    ```
(2)saveAsHadoopFile
该函数接受一种Hadoop输出格式作为参数,可以用这个函数将DStream保存成SequenceFile
    ```scala
    val pairs = words.map(x=>(new Text(x),new LongWritable(1)))
    val wordcounts = pairs.reduceByKey((x:LongWritable,y:LongWritable)=>new LongWritable(x.get()+y.get()))
    wordcounts.saveAsHadoopFiles[SequenceFileOutputFormat[Text,LongWritable]]("outdir","txt");
    ```

3. 存入外部存储系统,如mysql中
    ```scala
    wordcounts.foreachRDD({ // DStream中的每个RDD
      rdd => rdd.foreachPartition({  // 每台机器上的RDD都能公用一个分区
        item => pool.getConn.save(item) // 保存每一条数据
      })
    })
    ```

### **五. 输入源**
每个DStream与一个Receiver对象相关联,该对象从数据源接收数据并将其存储到spark集群的内存中。

1. 核心数据源
(1)文件流:监听一个hdfs下的文件夹,一旦有新文件进入,就将其作为输入源处理成DStream。这种方式,文件一旦进入该文件夹,就不能再修改。

    ```c
    ssc.fileStream[KeyClass, ValueClass, InputFormatClass](dir)
    ```
(2)自定义一个接收器acceptor,接收akka数据源。[Custom Receiver Guide][6]
(3)RDD队列模拟输入源:可以把一系列的RDD作为DStream的一批数据
    ```c
    ssc.queueStream(queueOfRDDs)
    ```
2. 附加数据源
(1)kafka数据源
(2)flume数据源

3. 多数据源与集群规模
(1)当使用类似union()将多个DStream合并时,使用多个接收器用来提高聚合操作中的数据获取吞吐量(一个接收器会成为系统的性能瓶颈)。此外,有时需要用不同接收器从不同数据源接受各种数据。此时应用分配的CPU个数至少为数据源个数+1(最后一个用来计算这些数据)

### **六  24/7不间断运行配置**
1. 检查点机制
检查点是streaming中容错性的主要机制。streaming可通过转化图的谱系图来重算状态,检查点机制则可以控制要在转化图中回溯多远。其次,如果是驱动器程序崩溃,用户在重启驱动器程序并让驱动器程序从检查点回复,则streaming可以读取之前运行的程序处理数据进度,并从这里继续。
2. 驱动器程序容错
让驱动器程序重启后,先从检查点恢复sparkstreamingcontext,再重新创建streamingcontext,保证错误恢复

    ```scala
    def createStreamingContext() = {
      val sparkConf = new SparkConf()
      val ssc = new StreamingContext(sparkConf, Seconds(1))
      ssc.checkpoint("./checkpoints") // 设置检查点
      ssc
    }
    val ssc = StreamingContext.getOrCreate("./checkpoints",createStreamingContext _)
    ```

3. 工作节点容错
(1)streaming使用与spark相同的容错机制,所有从外部数据源中收到的数据都会在多个工作节点上备份,所偶有RDD操作,都能容忍一个工作节点的失败,根据RDD谱系图,系统就能把丢失的数据从输入数据备份中计算出来。
(2)工作节点上的接收器容错:接受其提供如下保障:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a. 所有从hdfs中读取的数据都是可靠的,因为底层文件系统有备份,strreaming会记住那些数据放到了检查点中,并在应用崩溃后,从检查点处继续执行。
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b. 对于像kafka这种不可靠数据源,spark会把数据放到hdfs中,仍然确保不丢失数据。

###**七. 性能**
1. 批次和窗口大小
(1)streaming可使用的最小批次间隔一般为500毫秒
(2)这个结果是从一个较大的时间窗口(10s)逐步缩小实验而来。当减小时间窗口后,如果streaming用户界面现实的处理时间保持不变,就可以进一步减小批次大小,如果处理时间增大,则认为达到了应用极限。此外,滑动步长也对性能有着巨大影响,当计算代价巨大并成为系瓶颈,就应该考虑增加滑动步长。

2. 提高并行度
(1)增加接收器数目:
&nbsp;&nbsp;&nbsp;&nbsp;如果记录太多,导致单台机器来不及读入并分发数据,接收器就会成为系统瓶颈。此时可以通过创建多个输入DStream来增加接收器的数目,然后使用union合并为一个打的数据源。
(2)将收到的数据显式的重新分区
&nbsp;&nbsp;&nbsp;&nbsp;如果接收器数目无法增加,可以通过使用DStream。repartition来重新分区输入流,从而重新分配收到的数据源。
(3)提高聚合计算的并行度
对于像reduceByKeyy这样的操作,可以再第二个参数制定并行度。

3. 垃圾回收和内存使用
可以通过修改gc策略,使用CMS策略
    ```scala
    spark-submit --conf spark.executor.extraJavaOptions=-XX:+UseConcMarkSweepGC
    ```

## **第八章 spark 机器学习**





## **第九章 spark调试与调优**
### **一. 使用SparkConf配置Spark**
1. spark每个配置项都是基于字符串形式的键值对。eg:通过setAppName()设置spark.app.name
2. spark允许通过spark-submit脚本动态配置配置项,脚本会把这些配置项这知道运行时环境中。当一个新的SparkConf被创建出来时,这些环境变量会被检测出来并自动配置好。因此,用户只需要创建一个空的SparkConf,并直接传给SparkContext即可。

    ```shell
    spark-submit --class com.example.MyApp --name "My app"
    --conf spark.ui.port=36000
    myApp.jar
    ```
3. spark-submit脚本会查找conf/spark-defaults.conf文件,然后尝试读取该文件中以空格隔开的键值对数据。也可通过--properties-File自定义文件路径

    ```shell
    # 提交脚本
    spark-subnmit --class com.example.MyApp --properties-File myconfig.conf MyApp.jar
    # myconfig.conf内容
    spark.master local[4]
    spark.app.name "My App"
    spark.ui.port 36000
    ```
        
4.  **sparkconf的优先级选择**
(1)最高:用户显示调用的sparkconfig的set()方法设置的选项
(2)其次:spark-submit传递的参数
(3)写在配置文件中的值

### **二. RDD依赖关系**
1. **RDD依赖**
(1) 窄依赖:父RDD的每个Partition,最多被子RDD的1个分区所使用
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a ) 窄依赖分为两种:一对一依赖OneToOneDependcy,一对一范围依赖 RangeDependency
(2) 宽依赖:指计算中会产生shuffle操作的RDD依赖。表示一个父RDD的Partition会被多个子RDD的Partition使用
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a ) groupByKey就是常见的宽依赖算子

2. **DAG生成机制**
(1)DAG生成过程,就是对计算中stage的划分。
(2)对于窄依赖,RDD之间的数据不需要进行shuffle,这些处理操作可以在同一台机器的内存中完成,所以窄依赖在划分中被分成一个stage
(3)对于宽依赖,由于数据之间存在shuffle,必须等到父RDD所有数据shuffle完成之后才能进行后续操作,所以在此处进行stage划分

3. **RDD检查点** 
(1)checkpoint也是存储RDD结果的一种方式,它不同于persist将数据存储在本地磁盘,而是把结果存储在HDFS中
    ```shell
    scala> val wordcount = sc.textFile("/root/README.txt").flatMap(_.split(" ")).map(word=>(word,1)).reduceByKey(_+_)
    scala> wordcount.checkpoint
    ```
    
4. **RDD容错**
(1)RDD容错分为3个层面:调度层,RDD血统层,Checkpoint层
(2)调度层容错:分别在Stage输出时出错与计算时出错。stage输出出错,上层调度器DAGScheduler会进行重试。stage计算出错时,该task会自动被重新计算4次
(3)RDD LINEAGE血统层容错:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a ) 基于各RDD各项Transaction构成了compute chain,在部分结果丢失的时候可以根据Lineage重新计算
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b ) 窄依赖中,数据进行的流水线处理,子RDD的分区数据同样在父RDD的分区中,并不存在冗余计算
(4)CheckPoint层容错
在宽依赖上做检查点可以避免Lineage很长重新计算而带来的冗余计算


### **三. Spark执行步骤:作业,任务,步骤**
1. **关于作业**
(1)rdd.toDebugString方法查看RDD谱系图
(2)行动操作会触发生成一个作业,这个作业包含了transaction动作产生的多个步骤

2. **查找作业信息**
(1)4040端口展示了作业列表,里面包含stage执行的详情。该页面包含了一个作业的性能表现,若果有些步骤特别慢,还可以点击进去查看是哪段用户代码
(2)数据倾斜是导致性能问题的常见原因,当有少量任务对于其他任务需要花费大量时间时,一般就是发生了数据倾斜

### **四. 驱动器日志和执行器日志**
1. spark独立模式下:所有日志再主节点的网页中直接显示,存储于spark目录下的work目录中
2. Mesos模式下:日志存储在Mesos从节点的work目录中,可通过主节点用户界面访问
3. YARN模式下:当作业运行完毕,可以通过yarn logs -applicationId <app ID>来打包一个应用日志。如果要查看运行再YARN上的应用日志,可以从资源管理器的用户界面进入从节点页面,浏览特定节点容器的日志
4. log4j配置文件的示例在conf/log4j.properties.template,也可通过spark-submit --Files添加log4j.properties文件

### **五. 关键性能考量**
1. 并行度
2. 序列化格式
3. 内存管理
4. 硬件供给


  [1]: http://static.zybuluo.com/lj72808up/vixh17ux3ny0536r12re7jkj/Screenshot%20from%202017-05-04%2022-45-27.png
  [2]: http://static.zybuluo.com/lj72808up/2349rmqrpwn144x1kstddwv0/action.png
  [3]: http://static.zybuluo.com/lj72808up/d2w90ls7600pjgypnbz4yfop/pairddtrans.png
  [4]: http://static.zybuluo.com/lj72808up/d6dszvv4cye5zdox3ewywjh0/2pairRddtrans.png
  [5]: http://static.zybuluo.com/lj72808up/j3kn7lb44hz7zci9dvcl9ta7/2pairRddtrans2.png
  [6]: http://spark.apache.org/docs/1.6.0/streaming-custom-receivers.html
posted @ 2017-04-06 10:46  moon_lord  阅读(3418)  评论(1编辑  收藏  举报