以 Spark 2.1.0 的 examples 项目自带的 JavaWordCount 为例,从 Java 语言出发,介绍广为人知的 word count,并展示 Spark API 的使用。通过对此例子的介绍,还将帮助读者把调度系统、计算引擎、部署模式等内容串联起来,以对 Spark 核心知识的掌握上升到一个更高的层次。
JavaWordCount 的实现
public final class JavaWordCount {
private static final Pattern SPACE = Pattern.compile(" ");
public static void main(String[] args) throws Exception {
if (args.length < 1) {// 保证必须有参数,此参数代表待读取文件
System.err.println("Usage: JavaWordCount <file>");
System.exit(1);
}
SparkSession spark = SparkSession
.builder() // 创建SparkSession的构建器
.master("local[1]")// 设置部署模式
.appName("JavaWordCount")// 设置JavaWordCount例子的应用名称
.getOrCreate();// 使用构建器构造SparkSession实例
// 获取DataFrameReader,使用DataFrameReader将文本文件转换为DataFrame
JavaRDD<String> lines = spark.read().textFile(args[0]).javaRDD();
// 使用RDD的flatMap 方法对MapPartitionsRDD进行转换
JavaRDD<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
@Override
public Iterator<String> call(String s) {// 转换函数的作用是对每行文本进行单词拆分
return Arrays.asList(SPACE.split(s)).iterator();
}
});
// 使用RDD的mapToPair方法对MapPartitionsRDD进行转换
JavaPairRDD<String, Integer> ones = words.mapToPair(
new PairFunction<String, String, Integer>() {
@Override
public Tuple2<String, Integer> call(String s) { // 转换函数的作用是生成每个单词和1的对偶
return new Tuple2<>(s, 1);
}
});
// 使用RDD的reduceByKey方法对MapPartitionsRDD进行转换
JavaPairRDD<String, Integer> counts = ones.reduceByKey(
new Function2<Integer, Integer, Integer>() {
@Override
// 转换函数的作用是对每个单词的计数值累加
public Integer call(Integer i1, Integer i2) {
return i1 + i2;
}
});
// 使用RDD的collect方法对MapPartitionsRDD及其上游转换进行计算
List<Tuple2<String, Integer>> output = counts.collect();
for (Tuple2<?,?> tuple : output) {
System.out.println(tuple._1() + ": " + tuple._2());
}
spark.stop();// 停止SparkSession
}
}
Job 准备阶段
在 JavaWordCount 中,首先对 SparkSession 和 SparkContext 进行初始化,然后通过 Data-FrameReader 的 textFile 方法生成 DataFrame,最后调用 RDD 的一系列转换 API 对 RDD 进行转换并构造出 DAG。
SparkSession 与 SparkContext 的初始化
DataFrame 的生成
RDD 的转换与 DAG 的构建
Dataset 刚被实例化的时候,其属性 rdd 的语句块并未执行,所以当JavaWordCount 调用 DataSet 的 javaRDD 方法时,会使得 rdd 的语句块执行。根据我们对 rdd 语句块的分析,将会调用 QueryExecution 的 toRdd 方法。QueryExecution 的 toRdd 方法将使用 Spark SQL 的执行计划,首先构造 FileScanRDD,然后调用 RDD 的 mapPartitionsWithIndex 方法创建FileScanRDD 的下游 MapPartitionsRDD,最后调用 RDD 的mapPartitionsWithIndexInternal 方法创建更下游的 MapPartitionsRDD,完成对 RDD 的部分转换和依赖关系的构建,如图所示。
注意
由于 Spark SQL 不属于本书要讲解的内容 , 所以这里只是简单说明 RDD 的转换与 DAG 构建相关的内容 。 早期版本的 Spark 中 , Spark SQL 与 RDD 的转换及 DAG 的构建是互相分离的部分 , 现在的版本已经将部分 RDD 转换及 DAG 构建的工作放在了 Spark SQL 中 。在调用了 DataSet 的 javaRDD 方法(实际调用 RDD 的 toJavaRDD 方法)后,MapPartitionsRDD 被封装为类型为 JavaRDD 的 lines。
由于 JavaRDD 继承了特质 JavaRDDLike,所以 lines 的 flatMap 方法实际是继承自Java-RDDLike 的 flatMap 方法。在调用 JavaRDDLike 的 flatMap 方法时,以FlatMapFunction 的匿名实现类作为函数参数。JavaRDDLike 的 flatMap 方法的实现如下。
def flatMap[U](f: FlatMapFunction[T, U]): JavaRDD[U] = {
def fn: (T) => Iterator[U] = (x: T) => f.call(x).asScala
JavaRDD.fromRDD(rdd.flatMap(fn)(fakeClassTag[U]))(fakeClassTag[U])
}
此时
,
JavaRDD 内部的 rdd 属性实质是最下游的
MapPartitionsRDD
,
调用
Map-Parti-tionsRDD
的父类 RDD 的 flatMap 方法
构造下游的
MapPartitions-RDD
,
此时 RDD 的 DAG 如图所示
。
由于变量 words 的类型依然是 JavaRDD,所以调用 words 的 mapToPair 方法其实也继承自特质 JavaRDDLike,其实现如下。
def mapToPair[K2, V2](f: PairFunction[T, K2, V2]): JavaPairRDD[K2, V2] = {
def cm: ClassTag[(K2, V2)] = implicitly[ClassTag[(K2, V2)]]
new JavaPairRDD(rdd.map[(K2, V2)](f)(cm))(fakeClassTag[K2], fakeClassTag[V2])
}
根据
mapToPair
的实现
,
在调用 JavaRDD 内部的 rdd
(
最下游的
MapPartitionsRDD
)
的父类 RDD 的 map 方法
时
,
以
PairFunction
的匿名实现类作为函数参数
,
构造下游的
MapPartitionsRDD
,
并将此
MapPartitionsRDD
封装为
JavaPairRDD
,
此时 RDD 的 DAG 如图所示
。
def reduceByKey(func: JFunction2[V, V, V]): JavaPairRDD[K, V] = {
fromRDD(reduceByKey(defaultPartitioner(rdd), func))
}
JavaPairRDD
的
reduceByKey
方法首先调用
defaultPartitioner
方法
获取默认的分区计算器
,
然后调用
JavaPairRDD
中重载的另一个
reduceByKey
方法
。
def defaultPartitioner(rdd: RDD[_], others: RDD[_]*): Partitioner = {
val rdds = (Seq(rdd) ++ others)
val hasPartitioner = rdds.filter(_.partitioner.exists(_.numPartitions > 0))
if (hasPartitioner.nonEmpty) {
hasPartitioner.maxBy(_.partitions.length).partitioner.get
} else {
if (rdd.context.conf.contains("spark.default.parallelism")) {
new HashPartitioner(rdd.context.defaultParallelism)
} else {
new HashPartitioner(rdds.map(_.partitions.length).max)
}
}
}
可以看到 defaultPartitioner 方法的执行逻辑如下。
- 如果 RDD 中有分区计算器,且分区计算器计算得到的分区数量大于零,那么从这些分区计算器中挑选分区数量最多的那个分区计算器作为当前 RDD 的分区计算器。
- 如果 RDD 中没有分区计算器,则以 HashPartitioner 作为当前 RDD 的分区计算器。
def reduceByKey(partitioner: Partitioner, func: JFunction2[V, V, V]): JavaPair-RDD[K, V] =
fromRDD(rdd.reduceByKey(partitioner, func))
JavaPairRDD
的
reduceByKey
方法将首先调用
PairRDDFunctions
的
reduceByKey
方法
,
然后再次封装为
JavaPairRDD
。
PairRDDFunctions 的 reduceByKey 方法
def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
}
PairRDDFunctions 的 reduceByKey 方法将调用PairRDDFunctions 的 combineByKeyWithClassTag 方法。
PairRDDFunctions 的 combineByKeyWithClassTag 方法的实现如下。
def combineByKeyWithClassTag[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C,
partitioner: Partitioner,
mapSideCombine: Boolean = true,
serializer: Serializer = null)(implicit ct: ClassTag[C]): RDD[(K, C)] = self.withScope {
require(mergeCombiners != null, "mergeCombiners must be defined") // required as of Spark 0.9.0
if (keyClass.isArray) {
if (mapSideCombine) {
throw new SparkException("Cannot use map-side combining with array keys.")
}
if (partitioner.isInstanceOf[HashPartitioner]) {
throw new SparkException("HashPartitioner cannot partition array keys.")
}
}
val aggregator = new Aggregator[K, V, C](
self.context.clean(createCombiner),
self.context.clean(mergeValue),
self.context.clean(mergeCombiners))
if (self.partitioner == Some(partitioner)) {
self.mapPartitions(iter => {
val context = TaskContext.get()
new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, con-text))
}, preservesPartitioning = true)
} else {
new ShuffledRDD[K, V, C](self, partitioner)
.setSerializer(serializer)
.setAggregator(aggregator)
.setMapSideCombine(mapSideCombine)
}
}
根据 combineByKeyWithClassTag 方法的实现,其执行步骤如下。
- 创建聚合器(Aggregator)。
- 如果当前 RDD 的分区计算器与指定的分区计算器相同,则调用 RDD 的mapParti-tions 方法创建 MapPartitionsRDD。
- 如果当前 RDD 的分区计算器与指定的分区计算器不相同,则创建ShuffledRDD。
在 JavaWordCount 的例子中,调用 combineByKeyWithClassTag 方法将创建ShuffledRDD。需要注意的是,ShuffledRDD 的 deps 为 null,这是因为ShuffledRDD 的依赖 ShuffleDependency 是在其 getDependencies 方法被调用时才创建的。
ShuffleDependency 的 getDependencies 方法
override def getDependencies: Seq[Dependency[_]] = {
val serializer = userSpecifiedSerializer.getOrElse {
val serializerManager = SparkEnv.get.serializerManager
if (mapSideCombine) {
serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[Class-Tag[C]])
} else {
serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[Class-Tag[V]])
}
}
List(new ShuffleDependency(prev, part, serializer, keyOrdering, aggregator, mapSideCombine))
}
此时 RDD 的 DAG 如图所示
。
注意
由于本例使用了 SparkSesion 的 API 来实现 word count , 所以构建了图中的 RDD 及 DAG 。 如果我们采用 SparkContext 的 API 来实现 word count , 生成的 RDD 及 DAG 会有所不同 ( 比如会生成 HadoopRDD ) 。 在 《 深入理解 Spark : 核心思想与源码分析 》 中对使用 SparkContext 的 API 实现的 word count 例子有深入介绍 。Job 的提交与调度
在 JavaWordCount 的最后 , 调用了动作 API——collect , 这将引发对 Job 的提交和调度 。 Job 的提交与调度大致可以分为 Stage 的划分 、 ShuffleMapTask 的调度和执行及 Result-Task 的唤起 、 调度和执行 , 下面将对这些环节进行深入的分析 。Stage 的划分
由于 counts 的类型是 JavaPairRDD,所以调用 counts 的 collect 方法实际继承自父类 AbstractJavaRDDLike,其实现如下。
def collect(): JList[T] =
rdd.collect().toSeq.asJava
上面的代码中主要调用了 ShuffledRDD 的父类 RDD 的 collect 方法。根据 collect 方法的实现,将以图所示的由 RDD 组成的 DAG 为参数,调用 Spark-Context 的 runJob 方法。SparkContext 的 runJob 方法终将调用 run-Job 方法,进而将 RDD 组成的 DAG 提交给DAGScheduler 进行调度。根据对 DAGScheduler 的分析,对 DAG 中的 RDD 进行阶段划分后的 Stage 如图 所示。
图中除了 ShuffledRDD 被划入 ResultStage 外,其余的 RDD 都被划入到了Shuffle-MapStage 中。ShuffleMapStage 的 ID 为 0,ResultStage 的 ID 为 1。