图解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底层是用了一个数组来保存数据,数组保存数据的优点就是查找效率高,但是删除效率特别低,最坏的情况下需要移动所有的元素.在查找需求比较重要的 ...
随机推荐
- 驱动开发:内核解析PE结构节表
在笔者上一篇文章<驱动开发:内核解析PE结构导出表>介绍了如何解析内存导出表结构,本章将继续延申实现解析PE结构的PE头,PE节表等数据,总体而言内核中解析PE结构与应用层没什么不同,在上 ...
- 【VS Code 与 Qt6】QCheckBox的图标为什么不会切换?
本篇专门扯一下有关 QCheckBox 组件的一个问题.老周不水字数,直接上程序,你看了就明白. #include <QApplication> #include <QWidget& ...
- 公路堵车概率模型Python(Nagel-Schreckenberg交通流模型)
路面上有N辆车,以不同速度向前行驶,模拟堵车问题.有以下假设: 假设某辆车的当前速度是 v 如果 前方可见范围内没车,下一秒车速提高到 v+1 如果 前方有车,前车的距离为 d ,且 d < v ...
- mysql和neo4j集成多数据源和事务
在微服务大行其道的今天,按理说不应该有多数据源这种问题(嗯,主从库算是一个多数据源的很常见的场景.),但是也没人规定不能这样做. 就算有人规定的,曾经被奉为圭臬的数据库三大范式现在被宽表冲得七零八落, ...
- 前端仿新浪新闻 tabs 选项卡tabs标签页,根据文字多少自适应 tab项宽度
前端仿新浪新闻 tabs 选项卡tabs标签页,根据文字多少自适应 tab项宽度, 下载完整代码请访问uni-app插件市场地址: https://ext.dcloud.net.cn/plugin?i ...
- celery笔记八之数据库操作定时任务
本文首发于公众号:Hunter后端 原文链接:celery笔记八之数据库操作定时任务 前面我们介绍定时任务是在 celery.py 中的 app.conf.beat_schedule 定义,这一篇笔记 ...
- 在linux上启动arthas报“Can not find java process”
发生背景 完整报错信息: [***@localhost ~]$ java -jar arthas-boot.jar [INFO] JAVA_HOME: /usr/lib/jvm/java-1.8.0- ...
- 华为云GaussDB亮相2023可信数据库发展大会,荣获三项评测证书!
摘要:2023可信数据库发展大会上,华为云数据库服务产品部总经理苏光牛围绕华为云GaussDB的产品能力和实践进行了分享 本文分享自华为云社区<华为云GaussDB亮相2023可信数据库发展大会 ...
- Prometheus-3:一文详解promQL
读前提示: 本文字数较多且紧凑,最好预留15min一次性看完,好营养,易吸收. promQL详解 Prometheus提供了内置的数据查询语言PromQL(全称为Prometheus Query La ...
- Python爬虫突破验证码技巧 - 2Captcha
在互联网世界中,验证码作为一种防止机器人访问的工具,是爬虫最常遇到的阻碍.验证码的类型众多,从简单的数字.字母验证码,到复杂的图像识别验证码,再到更为高级的交互式验证码,每一种都有其独特的识别方法和应 ...