Spark Core之基础知识

Spark Core之基础知识

一、RDD概叙

  1. RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象
  2. 在代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合

二、RDD的五个主要属性

  1. 多个分区. 分区可以看成是数据集的基本组成单位
  2. 计算每个切片(分区)的函数
  3. 与其他 RDD 之间的依赖关系
  4. 对存储键值对的 RDD, 还有一个可选的分区器
  5. 存储每个切片优先(preferred location)位置的列表

三、RDD特点

  1. 弹性
  2. 分区
  3. 只读
  4. 依赖(血缘关系)
  5. 缓存
  6. checkpoint(检查点)

四、RDD的编程模型

  1. transformations:转换,创建一个新的RDD,对新的RDD进行操作更改,transformations操作并不会立即计算他们的结果, 而是记住了这个操作
  2. action:动作,可以向应用程序返回结果,或向存储系统保存数据,action来获取结果返回给驱动程序的时候这些转换操作才开始计算

五、RDD的创建模式

  1. 从已有的集合只创建

    1. parallelize创建

      val rdd1 = sc.parallelize(Array(10,20,30,40,50,60))
      
    2. mkRDD:底层调用parallelize

      val rdd1 = sc.makeRDD(Array(10,20,30,40,50,60))
      
    3. 上面两种创建还有一个重要的参数是把数据集切分成的分区数

      val rdd1 = sc.makeRDD(Array(10,20,30,40,50,60),2)
      
  2. 从外部的存储创建

    1. textFile创建:

      var distFile = sc.textFile("words.txt")
      
    2. 可以是本地文件系统, HDFS, Cassandra, HVase, Amazon S3 等;Spark 支持 文本文件, SequenceFiles, 和其他所有的 Hadoop InputFormat

    3. textFile还可以有第二个参数, 表示分区数. 默认情况下, 每个块对应一个分区.(对 HDFS 来说, 块大小默认是 128M). 可以传递一个大于块数的分区数, 但是不能传递一个比块数小的分区数

    4. 如果是使用的本地文件系统的路径, 则必须每个节点都要存在这个路径

    5. 所有基于文件的方法, 都支持目录, 压缩文件, 和通配符(*****)

  3. 从其他RDD转换为新的RDD

    1. 就是通过 RDD 的各种转换算子来得到新的 RDD

六、RDD 的转换(transformation)

  1. value类型:一个RDD的转换

    1. map(func):一进一出

    2. mapPartitions(func)类似于map(func), 但是是独立在每个分区上运行,所以:Iterator => Iterator

      map()和mapPartitions()的区别:

      1,map每次处理一条数据

      2,mapPartitions每次处理一个分区的数据

      3,当内存空间较大的时候建议使用mapPartitions(),以提高处理效率

    3. mapPartitionsWithIndex(func):和mapPartitions(func)类似. 但是会给func多提供一个Int值来表示分区的索引. 所以func的类型是:(Int, Iterator) => Iterator
    4. flatMap(func):类似于map,但是每一个输入元素可以被映射为 0 或多个输出元素(所以func应该返回一个序列,而不是单一元素 T => TraversableOnce[U])
    5. glom():将每一个分区的元素合并成一个数组,形成新的 RDD 类型是RDD[Array[T]]
    6. groupBy(func)按照func的返回值进行分组

    7. filter(func):过滤,返回一个新的 RDD 是由func的返回值为true的那些元素组成
    8. sample(withReplacement, fraction, seed)

      1. 以指定的随机种子随机抽样出比例为fraction的数据,(抽取到的数量是: size \ fraction). 需要注意的是得到的结果并不能保证准确的比例.

      2. withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样. 放回表示数据有可能会被重复抽取到, false 则不可能重复抽取到. 如果是false, 则fraction必须是:[0,1], 是 true 则大于等于0就可以了

      3. seed用于指定随机数生成器种子。 一般用默认的, 或者传入当前的时间戳

      4. 样例:

        //不放回抽样
            val rdd1 = sc.parallelize(1 to 10)
            rdd1.sample(false, 0.5).collect
            结果为:
            Array[Int] = Array(1, 3, 4, 7)
        //放回抽样
        	rdd1.sample(true, 2).collect
        	结果为:
        	Array[Int] = Array(1, 1, 2, 3, 3, 4, 4, 5, 5, 5, 5, 5, 6, 6, 7, 7, 8, 8, 9)
        
    9. distinct([numTasks])):对 RDD 中元素执行去重操作. 参数表示任务的数量.默认值和分区数保持一致
    10. coalesce(numPartitions)缩减分区数到指定的数量,用于大数据集过滤后,提高小数据集的执行效率
    11. repartition(numPartitions)据新的分区数, 重新 shuffle 所有的数据, 这个操作总会通过网络,新的分区数相比以前可以多, 也可以少

      coalasce和repartition的区别:

      1,coalesce重新分区,可以选择是否进行shuffle过程。由参数shuffle: Boolean = false/true决定

      2,repartition实际上是调用的coalesce,进行shuffle

      3,如果是减少分区, 尽量避免 shuffle

    12. sortBy(func,[ascending], [numTasks])用func先对数据进行处理,按照处理后结果排序,默认为正序

    13. pipe(command, [envVars]):管道,针对每个分区,把 RDD 中的每个数据通过管道传递给shell命令或脚本,返回输出的RDD。一个分区执行一次这个命令. 如果只有一个分区, 则执行一次命令注意:脚本要放在 worker 节点可以访问到的位置

      1. 创建一个脚本文件pipe.sh

        echo "hello"
        while read line;
        do
            echo ">>>"$line
        done
        
      2. 创建只有 1 个分区的RDD

        val rdd1 = sc.parallelize(Array(10,20,30,40), 1)
        rdd1.pipe("./pipe.sh").collect
        结果为:
        Array[String] = Array(hello, >>>10, >>>20, >>>30, >>>40)
        
      3. 创建有 2 个分区的 RDD

        val rdd1 = sc.parallelize(Array(10,20,30,40), 2)
        rdd1.pipe("./pipe.sh").collect
        结果为:
        Array[String] = Array(hello, >>>10, >>>20, hello, >>>30, >>>40)
        
      4. 总结: 每个分区执行一次脚本, 但是每个元素算是标准输入中的一行

  2. 双value类型:两个RDD的转换

    1. union(otherDataset)求并集. 对源 RDD 和参数 RDD 求并集后返回一个新的 RDD。union和++是等价的

    2. subtract (otherDataset)计算差集. 从原 RDD 中减去 原 RDD 和 otherDataset 中的共同的部分

    3. intersection(otherDataset):计算交集. 对源 RDD 和参数 RDD 求交集后返回一个新的 RDD
    4. cartesian(otherDataset)计算 2 个 RDD 的笛卡尔积. 尽量避免使用

    5. zip(otherDataset):拉链操作. 需要注意的是, 在 Spark 中, 两个 RDD 的元素的数量和分区数都必须相同, 否则会抛出异常。本质就是要求的每个分区的元素的数量相同
  3. key-value类型:其实就是存一个二维的元组。这些特殊操作大多都涉及到 shuffle 操作, 比如: 按照 key 分组(group), 聚集(aggregate)等.

    1. partitionBy对 pairRDD 进行分区操作,如果原有的 partionRDD 的分区器和传入的分区器相同, 则返回原 pairRDD,否则会生成 ShuffleRDD,即会产生 shuffle 过程

      val rdd1 = sc.parallelize(Array((1, "a"), (2, "b"), (3, "c"), (4, "d")))
      rdd1.partitions.length
      结果为:2
      rdd1.partitionBy(new org.apache.spark.HashPartitioner(3)).partitions.length
      结果为:3
      
    2. reduceByKey(func, [numTasks]):在一个(K,V)的 RDD 上调用,返回一个(K,V)的 RDD,使用指定的reduce函数,将相同keyvalue聚合到一起,reduce任务的个数可以通过第二个可选的参数来设置
    3. groupByKey():按照key进行分组

      reduceByKey和groupByKey的区别:

      1,reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]

      2,groupByKey:按照key进行分组,直接进行shuffle

      3,reduceByKey比groupByKey性能更好,建议使用。但是需要注意是否会影响业务逻辑

    4. aggregateByKey(zeroValue)(seqOp, combOp, [numTasks])使用给定的 combine 函数和一个初始化的zero value,对每个key的value进行聚合,第一个函数seqOp用于在一个分区进行合并, 第二个函数combOp用在两个分区间进行合并

    5. foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]aggregateByKey的简化操作,seqop和combop相同

    6. combineByKey[C](createCombiner: V => C,mergeValue: (C, V) => C,mergeCombiners: (C, C) => C)针对每个K, 将V进行合并成C, 得到RDD[(K,C)]

      1. createCombiner: combineByKey会遍历分区中的每个key-value对. 如果第一次碰到这个key, 则调用createCombiner函数,传入value, 得到一个C类型的值.(如果不是第一次碰到这个 key, 则不会调用这个方法)
      2. mergeValue: 如果不是第一个遇到这个key, 则调用这个函数进行合并操作. 分区内合并
      3. mergeCombiners 跨分区合并相同的key的值(C). 跨分区合并

      注意:reduceByKey、aggregateByKey、foldByKey的底层都是combineByKey

    7. sortByKey:在一个(K,V)的 RDD 上调用, K必须实现 Ordered[K] 接口(或者有一个隐式值: Ordering[K]), 返回一个按照key进行排序的(K,V)的 RDD
    8. mapValues针对(K,V)形式的类型只对V进行操作

    9. join(otherDataset, [numTasks])在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素对在一起的(K,(V,W))的RDD

    10. cogroup(otherDataset, [numTasks])在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD

七、RDD的操作(action)

  1. reduce(func):通过func函数聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据
  2. collect:以数组的形式返回 RDD 中的所有元素。所有的数据都会被拉到 driver 端,占用内存, 所以要慎用
  3. count():返回 RDD 中元素的个数
  4. take(n):返回 RDD 中前 n 个元素组成的数组。take 的数据也会拉到 driver 端, 应该只对小数据集使用
  5. first:返回 RDD 中的第一个元素. 类似于take(1)
  6. takeOrdered(n,[ordering]):返回排序后的前 n 个元素, 默认是升序排列。数据也会拉到 driver 端
  7. aggregate:aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致。注意:zeroValue 分区内聚合和分区间聚合的时候各会使用一次
  8. fold:折叠操作,aggregate的简化操作,seqopcombop一样的时候,可以使用fold
  9. saveAsTextFile(path):将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark 将会调用toString方法,将它装换为文件中的文本
  10. saveAsSequenceFile(path):将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以使 HDFS 或者其他 Hadoop 支持的文件系统
  11. saveAsObjectFile(path):用于将 RDD 中的元素序列化成对象,存储到文件中
  12. countByKey():针对(K,V)类型的 RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。应用: 可以用来查看数据是否倾斜
  13. foreach(func):针对 RDD 中的每个元素都执行一次func。注意:每个函数是在 Executor 上执行的, 不是在 driver 端执行的

八、RDD中的函数传递

  1. 我们进行 Spark 进行编程的时候, 初始化工作是在 driver端完成的, 而实际的运行程序是在executor端进行的. 所以就涉及到了进程间的通讯, 数据是需要序列化的

  2. Java 的序列化比较重, 能够序列化任何的类. 比较灵活,但是相当的慢, 并且序列化后对象的体积也比较大

  3. Spark 出于性能的考虑, 支持另外一种序列化机制: kryo (2.0开始支持). kryo 比较快和简洁.(速度是Serializable的10倍). 想获取更好的性能应该使用 kryo 来序列化

  4. 需要注意的是: 即使使用 kryo 序列化, 也要继承 Serializable 接口

  5. 样例:

    import org.apache.spark.{SparkConf, SparkContext}
    import org.apache.spark.rdd.RDD
    object SerDemo {
        def main(args: Array[String]): Unit = {
            val conf: SparkConf = new SparkConf()
                .setAppName("SerDemo")
                .setMaster("local[*]")
                // 注册需要使用 kryo 序列化的自定义类
                .registerKryoClasses(Array(classOf[Searcher]))
            val sc = new SparkContext(conf)
            val rdd: RDD[String] = sc.parallelize(Array("hello world", "hello kgg", "kgg", "hahah"), 2)
            val searcher = new Searcher("hello")
            val result: RDD[String] = searcher.getMatchedRDD1(rdd)
            result.collect.foreach(println)
        }
    }
    case class Searcher(val query: String) extends Serializable {
        // 判断 s 中是否包括子字符串 query
        def isMatch(s: String) = {
            s.contains(query)
        }
        // 过滤出包含 query字符串的字符串组成的新的 RDD
        def getMatchedRDD1(rdd: RDD[String]) = {
            rdd.filter(isMatch) //
        }
        // 过滤出包含 query字符串的字符串组成的新的 RDD
        def getMatchedRDD2(rdd: RDD[String]) = {
            val q = query
            rdd.filter(_.contains(q))
        }
    }
    

九、RDD 的依赖关系

  1. 查看 RDD 的血缘关系

    scala> rdd1.toDebugString
    res1: String =
    (2) ./words.txt MapPartitionsRDD[1] at textFile at <console>:24 []
    | ./words.txt HadoopRDD[0] at textFile at <console>:24 []
    //说明:圆括号中的数字表示 RDD 的并行度. 也就是有几个分区.
    
  2. 查看 RDD 的依赖关系

    scala> rdd1.dependencies
    res28: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@70dbde75)
    
  3. 窄依赖:

    1. 父 RDD 的每个分区最多被一个 RDD 的分区使用
    2. 比如mapfiltercoalesce操作
  4. 宽依赖:

    1. 父 RDD 的分区被不止一个子 RDD 的分区使用
    2. 包括: sort, reduceByKey, groupByKey, join, 和调用rePartition函数的任何操作

十、Spark Job的划分

  1. 针对每个 action, Spark 调度器就创建一个执行图(execution graph)和启动一个 Spark job
  2. 每个 job 由多个stages 组成, 这些 stages 就是实现最终的 RDD 所需的数据转换的步骤; 一个宽依赖划分一个 stage
  3. 每个 stage 由多个 tasks 来组成, 这些 tasks 就表示每个并行计算, 并且会在多个执行器上执行
  4. 在大多数情况下, 每个 stage 的所有 task 在下一个 stage 开启之前必须全部完成

十一、RDD的持久化(存储一个中间结果到磁盘或者内存中,方便下次直接读取)

  1. 每碰到一个 Action 就会产生一个 job, 每个 job 开始计算的时候,就是每调用一次 collect的时候,总是从这个 job 最开始的 RDD 开始计算,导致这些计算过程重复执行,原因是因为 rdd记录了整个计算过程. 如果计算的过程中出现哪个分区的数据损坏或丢失, 则可以从头开始计算来达到容错的目的
  2. 持久化的目的是有些时候不必要从头计算,这时就需要对RDD进行持久化到内存中。
  3. 可以使用方法persist()或者cache()来持久化一个 RDD
  4. 可以给persist()来传递存储级别,cache()方法是使用默认存储级别
  5. rdd2.cache() 等价于 rdd2.persist(StorageLevel.MEMORY_ONLY)
  6. 在实际使用的时候, 如果想重用数据, 仍然建议调用persistcache

十二、设置检查点(斩断了子RDD和父RDD的依赖关系)

  1. Spark 中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点是为了通过 Lineage(血缘)做容错的辅助

  2. Lineage 过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的 RDD 开始重做 Lineage,就会减少开销

  3. 检查点通过将数据写入到 HDFS 文件系统实现了 RDD 的检查点功能

  4. 为当前 RDD 设置检查点。该函数将会创建一个二进制的文件,并存储到 checkpoint 目录中,该目录是用 SparkContext.setCheckpointDir()设置的在 checkpoint 的过程中,该RDD 的所有依赖于父 RDD中 的信息将全部被移除

  5. 对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发, 在触发的时候需要对这个 RDD 重新计算

  6. 样例:

    import org.apache.spark.rdd.RDD
    import org.apache.spark.{SparkConf, SparkContext}
    object CheckPointDemo {
        def main(args: Array[String]): Unit = {
            // 要在SparkContext初始化之前设置, 都在无效
            System.setProperty("HADOOP_USER_NAME", "kgg")
            val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
            val sc = new SparkContext(conf)
            // 设置 checkpoint的目录. 如果spark运行在集群上, 则必须是 hdfs 目录
            sc.setCheckpointDir("hdfs://hadoop101:9000/checkpoint")
            val rdd1 = sc.parallelize(Array("abc"))
            val rdd2: RDD[String] = rdd1.map(_ + " : " + System.currentTimeMillis())
            /*
            标记 RDD2的 checkpoint.
            RDD2会被保存到文件中(文件位于前面设置的目录中), 并且会切断到父RDD的引用, 也就是切断了它向上的血缘关系
            该函数必须在job被执行之前调用.
            强烈建议把这个RDD序列化到内存中, 否则, 把他保存到文件的时候需要重新计算.
             */
            rdd2.checkpoint()
            rdd2.collect().foreach(println)
            rdd2.collect().foreach(println)
            rdd2.collect().foreach(println)
        }
    }
    

十三、持久化和检查点(checkpoint)的区别

  1. 持久化只是将数据保存在 BlockManager 中,而 RDD 的 Lineage 是不变的。但是checkpoint 执行完后,RDD 已经没有之前所谓的依赖 RDD 了,而只有一个强行为其设置的checkpointRDD,RDD 的 Lineage 改变了
  2. 持久化的数据丢失可能性更大,磁盘、内存都可能会存在数据丢失的情况。但是 checkpoint 的数据通常是存储在如 HDFS 等容错、高可用的文件系统,数据丢失可能性较小
  3. 注意: 默认情况下,如果某个 RDD 没有持久化,但是设置了checkpoint,会存在问题. 本来这个 job 都执行结束了,但是由于中间 RDD 没有持久化,checkpoint job 想要将 RDD 的数据写入外部文件系统的话,需要全部重新计算一次,再将计算出来的 RDD 数据 checkpoint到外部文件系统。 所以,建议对 checkpoint()的 RDD 使用持久化, 这样 RDD 只需要计算一次就可以了

十四、Key-Value 类型 RDD 的数据分区器

  1. 对于只存储 value的 RDD, 不需要分区器,只有存储Key-Value类型的才会需要分区器

  2. HashPartitioner:对于给定的key,计算其hashCode,并除以分区的个数取余,如果余数小于 0,则用余数+分区的个数(否则加0),最后返回的值就是这个key所属的分区ID

  3. RangePartitioner

    1. 作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内
    2. 实现:
      1. 第一步:先从整个 RDD 中抽取出样本数据,将样本数据排序,计算出每个分区的最大 key 值,形成一个Array[KEY]类型的数组变量 rangeBounds(边界数组)
      2. 第二步:判断keyrangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求 RDD 中的 KEY 类型必须是可以排序的
  4. 自定义分区器

    1. 继承 org.apache.spark.Partitioner,并且需要实现下面的方法

    2. numPartitions:该方法需要返回分区数, 必须要大于0

    3. getPartition(key):返回指定键的分区编号(0到numPartitions-1)

    4. equals:Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同

    5. hashCode:如果你覆写了equals, 则也应该覆写这个方法

    6. 样例:

      import org.apache.spark.rdd.RDD
      import org.apache.spark.{Partitioner, SparkConf, SparkContext}
      /*
      使用自定义的 Partitioner 是很容易的 :只要把它传给 partitionBy() 方法即可。
      Spark 中有许多依赖于数据混洗的方法,比如 join() 和 groupByKey(),
      它们也可以接收一个可选的 Partitioner 对象来控制输出数据的分区方式。
      */
      object MyPartitionerDemo {
          def main(args: Array[String]): Unit = {
              val conf = new SparkConf().setAppName("Practice").setMaster("local[*]")
              val sc = new SparkContext(conf)
              val rdd1 = sc.parallelize(
                  Array((10, "a"), (20, "b"), (30, "c"), (40, "d"), (50, "e"), (60, "f")),
                  3)
              val rdd2: RDD[(Int, String)] = rdd1.partitionBy(new MyPartitioner(4))
              val rdd3: RDD[(Int, String)] = rdd2.mapPartitionsWithIndex((index, items) => items.map(x => (index, x._1 + " : " + x._2)))
              println(rdd3.collect.mkString(" "))
          }
      }
      class MyPartitioner(numPars: Int) extends Partitioner {
          override def numPartitions: Int = numPars
          override def getPartition(key: Any): Int = {
              1
          }
      }
      

十五、文件中数据的读取和保存

  1. 读取Text文件

    val rdd1 = sc.textFile("./words.txt")
    rdd2.saveAsTextFile("hdfs://hadoop101:9000/words_output")
    
  2. 读取Json文件

    // 读取 json 数据的文件, 每行是一个 json 对象
    sc.textFile("./people.json")
    // 导入 scala 提供的可以解析 json 的工具类
    import scala.util.parsing.json.JSON
    // 使用 map 来解析 Json, 需要传入 JSON.parseFull
    val rdd2 = rdd1.map(JSON.parseFull)
    // 解析到的结果其实就是 Option 组成的数组, Option 存储的就是 Map 对象
    Array[Option[Any]] = Array(Some(Map(name -> Michael)), Some(Map(name -> Andy, age -> 30.0)), Some(Map(name -> Justin, age ->19.0)))
    
  3. 读写Sequence文件

    rdd1.saveAsSequenceFile("hdfs://hadoop101:9000/seqFiles")
    val rdd1 = sc.sequenceFile[String, Int]("hdfs://hadoop101:9000/seqFiles")
    
  4. 读写Object文件

    rdd1.saveAsObjectFile("hdfs://hadoop101:9000/obj_file")
    val rdd1 = sc.objectFile[(String, Int)]("hdfs://hadoop101:9000/obj_file")
    
  5. 从HDFS读写文件(略)

  6. 从Mysql读写文件

    1. 引入 Mysql 依赖

      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.27</version>
      </dependency>
      
    2. 从 Mysql 读取数据

      import java.sql.DriverManager
      import org.apache.spark.rdd.JdbcRDD
      import org.apache.spark.{SparkConf, SparkContext}
      object JDBCDemo {
          def main(args: Array[String]): Unit = {
              val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
              val sc = new SparkContext(conf)
              //定义连接mysql的参数
              val driver = "com.mysql.jdbc.Driver"
              val url = "jdbc:mysql://hadoop101:3306/rdd"
              val userName = "root"
              val passWd = "aaa"
              val rdd = new JdbcRDD(
                  sc,
                  () => {
                      Class.forName(driver)
                      DriverManager.getConnection(url, userName, passWd)
                  },
                  "select id, name from user where id >= ? and id <= ?",
                  1,
                  20,
                  2,
                  result => (result.getInt(1), result.getString(2))
              )
              rdd.collect.foreach(println)
          }
      }
      
    3. 往 Mysql 写入数据

      import java.sql.{Connection, DriverManager, PreparedStatement}
      import org.apache.spark.rdd.RDD
      import org.apache.spark.{SparkConf, SparkContext}
      object JDBCDemo2 {
          def main(args: Array[String]): Unit = {
              val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
              val sc = new SparkContext(conf)
              //定义连接mysql的参数
              val driver = "com.mysql.jdbc.Driver"
              val url = "jdbc:mysql://hadoop101:3306/rdd"
              val userName = "root"
              val passWd = "aaa"
              val rdd: RDD[(Int, String)] = sc.parallelize(Array((110, "police"), (119, "fire")))
              // 对每个分区执行 参数函数
              rdd.foreachPartition(it => {
                  Class.forName(driver)
                  val conn: Connection = DriverManager.getConnection(url, userName, passWd)
                  it.foreach(x => {
                      val statement: PreparedStatement = conn.prepareStatement("insert into user values(?, ?)")
                      statement.setInt(1, x._1)
                      statement.setString(2, x._2)
                      statement.executeUpdate()
                  })
              })
          }
      }
      
  7. 从Hbase读写文件

    1. 由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现,Spark 可以通过Hadoop输入格式访问 HBase

    2. 这个输入格式会返回键值对数据,其中键的类型为org. apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为org.apache.hadoop.hbase.client.Result

    3. 导入依赖

      <dependency>
          <groupId>org.apache.hbase</groupId>
          <artifactId>hbase-server</artifactId>
          <version>1.3.1</version>
          <exclusions>
              <exclusion>
                  <groupId>org.mortbay.jetty</groupId>
                  <artifactId>servlet-api-2.5</artifactId>
              </exclusion>
              <exclusion>
                  <groupId>javax.servlet</groupId>
                  <artifactId>servlet-api</artifactId>
              </exclusion>
          </exclusions>
      </dependency>
      <dependency>
          <groupId>org.apache.hbase</groupId>
          <artifactId>hbase-client</artifactId>
          <version>1.3.1</version>
          <exclusions>
              <exclusion>
                  <groupId>org.mortbay.jetty</groupId>
                  <artifactId>servlet-api-2.5</artifactId>
              </exclusion>
              <exclusion>
                  <groupId>javax.servlet</groupId>
                  <artifactId>servlet-api</artifactId>
              </exclusion>
          </exclusions>
      </dependency>
      
    4. 从 HBase 读取数据

      import org.apache.hadoop.conf.Configuration
      import org.apache.hadoop.hbase.HBaseConfiguration
      import org.apache.hadoop.hbase.client.Result
      import org.apache.hadoop.hbase.io.ImmutableBytesWritable
      import org.apache.hadoop.hbase.mapreduce.TableInputFormat
      import org.apache.hadoop.hbase.util.Bytes
      import org.apache.spark.rdd.RDD
      import org.apache.spark.{SparkConf, SparkContext}
      object HBaseDemo {
          def main(args: Array[String]): Unit = {
              val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
              val sc = new SparkContext(conf)
              val hbaseConf: Configuration = HBaseConfiguration.create()
              hbaseConf.set("hbase.zookeeper.quorum", "hadoop101,hadoop102,hadoop103")
              hbaseConf.set(TableInputFormat.INPUT_TABLE, "student")
              val rdd: RDD[(ImmutableBytesWritable, Result)] = sc.newAPIHadoopRDD(
                  hbaseConf,
                  classOf[TableInputFormat],
                  classOf[ImmutableBytesWritable],
                  classOf[Result])
              val rdd2: RDD[String] = rdd.map {
                  case (_, result) => Bytes.toString(result.getRow)
              }
              rdd2.collect.foreach(println)
              sc.stop()
          }
      }
      
    5. 往 HBase 写入数据

      import org.apache.hadoop.hbase.HBaseConfiguration
      import org.apache.hadoop.hbase.client.Put
      import org.apache.hadoop.hbase.io.ImmutableBytesWritable
      import org.apache.hadoop.hbase.mapreduce.TableOutputFormat
      import org.apache.hadoop.hbase.util.Bytes
      import org.apache.hadoop.mapreduce.Job
      import org.apache.spark.{SparkConf, SparkContext}
      object HBaseDemo2 {
          def main(args: Array[String]): Unit = {
              val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
              val sc = new SparkContext(conf)
              val hbaseConf = HBaseConfiguration.create()
              hbaseConf.set("hbase.zookeeper.quorum", "hadoop101,hadoop102,hadoop103")
              hbaseConf.set(TableOutputFormat.OUTPUT_TABLE, "student")
              // 通过job来设置输出的格式的类
              val job = Job.getInstance(hbaseConf)
              job.setOutputFormatClass(classOf[TableOutputFormat[ImmutableBytesWritable]])
              job.setOutputKeyClass(classOf[ImmutableBytesWritable])
              job.setOutputValueClass(classOf[Put])
              val initialRDD = sc.parallelize(List(("100", "apple", "11"), ("200", "banana", "12"), ("300", "pear", "13")))
              val hbaseRDD = initialRDD.map(x => {
                  val put = new Put(Bytes.toBytes(x._1))
                  put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes(x._2))
                  put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("weight"), Bytes.toBytes(x._3))
                  (new ImmutableBytesWritable(), put)
              })
              hbaseRDD.saveAsNewAPIHadoopDataset(job.getConfiguration)
          }
      }
      

十六、共享变量问题

  1. 正常情况下, 传递给 Spark 算子(比如: map, reduce 等)的函数都是在远程的集群节点上执行, 函数中用到的所有变量都是独立的拷贝,这些变量被拷贝到集群上的每个节点上, 都这些变量的更改不会传递回驱动程序

  2. 支持跨 task 之间共享变量通常是低效的, 但是 Spark 对共享变量也提供了两种支持:累加器、广播变量

  3. 累加器(Accumulator)

    1. 内置累加器,longAccumulator关键字

      // 得到一个 Long 类型的累加器.  将从 0 开始累加
      val acc= sc.longAccumulator
      
    2. 自定义累加器,通过继承类AccumulatorV2来自定义累加器,实现里面的5个方法,在使用自定义累加器的不要忘记注册sc.register(acc)

      import java.util
      import java.util.{ArrayList, Collections}
      import org.apache.spark.util.AccumulatorV2
      class MyAcc extends AccumulatorV2[String, java.util.List[String]] {
          private val _list: java.util.List[String] = Collections.synchronizedList(new ArrayList[String]())
          override def isZero: Boolean = _list.isEmpty
          override def copy(): AccumulatorV2[String, util.List[String]] = {
              val newAcc = new MyAcc
              _list.synchronized {
                  newAcc._list.addAll(_list)
              }
              newAcc
          }
          override def reset(): Unit = _list.clear()
          override def add(v: String): Unit = _list.add(v)
          override def merge(other: AccumulatorV2[String, util.List[String]]): Unit =other match {
              case o: MyAcc => _list.addAll(o.value)
              case _ => throw new UnsupportedOperationException(
                  s"Cannot merge ${this.getClass.getName} with ${other.getClass.getName}")
          }
          override def value: util.List[String] = java.util.Collections.unmodifiableList(new util.ArrayList[String](_list))
      }
      测试:
      object MyAccDemo {
          def main(args: Array[String]): Unit = {
              val pattern = """^\d+$"""
              val conf = new SparkConf().setAppName("Practice").setMaster("local[2]")
              val sc = new SparkContext(conf)
              // 统计出来非纯数字, 并计算纯数字元素的和
              val rdd1 = sc.parallelize(Array("abc", "a30b", "aaabb2", "60", "20"))
              val acc = new MyAcc
              sc.register(acc)
              val rdd2: RDD[Int] = rdd1.filter(x => {
                  val flag: Boolean = x.matches(pattern)
                  if (!flag) acc.add(x)
                  flag
              }).map(_.toInt)
              println(rdd2.reduce(_ + _))
              println(acc.value)
          }
      }
      
  4. 广播变量

    1. 广播变量在每个节点上保存一个只读的变量的缓存, 而不用给每个 task 来传送一个 copy

    2. 广播变量通过调用SparkContext.broadcast(v)来创建. 广播变量是对v的包装, 通过调用广播变量的 value方法可以访问

      val broadcastVar = sc.broadcast(Array(1, 2, 3))
      broadcastVar.value
      
    3. 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spark-Core文档是本人经三年总结笔记汇总而来,对于自我学习Spark核心基础知识非常方便,资料例举完善,内容丰富。具体目录如下: 目录 第一章 Spark简介与计算模型 3 1 What is Spark 3 2 Spark简介 3 3 Spark历史 4 4 BDAS生态系统 4 5 Spark与Hadoop的差异 5 6 Spark的适用场景 6 7 Spark成功案例 6 第二章 Spark开发环境搭建 8 1 Spark运行模式 8 2 Spark环境搭建 8 2.1Scala的安装 8 2.2Spark的单节点配置 9 2.3Spark-Standalone集群配置 9 2.4Spark-on-Yarn模式配置 12 2.5Spark-on-Mesos模式配置 13 2.6Hive-on-Spark配置 13 第三章 Spark计算模型 15 1 RDD编程 15 1.1弹性分布式数据集RDD 15 1.2构建RDD对象 15 2RDD操作 15 2.1将函数传递给Spark 16 2.2了解闭包 16 2.3Pair RDD模型 17 2.4Spark常见转换操作 18 2.5Spark常见行动操作 20 2.6RDD持久化操作 21 2.7注意事项 23 2.7并行度调优 24 2.8分区方式 25 3Examle:PageRank 27 第四章 Spark编程进阶 29 1共享变量 29 1.1累加器 30 1.2广播变量 31 2基于分区进行操作 32 3与外部程序间的管道 33 4数值RDD的操作 34 5 Spark Shuffle机制 34 第五章 Spark调优与调试 39 1开发调优: 40 1.1调优概述 40 1.2原则一:避免创建重复的RDD 40 1.3原则二:尽可能复用同一个RDD 41 1.4原则三:对多次使用的RDD进行持久化 42 1.5原则四:尽量避免使用shuffle类算子 43 1.6原则五:使用map-side预聚合的shuffle操作 44 1.7原则六:使用高性能的算子 45 1.8原则七:广播大变量 46 1.9原则八:使用Kryo优化序列化性能 47 1.10原则九:优化数据结构 48 2资源调优 48 2.1调优概述 48 2.2 Spark作业基本运行原理 49 2.3资源参数调优 50 第六章 Spark架构和工作机制 52 1 Spark架构 52 1.1 Spark架构组件简介 52 1.2 Spark架构图 54 2 Spark工作机制 54 2.1 Spark作业基本概念 54 2.2 Spark程序与作业概念映射 55 2.3 Spark作业运行流程 55 3 Spark工作原理 55 3.1 作业调度简介 55 3.2 Application调度 56 3.3 Job调度 56 3.4 Tasks延时调度 56 第七章 Spark运行原理 57 1 Spark运行基本流程 57 2 Spark在不同集群的运行架构 58 2.1 Spark on Standalone运行过程 59 2.2 Spark on YARN运行过程 60

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值