目录
1:spark为什么需要调优
2.调优的本质
3.什么是RDD
从薯片加工了解rdd
RDD的特征和属性
4.关于内存计算
什么是内存计算:
什么是DAG
stage的划分
stage中的内存计算是怎么样的
5.调优之数据不动代码动(调度系统)
spark调度系统如何工作的
spark天生执行效率高,为什么还需要调优看下面个简单的例子:
demo1
//实现方案1 —— 反例
val extractFields: Seq[Row] => Seq[(String, Int)] = {
(rows: Seq[Row]) => {
var fields = Seq[(String, Int)]()
rows.map(row => {
//此处不该用map用foreach更优,且每次都会创建新的fields,调用开销很大。
fields = fields :+ (row.getString(2), row.getInt(4))
})
fields
}
}
//实现方案1 —— 正例
val extractFields1:Seq[Row]=>Seq[(String,Int)]={
(rows:Seq[Row])=>{
rows.map(row => {
(row.getString(2), row.getInt(4))
})
}
}
}
就简单的减少一个中间变量就可以端到端提升一倍性能
demo2
//实现方案1 —— 反例
def createInstance(factDF: DataFrame, startDate: String, endDate: String): DataFrame = {
val instanceDF = factDF
.filter(col("eventDate") > lit(startDate) && col("eventDate") <= lit(endDate))
.groupBy("dim1", "dim2", "dim3", "event_date")
.agg(sum("value") as "sum_value")
instanceDF
}
pairDF.collect.foreach{
case (startDate: String, endDate: String) =>
val instance = createInstance(factDF, startDate, endDate)
val outPath = s"${rootPath}/endDate=${endDate}/startDate=${startDate}"
instance.write.parquet(outPath)
}
//实现方案2 —— 正例
val instances = factDF
.join(pairDF, factDF("eventDate") > pairDF("startDate") && factDF("eventDate") <= pairDF("endDate"))
.groupBy("dim1", "dim2", "dim3", "eventDate", "startDate", "endDate")
.agg(sum("value") as "sum_value")
两份代码都能实现效果,所以第二份代码优点在哪。
1:第一份代码,是将时间pairdf的数据collect到driver端进行遍历逐条以factdf,时间为形参调用createInstance函数。collect是一个风险点,如果pairdf数据很大drive端可能会成为性能瓶颈。所幸这里pairdf数据量不大可能就几百条那么driver的数据量没问题。
2:collect的时间数据梳百词循环调用createInstance函数,数以百次的对factdf进行遍历自然效率不高
3:第二份代码通过df进行join以不等式关联,首先是分布式的没有driver的性能瓶颈。其次虽然spark中的不等式join是nested loop join。(注:nested loop join是merge join,shuffle join ,hashjoin,broadcast join中性能最差的一种join,且没优化空间)但是这两个df关联只需要扫描一次全量数据。这点就比第一份好。当然还可以采用barodcastjoin,让小数据分发到executor上更加高效。
在databricks的官方博客以及很多技术文章中会提示尽量不要自定义udf实现业务,尽可能使用内置sqlfunction。这是为啥,究其原因是因为spark sql的catalyst optimizer能清楚感知到sql function每一步在做什么,因此有足够的优化空间。反之封装的udf对于spark来说就是一个黑盒子,除了把udf放到闭包里面去,没有太多优化点。究其本质可归纳为以下几点:
1:性能调优是不断动态调整,持续优化的一个过程,优化了一个短板但是可能引发其他短板或者引出新的问题
2:性能调优针对瓶颈事半功倍
3:性能调优的过程收敛于缺陷齐平,没有明显瓶颈
rdd的全称是Resilient Distributed Dataset弹性分布式数据集的缩写,通过源码可以看到注释它有如下五大特性:
* Internally, each RDD is characterized by five main properties:
*
* - A list of partitions
* - A function for computing each split
* - A list of dependencies on other RDDs
* - Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)
* - Optionally, a list of preferred locations to compute each split on (e.g. block locations for
* an HDFS file)
1) A list of partitions 一组分片(Partition),即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。
2) A function for computing each split 一个计算每个分区的函数。Spark中RDD的计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。
3) A list of dependencies on other RDDs RDD之间的依赖关系。RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。
4) Optionally,a Partitoner for key-value RDDs 一个Partitioner,即RDD的分片函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于key-value的RDD,才会有Partitioner,非key-value的RDD的Partitioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量。
5) Optionally,a list of preferred locations to compute each split on 一个列表,存储每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Patition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。
随着spark的发展以及为了降低上手难度,dataframe和dataset发展非常迅速。但是了解rdd依然非常有必要。
rdd作为spark对于分布式数据模型的抽象,是内存计算引擎的基石。DAG和调度系统都衍生子RDD,不论事基于dataframe还是dataset开发spark内部都会转化为rdd上的计算,所以想要更好的定位运行时性能瓶颈就要对RDD有足够的了解。
在上述demo1的案例中为之所以会出现循环调用factDF的计算逻辑是因为习惯了for循环,并且把factDF当成了一个普通的变量,但是忽略了它是一个数据量非常庞大,且跨服务器分布式存在外部的for循环会循环的对大体量数据反复扫描。
注:案例以及相应图片来自极客时间吴磊老师的spark性能调优课程,连接如下http://gk.link/a/10pLS
生产薯片的小作坊为了降低生产成本,使用三条流水线同时生产三种不同尺寸的薯片。每条流水线都可以同时加工三个土豆,每条产品线流程都是一样的。清洗,切片,烘焙,分发,装桶。分发环节用于三种型号的薯片流程如下图所示
通过类比上述故事,原始的土豆到清洗后干净土豆,土豆片,烘焙后的薯片这些不同形态的土豆就像是rdd不同状态下的数据集合的抽象。每一个形态的土豆都依赖于前一种形态的土豆,这就像spark rdd的血缘关系,dependency属性记录的依赖关系,不同缓解的加工过程就是rdd的cpmpute属性。
abstract class RDD[T: ClassTag](
@transient private var _sc: SparkContext,
@transient private var deps: Seq[Dependency[_]]//在rdd里通过一个seq记录了依赖关系
) extends Serializable with Logging {
比如我们可以命名这个土豆集合叫做potatoRDD,每一个土豆都是这个rdd的一个分片,三个土豆对应rdd的partitions属性。此数聚集具有三个分区。不同尺寸的土豆会被划分到不同的数据分片中,这种划分规则就是rdd里的partitioner属性,常见的如hash或者自定义规则。
通过刚才的故事就可以得知,rdd具有partitions,dependency,partitioner,compute四大属性。因为这四大属性,rdd具有了分布式和容错的特征
partitions和partitioner:
在分布式运行环境中,rdd封装的数据物理散落在不同计算节点的内存或者磁盘中,这些散落的数据称之为数据分片。分区规则决定了数据落在哪个地方,partitions对应着所有的数据分片,partitioner对应着划分数据分片的规则如hash分区或者按区间划分。这两个属性承担了spar rdd的横向扩展属性。
dependency和compute:
spark中每一个rdd都是通过计算逻辑从数据源转换而来,dependencies记录生成rdd所需的数据源依赖,conpute封装了从父rdd到子rdd的计算转换逻辑。
所以如果程序出现问题,或者某个节点,故障,宕机造成数据分片丢失,就可以根据该属性追根溯源,通过重新计算重新得到rdd,这两个属性提供了容错能力。rdd也因为这两个属性纵向延展构造了一张有向无环图,也就是术语上的DAG
最后rdd还有一个属性prefferedlocation,能够提升io效率。该属性决定了rdd从哪里取数据,rdd会就近原则取数据以减少网络io。但是在存算分离的场景里该属性不起作用,比如存储在hdfs,计算在远程的k82鸡群里
通过网友的总结可戏称为:
RDD的四大特性:两横两纵一漂移,漂移就是prefered location,选择在哪个RDD上取数据。
1:在开发中经常为了对数据进行复用,避免重复调用rdd的计算过程。有时候会将rdd cache缓存起到内存中。但是错误的cache有时候反而事倍功半
2:shuffle是spark中最容易出现性能瓶颈的地方,在应用时应该尽量避免shuffle,但是对于很多经验不足的能完成业务就很不错了,可能就忽略了shuffle对性能影响。
第一层含义:数据可以缓存在内存中进行高效的访问,cache是性能优化一大利器,但是不是所有数据都需要cache的只有频繁调用的数据及才有必要缓存起来,不然反而是额外的开销,适得其反。
第二层含义:stage内的流水线计算模式
DAG是direct acyclic graph的缩写,翻译过来就是有向无环图。任何图形都包含顶点和边,在spark dage中顶点就是一个个rdd,边就是各个rdd之间的dependency父子关系。在对rdd,dataframe或者dataset进行操作计算,这个过程就会产生新的rdd,子rdd会把dependency属性赋值父rdd上,把compute属性赋值到算子封装的计算逻辑。不断地调用其他算子生成新的rdd,循环往复便构成DAG
DAG是一个个算子构建起来的一个流程图,spark内部需要把这张流程图切分成一个个分布式任务,才能体现出分布式集全集群的优势。
DAG划分为一个个分布式计算任务会经历如下四个阶段:
回溯DAG并划分stages
在stage中创建分布式任务
分布式任务的分发
分布式人物的执行
首先要搞清楚stages是如何划分的,简而言之就是以Actions算子为七点从后往前推回溯DAG,以shuffle操作为边界去划分stages。就比如之前的土豆这个故事,有两个地方涉及到薯片的分发(shuffle),不同尺寸的薯片要分发到不同流水线,不同口味的薯片要分发到不同流水线去操作。所以可以划分为三个stages
spark基于内存计算的模型是踩着hadoop的肩膀发展而来,雏形就是hadoop的mr(mapreduce)模型。mr提供map和reduce两个抽象计算接口,用户能够是自己去实现这两个接口然后在内部实现逻辑的计算。map用于数据逻辑处理,reduce用于封装数据聚合。mr之所以被淘汰是因为map和reduce阶段中建桥梁都是以磁盘为媒介,涉及到大量的读写操,磁盘io拖累了性能。所以在spark的官方文档中介绍spark在内存中计算速度是mr的100倍
简单来看仿佛就是把计算和结果从磁盘搬到了内存,看起来很简单。但是这只是其中一个快的点,如果每个rdd都进行一次cache不见得比mr模式快得了多少。其核心在于流水线计算,spark中的流水线计算模式指的是:在同一个stage中,所有算子不会单独计算会合并为一个函数,计算结果由这个函数一次性输入输出。
通过这个图形可以更直观的感受,就像之前的例子如果把流水线看做内存,每一步都会生成临时数据,就像clean和slice这些临时数据都会缓存到内存里。但是在第二个方式里所有的操作为糅合在一起一次性对土豆进行操作,中间没有任何形态的中间数据。
因此内存计算不仅指的是数据可以缓存在内存中,还会通过融合计算来提升转换效率。那么spark是如何知道一个操作是否会引发shuffle?以及同一个stage中的函数会融合,是如何做到的?
rdd 会有 dep 属性,用来区分是否是 shuffle 生成的 rdd. 而 dep 属性的确定主要是根据子 rdd 是否依赖父 rdd 的某一部分数据,这个就得看他两的分区器(如果 tranf/action 有的话)。如果分区器一致,就不会产生 shuffle。
问题2: 在 task 启动后,会调用 rdd iterator 进行算子链的递归生成,调用 stage 图中最后一个 rdd 的 compute 方法,一般如果是 spark 提供的 rdd,compute 函数大都会继续调用父 rdd 的 iterator 方法,直到到 stage 的根 rdd,一般都是 sourceRdd,比如 hadoopRdd,KakaRdd,就会返回 source iterator。开始返回,如果子rdd 是 map 转换的,就会组成 itr.map(f)。如果再下一个是 filter 转换,就会组成 itr.map(f1).filter(f2),以此类推。
rdd算子来说,在同一个stage内部,spark不会真的去创建、合成一个函数链,而是通过不同rdd算子Iterator的嵌套,在逻辑上,形成一个函数链。这里我们说“捏合”,坦白说不够严谨,不过重点在于表达“内存计算”的第二层含义。tungsten,它是真的真的会把一个stage内部的code,在运行时on the fly地重构出一份新的代码出来。这两者有本质的不同。
在开发中我们有时候会手动调节并行度来提升cpu效率,在spark里该参数是spark.default.parallelism。看起来增加了并行度就能运用闲置cpu线程,但是这个参数不宜过大过大反而会引起调度开销,资源浪费。首先我们先了解如下知下问题
1:spark将dag拆分为不同的stages
2:创建分布式任务Tasks,和任务组TaskSet
3:实时获取集群资源情况
4:按照调度规则决定优先调度哪些任务/组
5:一次将分布式任务分发到各个executor上
调度系统核心组件有三个DagScheduler,TaskScheduler,SchedulerBackend。三个组件都在Driver进程里
1:DagScheduler
DagScheduler主要有两个职责一是将用户DAG拆分为stages,二是在stage内创建计算任务task,该任务粒度里包含用户自定义的各种转换逻辑。然后executor收到任务后就会将其中封装的函数应用到分片数据上进行计算。如果我们给繁忙或者高负荷的executor分配了任务就会造成人任务阻塞,效果就会变差。调度系统如何判断executor的闲忙,这个时候SchedulerBackend就粉墨登场了
2:SchedulerBackend
该类是调度器的封装与抽象,为市面上的各种调度器例如yarn,mesos,standalone提供了对应的实现类。根据参数决定实例化成哪种对象,如--master spark://ip:host就是standalone模式,--master yarn就是yarn模式。
SchedulerBackend里面维护了一个HashMap叫做ExecutorDataMap,key是标记executor的字符串,value就是ExecutorData的结构,里面维护了各个executor的资源情况,如rpc地址,可用cpu核数,满配cpu核数等等。
对内SchedulerBackend对executor进行资源画像,对外SchedulerBackend以WorkOffer为粒度提供计算资源,WorkOffer封装了ExcutorId,主机地址和cpu核数,用来表示一份可供调度的空闲资源。从供需角度来看DagScheduler是需求端,ScheduBackend是供给端。
3:TaskScheduler
前有需求,后有供给。这个时候就需要一个中间桥梁来连接他们了。此时TaskScheduler就扮演了这个角色,基于既定的规则和策略达成双方需求的匹配。
TaskScheduler的调度策略分为两种,一是不同stage之间的调度优先级,二是同stage内不同任务之间的调度优先级。对于两个及以上stage时候且各个stage之间不存在依赖关系,那么他们就会存在竞争关系。TasksScheduler提供了FIFO先进先出,以及FAIR公平调度模式。FIFO比较好理解就是先到先得,FAIR的话取决于用户在fairscheduler.xml中的定义,用户可以自定义调度池每个调度池有不同的优先级。在开发过程中就关联不同的作业进行调度。
在同一个stage里面不同的task任务的调度。当TaskScheduler接收到来自SchedulerBackend的workoffer后TaskScheduler会优先挑选满足本地性级别的的任务进行分配。spark里通用的本地行级别分为process local Dagscheduler在划分stage,创建任务时候就会为每一个任务指定本地行级别,本地行级别中会记录该任务更有意向的计算节点甚至是executor的id,既然人家意愿强烈,TaskScheduler作为中间商肯定要尽量优先满足。 总结:Spark的调度系统主要遵循尽可能让数据不动,让计算任务分发到里数据最近的地方,从而减少网络开销提升效率。 1:RDD缓存将RDD以缓存的形式物化到内存或磁盘。对于一些计算成本和访问频率都比较高的RDD来说,cache有两个好处。一是通过截断DAG可以降低失败重试的计算开销;而是对缓存内容的访问可以减少从头计算的次数,从整体提升了作业端到端性能 2:shuffle中间文件 shuffle分为两个阶段 Map阶段:ShuffleWriter按照Reducer的分区规则将中间数据写入磁盘 Reduce阶段:ShuffleReader从各个节点下载数据分片,根据需求进行聚合计算 Shuffle中间文件实际上就是shufflemap阶段的输出结果,这些结果以文件形式暂存本地磁盘,Reduce阶段要进行聚合,计算统计等操作必须知道哪些文件数据属于自己的那一部分,并且这一部分数据在哪些节点上,处于什么位置。这些信息都在Spark的存储系统进行管理的 3:广播变量,利用存储系统,广播变量可以再Executor进程级别内保存全量数据,这样处于同一executor内所有计算任务都可以以process_local性能级别来共享使用广播变量中携带的全量数据。 存储系统包含了BlockManager,BlockManagerMaster,MemoryStore,DiskStore和DiskBlockManager等。 BlockManager是话事人,在Executor端读者统一管理和协调数据的本地存取和跨节点传输。从两个方面理解如下: 对外,BlockManager与DriverBlockManagerMaster定期通信汇报本地源数据,还有定期拉取全局数据存储状态。不同executor的BlockManager之间也会以Server/Client的模式跨节点推送和拉取数据块。 对内,BlockManager通过组合存储系统内部组件的功能来存取数据的存与取,收与发。 Spark存储系统提供了两只存储抽象对象,MemoryStore和DiskStore。BlockManager利用他们的不同实现类分别管理内存和磁盘的数据存储。其中广播变量的全量数据存储在Executor进程中,因此由Memstore管理,Shuffle中间文件往往会落盘到本地节点,所以这些文件的落盘和访问就要经由DiskStore。 有了这两个抽象类,解决了数据存在哪的问题。但是数以什么形式存储到MemoryStore和DiskStore。Spark存储系统支持两种数据类型:对象值(Object Value)和字节数组(Byte Array)。他们之间可以互相转化,对象值压缩为字节数组的过程称之为序列化,字节数组还原为原始对象的过程称之为反序列化。这两种存在转换和博弈关系,这也就是空间换时间和时间换空间两种形式,该如何取舍。最直接的原则就是想要省空间就是字节数组好,想要访问速度快则对象值更好。当然这种选择都只存在MemoryStore中,DIskStore想要落盘都得序列化。 MemoryStore同时支持对象值存储和字节数组存储两种不同数据形式,并且统一采用MemoryEntry抽象对象对它进行封装。 MemoryEntry有两个实现类:DeserilizedMemoryEntey和SerializedMemoryEntey,分别用于存储对象值和字节数组。DeserilizedMemoryEntey用Array[T]来存储对象值,其中T是对象类型,而SerializedMemoryEntey使用ByteBuffer来存储序列化后的字节。 因为MemoryEntey对对象值和字节数组的统一封装,Memorystore能够借助一种高效的数据结构来统一存储于访问数据库:LinkedHashMap[BlockId,MemoryEntry],即key是blockid,value是MemoryEntry的链式哈希字典,这里的MemoryEntry可以是DeserializedMemoryEntey也可以是SeriliazedMemoryEntry。通过这个字典可以根据blockid快速定位实现数据的快速存取。。 在RDD的语境下我们常说数据分片表示一个分布式数据,从存储系统的语境下我们用block块表示数据存储的基本单元。在逻辑关系上RDD的数据分片与存储系统的blockid一一对应,也就是说RDD数据分片会物化成一个内存或者磁盘的Block。 因此RDD的缓存过程就是将RDD计算数据的迭代器Iterator进行物化的过程。流程如下所示,分为三步走 既然要把数据缓存下来,则就要把RDD的迭代器展开成实实在在的数值才行。因此 第一步:通过调用putIteratorAsValues或putIteratorAsBytes把RDD迭代器展开为数值,然后把这些数据暂存到一个叫做valuesholder的数据结构里。这一步我们把它叫做unroll 第二步:为了节省内存开销,我们可以把ValuesHolder上直接调用toArray或者toByteBuffer操作,把valuesHolder直接转换为MemoryEntry数据结构。这一步操作不涉及内存拷贝,也不产生额外内存开销,因此spark官方把这一步叫做Unroll Memory到StorageMemory的Transfer(转移)。 第三步:这些包含RDD数值的MemoryEntry和与之对应的BlockId,会被一起存入key为blockid value为MemoryEntry的链式哈希字典中。因此LinkedHashMap[BlockId,MemoryEntry]缓存的事关于数据存储的源数据,MemoryEntry才是真正保存RDD数据实体的存储单元。所以占内存的不是LInkedHashMap而是一个又一个的MemoryEntey。 总结就是RDD数据分片,Block和MemoryEntry三者是一一对应的,当所有的RDD都物化为MemoryEntry且生成链式结构时候就完成了数据缓存到内存的过程。 如果内存不够大道容纳整个RDD怎么办?spark会按照lru策略清楚字典中最近未使用的blockid 相比较MemoryStore,DiskStore就相对简单很多,因为他不需要很多中间数据结构才能完成数据存储。DiskStore本质就是数据与磁盘文件的转换。它苹果putBytes和getBytes自己序列写入磁盘以及转换为数据。MemoryStore采用链式哈希字典维护对应关系等元数据,DiskStore并没有亲自维护这些元数据,而是请了DiskBlockManager来帮忙维护。 DiskBlockManager的主要职责就是记录数据块Block与磁盘文件系统中物理文件的对应关系,每个block都对应一个磁盘文件。 DiskBlockManager在初始化时候首先根据配置spark.local.dir在磁盘创建文件目录。然后在该目录下创建子目录,子目录个数由配置项spark.diskstore.subDirectories控制,默认是64。这些目录均用于存储通过DsikStore物化的数据文件如RDD缓存文件,shuffle中间结果文件等。 spark默认采用SortShuffleManager来管理stage之间数据的分发,在ShuffleWrite过程中有三类结果文件:tmp_shuffle_xxx,shfulle_xxx.data和shuffle_xxx.index。Data文件储存分区数据。它是由tmp文件合并而来,而index文件记录data文件内不同分区的偏移地址。Shuffle中间文件就是指data文件和index文件。tmp文件作为暂存盘文件最终会被删除。 在shuffle write的不同阶段,shuffle manager通过BlockManager调用DiskStore的getBytes方法将数据块写入文件中。文件由DiskBlockManager创建,文件么就是putBytes方法中的blockId,这些文件会以“temp_shuffle”或“shuffle”开头,保存在spark.local.dir目录下的子目录里。在shuffle read阶段,shuffle manager再次通过BlockManager调用DiskStore的getBytes方法,读取data文件和index文件,将文件内内容转为数据块,最终这些数据块会通过网络分发到所有reducer进行聚合 spark会区分堆内内存和堆外内存,这里的堆就是jvm的堆。堆外内存是通过java unsafae api直接从操作系统中申请和释放内存。堆内内存的申请和释放是由jvm代劳。比如spark要实例化对象jvm负责从堆内分配空间并创建对象,然后把对象引用返回,最后由spark保存引用,同事记录内存消耗。反过来也一样,spark申请删除对象会同时记录内存,jvm负责把这样的对象标记为待删除,然后通过垃圾回收机制将对象清除真正释放内存。 由上可知spark不直接管理内存,底层还是jvm在操作。这样spark对内存的释放有一定延迟。因此,spark估算内存时候会高估堆内的可用内存。 堆外内存则不同,spark通过调用unsafe的allocateMemory和freeMemory方法直接在操作系统内申请和操作内存。这样的好处就是内存空间的申请和释放更为精确,不再需要jvm的垃圾回收机制避免了频繁扫描带来的性能开销。 spark内存管理分为堆内内存和堆外内存。 堆外内存:spark把堆外内存划分为两块区域,一块用于执行分布式任务,如shuffle,sort,aggregate等这部分内存叫做Execution Memory;一块用于缓存广播变量和RDD等数据这块内存成为Storage Memory。 堆内内存:堆内内存和堆外内存差不多,spark也会划出用于执行和缓存的两份内存空间。不仅如此,spark在堆内还会划分出一片叫做User Memory的内存空间,用于存储开发者子自定义数据结构。 除此之外,spark还会预留一部分内存叫做Reservrd Memory用于存储各种spark内部对象比如BlockManager,DiskBlockManager等。6.空间与时间的互换(存储系统)
Spark的存储系统主要用于以下几个地方:
存储系统基本组件
通过RDD看MemoryStore
通过Shuffle看DiskStore
7.Spark如何高效利用有限的内存空间
内存的管理模式
内存区域的划分