图解Spark排序算子sortBy的核心源码

原创/朱季谦
一、案例说明
以前刚开始学习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的核心源码的更多相关文章
- Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 上一篇基于哈希表实现HashMap核心源码彻底分析 分析了HashMap的源码,主要分析了扩容机制,如果感兴趣的可以去看看,扩容机制那几行最难懂的 ...
- HashMap的结构以及核心源码分析
摘要 对于Java开发人员来说,能够熟练地掌握java的集合类是必须的,本节想要跟大家共同学习一下JDK1.8中HashMap的底层实现与源码分析.HashMap是开发中使用频率最高的用于映射(键值对 ...
- Rank & Sort Loss for Object Detection and Instance Segmentation 论文解读(含核心源码详解)
第一印象 Rank & Sort Loss for Object Detection and Instance Segmentation 这篇文章算是我读的 detection 文章里面比较难 ...
- Java内存管理-掌握类加载器的核心源码和设计模式(六)
勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 上一篇文章介绍了类加载器分类以及类加载器的双亲委派模型,让我们能够从整体上对类加载器有 ...
- 并发编程之 SynchronousQueue 核心源码分析
前言 SynchronousQueue 是一个普通用户不怎么常用的队列,通常在创建无界线程池(Executors.newCachedThreadPool())的时候使用,也就是那个非常危险的线程池 ^ ...
- iOS 开源库系列 Aspects核心源码分析---面向切面编程之疯狂的 Aspects
Aspects的源码学习,我学到的有几下几点 Objective-C Runtime 理解OC的消息分发机制 KVO中的指针交换技术 Block 在内存中的数据结构 const 的修饰区别 block ...
- Backbone事件机制核心源码(仅包含Events、Model模块)
一.应用场景 为了改善酷版139邮箱的代码结构,引入backbone的事件机制,按照MVC的分层思想搭建酷版云邮局的代码框架.力求在保持酷版轻量级的基础上提高代码的可维护性. 二.遗留问题 1.b ...
- 6 手写Java LinkedHashMap 核心源码
概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...
- 3 手写Java HashMap核心源码
手写Java HashMap核心源码 上一章手写LinkedList核心源码,本章我们来手写Java HashMap的核心源码. 我们来先了解一下HashMap的原理.HashMap 字面意思 has ...
- 2 手写Java LinkedList核心源码
上一章我们手写了ArrayList的核心源码,ArrayList底层是用了一个数组来保存数据,数组保存数据的优点就是查找效率高,但是删除效率特别低,最坏的情况下需要移动所有的元素.在查找需求比较重要的 ...
随机推荐
- 【Python&GIS】矢量数据投影转换(WGS84转地方坐标系)
又是掉头发的一天,今天的任务是将WGS84坐标系的点转成地方坐标系,并判断点是否在某个面内,找了半天的资料什么四参数.七参数啥的太复杂了.这里使用Python的ogr, osr库内置的坐标转 ...
- 有关 python 切片的趣事
哈喽大家好,我是咸鱼 今天来讲一个我在实现 python 列表切片时遇到的趣事 在正式开始之前,我们先来了解一下切片(slice) 切片操作是访问序列(列表.字符串......)中元素的另一种方法,它 ...
- Python modbus_tk 库源码分析
modbus_tk 源代码分析 前言 modbus_tcp 协议是工业项目中常见的一种基于 TCP/IP 协议的设备数据交互协议. 作为 TCP/IP 协议的上层协议,modbus_tcp 协议涉及到 ...
- 在 Istio 服务网格内连接外部 MySQL 数据库
为了方便理解,以 Istio 官方提供的 Bookinfo 应用示例为例,利用 ratings 服务外部 MySQL 数据库. Bookinfo应用的架构图如下: 其中,包含四个单独的微服务: pro ...
- Mysql基础篇(二)之函数和约束
一. 函数 Mysql中的函数主要分为四类:字符串函数.数值函数.日期函数.流程函数 1. 字符串函数 常用函数如下: 函数 功能 CONCAT(S1, S2, ......Sn) 字符串拼接,将S1 ...
- SpringBoot项目从0到1配置logback日志打印
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教. 以下是正文! 一.写文背景 我们在写后端项目的时候 ...
- 「Python实用秘技15」pandas中基于范围条件进行表连接
本文完整示例代码及文件已上传至我的Github仓库https://github.com/CNFeffery/PythonPracticalSkills 这是我的系列文章「Python实用秘技」的第15 ...
- MyBatis使用注解开发(及Sqlsession连接器的本质)
使用注解开发 底层实现机制是反射和,动态代码.反射可以获得这个类的方法属性还可以创建对象,执行方法. 面向接口编程 之前学过,面向对象编程,也学习过接口.但是真正的开发中,很多时候我们会选择面向接口编 ...
- 初识C语言中的typedef、define以及Status
小阿杰最近开始看数据结构啦嘿嘿嘿, 可惜小阿杰C语言功底稀薄,以此篇随笔记录一下我卑微的学习之路/苦涩/苦涩 首先define没啥好说的,在文件开头,定义一个固定不变的值. #define MAXN ...
- Python和PyTorch深入实现线性回归模型:一篇文章全面掌握基础机器学习技术
1. 简介 1.1 线性回归模型概述 线性回归是一种统计学中的预测分析,该方法用于建立两种或两种以上变量间的关系模型.线性回归使用最佳的拟合直线(也称为回归线)在独立(输入)变量和因变量(输出)之间建 ...