原创/朱季谦

一、案例说明

以前刚开始学习Spark的时候,在练习排序算子sortBy的时候,曾发现一个有趣的现象是,在使用排序算子sortBy后直接打印的话,发现打印的结果是乱序的,并没有出现完整排序。

例如,有一个包含多个(姓名,金额)结构的List数据,将这些数据按照金额降序排序时,代码及打印效果如下:

val money = ss.sparkContext.parallelize(
List(("Alice", 9973),
("Bob", 6084),
("Charlie", 3160),
("David", 8588),
("Emma", 8241),
("Frank", 117),
("Grace", 5217),
("Hannah", 5811),
("Ivy", 4355),
("Jack", 2106))
)
money.sortBy(x =>x._2, false).foreach(println) 打印结果——
(Ivy,4355)
(Grace,5217)
(Jack,2106)
(Frank,117)
(Emma,8241)
(Alice,9973)
(Charlie,3160)
(Bob,6084)
(Hannah,5811)
(David,8588)

可见,这样的执行结果并没有按照金额进行降序排序。但是,如果使用collect或者重新将分区设置为1以及直接将结果进行save保存时,发现结果都是能够按照金额进行降序排序。(注意一点,按照save保存结果,虽然可能生成很多part-00000 ~part-00005的文件,但从part-00000到part-00005,内部数据其实也按照金额进行了降序排序)。

money.sortBy(x =>x._2, false).collect().foreach(println)
或者
money.repartition(1).sortBy(x =>x._2, false).foreach(println)
或者
money.sortBy(x =>x._2, false).saveAsTextFile("result") 最后结果——
(Alice,9973)
(David,8588)
(Emma,8241)
(Bob,6084)
(Hannah,5811)
(Grace,5217)
(Ivy,4355)
(Charlie,3160)
(Jack,2106)
(Frank,117)

二、sortBy源码分析

为何单独通过sortBy后对数据打印,是乱序的,而在sortBy之后通过collect、save或者重分区为1个分区repartition(1),数据就是有序的呢?

带着这个疑问,去看一下sortBy底层源码——

def sortBy[K](
f: (T) => K,
ascending: Boolean = true,
numPartitions: Int = this.partitions.length)
(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T] = withScope {
this.keyBy[K](f)
.sortByKey(ascending, numPartitions)
.values
}

可以看到,核心源码是 this.keyBy[K](f).sortByKey(ascending, numPartitions).values,我会将该源码分成this.keyBy[K](f) , sortByKey(ascending, numPartitions)以及values三部分讲解——

2.1、逐节分析sortBy源码之一:this.keyByK

this.keyBy[K](f)这行代码是基于_.sortBy(x =>x._2, false)传进来的x =>x._2重新生成一个新RDD数据,可以进入到其底层源码看一下——

def keyBy[K](f: T => K): RDD[(K, T)] = withScope {
val cleanedF = sc.clean(f)
map(x => (cleanedF(x), x))
}

若执行的是_.sortBy(x =>x._2, false),那么f: T => K匿名函数就是x =>x._2,因此,keyBy函数的里面代码真面目是这样——

map(x => (sc.clean(x =>x._2), x))

sc.clean(x =>x._2)这个clean相当是对传入的函数做序列化,因为最后会将这个函数得到结果当作排序key分发到不同分区节点做排序,故而涉及到网络传输,因此做序列化后就方便在分布式计算中在不同节点之间传递和执行函数,clean最终底层实现是这行代码SparkEnv.get.closureSerializer.newInstance().serialize(func),感兴趣可以深入研究。

keyBy最终会生成一个新的RDD,至于这个结构是怎样的,通过原先的测试数据调用keyBy打印一下就一目了然——

val money = ss.sparkContext.parallelize(
List(("Alice", 9973),
("Bob", 6084),
("Charlie", 3160),
("David", 8588),
("Emma", 8241),
("Frank", 117),
("Grace", 5217),
("Hannah", 5811),
("Ivy", 4355),
("Jack", 2106))
)
money.keyBy(x =>x._2).foreach(println) 打印结果——
(5217,(Grace,5217))
(5811,(Hannah,5811))
(8588,(David,8588))
(8241,(Emma,8241))
(9973,(Alice,9973))
(3160,(Charlie,3160))
(4355,(Ivy,4355))
(2106,(Jack,2106))
(117,(Frank,117))
(6084,(Bob,6084))

由此可知,原先这样("Alice", 9973)结构的RDD,通过keyBy源码里的map(x => (sc.clean(x =>x._2), x))代码,最终会生成这样结构的数据(x._2,x)也就是,(9973,(Alice,9973)), 就是重新将需要排序的字段金额当作了新RDD的key。

2.2、逐节分析sortBy源码之二:sortByKey

通过 this.keyBy[K](f)得到结构为(x._2,x)的RDD后,可以看到,虽然我们前面调用money.sortBy(x =>x._2, false)来排序,但底层本质还是调用了另一个排序算子sortByKey,它有两个参数,一个是布尔值的ascending,true表示按升序排序,false表示按降序排序,我们这里传进来的是false。另一个参数numPartitions,表示分区数,可以通过定义的rdd.partitions.size知道所在环境分区数。

进入到sortByKey源码里,通过以下函数注释,就可以知道sortByKey函数都做了什么事情——

/**
* Sort the RDD by key, so that each partition contains a sorted range of the elements. Calling
* `collect` or `save` on the resulting RDD will return or output an ordered list of records
* (in the `save` case, they will be written to multiple `part-X` files in the filesystem, in
* order of the keys).
*
*按键对RDD进行排序,以便每个分区包含一个已排序的元素范围。
在结果RDD上调用collect或save将返回或输出一个有序的记录列表
(在save情况下,它们将按照键的顺序写入文件系统中的多个part-X文件)。
*/
// TODO: this currently doesn't work on P other than Tuple2!
def sortByKey(ascending: Boolean = true, numPartitions: Int = self.partitions.length)
: RDD[(K, V)] = self.withScope
{
val part = new RangePartitioner(numPartitions, self, ascending)
new ShuffledRDD[K, V, V](self, part)
.setKeyOrdering(if (ascending) ordering else ordering.reverse)
}

到这里,基于注解就可以知道sortByKey做了什么事情了——

第一步,每个分区按键对RDD进行shuffle洗牌后将相同Key划分到同一个分区,进行排序。

第二步,在调用collect或save后,会对各个已经排序好的各个分区进行合并,最终得到一个完整的排序结果。

这就意味着,若没有调用collect或save将各个分区结果进行汇总返回给master驱动进程话,虽然分区内的数据是排序的,但分区间就不一定是有序的。这时若直接foreach打印,因为打印是并行执行的,即使分区内有序,但并行一块打印就乱七八糟了。

可以写段代码验证一下,各个分区内是不是有序的——

money.sortBy(x => x._2, false).foreachPartition(x => {
val partitionId = TaskContext.get.partitionId
//val index = UUID.randomUUID()
x.foreach(x => {
println("分区号" + partitionId + ": " + x)
})
}) 打印结果——
分区号2: (Ivy,4355)
分区号2: (Charlie,3160)
分区号2: (Jack,2106)
分区号2: (Frank,117) 分区号1: (Bob,6084)
分区号1: (Hannah,5811)
分区号1: (Grace,5217) 分区号0: (Alice,9973)
分区号0: (David,8588)
分区号0: (Emma,8241)

设置环境为3个分区,可见每个分区里的数据已经是降序排序了。

若是只有一个分区的话,该分区里的数据也会变成降序排序,这就是为何money.repartition(1).sortBy(x =>x._2, false).foreach(println)得到的数据也是排序结果。

sortBy主要流程如下,假设运行环境有3个分区,读取的数据去创建一个RDD的时候,会按照默认Hash分区器将数据分到3个分区里。

在调用sortBy后,RDD会通过 this.keyBy[K](f)重新生成一个新的RDD,例如结构如(8588, (David,8588)),接着进行shuffle操作,把RDD数据洗牌打散,将相应范围的key重新分到同一个分区里,意味着,同key值的数据就会分发到了同一个分区,例如下图的(2106, (Jack,2106)),(999, (Alice,999)),(999, (Frank,999)),(999, (Hannah,999))含同一个Key都在一起了。

shuffleRDD中,使用mapPartitions会对每个分区的数据按照key进行相应的升序或者降序排序,得到分区内有序的结果集。

2.3、逐节分析sortBy源码之三:.values

sortBy底层源码里 this.keyBy[K](f).sortByKey(ascending, numPartitions).values,在sortByKey之后,最后调用了.values。源码.values里面是def values: RDD[V] = self.map(_._2),就意味着,排序完成后,只返回x._2的数据,用于排序生成的RDD。类似排序过程中RDD是(5217,(Grace,5217))这样结构,排序后,若只返回x._2,就只返回(Grace,5217)这样结构的RDD即可。



可以看到,shuffleRDD将相应范围的key重新分到同一个分区里,例如,0~100划到分区0,101~200划分到分区1,201~300划分到分区2,这样还有一个好处——当0,1,2分区内部的数据已经有序时,这时从整体按照0,1,2分区全局来看,其实就已经是全局有序了,当然,若要实现全局有序,还需要将其合并返回给驱动程序。

三、合并各个分区的排序,返回全局排序

调用collect或save就是把各个分区结果进行汇总,相当做了一个归并排序操作——

以上,就是关于sortBy核心源码的讲解。

图解Spark排序算子sortBy的核心源码的更多相关文章

  1. Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 上一篇基于哈希表实现HashMap核心源码彻底分析 分析了HashMap的源码,主要分析了扩容机制,如果感兴趣的可以去看看,扩容机制那几行最难懂的 ...

  2. HashMap的结构以及核心源码分析

    摘要 对于Java开发人员来说,能够熟练地掌握java的集合类是必须的,本节想要跟大家共同学习一下JDK1.8中HashMap的底层实现与源码分析.HashMap是开发中使用频率最高的用于映射(键值对 ...

  3. Rank & Sort Loss for Object Detection and Instance Segmentation 论文解读(含核心源码详解)

    第一印象 Rank & Sort Loss for Object Detection and Instance Segmentation 这篇文章算是我读的 detection 文章里面比较难 ...

  4. Java内存管理-掌握类加载器的核心源码和设计模式(六)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 上一篇文章介绍了类加载器分类以及类加载器的双亲委派模型,让我们能够从整体上对类加载器有 ...

  5. 并发编程之 SynchronousQueue 核心源码分析

    前言 SynchronousQueue 是一个普通用户不怎么常用的队列,通常在创建无界线程池(Executors.newCachedThreadPool())的时候使用,也就是那个非常危险的线程池 ^ ...

  6. iOS 开源库系列 Aspects核心源码分析---面向切面编程之疯狂的 Aspects

    Aspects的源码学习,我学到的有几下几点 Objective-C Runtime 理解OC的消息分发机制 KVO中的指针交换技术 Block 在内存中的数据结构 const 的修饰区别 block ...

  7. Backbone事件机制核心源码(仅包含Events、Model模块)

    一.应用场景 为了改善酷版139邮箱的代码结构,引入backbone的事件机制,按照MVC的分层思想搭建酷版云邮局的代码框架.力求在保持酷版轻量级的基础上提高代码的可维护性.   二.遗留问题 1.b ...

  8. 6 手写Java LinkedHashMap 核心源码

    概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...

  9. 3 手写Java HashMap核心源码

    手写Java HashMap核心源码 上一章手写LinkedList核心源码,本章我们来手写Java HashMap的核心源码. 我们来先了解一下HashMap的原理.HashMap 字面意思 has ...

  10. 2 手写Java LinkedList核心源码

    上一章我们手写了ArrayList的核心源码,ArrayList底层是用了一个数组来保存数据,数组保存数据的优点就是查找效率高,但是删除效率特别低,最坏的情况下需要移动所有的元素.在查找需求比较重要的 ...

随机推荐

  1. 现代C++学习指南-方向篇

    C++是一门有着四十年历史的语言,先后经历过四次版本大升级(诞生.98.11.17(20),14算小升级).每次升级都是很多问题和解决方案的取舍.了解这些历史,能更好地帮助我们理清语言的发展脉络.所以 ...

  2. 【TVM教程】 自定义relay算子

    本文地址:https://www.cnblogs.com/wanger-sjtu/p/15046641.html 本文为tvm 教程的翻译版.这部分介绍了如何在tvm中添加新的relay算子,具体的是 ...

  3. 2. 搭建Mybatis

    确认开发环境​ MySQL不同版本的注意事项 1.驱动类driver-class-name MySQL 5版本使用jdbc5驱动,驱动类使用:com.mysql.jdbc.Driver MySQL 8 ...

  4. AWSBackup:备份您的云计算资源及数据

    目录 <AWS Backup:备份您的云计算资源及数据> 背景介绍 随着云计算技术的不断普及,越来越多的企业开始将云计算资源视为其关键业务数据的潜在来源.同时,随着数据价值的不断增加,备份 ...

  5. 一个C#跨平台的机器视觉和机器学习的开源库

    大家都知道OpenCV是一个跨平台的机器视觉和机器学习的开源库,可以运行在Linux.Windows.Android和Mac OS操作系统上,由C++开发. 今天给大家介绍一个用C#对OpenCV封装 ...

  6. 4.1 探索LyScript漏洞挖掘插件

    在第一章中我们介绍了x64dbg这款强大的调试软件,通过该软件逆向工程师们可以手动完成对特定进程的漏洞挖掘及脱壳等操作,虽然x64dbg支持内置Script脚本执行模块,但脚本引擎通常来说是不够强大的 ...

  7. 用git bash 生成 hexo 博客文件,报错node: command not found

    有git bash,也安装了node.js, 为什么在bash里面会说node: command not found? 电脑有段时间不能上网,下载了360系统急救箱,删了很多文件,包括一些exe文件, ...

  8. 【NestJS系列】DI依赖注入与IOC控制反转

    前言 上篇文章我们学习了如何使用nest-cli来快速生成一个NestJS后端项目,当我们打开编辑器查看代码时,会发现整个代码风格有点类似JAVA的spring框架,并且你会发现一些service类在 ...

  9. nginx配置文件内容(1)

    nginx.conf内容 在Nginx服务器的主配置文件nginx.conf中,包括全局配置.I/O事件配置.HTTP配置这三大块内容,配置语句的格式为"关键字  值:"(末尾以分 ...

  10. Tomcat改jar

    Tomcat改jar 插件修改 <!-- <plugin>--> <!-- <groupId>org.apache.maven.plugins</gro ...