引言
相对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集群部署和应用程序提交
Spark集群部署
Spark集群是由Cluster Manager和Worker Node组成。当前Spark支持如下三种不同的Cluster Manager:
- Standalone – 使用./sbin/start-master.sh和./sbin/start-slave.sh或者./sbin/start-all.sh即可非常容 易地将Spark集群搭建起来,Spark内置的集群资源管理器。当前我们的实验Spark集群是基于Standalone的集群管理模式搭建起来的
- Apache Mesos – 基于Mesos资源管理器来管理机器资源
- Hadoop Yarn – 基于Hadoop 2里面的资源管理器来管理机器资源
提交Spark应用程序
不管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. 整个操作过程如下:
- Driver通过AppClient向Master发送了RegisterApplication消息来注册Application
- Master收到消息之后会发送RegisteredApplication通知Driver注册成功,Driver的接收类还是AppClient
- Master接受到RegisterApplication之后会触发调度过程,在资源足够的情况下会向Woker和Driver分别发送LaunchExecutor、ExecutorAdded消息
- Worker接收到LaunchExecutor消息之后,会执行消息中携带的命令,执行 CoarseGrainedExecutorBackend类(图中仅以它继承的接口ExecutorBackend代替),执行完毕之后会发送 ExecutorStateChanged消息给Master
- Master接收ExecutorStateChanged之后,立即发送ExecutorUpdated消息通知Driver。Driver中的AppClient接收到Master发过来的ExecutorAdded和ExecutorUpdated后进行相应的处理
- 启动之后的CoarseGrainedExecutorBackend会向Driver发送RegisterExecutor消息
- Driver中的SparkDeploySchedulerBackend(具体代码在 CoarseGrainedSchedulerBackend里面)接收到RegisterExecutor消息,回复注册成功的消息 RegisteredExecutor给ExecutorBackend,并且立马准备给它发送任务
最后,CoarseGrainedExecutorBackend接收到RegisteredExecutor消息之后,实例化一个Executor等待任务的到来
Spark执行模型
Spark执行模型分如下三步:
- 创建应用程序计算RDD DAG (Directed acyclic graph,有向无环图)
- 创建RDD DAG逻辑执行方案,即将整个计算过程对应到Stage上
- 根据上面介绍获取到Executor来进行调度并执行各个Stage对应的ShuffleMapResult和ResultTask等任务。必须是执行一个Stage完成之后,才能往下执行接下来的Stage
下面以WordCount例子来说明Spark执行模型
WordCount Job例子
下面以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 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 DAG只是从整体的RDD角度来查看整个Job的执行过程。在RDD DAG逻辑执行方案,需要查看各个RDD中各个Partition的情况,以及各个RDD的Partition的依赖情况来决定如何划分Stage。
根据RDD论文,将RDD的各个Partition的依赖情况划分为Narrow Dependencies和Wide Dependencies:
- Narrow Dependencies – parent RDD中的一个Partition最多被child RDD中的一个Partition所依赖
- Wide Dependencies – parent RDD中的一个Partition被child RDD中的多个Partition所依赖
如图所示,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即执行完成。
RDD Task执行
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数目”为:
- 100-10000
- 最少要有2倍于申请的CPU核数
- 每个Partition对应的Task最少要运行100ms以上
结束语
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应用程序 的分布式计算。