每个spark应用都是由一个驱动器程序(driver program)发起集群上的各种并行操作;驱动器程序包含应用的main函数,定义了集群上的分布式数据集及相关应用操作;驱动器通过一个SparkContext对象访问spark.shell启动时会自动创建一个SparkContext对象——一个命名为sr变量;
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
// local:本地模式运行
val conf = new SparkConf().setMaster('local').setAppName("appname")
val sc = new SparkContext(conf)
lines = sc.textFile("README.md")
val lines = SC.parallelize(List(1,2,3,4))
传递一个对象的方法或者字段时,把包含整个对象的引用,可以把需要的字段放到一个局部变量中,避免传递包含该字段的整个对象,这里在python中也是一样的;
所传递的函数或数据需要是可序列化的(实现了 Java 的 Serializable 接口);如果在 Scala 中出现了 NotSerializableException,通常问题是传递了一个不可序列 化的类中的函数或字段。传递局部可序列化变量或顶级对象中的函数始终是安全的。
class SearchFunctions(val query: String) {
def isMatch(s: String): Boolean = {
s.contains(query)
}
def getMatchesFunctionReference(rdd: RDD[String]): RDD[String] = {
// 问题:"isMatch"表示"this.isMatch",因此我们要传递整个"this"
rdd.map(isMatch)
}
def getMatchesFieldReference(rdd: RDD[String]): RDD[String] = {
// 问题: "query"表示"this.query",因此我们要传递整个"this"
rdd.map(x => x.split(query))
}
def getMatchesNoReference(rdd: RDD[String]): RDD[String] = {
// 安全:只把我们需要的字段拿出来放入局部变量中
val query_ = this.query
rdd.map(x => x.split(query_))
}
}
val lines = SC.parallelize(List(1,2,3,4))
val lines1 = lines.map(e => e * e) // 所有元素取平方
val lines = SC.parallelize(List(1,2,3,4))
val lines1 = lines.filter(e => e > 2) // 筛选大于2
val lines = SC.parallelize(List("hello world","scala"))
val lines1 = lines.flatMap(e => e.split(" "))
println(lines1.first()) // hello
RDD不是严格意义上的集合,但支持许多数据学集合操作,比如:
RDD.distinct()
去重:操作开销比较大,需要将所有数据通过网络进行混洗(shufflle),以确保每个元素的唯一性;
RDD.union(RDD1)
合并:会把两个RDD合并成一个RDD,新的RDD不会去重,sql中的union all操作会去重;
RDD.intersection(RDD1)
交集:类似于sql中的inner join操作,返回两个RDD共有的元素。**intersection()**在运行时会去掉所有重复操作,该操作也需要通过网络混洗拿数据来发现共有的元素;
RDD.subtract(RDD1)
差集:返回只存在于RDD不存在于RDD1中的元素,与intersection()一样,也需要进行数据混洗;
RDD.cartesian(RDD1)
笛卡尔积:返回RDD元素和RDD1元素组合(a,b)的所有组合对;
RDD.sample(withReplacement,fraction,[seed])
对RDD采样,以及是否替换
rdd.sample(false,0.5) // 抽样50%
reduce(func)
接受一个函数作为参数,函数要求两个RDD的元素类型一致,并返回同样类型元素;
val sum = rdd.reduce((x,y) = > x + y) // 求和操作
fold()和reduce一样,都要求函数的返回值类型需要和我们所操作的RDD中的元素类型相同;有时候也会需要返回一个不同类型的值,例如求和和计数同时运行,这里可以对RDD进行map操作,把元素转化为该元素和1的二元组,然后再调用reduce操作;
rdd = sc.parallelize(List(1,2,3,4))
rdd.fold(0)((x,y) = > x + y) // 10
collect()
会将整个RDD内容返回驱动程序中,通常在单元测试中使用(RDD数据块比较小),所有数据能放入同一台机器中;不然是在各个worker节点上执行操作;
take(n)
返回RDD的n个元素,并且尝试只访问尽量少的分区,该操作会得到一个不均衡的集合。操作返回元素的顺序可能会与预期不一样;
top(n)
如果定义了顺序,top()从RDD获取前几个元素;使用默认顺序;
foreach(func)
可以对RDD中的每个元素进行操作,而不需要把RDD返回到本地;
count()
统计元素个数
countByValue()
统计各元素在RDD中出现的次数;
rdd = sc.parallelize(List(1,2,2,2))
rdd.countByValue() // {(1,2),(2,3)}
takeOrdered(n)(ordering)
从RDD中按照提供的顺序返回最前面的n个元素
rdd.takeOrdered(2)(myOrdering)
takeSample(withReplacement,num,[seed])
从RDD中返回任意一些元素
rdd.takeSample(false,1) // 返回元素非确定的
aggregate(zeroValue)(seqOp,comOp)
和reduce相似,但是通常返回不同类型的函数;
rdd.aggregate((0,0))
((x, y) => (x._1 + y, x._2 + 1),
(x, y) => (x._1 + y._1, x._2 + y._2))
org.apache.spark.storage.StorageLevel中的持久化级别。有必要,也可以在存储级别末位加上**_2**来把持久化数据保存两份;
级别 | 使用的空间 | CPU时间 | 是否在内存中 | 是否在磁盘上 | 备注 |
---|---|---|---|---|---|
MEMORY_ONLY | 高 | 低 | 是 | 否 | |
MEMORY_ONLY_SER | 低 | 高 | 是 | 否 | |
MEMORY_AND_DISK | 高 | 中等 | 部分 | 部分 | 如果数据内存中放不下,则溢写到磁盘上; |
MEMORY_AND_DISK_SER | 低 | 高 | 部分 | 部分 | 如果数据内存中放不下,则溢写到磁盘上;在内存中存放序列化后的数据; |
DISK_ONLY | 低 | 高 | 否 | 是 |
val res = rdd.map(x => x + 1)
res.persist(StorageLevel.MEMORY_ONLY)
println(res.count()) // 调用行动操作
res.reduce((x,y) => x + y) // 再次调用行动reuce操作,已经缓存,无需再重新进行转化操作map计算;
数据缓存多,spark会自动利用最近最少使用的(LRU)的缓存策略把最老的分区从内存中移除。对于仅把数据存放在内存中的缓存级别,下一次要用到已经被移除的分区时,这些分区需要重新计算;对于使用内存与磁盘的缓存级别的分区来说,被移除的分区都会写入磁盘。
// scala中使用第一个单词作为键创建一个pair RDD
val pairs = lines.map(x => (x.split(" ")(0),x))
val pairs = sc.parallelize(List((1,2),(3,4),(3,6)))
函数名称 | 功用 | 示例 | 结果 |
---|---|---|---|
reduceByKey(func) | 合并具有相同键的值 | rdd.reduceByKey(_ + _) | {(1,2), (3,10)} |
groupByKey(func) | 对具有相同键的值进行分组 | rdd.groupByKey() | {(1,[2]), (3, [4,6])} |
combineByKey(createCombiner, mergeValue, mergeCombines, partitioner) |
使用不同的返回类型合并具有相同键的值 | ||
mapValues(func) 等同于map{case (k,v) => (k,func(v))} |
对pair RDD中的每个值应用func函数,键不改变; | rdd.mapValues(_ + 1) | {(1,2), (1,3), (1,4), (1,5), (3,4), (3,5)} |
flatMapValues(func) | 对每个pair RDD中的值应用一个返回迭代器的函数,然后对返回的每个元素都生成一个对应原键的键值对记录 | rdd.flatMapValues(x => x to 4) | {(1,2), (1,3), (1,4), (3,4)} |
keys() | 返回一个仅包含键的RDD | rdd.keys() | {1, 3,3} |
values() | 返回一个仅包含值的RDD | rdd.values() | {2, 4,6} |
sortByKey() | 返回一个根据键排序的RDD | rdd.sortByKey() | {(1,2), (3,4), (3,6)} |
keyBy() | 返回一个新的RDD,键不变,value为原来的键+value元组 | rdd.keyBy() | {(1,(1,2)), (3,(3,4)), (3,(3,6))} |
val rdd = sc.parallelize(List((1,2),(3,4),(3,6)))
val other = sc.parallelize(List((3,9)))
函数名称 | 功用 | 示例 | 结果 |
---|---|---|---|
subtractByKey | 删掉RDD中键与other RDD中的键相同的元素 | rdd.subtractByKey(other) | {(1, 2)} |
join | 对两个RDD进行内连接 | rdd.join(other) | {(3, (4, 9)), (3,(6, 9))} |
rightOuterJoin | 右连接,以右RDD键为基准进行连接,类似于sql | rdd.rightOuterJoin(other) | {(3,(Some(4),9)), (3,(Some(6),9))} |
leftOuterJoin | 左连接,类似于sql左连接,已左RDD键为基准进行合并 | rdd.leftOuterJoin(other) | {(1,(2,None)), (3,(4,Some(9))), (3,(6,Some(9)))} |
cogroup | 将两个RDD中拥有相同键的数据分组到一起 | rdd.cogroup(other) | {(1,([2],[])), (3,([4, 6],[9]))} |
// 筛选第二个元素长度小于10
pairs.filter{case (k,v) => v.length < 10}
并行度调优
pairs.reduceByKey(_ + _) // 默认并行度
pairs.reduceByKey(_ + _, 10) // 自定义并行度
reduceByKey()
foldByKey(0)
mapValues()
pairs.mapValues((_,1)).
reduceByKey((x,y) => (x._1 + y._1,x._2 + y._2)).
mapValues(x => x._1 / x._1.toFloat).
collectAsMap
combineByKey()
val result = input.combineByKey(
(v) => (v, 1), // createCombiner初始化累加器
(acc: (Int, Int), v) => (acc._1 + v, acc._2 + 1), // mergeValue累加
// mergeCombiners()合并
(acc1: (Int, Int), acc2: (Int, Int)) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
// sum / count = avg
).map{ case (key, value) => (key, value._1 / value._2.toFloat) }
// 结果转化为映射打印出来
result.collectAsMap().map(println(_))
// wordcount计数:两者都可以实现,但具体内部运算过程是不一样的;
val ls = List("python","scala","java","python","scala","scala")
var rdd = sc.parallelize(ls)
var pairRdd = rdd.map((_,1))
val wordCountByReduce = pairRdd.reduceByKey(_ + _)
// 使用groupByKey聚合map计数
val wordCountByGroup = pairRdd.groupByKey().map(x => (x._1,x._2.sum))
// 创建键值对rdd
val rdd = sc.parallelize(Array(("python",3),("hadoop",2),("spark",5),(python,"5)))
// 先分别进行求和和计数,再使用mapValues对value进行求平均操作
rdd.mapValues((_,1)).reduceByKey((x,y) => (x._1 + y._1,x._2 + y._2)).mapValues(x => x._2 / x._1.toFloat).collect
val pairs = sc.parallelize(List((-100,1),(20,2)))
pairs.sortByKey(ascending=false) // 升序参数传入true,默认是true升序排序
// 全局排序
pairs.sortByKey(numPartitions=1)
// 创建键值对rdd
val rdd = sc.parallelize(Array(("a",1),("b",19),("c",9),("d",100),("e",30),("a",200)))
// 按照key降序排序
rdd.reduceByKey(_ + _).sortByKey(false).collect
// res0: Array[(String, Int)] = Array((e,30), (d,100), (c,9), (b,19), (a,201))
// 按照value降序排序
rdd.reduceByKey(_ + _).sortBy(_._2,false).collect
// rdd.reduceByKey(_ + _).sortBy(x => x._2,false).collect 下划线匿名函数
// res1: Array[(String, Int)] = Array((a,201), (d,100), (e,30), (b,19), (c,9))
// 使用sortByKey对value进行排序,通过map函数对换key与value
rdd.map(x => (x._2,x._1)).sortByKey(false).map(x => (x._2,x._1)).foreach(println)
RDD分区的一个分区原则是使得分区的个数尽量等于集群中的CPU核心数目;对于不同的Spark部署模式而言(本地模式、Standa Lone模式、YARN模式、Mesos模式),都可以通过设置spark.default.parallelism这个参数的值配置默认分区数目;
本地模式:默认为本地机器的CPU数目,若设置了loacl[N],则默认为N;Apache Mesos默认分区数为8;Standalone或YARN:在‘集群中所有CPU核心数目总和’和‘2’二者中取较大值作为默认值;
// 手动设置为一个分区
var rdd2 = rdd.repartition(1)
rdd2.partition.size // 查看分区个数:1
var rdd2 = rdd.repartition(4) // 设置分区个数为4
import org.apache.spark.{Partitioner,SparkContext,SparkConf}
// 自定义分区类,继承Partition类
class UserPartition(numParts:Int) extends Partitioner{
// 重写覆盖分区数
override def numPartitions:Int = numParts
// 覆盖分区号获取函数:分区划分规则
override def getPartition(key:Any):Int = {
key.toString.toInt%10 // 对10取余数
}
}
object SetPartition {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
val sc = new SparkContext(conf)
// 模拟设置5个分区
val data = sc.parallelize(1 to 10,5)
// 根据尾号转变为10个分区,分别写到10个文件;
data.map((_,1)).partitionBy(new UserPartition(10)).
map(_._1).saveAsTextFile("file:///usr/local/output")
}
}
// 变量广播
val broadcastVar = sc.broadcast(Array(3,4,5))
// 访问广播变量值
broadcastVar.value(1) // 4
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
object BroadCastValue {
def main(args:Array[String]):Unit = {
val conf = new SparkConf().setAppName("broadcastAppname").setMaster("local[*]")
// 获取SparkContext
val sc = SparkContext(conf)
// 创建广播变量:10
val broadcastVar = sc.broadcast(10)
// 创建一个测试Array
val intArray = Array(1,2,3,4,5,6,7)
// 转化为rdd(并行化)
val rdd = sc.parallelize(intArray)
// 使用map进行转化操作:所有变量乘以广播变量值:10
val res = rdd.map(_ * broadcastVar.value)
// 打印下结果
res.foreach(println)
}
}
val accum = sc.longAccumulator("My Accumulator")
// accum: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 651, name: Some(My Accumulator), value: 0)
sc.parallelize(Array(4,5,6,7,8)).foreach(x => accum.add(x)) // 进行累加操作
accum.value // 返回累加结果值:Long = 30
读取本地文件,文件路径必须以**file:///**开头;转化操作是惰性计算,在未遇到行动操作时不会真实运行;所以假设在行动操作之前,假设文件不存在,也不会报错;
val textFile = sc.textFile("file:///usr/local/spark/mycode/wordcount/word.txt")
// 如果是读取windows本地文件
val textFile = sc.textFile("file:///F:/test.txt")
使用saveAsTextFile+完成路径写入本地
// 实际是生成word.txt文件夹,里面有数据分区块文件
textFile.saveAsTextFile("file:///usr/local/spark/mycode/wordcount/wordback.txt")
切换到这个目录下
cd usr/local/spark/mycode/wordcount/wordback.txt/
ls
会出现
part-0000:只有一个分区为part-0000,如果有第二个分区,会出现part-0001
_SUCCES:表示成功
加载保存的数据
// 会读取这个目录下所有文件
val textFile = sc.textFile("file:///usr/local/spark/mycode/wordcount/wordback.txt")
// 路径localhost:9000是当时hdfs配置信息
val textFile = sc.textFile("hdfs://loaclhost:9000/user/hadoop/word.txt")
val textFile = sc.textFile("/user/hadoop/word.txt")
val textFile = sc.textFile("word.txt")
// 以上三条语句是等价的,都是读取hdfs-hadoop用户目录下的文件;这里的hadoop是用户名称
val textFile = sc.textFile("word.txt")
textFile.saveAsTextFile("writeback.txt") // 文件写入到hdfs中,生成的也是一个目录
// spark安装目录下有这样一个样本文件
val jsonRdd = sc.textFile("file:///usr/local/spark/examples/src/main/resources/people.json")
scala自带有一个JSON库,scala.util.parsing.json.Json,可以解析JSON数据;
JSON.parseFull(jsonString:String)函数,输入json字符串,解析成功返回Some(map:Map[String,Any]),失败返回None;
// 生成rdd;用户+数据日期
val rdd = sc.parallelize(List("123,1","234,4","123,2","123,5","344,5","123,3","234,6","007,5","007,7","007,6","234,9"))
// 转化为键值对rdd
val rdd1 = rdd.map(_.split(",")).map(e => (e(0) toInt,e(1)))
// 找出用户连续活跃最大的天数
val rdd2 = rdd1.groupByKey().mapValues(
iter => {
var num = 1
var max = 1
iter.map(x => x.toInt).toArray.sorted.reduce( // 转化成数组升序排序
(x,y) => {
if(y - x == 1){
num += 1 // 如果y,x差为1,num+1
num
}else{
num=1 // 如果y,x差不为1,另num初始为1,重新累加
num
}
if(max<num){max=num} // 该变量用户记录用户最大连续值
y // reduce这里本身不需要进行其他计算返回,只需要比较前后两个数差值机型,返回y
}
)
max // map返回用户最大连续值;
}
)
// 取出有连续大等于3天活跃记录的用户,返回到driver节点上查看;(量级少)
val result = rdd2.filter(_._2>=3).map(_._1).collect