以 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。