本文转自:《Spark分布式计算执行模型》 作者:火光摇曳
亮点:通过最基础的WordCount程序介绍了RDD,Partition,和如何通过RDD之间的依赖关系生成RDD DAG(Stage),使我们可以非常容易的理解Spark的分布式计算执行模型。
相对Hadoop, Spark在处理需要迭代运算的机器学习训练等任务上有着很大性能提升,同时提供了批处理、实时数据处理、机器学习以及图算法等一站式的服务,因此最近大家一起来学习Spark,特别是MLLib。
Spark中使用了RDD(Resilient Distributed Datasets, 弹性分布式数据集)抽象分布式计算,即使用RDD以及对应的transform/action等操作来执行分布式计算;并且基于RDD之间的依赖关系组成lineage以及checkpoint等机制来保证整个分布式计算的容错性。因此,在学习MLLib的时候,很多时候看到的都是RDD的一些操作,而没有涉及到分布式计算上来,如下面的代码:
// 创建SparkContext对象,作为Spark的Driver应用程序,同Spark集群进行交互
val conf = new SparkConf().setAppName(s"LinearRegression with $params")
val sc = new SparkContext(conf)
// 加载libsvm格式的训练、测试instances数据
val examples = MLUtils.loadLibSVMFile(sc, params.input, multiclass = true).cache()
// 将数据分为训练集和测试集
val splits = examples.randomSplit(Array(0.8, 0.2))
val training = splits(0).cache()
val test = splits(1).cache()
examples.unpersist(blocking = false)
// 设置离线训练算法
val updater = params.regType match {
case NONE => new SimpleUpdater()
case L1 => new L1Updater()
case L2 => new SquaredL2Updater()
}
val algorithm = new LinearRegressionWithSGD()
algorithm.optimizer
.setNumIterations(params.numIterations)
.setStepSize(params.stepSize)
.setUpdater(updater)
.setRegParam(params.regParam)
// 根据设置的训练算法和训练数据集,得到模型
val model = algorithm.run(training)
// 使用模型来预测测试集合上对应的值
val prediction = model.predict(test.map(_.features))
由于Spark使用RDD抽象了分布式计算的操作,因此,上面的代码只是涉及到训练集和测试集以及上面的操作,感觉不到该程序是单机模型训练还是分布式模型训练。因此,有同学提出来问题,这些RDD操作如何进行分布式计算的呢?这涉及Spark分布式计算执行模型。下面从Spark集群部署和应用程序提交、执行模型来展开介绍Spark如何进行分布式计算。
Spark集群是由Cluster Manager和Worker Node组成。当前Spark支持如下三种不同的Cluster Manager:
不管Spark集群是基于什么样资源管理器进行管理,通过spark-submit往Spark集群上提交应用程序(包括有SparkContext对象的应用程序叫做Driver),提交Driver应用程序的时候,需指定Cluster Manager的地址,所需要的CPU核数、内存数目等。具体例子如下:
# Run on a Spark standalone cluster
./bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://207.184.161.138:7077 \
--executor-memory 20G \
--total-executor-cores 100 \
/path/to/examples.jar \
1000
# Run on a YARN cluster
export HADOOP_CONF_DIR=XXX
./bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn-cluster \ # can also be `yarn-client` for client mode
--executor-memory 20G \
--num-executors 50 \
/path/to/examples.jar \
1000
提交完应用程序之后,即Driver通过Cluster Manager去Worker Node上分配所需要的CPU和内存。从而有了分布式计算所需要的物理资源,即拥有分布在Spark集群中Worker Node上的ExecutorBackend进程,该进程等待来自Driver的Task任务。
下面以Standalone集群模式为例说明整个过程,Master对应的时Cluster Manager, Worker对应为Worker Node. 整个操作过程如下:
最后,CoarseGrainedExecutorBackend接收到RegisteredExecutor消息之后,实例化一个Executor等待任务的到来
Spark执行模型分如下三步:
下面以WordCount例子来说明Spark执行模型
下面以map-reduce中经典例子WordCount为例解释Spark执行模型,整个WordCount Job是计算README.md这个文档中各个单词出现的频次,并且将最终结果保存到wordcount_result目录中去
val wordCountResult = sc.textFile("README.md", 4)
.flatMap(line => line.split(" ")).map(word => (word, 1))
.reduceByKey(_ + _, 2)
vordCountResult.saveAsTextFile("wordcount_result")
RDD DAG描述的是各个RDD之间的依赖关系。上面例子从RDD DAG的角度来看如下:
即该RDD DAG主要是包括有MappedRDD->FlatMappedRDD->MappedRDD->ShuffledRDD四个RDD的转换(Transform), 根据Spark实现,RDD的转换操作是不会提交给Spark集群来执行的,因此,上面的操作必须要由Spark的行为(Action)来触发,因此,在最后调用saveAsTextFile这个行为来将整个WordCount Job提交到Spark集群中来执行。(备注:所有的Spark的转换、行为操作可以参考文档Spark Programming Guide)
RDD DAG只是从整体的RDD角度来查看整个Job的执行过程。在RDD DAG逻辑执行方案,需要查看各个RDD中各个Partition的情况,以及各个RDD的Partition的依赖情况来决定如何划分Stage。
根据RDD论文,将RDD的各个Partition的依赖情况划分为Narrow Dependencies和Wide Dependencies:
如图所示,map是Narrow Dependencies, groupByKey是Wide Dependencies。若在Job中存在有Wide Dependencies,就划分为不同的Stage。
具体到WordCount Job,具体的Stage划分如下:
由于flatMap、map等操作对RDD进行转换得到的RDD的partition和parent RDD的partition是Narrow Dependencies关系,因此处于在同一个Stage中,即都在Stage 1中;而reduceByKey这个转换,其对应的是Wide Dependencies关系,因此,需要新建一个Stage出来,即所在为Stage 2,独立于Stage 1。
当Job执行saveAsTextFile这个行为的时候,其依赖于Stage 2中ShuffledRDD,而Stage 2又依赖于Stage 1,因此,需要先执行完Stage 1中所有的Task之后,才执行Stage 2中的所有的Task。当Stage 2中所有的任务执行完之后,整个Job即执行完成。
Spark通过分析各个RDD的依赖关系生成了RDD DAG,然后再通过分析各个RDD中的partition之间的依赖关系来将执行过程进行逻辑划分成不同的Stage。有了这些Stage的依赖关系之后,从最parent stage开始执行,执行完了parent stage的所有的task再执行child stage中的所有的task,直到所有的Stage都执行完成。
针对WordCount这个例子来看,Stage 2依赖Stage 1,因此,先执行Stage 1中的Task。而Stage 1中各个RDD中是有4个partition(见textFile(“README.md”, 4)中的第二个参数来指定RDD中需要划分多少partition,当然对于RDD也可以通过调用repartition和coalesce来改变partition数目),因此,在Stage 1中由Driver应用程序生成4个ShuffleMapTask并提交给之前分配得到的Executor中执行。当Stage 1中4个ShuffledMapTask执行完成之后,再开始执行Stage 2中的2个ResultTask(由于reduceByKey(_ + _, 2)中的第二个参数指定只需要2个reduce,因此,在ShuffledRDD中只有2个partition,因此,也只有对应的2个ResultTask).当Stage 2中的2个ResultTask执行完之后,saveAsTextFile会将ShuffledRDD中的内容落地到文件中中,即保存到wordcount_result目录中去。从而完成了整个WordCount Job的执行任务。
从上面的描述可以看到,Stage 1中会生成4个ShuffleMapTask, 在提交WordCount Job应用程序给Spark集群时候,获取得到的Executor数目大于等于4个,那么该4个ShuffleMapTask可以在这些Executor进行并行运行,从而实现了在不同的Executor进行分布式计算。
最后说明下,RDD的Partition数目决定了执行过程中生成多少个Task,即决定于并行计算的数目,该参数是Spark应用程序中非常重要的参数,Partition设置的越大,并行度越高,在Executor资源有限的情况下,任务之间调度开销会变大,同时若有Wide Dependencies的时候,Shuffle的代价也比较多,因此在实际应用中需要谨慎调整该参数。Spark作者推荐的“比较合理的partition数目”为:
RDD为Spark抽象了分布式计算的操作,即将任务进行分布式计算转成RDD的转换和行为上。为了了解Spark究竟如何进行分布式计算的,本文首先介绍提交Driver应用程序给Spark集群,通过同Cluster Manager和Worker Node进行交互,得到该Driver所需要的Executor资源,然后再由Spark应用程序通过分析RDD DAG依赖关系,以及各个RDD之间partition的依赖关系来生成不同的Stage,再将Stage中的任务,按照RDD的partition个数生成相同数目的Task提交给Executor来执行,从而实现了Task在不同的Executor中进行分布式计算,最终实现整个Driver应用程序的分布式计算。