Spark Core之基础知识
一、RDD概叙
- RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象
- 在代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合
二、RDD的五个主要属性
- 多个分区. 分区可以看成是数据集的基本组成单位
- 计算每个切片(分区)的函数
- 与其他 RDD 之间的依赖关系
- 对存储键值对的 RDD, 还有一个可选的分区器
- 存储每个切片优先(preferred location)位置的列表
三、RDD特点
- 弹性
- 分区
- 只读
- 依赖(血缘关系)
- 缓存
- checkpoint(检查点)
四、RDD的编程模型
- transformations:转换,创建一个新的RDD,对新的RDD进行操作更改,transformations操作并不会立即计算他们的结果, 而是记住了这个操作
- action:动作,可以向应用程序返回结果,或向存储系统保存数据,action来获取结果返回给驱动程序的时候这些转换操作才开始计算
五、RDD的创建模式
-
从已有的集合只创建
-
parallelize创建
val rdd1 = sc.parallelize(Array(10,20,30,40,50,60))
-
mkRDD:底层调用parallelize
val rdd1 = sc.makeRDD(Array(10,20,30,40,50,60))
-
上面两种创建还有一个重要的参数是把数据集切分成的分区数
val rdd1 = sc.makeRDD(Array(10,20,30,40,50,60),2)
-
-
从外部的存储创建
-
textFile创建:
var distFile = sc.textFile("words.txt")
-
可以是本地文件系统, HDFS, Cassandra, HVase, Amazon S3 等;Spark 支持 文本文件, SequenceFiles, 和其他所有的 Hadoop InputFormat
-
textFile还可以有第二个参数, 表示分区数. 默认情况下, 每个块对应一个分区.(对 HDFS 来说, 块大小默认是 128M). 可以传递一个大于块数的分区数, 但是不能传递一个比块数小的分区数
-
如果是使用的本地文件系统的路径, 则必须每个节点都要存在这个路径
-
所有基于文件的方法, 都支持目录, 压缩文件, 和通配符(*****)
-
-
从其他RDD转换为新的RDD
- 就是通过 RDD 的各种转换算子来得到新的 RDD
六、RDD 的转换(transformation)
-
value类型:一个RDD的转换
-
map(func):一进一出
-
mapPartitions(func):类似于map(func), 但是是独立在每个分区上运行,所以:Iterator => Iterator
map()和mapPartitions()的区别:
1,map每次处理一条数据
2,mapPartitions每次处理一个分区的数据
3,当内存空间较大的时候建议使用mapPartitions(),以提高处理效率
-
mapPartitionsWithIndex(func):和mapPartitions(func)类似. 但是会给func多提供一个Int值来表示分区的索引. 所以func的类型是:(Int, Iterator) => Iterator
-
flatMap(func):类似于map,但是每一个输入元素可以被映射为 0 或多个输出元素(所以func应该返回一个序列,而不是单一元素 T => TraversableOnce[U])
-
glom():将每一个分区的元素合并成一个数组,形成新的 RDD 类型是RDD[Array[T]]
-
groupBy(func):按照func的返回值进行分组
-
filter(func):过滤,返回一个新的 RDD 是由func的返回值为true的那些元素组成
-
sample(withReplacement, fraction, seed):
-
以指定的随机种子随机抽样出比例为fraction的数据,(抽取到的数量是: size \ fraction). 需要注意的是得到的结果并不能保证准确的比例.
-
withReplacement表示是抽出的数据是否放回,true为有放回的抽样,false为无放回的抽样. 放回表示数据有可能会被重复抽取到, false 则不可能重复抽取到. 如果是false, 则fraction必须是:[0,1], 是 true 则大于等于0就可以了
-
seed用于指定随机数生成器种子。 一般用默认的, 或者传入当前的时间戳
-
样例:
//不放回抽样 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)
-
-
distinct([numTasks])):对 RDD 中元素执行去重操作. 参数表示任务的数量.默认值和分区数保持一致
-
coalesce(numPartitions):缩减分区数到指定的数量,用于大数据集过滤后,提高小数据集的执行效率
-
repartition(numPartitions):据新的分区数, 重新 shuffle 所有的数据, 这个操作总会通过网络,新的分区数相比以前可以多, 也可以少
coalasce和repartition的区别:
1,coalesce重新分区,可以选择是否进行shuffle过程。由参数shuffle: Boolean = false/true决定
2,repartition实际上是调用的coalesce,进行shuffle
3,如果是减少分区, 尽量避免 shuffle
-
sortBy(func,[ascending], [numTasks]):用func先对数据进行处理,按照处理后结果排序,默认为正序
-
pipe(command, [envVars]):管道,针对每个分区,把 RDD 中的每个数据通过管道传递给shell命令或脚本,返回输出的RDD。一个分区执行一次这个命令. 如果只有一个分区, 则执行一次命令。注意:脚本要放在 worker 节点可以访问到的位置
-
创建一个脚本文件pipe.sh
echo "hello" while read line; do echo ">>>"$line done
-
创建只有 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)
-
创建有 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)
-
总结: 每个分区执行一次脚本, 但是每个元素算是标准输入中的一行
-
-
-
双value类型:两个RDD的转换
-
union(otherDataset):求并集. 对源 RDD 和参数 RDD 求并集后返回一个新的 RDD。union和++是等价的
-
subtract (otherDataset):计算差集. 从原 RDD 中减去 原 RDD 和 otherDataset 中的共同的部分
-
intersection(otherDataset):计算交集. 对源 RDD 和参数 RDD 求交集后返回一个新的 RDD
-
cartesian(otherDataset):计算 2 个 RDD 的笛卡尔积. 尽量避免使用
-
zip(otherDataset):拉链操作. 需要注意的是, 在 Spark 中, 两个 RDD 的元素的数量和分区数都必须相同, 否则会抛出异常。本质就是要求的每个分区的元素的数量相同
-
-
key-value类型:其实就是存一个二维的元组。这些特殊操作大多都涉及到 shuffle 操作, 比如: 按照 key 分组(group), 聚集(aggregate)等.
-
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
-
reduceByKey(func, [numTasks]):在一个(K,V)的 RDD 上调用,返回一个(K,V)的 RDD,使用指定的reduce函数,将相同key的value聚合到一起,reduce任务的个数可以通过第二个可选的参数来设置
-
groupByKey():按照key进行分组
reduceByKey和groupByKey的区别:
1,reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]
2,groupByKey:按照key进行分组,直接进行shuffle
3,reduceByKey比groupByKey性能更好,建议使用。但是需要注意是否会影响业务逻辑
-
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]):使用给定的 combine 函数和一个初始化的zero value,对每个key的value进行聚合,第一个函数seqOp用于在一个分区进行合并, 第二个函数combOp用在两个分区间进行合并
-
foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]:aggregateByKey的简化操作,seqop和combop相同
-
combineByKey[C](createCombiner: V => C,mergeValue: (C, V) => C,mergeCombiners: (C, C) => C):针对每个K, 将V进行合并成C, 得到RDD[(K,C)]
- createCombiner: combineByKey会遍历分区中的每个key-value对. 如果第一次碰到这个key, 则调用createCombiner函数,传入value, 得到一个C类型的值.(如果不是第一次碰到这个 key, 则不会调用这个方法)
- mergeValue: 如果不是第一个遇到这个key, 则调用这个函数进行合并操作. 分区内合并
- mergeCombiners 跨分区合并相同的key的值(C). 跨分区合并
注意:reduceByKey、aggregateByKey、foldByKey的底层都是combineByKey
-
sortByKey:在一个(K,V)的 RDD 上调用, K必须实现 Ordered[K] 接口(或者有一个隐式值: Ordering[K]), 返回一个按照key进行排序的(K,V)的 RDD
-
mapValues:针对(K,V)形式的类型只对V进行操作
-
join(otherDataset, [numTasks]):在类型为(K,V)和(K,W)的 RDD 上调用,返回一个相同 key 对应的所有元素对在一起的(K,(V,W))的RDD
-
cogroup(otherDataset, [numTasks]):在类型为(K,V)和(K,W)的 RDD 上调用,返回一个(K,(Iterable,Iterable))类型的 RDD
-
七、RDD的操作(action)
- reduce(func):通过func函数聚集 RDD 中的所有元素,先聚合分区内数据,再聚合分区间数据
- collect:以数组的形式返回 RDD 中的所有元素。所有的数据都会被拉到 driver 端,占用内存, 所以要慎用
- count():返回 RDD 中元素的个数
- take(n):返回 RDD 中前 n 个元素组成的数组。take 的数据也会拉到 driver 端, 应该只对小数据集使用
- first:返回 RDD 中的第一个元素. 类似于take(1)
- takeOrdered(n,[ordering]):返回排序后的前 n 个元素, 默认是升序排列。数据也会拉到 driver 端
- aggregate:aggregate函数将每个分区里面的元素通过seqOp和初始值进行聚合,然后用combine函数将每个分区的结果和初始值(zeroValue)进行combine操作。这个函数最终返回的类型不需要和RDD中元素类型一致。注意:zeroValue 分区内聚合和分区间聚合的时候各会使用一次
- fold:折叠操作,aggregate的简化操作,seqop和combop一样的时候,可以使用fold
- saveAsTextFile(path):将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark 将会调用toString方法,将它装换为文件中的文本
- saveAsSequenceFile(path):将数据集中的元素以 Hadoop sequencefile 的格式保存到指定的目录下,可以使 HDFS 或者其他 Hadoop 支持的文件系统
- saveAsObjectFile(path):用于将 RDD 中的元素序列化成对象,存储到文件中
- countByKey():针对(K,V)类型的 RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。应用: 可以用来查看数据是否倾斜
- foreach(func):针对 RDD 中的每个元素都执行一次func。注意:每个函数是在 Executor 上执行的, 不是在 driver 端执行的
八、RDD中的函数传递
-
我们进行 Spark 进行编程的时候, 初始化工作是在 driver端完成的, 而实际的运行程序是在executor端进行的. 所以就涉及到了进程间的通讯, 数据是需要序列化的
-
Java 的序列化比较重, 能够序列化任何的类. 比较灵活,但是相当的慢, 并且序列化后对象的体积也比较大
-
Spark 出于性能的考虑, 支持另外一种序列化机制: kryo (2.0开始支持). kryo 比较快和简洁.(速度是Serializable的10倍). 想获取更好的性能应该使用 kryo 来序列化
-
需要注意的是: 即使使用 kryo 序列化, 也要继承 Serializable 接口
-
样例:
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 的依赖关系
-
查看 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 的并行度. 也就是有几个分区.
-
查看 RDD 的依赖关系
scala> rdd1.dependencies res28: Seq[org.apache.spark.Dependency[_]] = List(org.apache.spark.OneToOneDependency@70dbde75)
-
窄依赖:
- 父 RDD 的每个分区最多被一个 RDD 的分区使用
- 比如map, filter,coalesce操作
-
宽依赖:
- 父 RDD 的分区被不止一个子 RDD 的分区使用
- 包括: sort, reduceByKey, groupByKey, join, 和调用rePartition函数的任何操作
十、Spark Job的划分
- 针对每个 action, Spark 调度器就创建一个执行图(execution graph)和启动一个 Spark job
- 每个 job 由多个stages 组成, 这些 stages 就是实现最终的 RDD 所需的数据转换的步骤; 一个宽依赖划分一个 stage
- 每个 stage 由多个 tasks 来组成, 这些 tasks 就表示每个并行计算, 并且会在多个执行器上执行
- 在大多数情况下, 每个 stage 的所有 task 在下一个 stage 开启之前必须全部完成
十一、RDD的持久化(存储一个中间结果到磁盘或者内存中,方便下次直接读取)
- 每碰到一个 Action 就会产生一个 job, 每个 job 开始计算的时候,就是每调用一次 collect的时候,总是从这个 job 最开始的 RDD 开始计算,导致这些计算过程重复执行,原因是因为 rdd记录了整个计算过程. 如果计算的过程中出现哪个分区的数据损坏或丢失, 则可以从头开始计算来达到容错的目的。
- 持久化的目的是有些时候不必要从头计算,这时就需要对RDD进行持久化到内存中。
- 可以使用方法persist()或者cache()来持久化一个 RDD
- 可以给persist()来传递存储级别,cache()方法是使用默认存储级别
- rdd2.cache() 等价于 rdd2.persist(StorageLevel.MEMORY_ONLY)
- 在实际使用的时候, 如果想重用数据, 仍然建议调用persist 或 cache
十二、设置检查点(斩断了子RDD和父RDD的依赖关系)
-
Spark 中对于数据的保存除了持久化操作之外,还提供了一种检查点的机制,检查点(本质是通过将RDD写入Disk做检查点)是为了通过 Lineage(血缘)做容错的辅助
-
Lineage 过长会造成容错成本过高,这样就不如在中间阶段做检查点容错,如果之后有节点出现问题而丢失分区,从做检查点的 RDD 开始重做 Lineage,就会减少开销
-
检查点通过将数据写入到 HDFS 文件系统实现了 RDD 的检查点功能
-
为当前 RDD 设置检查点。该函数将会创建一个二进制的文件,并存储到 checkpoint 目录中,该目录是用 SparkContext.setCheckpointDir()设置的。在 checkpoint 的过程中,该RDD 的所有依赖于父 RDD中 的信息将全部被移除
-
对 RDD 进行 checkpoint 操作并不会马上被执行,必须执行 Action 操作才能触发, 在触发的时候需要对这个 RDD 重新计算
-
样例:
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)的区别
- 持久化只是将数据保存在 BlockManager 中,而 RDD 的 Lineage 是不变的。但是checkpoint 执行完后,RDD 已经没有之前所谓的依赖 RDD 了,而只有一个强行为其设置的checkpointRDD,RDD 的 Lineage 改变了
- 持久化的数据丢失可能性更大,磁盘、内存都可能会存在数据丢失的情况。但是 checkpoint 的数据通常是存储在如 HDFS 等容错、高可用的文件系统,数据丢失可能性较小
- 注意: 默认情况下,如果某个 RDD 没有持久化,但是设置了checkpoint,会存在问题. 本来这个 job 都执行结束了,但是由于中间 RDD 没有持久化,checkpoint job 想要将 RDD 的数据写入外部文件系统的话,需要全部重新计算一次,再将计算出来的 RDD 数据 checkpoint到外部文件系统。 所以,建议对 checkpoint()的 RDD 使用持久化, 这样 RDD 只需要计算一次就可以了
十四、Key-Value 类型 RDD 的数据分区器
-
对于只存储 value的 RDD, 不需要分区器,只有存储Key-Value类型的才会需要分区器
-
HashPartitioner:对于给定的key,计算其hashCode,并除以分区的个数取余,如果余数小于 0,则用余数+分区的个数(否则加0),最后返回的值就是这个key所属的分区ID
-
RangePartitioner:
- 作用:将一定范围内的数映射到某一个分区内,尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内
- 实现:
- 第一步:先从整个 RDD 中抽取出样本数据,将样本数据排序,计算出每个分区的最大 key 值,形成一个Array[KEY]类型的数组变量 rangeBounds(边界数组)
- 第二步:判断key在rangeBounds中所处的范围,给出该key值在下一个RDD中的分区id下标;该分区器要求 RDD 中的 KEY 类型必须是可以排序的
-
自定义分区器:
-
继承 org.apache.spark.Partitioner,并且需要实现下面的方法
-
numPartitions:该方法需要返回分区数, 必须要大于0
-
getPartition(key):返回指定键的分区编号(0到numPartitions-1)
-
equals:Java 判断相等性的标准方法。这个方法的实现非常重要,Spark 需要用这个方法来检查你的分区器对象是否和其他分区器实例相同,这样 Spark 才可以判断两个 RDD 的分区方式是否相同
-
hashCode:如果你覆写了equals, 则也应该覆写这个方法
-
样例:
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 } }
-
十五、文件中数据的读取和保存
-
读取Text文件
val rdd1 = sc.textFile("./words.txt") rdd2.saveAsTextFile("hdfs://hadoop101:9000/words_output")
-
读取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)))
-
读写Sequence文件
rdd1.saveAsSequenceFile("hdfs://hadoop101:9000/seqFiles") val rdd1 = sc.sequenceFile[String, Int]("hdfs://hadoop101:9000/seqFiles")
-
读写Object文件
rdd1.saveAsObjectFile("hdfs://hadoop101:9000/obj_file") val rdd1 = sc.objectFile[(String, Int)]("hdfs://hadoop101:9000/obj_file")
-
从HDFS读写文件(略)
-
从Mysql读写文件
-
引入 Mysql 依赖
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.27</version> </dependency>
-
从 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) } }
-
往 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() }) }) } }
-
-
从Hbase读写文件
-
由于 org.apache.hadoop.hbase.mapreduce.TableInputFormat 类的实现,Spark 可以通过Hadoop输入格式访问 HBase
-
这个输入格式会返回键值对数据,其中键的类型为org. apache.hadoop.hbase.io.ImmutableBytesWritable,而值的类型为org.apache.hadoop.hbase.client.Result
-
导入依赖
<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>
-
从 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() } }
-
往 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) } }
-
十六、共享变量问题
-
正常情况下, 传递给 Spark 算子(比如: map, reduce 等)的函数都是在远程的集群节点上执行, 函数中用到的所有变量都是独立的拷贝,这些变量被拷贝到集群上的每个节点上, 都这些变量的更改不会传递回驱动程序
-
支持跨 task 之间共享变量通常是低效的, 但是 Spark 对共享变量也提供了两种支持:累加器、广播变量
-
累加器(Accumulator)
-
内置累加器,longAccumulator关键字
// 得到一个 Long 类型的累加器. 将从 0 开始累加 val acc= sc.longAccumulator
-
自定义累加器,通过继承类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) } }
-
-
广播变量
-
广播变量在每个节点上保存一个只读的变量的缓存, 而不用给每个 task 来传送一个 copy
-
广播变量通过调用SparkContext.broadcast(v)来创建. 广播变量是对v的包装, 通过调用广播变量的 value方法可以访问
val broadcastVar = sc.broadcast(Array(1, 2, 3)) broadcastVar.value
-
变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)
-