Spark是一个分布式内存计算框架。关键词:分布式,内存。因此学习它要学习它的分布式架构以及它实现高速并行计算的机理。
架构
主从结构
所谓分布式就是网络中多个主机上可以同时协同工作。所有的分布式框架,无论用于存储还是计算,分布式结构是前提。大部分分布式框架都是主从式结构。(HDFS是namenode-datanode,YARN是ResourceManager-NodeManager. )
作为“主”,需要解决以下问题:
1)资源由谁分配,资源如何分配
2)任务由谁分配,任务如何分配
3)资源由谁监控,资源如何监控
4)任务由谁监控,任务如何监控
(本文将只回答四个问题的前半部分,后半部分需要更深入的知识。)
Spark中的“主”称为ClusterManager,“从”称为Worker,有所不同的是Spark是计算框架,分配和调度并非它的本职,因此Spark可以与其他资源管理框架一同使用,比如Spark on YARN, Spark on Mesos.这时,ClusterManager就是YARN和Mesos。
因此,粗粒度上,以上四个任务也就交由对应的ClusterManager管理。
任务分配
Spark的任务是计算,一个计算任务从发起到结束需要经历大概以下流程:
发起->启动->运行(->运行失败)->结束->返回结果。
任务的发起者和最终获取结果的对象是Client,是甲方。
Client发起任务请求,交给ClusterManager,ClusterManager是乙方的老大,他指派一个项目经理Driver(类似于YARN中的ApplicationMaster),Driver权利是很大的,Driver从ClusterManager领到任务和可以调用的资源,开始筹备工作。任务就是Client提交过来的代码,资源就是集群中的Worker,Worker可以理解为ClusterManager手下的工程队,平时和老板联络,问有没有活干,老板说来活了,你去跟着项目经理干,所以Worker始终保持和ClusterManager通信,在执行任务时听从Driver调遣,对于一个任务,工程队可以派一个人去干,也可能派多几个人一起干,每个人就是一个Executer,是具体计算任务的最终执行者。
Executor对Worker负责,Worker对Driver负责,Driver对ClusterManager负责,ClusterManager对Client负责。因为Driver是实际负责人,所以Client也会直接和Driver对接联络。
这时就出现一个需求,客户Client觉得每次都要大老远地去和项目经理Driver对接不方便嘛,要不你来我这常驻办公。于是Spark任务执行就有了两种方式:一种是yarn-cluster(远程办公),一种是yarn-client(驻客户办公),仅限Spark On Yarn。Mesos不了解。yarn-client的好处是可以实时了解任务运行状况,但要占用client资源。
Spark为什么快
使用内存计算
所谓内存是相对于其堂兄MapReduce来说的,MapReduce将每一个中间计算结果都存储在磁盘中,然后在需要用到时再去读取,好处是可以记录中间结果,计算失败可以从中间恢复,坏处是有大量的IO操作,计算速度慢。
Spark计算是主要运行到内存中的(注意主要两字,实际Spark也可能将中间数据结果存到磁盘,这个需要使用者按需设置)。
使用内存计算的好处就是快,坏处就是一旦失败没有记录,需要重来,因此Spark的开发者也逐渐改善这个问题,例如支持cache(),persist()和checkpoint,cache()和persisit()是把某个内存对象缓存起来,checkpoint是把内存的状态记录到磁盘。在程序的适当位置执行cache、persist和checkpoint有利于程序性能。
RDD
Spark快速的另一个原因是它独特的数据结构RDD和以此为基础的DAG任务调度。
RDD(Resilient Distributed Dataset):
每个RDD有五个主要属性:
一组分片,partition,即数据集的基本组成单位;
一个计算每个分片的函数;
对parent RDD的依赖,这个以来描述了RDD之间的lineage;
【可选】对于k-v的RDD,一个Partitioner
【可选】一个列表,存储存取每个partition的preferred的位置,对于HDFS文件就是存储每个partition所在块的位置。
所谓Resilient,弹性,就是它不一定真实存在,它就像你在计算一道题,先把计算步骤写下来,但没有具体算术,当有人要求具体的结果的时候,才会按照计算步骤算下来。在Spark里就是一个计算直到需要结果(即一个action)时,才会计算所有前序需要的操作,才会真正产生内存数据。而要进行什么计算就存在上面说的RDD的函数属性中。
这个函数,我们可以称为算子,或者操作,或者变换。它们分为三种:transformation,action和controller。上树的cache和persist是controller算子,用于数据持久化。除此之外的两个都是关于计算的,action就是明确要结果的计算步骤,transformation就是可以暂时写在纸上不需要马上计算的计算步骤。
这种弹性带来一系列的好处,比如:
计算的时间集中:直到action的时候才集中计算之前的transformation,中间变量在内存的停留时间短,从总体上来说,节约了系统资源。
计算优化:例如一个计算是将一个RDD +1 ,-1 ,再+1, 在-1 ,其他的计算框架会计算四步,但在Spark通过分析优化,一步也不用算了,直接输出。
另外需要说的一点是RDD是不可变量,即便是+1这样的简单操作也是要新建一个RDD,不会在原RDD上修改值,这是因为可变量会增加框架实现的复杂性。Spark通过不可变量的设定简化框架,也方便DAG,Lineage机制的实现。
DAG
我们知道有向无环图DAG是一种拓扑结构,对于实际工作生活中相互依赖,具有先后顺序的任务可以建立有向无环图来模拟。有没有可能无向?有环?那日子还怎么过。
计算任务也可以抽象成有向无环图,从上述RDD的数据结构可以知道,一个RDD对象可以理解记录的是图中的一个节点,它记录着它依赖的前置节点有哪些(parent)以及要执行的计算方法。根据计算流程中创建的所有RDD之间的依赖关系,可以创建出一张完整的DAG。于是就可以使用图论方法,将它合理分割,分成可以独立计算的几个子计算任务(Spark里称为stage),这样就可以实现分布式计算了。至于具体怎么分割,放在之后详细学习RDD时再说。
负责DAG切分的是一个叫DAGScheduler的组件,显然它应该放在Driver上。和DAGScheduler一样在Driver上负责任务调度的还有TaskScheduler,是负责将stage再次细分的,暂不细表。
Lineage
Lineage意思是血统,即RDD构建起的继承关系。通过血缘可以实现高效得容错。即当计算运行到某一个RDD失败了,可以根据Lineage回溯找到前辈RDD,重新这部分计算。这种容错机制在分布式计算中至关重要,保证了整体的速度。