RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,
(1)定义:spark的数据来源
(2)分类:
(1)RDD是一个对象
(2)是Spark的第一个类的入口,负责集群的交互
(3)用于连接Spark集群,创建RDD,累加器,广播变量… …
(4)Method : Transformation (转换)
Action (动作)
(5)Spark 的物理模型
Driver 主要是对SparkContext进行配置、初始化以及关闭。初始化SparkContext是为了构建Spark应用程序的运行环境,在初始化SparkContext,要先导入一些Spark的类和隐式转换;在Executor部分运行完毕后,需要将SparkContext关闭。
在Executor中完成数据的处理,数据有以下几种:
1、Scala集合数据(测试)
2、文件系统、DB(SQL、NOSQL)的数据
3、RDD
4、网络
Driver : 主节点 ,主要是对SparkContext进行配置、初始化以及关闭。初始化就是构建运行环境(导入类和隐式转换)
RAM: 随机存取存储器(内存)
如果Spark集群是服务器则Driver是客户端:driver发送tasks给工作节点,worker返回结果给driver
是Spark的配置类,配置项包括:master、appName、Jars、ExecutorEnv等等 (键值对形式存储)
a.利用Rpc协议 ---->心跳机制 传输数据
b.维护Spark的执行环境,有:serializer、RpcEnv、Block Manager、内存管理等
a.高层调度器
b.将Job按照RDD的依赖关系划分成若干个TaskSet(任务集),也称为Stage(阶段,时期);再结合当前缓存情况及数据就近的原则,将Stage提交给TaskScheduler
负责任务调度资源的分配.
负责集群资源的获取和调度。
一组分片(Partition),即数据集的基本组成单位。每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目(设置的最大core数)。
(2)一个计算每个分区的函数。RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分区的数据.Spark中RDD的计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。
(3)RDD之间的依赖关系。RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系(窄依赖(有一对一),宽依赖(多对多))。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。
(4)一个Partitioner,即RDD的分片函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量。
(5)一个列表,存储存取每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
(6)只读:RDD是只读的,要想改变RDD中的数据,只能创建一个新的RDD由一个RDD转换到另一个RDD,通过操作算子(map、filter、union、join、reduceByKey… …)实现,不再像MR那样只能写map和reduce了。
RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分区的数据。如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。
RDD分区
要处理的原始数据很大,会被分成很多个分区,分别保存在不同的节点上。
设置合理的并行度,提高数据处理的性能。
RDD分区的一个分区原则是:
尽可能使得分区的个数,等于集群核心数目;
尽可能使同一 RDD 不同分区内的记录的数量一致;
Spark包含两种数据分区方式:HashPartitioner(哈希分区)和RangePartitioner(范围分区),数据分区方式只作用于
HashPartitioner采用哈希的方式对
Spark引入RangePartitioner的目的是为了解决HashPartitioner所带来的分区倾斜问题,也即分区中包含的数据量不均衡问题。HashPartitioner采用哈希的方式将同一类型的Key分配到同一个Partition中,因此当某一或某几种类型数据量较多时,就会造成若干Partition中包含的数据过大问题,而在Job执行过程中,一个Partition对应一个Task,此时就会使得某几个Task运行过慢。RangePartitioner基于抽样的思想来对数据进行分区。
先Hash数据倾斜时在sample重新采样
HashPartitioner & RangePartitioner
决定了RDD中分区的个数;
RDD中每条数据经过Shuffle过程属于哪个分区;
reduce的个数;
注意:
hashCode%分区数=余数—>决定在那个区
该分区方法保证key相同的数据出现在同一个分区中。
易发生数据倾斜.
定义:
简单的说就是将一定范围内的数映射到某一个分区内
sample采样抽样确定边界
如果是对键操作,则子RDD不再继承父RDD的分区器,但是分区数会继承.
使用map()算子生成的RDD,由于该转换操作理论上可能会改变元素的键(Spark并不会去判断是否真的改变了键),所以不再继承父RDD的分区器
对于两个或多个RDD的操作,生成的新的RDD,其分区方式,取决于父RDD的分区方式。如果两个父RDD都设置过分区方式,则会选择第一个父RDD的分区方式。
rdd.partitioner
rdd.getNumPartitions == rdd.partitions.size
getElement(rdd)
rdd.defsultParallelism
用户可通过partitionBy主动使用分区器,通过partitions参数指定想要分区的数量。
可重新定义partitioner主动使用分区
val rdd1= rdd.partitionBy(new arg.apache.spark.HashPartitioner(2))
coalesce() 和 repartition()
val rdd=sc.makeRDD(arr,9)
rdd.coalesce(num,false)=rdd.coalesce(num)
注意:num的数值必须小于原分区的数量(因为是false)
rdd.repartition(num)==rdd.coalesce(num,true)
注意: num可大可小于原分区的数值
RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD;由一个RDD转换到另一个RDD,可以通过丰富的操作算子(map、filter、union、join、reduceByKey… …)实现,不再像MR那样只能写map和reduce了。
RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护着这种血缘关系(lineage),也称之为依赖。依赖包括两种,一种是窄依赖,RDDs之间分区是一一对应的;另一种是宽依赖,下游RDD的每个分区与上游RDD(也称之为父RDD)的每个分区都有关,是多对多的关系。
每个父RDD的一个Partition最多被子RDD的一个Partition所使用,(1:1 n:1)
一个父RDD的Partition会被多个或所有子RDD的Partition所使用,(1:n n:n)
其一用来解决数据容错;
其二用来划分stage。
action触发job,依照RDD依赖关系切分若干TaskSet即(stage) ; task任务处理的最小单元
划分Stage:
1.从后向前遍历,遇到宽依赖就断开,遇到窄依赖就把当前的RDD加入到Stage中;
2.每个Stage里面的Task的数量是由该Stage中最后一个RDD的Partition数量决定的
3.最后一个Stage里面的任务的类型是ResultTask,前面所有其他Stage里面的任务类型都是ShuffleMapTask
4.代表当前Stage的算子一定是该Stage的最后一个计算步骤
相比于宽依赖,窄依赖对优化很有利,
rdd1.dependencies.size
rdd1.dependencies.collect
rdd1.dependencies(0).rdd.collect.asInstanceOf[Array[Int]]
可以控制存储级别(内存、磁盘等)来进行持久化。如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。
cache实际上是persist的一种简化方式,是一种懒执行的,执行action类算子才会触发,cahce后返回值要赋值给一个变量,下一个job直接基于变量进行操作。
可以指定持久化的级别。最常用的是MEMORY_ONLY,MEMORY_AND_DISK,MEMORY_ONLY_SER,MEMORY_AND_DISK_SER。”_2”表示有副本数。
cache和persist算子后不能立即紧跟action算子。因为rdd.cache().count() 返回的不是持久化的RDD,而是一个数值了。
checkpoint将RDD持久化到磁盘,还可以切断RDD之间的依赖关系。
checkpoint 的执行原理:
(1) 当RDD的job执行完毕后,会从finalRDD从后往前回溯。
(2) 当回溯到某一个RDD调用了checkpoint方法,会对当前的RDD做一个标记。
(3) Spark框架会自动启动一个新的job,重新计算这个RDD的数据,将数据持久化到HDFS上。
优化:
对RDD执行checkpoint之前,最好对这个RDD先执行cache,这样新启动的job只需要将内存中的数据拷贝到HDFS上就可以,省去了重新计算这一步。
spark中控制算子也是懒执行的,需要Action算子触发才能执行,主要是为了对数据进行缓存。可以构建迭代式算法和快速交互式查询.
当存储于内存的数据由于内存不足而被删除时,RDD的缓存的容错机制执行,丢失的数据会被重算。RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition.
为了防止计算失败后从头开始计算造成的大量开销,RDD会checkpoint计算过程的信息,这样作业失败后从checkpoint点重新计算即可,提高效率过对RDD启动Checkpoint机制来实现容错和高可用;
每一个节点都将把计算的分片结果缓存到内存rdd.cache()==rdd.persist(MERMORY_ONLY)
①指定参数缓存在缓存或磁盘
②rdd.unpersist() 把持久化的RDD从缓存中移除;
缓存在hdfs和磁盘,并切断依赖
开头要写sc.setCheckPointDir()
在执行checkpoint()
MEMORY_ONLY | 将RDD 作为反序列化的的对象存储JVM中。如果RDD不能被内存装下,一些分区将不会被缓存,并且在需要的时候被重新计算,按照LRU(Least recently used,最近最少使用)原则替换缓存中的内容。默认的缓存级别 等于cache() |
MEMORY_AND_DISK | 将RDD作为反序列化的的对象存储在JVM中。如果RDD不能被与内存装下,超出的分区将被保存在硬盘上,并且在需要时被读取(优先存取内存) |
MEMORY_ONLY_SER | 将RDD作为序列化的的对象进行存储(每一分区占用一个字节数组)。通常来说,这比将对象反序列化的空间利用率更高,尤其当使用fast serializer,但在读取时会比较占用CPU |
MEMORY_AND_DISK_SER | 与MEMORY_ONLY_SER 相似,但是把超出内存的分区将存储在硬盘上而不是在每次需要的时候重新计算 |
DISK_ONLY | 只将RDD分区存储在硬盘上 |
DISK_ONLY_2 、等带2的 | 与上述的存储级别一样,但是将每一个分区都复制到集群的两个结点上 |
通过集合并行化方式创建RDD,适用于本地测试,做实验
scala中的本地集合 -->Spark RDD
spark-shell --master spark://master:7077
调用 Transformation 类的方法,生成新的 RDD只要调用transformation类的算子,都会生成一个新的RDD。RDD中的数据类型,由传入给算子的函数的返回值类型决定.
注意:action类的算子,不会生成新的 RDD
scala> rdd.collect
res3: Array[Int] = Array(1, 2, 3, 4, 5)
scala> val rdd = sc.parallelize(arr)
scala> val rdd2 = rdd.map(_*100)
scala> rdd2.collect
res4: Array[Int] = Array(100, 200, 300, 400, 500)
使用 rdd.partitions.size 查看分区数量
scala> rdd.partitions.size
res7: Int = 4
scala> rdd2.partitions.size
res8: Int = 4
RDD是惰性求值的,整个转换过程只是记录了转换的轨迹,没有发生真正的计算,只有遇到行动操作时,才会发生真正的计算,开始从血缘关系源头开始,进行物理的转换操作.
这是一个全的RDD算子案例
http://homepage.cs.latrobe.edu.au/zhe/ZhenHeSparkRDDAPIExamples.html
动作 | 含义 |
---|---|
reduce(func) | 通过func函数对RDD中的所有元素处理,传入函数必须满足交换律和结合律(传入两个参数输入返回一个值) |
collect() | 在驱动程序中,以数组的形式返回数据集的所有元素 |
count() | 返回RDD的元素个数 |
first() | 返回RDD的第一个元素(类似于take(1)) |
take(n) | 返回一个由数据集的前n个元素组成的数组 |
takeSample(withReplacement,num, [seed]) | 返回一个数组,该数组由从数据集中随机采样的num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定随机数生成器种子初始值 |
saveAsTextFile(path) | 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它封装为文件中的文本 |
saveAsSequenceFile(path) | 将数据集中的元素以二进制的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。 |
countByKey() | 针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。 |
foreach(func) | 在数据集的每一个元素上,运行函数func进行更新。 |
一般会采用语句rdd.foreach(println)
键值对(K,V),RDD的数据集,RDD的键值对操作
(1)
val list = List(“Hadoop”,“Spark”,“Hive”,“Spark”)
val rdd = sc.parallelize(list)
val pairRDD = rdd.map(word => (word,1))
val pairRDD = rdd.map((,1))
(2)
val lines = sc.textFile(“file://+Path”)
val pairRDD = lines.flatMap(.split(" ")).map((_,1))
用于对每个key对应的多个value进行merge操作,它能够在本地先进行merge操作
每个分区先进行merge操作然后再所有分区进行汇总merge操作,reduceByKey的效率高
groupByKey:
对每个key进行操作,但只生成一个sequence。
所有分区进行汇总再merge操作,集群节点之间的开销很大
转换 | 含义 |
---|---|
map(func) | 返回一个新的RDD,该RDD由每一个输入元素经过func函数转换后组成 |
filter(func) | 返回一个新的RDD,该RDD由经过func函数计算后返回值为true的输入元素组成 |
flatMap(func) | 类似于map,但是每一个输入元素可以被映射为0或多个输出元素(所以func应该返回一个序列,而不是单一元素) |
mapPartitions(func) | 类似于map,但独立地在RDD的每一个分片上运行,因此在类型为T的RDD上运行时,func的函数类型必须是Iterator[T] => Iterator[U] |
mapPartitionsWithIndex(func) | 类似于mapPartitions,但func带有一个整数参数表示分片的索引值,因此在类型为T的RDD上运行时,func的函数类型必须是(Int, Interator[T]) => Iterator[U]sample(withReplacement, fraction, seed)根据fraction指定的比例对数据进行采样,可以选择是否使用随机数进行替换,seed用于指定随机数生成器种子 |
union(otherDataset) | 对源RDD和参数RDD求并集后返回一个新的RDD |
intersection(otherDataset) | 对源RDD和参数RDD求交集后返回一个新的RDD |
distinct([numTasks])) | 对源RDD进行去重后返回一个新的RDD |
groupByKey([numTasks]) | 在一个(K,V)的RDD上调用,返回一个(K, Iterator[V])的RDD |
reduceByKey(func, [numTasks]) | 在一个(K,V)的RDD上调用,返回一个(K,V)的RDD,使用指定的reduce函数,将相同key的值聚合到一起,与groupByKey类似,reduce任务的个数可以通过第二个可选的参数来设置 |
aggregateByKey(zeroValue)(seqOp, combOp, [numTasks]) | 先按分区聚合 再总的聚合 每次要跟初始值交流 例如:aggregateByKey(0)(+,+) 对k/y的RDD进行操作 |
sortByKey([ascending], [numTasks]) | 在一个(K,V)的RDD上调用,K必须实现Ordered接口,返回一个按照key进行排序的(K,V)的RDD |
sortBy(func,[ascending], [numTasks]) | 与sortByKey类似,但是更灵活 第一个参数是根据什么排序 第二个是怎么排序 false倒序 第三个排序后分区数 默认与原RDD一样 |
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 |
cartesian(otherDataset) | 两个RDD的笛卡尔积 的成很多个K/V |
pipe(command, [envVars]) | 调用外部程序 |
coalesce(numPartitions) | 重新分区 第一个参数是要分多少区,第二个参数是否shuffle 默认false 少分区变多分区 true 多分区变少分区 false |
repartition(numPartitions) | 重新分区 必须shuffle 参数是要分多少区 少变多 |
repartitionAndSortWithinPartitions(partitioner) | 重新分区+排序 比先分区再排序效率高 对K/V的RDD进行操作 |
foldByKey(zeroValue)(seqOp) | 该函数用于K/V做折叠,合并处理 ,与aggregate类似 第一个括号的参数应用于每个V值 第二括号函数是聚合例如:+ |
combineByKey | 合并相同的key的值 rdd1.combineByKey(x => x, (a: Int, b: Int) => a + b, (m: Int, n: Int) => m + n) |
partitionBy(partitioner) | 对RDD进行分区 partitioner是分区器 例如new HashPartition(2) |
cache、persist | RDD缓存,可以避免重复计算从而减少时间,区别:cache内部调用了persist算子,cache默认就一个缓存级别MEMORY-ONLY ,而persist则可以选择缓存级别 |
Subtract(rdd) | 返回前rdd元素不在后rdd的rdd |
leftOuterJoin | leftOuterJoin类似于SQL中的左外关联left outer join,返回结果以前面的RDD为主,关联不上的记录为空。只能用于两个RDD之间的关联,如果要多个RDD关联,多关联几次即可。 |
rightOuterJoin | rightOuterJoin类似于SQL中的有外关联right outer join,返回结果以参数中的RDD为主,关联不上的记录为空。只能用于两个RDD之间的关联,如果要多个RDD关联,多关联几次即可 |
subtractByKey | substractByKey和基本转换操作中的subtract类似只不过这里是针对K的,返回在主RDD中出现,并且不在otherRDD中出现的元素 |
触发代码的运行,我们一段spark代码里面至少需要有一个action操作。
动作 | 含义 |
---|---|
reduce(func) | 通过func函数聚集RDD中的所有元素,这个功能必须是课交换且可并联的 |
collect() | 在驱动程序中,以数组的形式返回数据集的所有元素 |
count() | 返回RDD的元素个数 |
first() | 返回RDD的第一个元素(类似于take(1)) |
take(n) | 返回一个由数据集的前n个元素组成的数组 |
takeSample(withReplacement,num, [seed]) | 返回一个数组,该数组由从数据集中随机采样的num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定随机数生成器种子 |
takeOrdered(n, [ordering]) | |
saveAsTextFile(path) | 将数据集的元素以textfile的形式保存到HDFS文件系统或者其他支持的文件系统,对于每个元素,Spark将会调用toString方法,将它装换为文件中的文本 |
saveAsSequenceFile(path) | 将数据集中的元素以Hadoop sequencefile的格式保存到指定的目录下,可以使HDFS或者其他Hadoop支持的文件系统。 |
saveAsObjectFile(path) | |
countByKey() | 针对(K,V)类型的RDD,返回一个(K,Int)的map,表示每一个key对应的元素个数。 |
foreach(func) | 在数据集的每一个元素上,运行函数func进行更新。 |
aggregate | 先对分区进行操作,在总体操作 |
Spark在集群的多个不同节点的多个任务上并行运行一个函数时,把一个变量设为共享变量,就可以在不同节点使用其变量的值. 注意: 共享变量只读
广播变量(broadcast variables)
累加器(accumulators)
Spark的Action操作会跨越多个阶段(stage),对于每个阶段内的所有任务所需要的公共数据,Spark都会自动进行广播
val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar.value
sc.longAccumulator()
sc.doubleAccumulator()
交集: 数据集1.intersection(数据集2)
并集: 数据集1.union(数据集2)
差集: 数据集1.subtract(数据集2)
cogroup(otherDataset, [numTasks])
在类型为(K,V)和(K,W)的RDD上调用,返回一个(K,(Iterable,Iterable))类型的RDD, 也就是相同K的数据集不同为空(k,(,v2))或者(k,(v1,)),相同的如统join类似(k,(v1,v2)) ,numTasks为并发的任务数
// 同时找最大、最小值。缺点:不适合在大数据量情况下运行
val rdd1 = sc.makeRDD((1 to 20).toArray)
rdd1.map((1,_)).groupByKey()
.map(x=>(x._2.min, x._2.max)).collect
// 对于仅有一个元素(基本数据类型)的RDD有更简便的方法
rdd1.stats / rdd1.max / rdd1.min / rdd1.mean
val out=sc.textFile().filter(_.trim.size!=0) //空行排除
.map(_.split(“ ”).trim.toInt) //拆分并去除首尾空格
.sortBy(_,false) //排序方式
.take(num) //取前几
.foreach(println)
val out=sc.textFile()
.filter(_.trim.size!=0)
.map(_.split(“ “).trim)
.map(x=>(x(0),x(1))) //变为元组
.groupByKey() //生成(k,(v1,v2..))
//排序
.map(x=>(x._1,x._2.toList.sorted.reverse.take(3)))
.collect
val rdd1 = sc.textFile("").map(line => {
val value = line.split(" ")
(value(0), value(1).toDouble)
}).groupByKey()
.map(x=>(x._1,x._2.toList.sorted.reverse.take(3)))
.collect
这两个相同只不过map拆开而已
val ar=Array((“spark”,2),(“hadoop”,6),(“hadoop”,4),(“spark”,6))
val rdd=sc.makeRDD(ar)
生成一个RDD,每行包含2个数字,要计算这两列数据的平均值
import scala.util.Random
val random = new Random(100)
val arr =(1 to 100).map(x=>(random.nextInt(1000), random.nextInt(1000))).toArray
val rdd = sc.makeRDD(arr)
使用stat分别计算,算法简单,效率低)
val (firstAvg,secondAvg)=(rdd.map(_._1).stats.mean,
rdd.map(_._2).stats.mean)
如果有一列就得计算一次效率低
(利用了Vector,高效。需要借助外部的数据结构):
Vector数组相加里面相同下标的元素相加(仅有它是可以的),但也要加上标志位好知道个数
import breeze.linalg.Vector
val (sums, count)=
rdd.map(x=>((Vector(x._1.toDouble, x._2.toDouble), 1.0)))
.reduce((x,y) => (x._1+y._1,x._2 + y._2))
val means = sums/count
(自定义方法实现:数组累加):
不使用Vector数组的相加,使用常用数组拉链的方法,
再map映射成一个内部元素相加和的数组
但也要加标志位1看个数
def arrAdd(x: Array[Int], y:Array[Int]): Array[Int] = x.zip(y).map(x=>x._1+x._2)
val (sums, count) =
rdd.map(x=>(Array(x._1, x._2), 1)) //加标志位
.reduce((x,y) => (arrAdd(x._1, y._1), x._2+y._2))
//数组执行方法,数量相加
val means = sums.map(_.toDouble/count)
数据来源
import scala.util.Random
val random = new Random(100)
val arr = (1 to 10000).map(x=>(random.nextInt(100), random.nextInt(100)))
自定义函数继承Ordered接口或实现Orderring方法
case class MyObject(x:Int, y:Int) extends Ordered[MyObject]{
def compare(other:MyObject):Int = {
if (x - other.x !=0) x - other.x else y - other.y }}
调用方法排序
val rdd = sc.makeRDD(arr)
val rdd1 = rdd.map(pair=>(new MyObject(pair._1, pair._2),pair))
val sorted = rdd1.sortByKey(false)
注意:二次排序后的结果写成文件,文件内及文件间的数据是有顺序的。但是文件间的顺序是不保证的!!!
val lines = sc.textFile(" ")
val rdd1 = lines.map(line=>(new MyObject(line.split(" ")(0).toInt, line.split(" ")(1).toInt),line) ) //元组((k0,k1),k)
val sorted = rdd1.sortByKey(false)
val result = sorted.map(sortedLine =>sortedLine._2) //要k1
result.collect().foreach (println)