基于Spark的大数据访存行为跨层分析工具
2020-06-23许丹亚张伟功2
许丹亚 王 晶,2 王 利 张伟功2,
1(首都师范大学信息工程学院 北京 100048)2(高可靠嵌入式技术北京市工程研究中心(首都师范大学) 北京 100048)3(北京成像理论与技术高精尖创新中心(首都师范大学) 北京 100048)
大数据时代的到来,为信息处理技术带来了新的挑战.大数据时代的信息具有数据量大、数据类型多、增长速度快、价值密度低等特点[1].MapReduce有效地解决了海量数据处理的扩展性问题[2],然而MapReduce在处理过程中,将数据和中间结果以文件的形式存放到磁盘上,例如Map阶段的计算结果,频繁地访问磁盘,导致磁盘的压力成为计算中的性能瓶颈[3].对此,Spark以内存计算的方式进行了改进,即数据的存储和处理都位于内存之中.目前Spark已经得到了工业界的广泛应用,腾讯、百度、淘宝、亚马逊、网易等企业都在使用Spark构建自己的大数据业务[4].内存计算显著地提高了程序的运行速度,但同时也对内存系统造成了巨大的压力,使得内存系统的性能成为影响数据处理速度的瓶颈[5-6].
Fig.1 Spark memory system semantics and syntax conversion图1 Spark系统中各层内存对象的语义及各层语义转换流程
传统的计算机体系结构和存储系统设计已经无法适应处理大数据的要求.内存计算方式的Spark采用了“统一内存管理”模型,将存储系统化分为Spark层、JVM层、操作系统(OS)层和硬件层,完成数据的传输、元数据的管理,以及内存和磁盘等硬件的数据管理.层次化的存储系统给设计和维护带来了便利.然而,上层应用程序对系统内存、磁盘、CPU、网络等资源敏感,需要底层优化以达到性能最优.因此理解底层系统行为与上层应用特征之间的关系,对于优化系统性能十分有意义,而此时层次化设计中底层对上层透明的工作方式给系统优化带来了困难.例如,位于硬件和软件交界面的操作系统从应用层角度常被作为黑盒对待,丰富灵活的系统配置参数对普通用户难以准确理解和使用,进而导致其对上层应用程序的影响不得而知,陷入“不知道”与“不使用”的循环.因此,跨层的理解Spark系统的行为是十分重要且必要的.然而,计算机体系结构中“高内聚、低耦合”的分层设计,使得Spark系统中跨层的分析存在4个挑战:
1) 在Spark系统中,Spark应用层、JVM层和OS层每一层都有自己的内存管理机制和本层特有的语义.从图1 Spark系统中各层内存对象的语义及各层语义转换流程可以看出,每层的语义对外暴露较少,因此如何在不破坏其原有工作方式的同时获取其内存对象是Spark分析的挑战之一.
2) 现有的性能分析工具大多只工作在某一层,尽管我们可以在每一层利用各种工具来获取程序行为、抓取性能指标,但无法将每一层的结果相互对应起来进行分析,Spark语义与底层动作脱离,在OS层分析Spark性能时如何感知Spark语义是Spark分析的挑战之二.
3) OS层性能表现与上层应用程序特征并不是清晰可见的,我们在OS层观测到的性能指标,除了体现Spark应用程序特征之外,还包含其他因素的影响,如Spark框架、JVM自身运行引发的开销,如何排除逻辑上不相关的参数通过其他机制产生的间接影响是Spark分析和实验的挑战之三.
4) 物理存储对上层应用的性能存在影响.当前数据中心里的商用服务器内存通常较大,很多已达到128 GB以上.在这类场景下,为了提高服务器内存访问的吞吐率,通常采用多内存条结构,即多个内存条均匀地分布在多个CPU内存控制器上,尽可能地利用所有CPU的内存控制器.但在对上层类似Spark自己管理内存的应用,通常仅利用虚拟地址进行内存管理,无法感知底层的内存分布,容易造成NUMA架构下的内存访问不均衡(CPU访问非本地内存会有较大的时延;多CPU同时访问同一个内存通道也有性能瓶颈),从而影响性能.并且上层应用对虚拟地址以外的内存地址无法感知,也没有现有的工具建立Spark存储对象的关联关系,因此如何对Spark物理页进行追踪是性能分析工具需要解决的挑战之四.
为了解决上述问题,本文设计了跨层的Spark内存追踪工具SMTT(Spark memory tracing toolkit).SMTT垂直打通了Spark层、JVM层和OS层,将上层应用程序的语义与底层物理内存信息建立了联系,从而有助于对应用程序的访存行为进行跟踪和分析.
在Spark运行期间,SMTT会在Spark应用层抓取到Spark应用程序对数据的访问序列,并记录每层的使用信息.SMTT针对Spark系统中特有的执行内存和存储内存分别设计了不同的追踪方案.对于执行内存,我们将RDD(resilient distributed datasets)与其对应的虚拟内存地址联系起来.对于存储内存,我们逐层剥去封装数据的外层数据结构,获取存放数据的真正虚拟地址,并将虚拟地址与数据在Spark应用层的语义对应起来,从而有效地分析Spark的分布式执行和RDD的存储访问等重要信息.
Spark一直处于快速发展之中,伴随着频繁的版本迭代,很多重要的特性也发生了变化.然而,目前为止,本文所关注的“统一内存管理”和“迭代计算”并未发生根本性的改变,因此,本文的研究方法对于较新版本的Spark仍有借鉴意义.SMTT能够提供内存对象的各个层次对应关系,为系统优化提供支持,例如通过SMTT得到的物理页存储关系,能够为基于NUMA感知的内存调度等优化策略提供指导,实现对分布式内存结构的高效利用,为Spark的性能分析和优化奠定了基础.
1 相关工作
针对Spark和Hadoop的对比分析无论是从日志文件角度[7]、从页排序和分类算法的角度,还是词频统计算法方面[8-9],结果都表明面向云负载,内存计算方式的Spark优于Hadoop.随着Spark的广泛应用,其性能优化问题成为研究人员普遍关注的热点.Spark性能可以通过配置Spark参数、重构Spark应用代码、优化JVM等手段,从调度与分区、内存存储、网络传输、序列化与压缩等方面优化[4,10].然而,无论哪种优化方案,都需要首先对Spark行为特征进行分析.如基于Spark的弹性分布式数据集设计首先分析了Spark屏幕终端日志在迭代算法和迭代数据挖掘应用场景下的效率问题,观察到内存数据管理和存放策略对性能的影响,从而提出了基于共享内存和粗粒度交易的容错系统设计[11].将Spark应用程序运行的历史数据建立成数据库,根据对历史数据的分析来自动化配置Spark参数,能够大大降低调优的门槛,同时也使调优的效果变得更为可信[12].Spark SQL分析了应用层的API接口、基本操作行为和数据模型,并基于分析提出了新的Apach Spark集成模型[13].江涛等人分析了典型算法Spark实现的执行时间、每秒钟磁盘读写次数、内存带宽、内存页访问频率等[6].现有性能分析工作对于我们认识Spark负载的性能特征具有很大的参考价值,但这些研究大多在系统单一层面开展,缺乏全面、有效和系统的分析方案以及分析工具.
当前,云服务器面临着严峻的存储墙问题,对访存行为的分析和追踪是优化存储系统的重要基础.现有的内存追踪工具分别从操作系统层、应用层和硬件层展开分析.
1) 操作系统层分析工具.perf[14]是广泛使用的系统性能分析工具,通过它应用程序可以利用PMU,tracepoint和内核中的特殊计数器来进行性能统计,但perf只能看到操作系统层面的行为,无法感知上层逻辑.Simics[15]和QEMU[16]等软件模拟器能够生成访存踪迹并评测存储系统性能,但现有模拟器大多只能支持桌面应用,难以模拟Spark和Hadoop这类复杂系统的行为.HMTT可以跟踪到物理内存的访问轨迹和对应的程序语义[17-18].然而这里的“程序”仅限于OS层面的程序,无法感知Spark层的语义,如RDD、分区等,因此不适用于Spark的应用场景.
2) 应用层分析工具.Pin[19]和Valgrind[20]等二进制插桩工具能够分析应用程序的虚拟地址,但难以对内核插桩,无法分析内核层面的信息.Spark UI[21]是针对Spark的应用层分析工具,可以跟踪Spark的执行情况,同样无法看到其引起的底层动作.由于Spark语义与底层动作的关联缺失,无法完整地看到一个Spark程序执行的来龙去脉,更无法解释什么样的程序会引起什么样的系统性能变化.应用层的分析工具都把操作系统看成了黑盒、忽略了它的可配置性,导致数以千计的内核参数并没有发挥其该有的作用.
3) 硬件层分析工具.Awan等人通过双端口服务器的硬件计数器分析了批处理和流处理负载的体系结构特征,发现了同时多线程、Cache预取和NUMA结构对于性能的影响[22].这类基于硬件计数器的方法只能看到系统中的一部分硬件事件,无法对所有访存行为进行追踪.
Fig.2 Framework of SMTT图2 SMTT整体架构
针对上述单一层面分析工具的问题,本文设计了打通Spark,JVM,OS三层的内存跟踪工具SMTT,以满足跨层内存跟踪的需要.SMTT从OS层对Spark应用程序进行性能分析,在不丢失Spark语义的情况下,分析Spark负载的行为特征与底层系统性能之间的关系,为Spark应用的调优提供依据.此外,SMTT在执行节点层对访存进行追踪,能够为Spark Streaming[23]和Spark SQL[12]等上层框架提供细粒度的内存追踪结果.
2 SMTT整体设计
内存计算是Spark的重要特征,即数据的存储和处理都在内存之中.与JVM层一样,Spark层也有着自己的内存管理机制.Spark层采用统一内存管理模型,将所管理的内存分为保留内存、用户内存和Spark内存这3部分[24].保留内存由系统保留,对Spark应用程序是透明的,其中存放了许多Spark内部对象.用户内存由Spark应用程序使用,比如:应用程序如果在内存中保留了大量用户数据,那么将从这一部分中分配空间用于存储.Spark在集群层次的主要工作是进行任务调度,其具体的对象级内存管理过程属于节点级,主要体现在executor节点上的内存管理.因此,本文针对目标为Spark任务执行节点内存状态进行追踪,对应Spark整体集群的内存管理状态,可以通过对单节点采集信息的聚合得到.Spark内存由Spark层进行管理,按照Spark的工作方式来存储应用程序数据.Spark内存又分为存储内存和执行内存2部分.其中,存储内存用于存储持久化的RDD数据、广播变量等;执行内存用于存储运行期间的中间数据,如洗牌过程中在映射端的缓存.
Spark所管理的内存可以来自JVM堆,也可以直接从操作系统获得.对Spark应用进行性能分析时,Spark层、JVM层和OS层都有追踪工具可以利用,但无法将各层追踪到的结果建立联系.例如,Spark层的评测系统可以看到被存储的RDD以及它们的大小,但无法知道它们位于内存何处;JVM工具可以看到当前堆的状态,却无法知道堆内具体存储什么数据对象,导致我们无法讨论上层应用程序行为与系统性能的关系;OS层无法跨越JVM层将性能指标与Spark应用程序行为建立联系,导致底层观测丢失了Spark语义.尽管JVM层也有对象的创建、回收和复用机制,但Spark的数据有很大一部分不来源于JVM堆,对于这类数据JVM分析工具无能为力.SMTT将Spark语义延伸至硬件,整体架构如图2所示.逻辑上Spark分为计算引擎和存储系统2部分.计算引擎负责对数据的处理;存储系统负责从底层(JVM或OS)获取内存,对计算过程中的内存进行动态地分配和回收.代码层面上,Spark存储系统相对独立,统一封装对外的接口.计算引擎在执行过程中调用存储系统的接口来分配和释放内存.
SMTT首先在不破坏原有执行逻辑的情况下对这些接口进行了代码插桩,获取当前操作的数据信息、数据所在的数据结构和数据访问序列.其中,代码插桩只监听Spark的数据结构,不改变其固有的接口与逻辑,因此在修改源码和重新编译后不影响Spark与其他组件,如Yarn和Flink等的集成.然后,SMTT分别对JVM堆内和堆外的数据进行处理,获得虚拟地址:对于从JVM堆内获取的内存,找到数据所对应的JVM对象,并将对象转换为OS层的虚拟地址.对于从JVM堆外获取的内存,SMTT找到起始地址,此时这个起始地址直接就是OS层的虚拟地址.最后,SMTT将虚拟地址转换成物理地址,并获得物理页号、是否被交换到外存、所在NUMA节点等数据在物理硬件上的分布信息.经过这样的流程,Spark数据在物理硬件上的分布一目了然,便于从性能的根源分析Spark应用程序的数据访问特性与系统性能的关系.
SMTT按照数据被访问的顺序给出访问序列,记录Spark语义与底层地址信息的对应关系,其结果格式如图3所示,包括:
1) 访问时间.何时发起的本次访问.
2) 访问类型.对数据的访问是读还是写.
3) Spark语义.被访问数据在Spark应用层的意义,如所属RDD和分区.
4) 虚拟地址.被访问数据在OS中分配的虚拟地址.
5) 虚拟页信息.被访问数据属于哪些虚拟页、数据是否跨页了、页是在内存中还是被交换到了外存等影响性能的重要因素.
6) 物理地址.被访问数据在物理内存中的地址.
7) 物理页信息.被访问数据所属物理页、物理页的热度,以及在NUMA系统中该物理页是否跨节点等.
由于执行内存和存储内存的用途不同、工作原理不同,它们在Spark源码中的存取接口也完全不一样.因此,SMTT针对这2种内存分别设计了追踪方案.
Fig.3 Output formation of SMTT图3 SMTT结果格式
2.1 执行内存追踪方案
执行内存在Spark任务执行期间按需分配,例如,当数据需要排序、合并等整理操作时,系统会在执行内存中分配空间进行处理,使用之后立即释放.
对于RDD中的每一个分区,Spark会启动一个Task线程处理其中的数据.但是,Task只包含RDD信息,却无法获得该RDD内数据对应的虚拟内存地址;Sorter中包括虚拟内存地址,但无法获得该地址中存放的数据所对应的RDD信息.为了打通各部分的语义,我们设计了如图4所示的针对执行内存的追踪方案,其中箭头表示追踪流程:
1) 在Task中,将所用Writer的Hash码(Hashcode)和RDD信息传给SMTT,SMTT将其写入一张以Writer的Hash码为键(key)、RDD信息为值(value)的Hash表;
2) 在Writer中,将所用Sorter的Hash码和当前Writer的Hash码传给SMTT,SMTT将其写入一张以Sorter的Hash码为键、Writer的Hash码为值的Hash表;
3) 在Sorter中,将当前Sorter的Hash码,以及数据申请到的虚拟内存地址传给SMTT;
Fig.4 Tracing scheme of execution memory图4 执行内存的追踪方案
4) 根据Sorter的Hash码找到Writer的Hash码,根据Writer的Hash码找到RDD信息;
5) 通过访问OS页表,根据传进来的虚拟内存地址得到对应的物理内存信息;
6) 将RDD信息、虚拟地址、物理地址信息对应起来,作为一条记录保存至文件.
7) 缓存中的数据和溢出文件中的数据会被合并到一个文件中.
通过上述步骤,我们将Spark应用层RDD的信息,同JVM层的信息,与底层物理内存信息联系起来,获得了一一对应的关系,实现了任意层观测到的信息与其他层特征和行为之间的因果关系分析.
2.2 存储内存追踪方案
Fig.5 Tracing scheme of storage memory图5 存储内存的追踪方案
Spark迭代计算节约了内存,但牺牲了时间.为了改善性能,Spark提供了缓存机制:被缓存在内存中的RDD数据,后续被访问时可直接从内存中获取,而无需从头计算,缓存RDD的内存来自于存储内存.对于持久化到内存中的RDD,Spark的Memory-Store对象提供了统一的存/取接口.其内部维护了一个以Spark的BlockID对象为键、以MemoryEntry对象为值的Hash表.其中BlockID对象的值就是RDD的ID与分区的ID按照特定格式的组合.MemoryEntry对象用于描述被存储的数据,其内部有一个Java对象ByteBuffer数组,而每一个Byte-Buffer对象内部有一个存放数据的字节数组.
对于存储内存的追踪,我们主要解决的问题是如何逐层剥去封装数据的外层数据结构,获得虚拟地址,并将其与数据在Spark层的语义对应起来.对此,我们的设计方案如图5所示,箭头表示追踪流程:
1) 在MemoryStore对象内部,当对Hash表进行存/取时,将此时的BlockID对象和MemoryEntry对象传给SMTT;
2) SMTT获取MemoryEntry对象的成员变量ByteBuffer数组;
3) 对于每一个ByteBuffer实例,通过Unsafe对象提供的接口获取其中的成员变量字节数组在JVM堆中的起始地址;
4) 获得字节数组内数据的起始地址,在不使用指针压缩的情况下,数组在JVM堆中前24 B为头信息,其后紧跟着数据,所以根据头信息的长度获得数据的虚拟地址,然后使用JNI技术直接访问内存地址;
5) 通过访问操作系统页表,将虚拟地址转换成物理地址;
6) 将传入SMTT的RDD信息、数据虚拟地址信息、物理地址信息对应起来,保存至文件.
通过上述步骤,完成了存储内存中不同层语义之间的关联,为访存行为分析建立了通路.
Fig.6 Distributed computing process of Spark for word count algorithm图6 词频统计算法中Spark分布式计算的过程
3 基于SMTT的访存行为分析
SMTT给出了跨层的追踪方式,建立了不同层的语义.本节借助于SMTT工具对Spark的迭代计算过程中内存的访问过程,以及执行内存和存储内存的使用过程进行详细的跟踪,解释了Spark中RDD的数据对象从应用层开始,经JVM层和OS层对应到物理内存的流程.
3.1 Spark计算过程追踪
本节以统计词频的Spark应用程序为例,分析其分布式计算的完整过程.如图6所示,一个RDD访问完整过程可视为一个有向无环图,分成2个阶段:第1个阶段Stage0完成Shuffle写,即Map映射;第2个阶段Stage1完成Shuffle读,即Reduce化简.每个阶段的执行起点是当前阶段的最后一个RDD,这个RDD的每一个分区会交给一个Spark创建的独立Task线程进行处理.Task会调用最后一个RDD(即RDD3)的iterator()方法获得其负责处理的分区数据的迭代器,在RDD没有被持久化的情况下,这个iterator()方法会调用当前RDD的compute()方法.这样一直递归调用到第1个RDD(即RDD0)的compute()方法,这时的compute()会返回一个文本数据的迭代器给它的下一个RDD(即RDD1),下一个RDD会新创建一个迭代器对象Iter1,并重写其中的next()方法,用自身生成时的转换函数f0包装前一个RDD传过来的迭代器Iter0的next()方法,然后将这个新创建的迭代器返回给它的下一个RDD;它的下一个RDD还会采取相同的操作,一直到最后一个RDD(即RDD3)的compute()返回给Task.此时Task拿到迭代器之后,每调用一次next()方法,就会从文件系统读取一条记录并经过逐层转换函数的加工返回给它.
值得注意的是,第1个RDD从文件系统中读取数据的时候使用了一块缓存,即图6中JVM堆中的Data部分,这块缓存在迭代器后续的数据读取过程中是被重复使用的.从上述的迭代过程可以看出,如果不做持久化,RDD的数据是不会全部存在内存中.这就解释了,尽管一个Task需要处理大量的数据,但其实际内存占用量并不大的原因.有效的复用提高了存储空间的利用率.
3.2 执行内存使用过程追踪
在Spark中执行内存通常在洗牌过程以Unsafe方式、Sort方式或Bypass方式使用.图7描述了使用SMTT工具对Unsafe方式和Sort方式的工作过程进行追踪的过程,其中①~⑦表示当前任务通过迭代计算获取它所负责分区的数据迭代器,然后将数据逐条写到执行内存分配的缓存空间中,然后进行排序、合并等操作.每向缓存中插入一条记录,当前任务都需要判断缓存是否够大,如果不够则通过acquirExecutionMemory()方法向Spark申请新的执行内存空间,如图7中的步骤⑧⑨所示.写缓存期间,如果所占内存超过一定阈值,当前任务便将数据写回到硬盘文件,这个过程称为溢出(Spill).最后,步骤⑩缓存中的数据和溢出文件中的数据会被合并到一个文件中以供Stage2读取.对于Bypass方式,与Unsafe方式和Sort方式不同之处在于,不将数据写到缓存,而是直接写到硬盘文件.
Fig.7 Memory access process of execution memory in Unsafe mode and Sort mode图7 Unsafe和Sort方式中执行内存使用过程
3.3 存储内存使用过程追踪
我们使用SMTT对Spark中存储内存的使用方式进行了追踪和分析,如图8的步骤①~表示Task递归获取迭代器的过程.存储内存的使用过程跟执行内存相似,区别是从步骤⑦开始,被存储的RDD(RDD2)的迭代器不返回给它的下一个RDD,而是交给BlockManager对象.随后,BlockManager对象使用获得的迭代器将数据逐条存入到内存.每向内存中写入一条记录,Task都会检查内存是否够用,如果不够则通过acquirStorageMemory()方法向Spark申请新的存储内存空间.在全部数据写入内存之后,被持久化的RDD会创建一个遍历这块内存的迭代器并返回给下一个RDD,从而完成Task递归地获取迭代器的过程.由此可见,访问持久化的RDD数据实际上访问的是内存的某一段空间.
Fig.8 Memory access process of storage memory图8 存储内存使用过程
存储内存除了存放持久化的RDD数据之外,还存放反序列化过程中“unroll”过程的数据,以及用于全局的广播变量等.无论何种数据,存储内存同执行内存一样,既可以从堆上分配,也可以从堆外分配,返回的都是连续的地址空间,从而保证了当前线程访存的局部性.
4 实验评测
4.1 实验环境
本文采用如图9所示的Spark运行模式,主要组件包括:1个驱动器节点、1个主节点、多个从节点.其中,1个主节点和多个从节点组成集群,主节点用于管理计算资源,而从节点用于执行具体的计算;驱动器节点位于应用程序端,负责向集群提交任务.
Fig.9 The development model of Spark图9 Spark部署模型
在实际的生产环境中,集群的情况往往比较复杂.首先,同一个物理集群上运行的除了Spark,可能还有HDFS,Yarn,Presto等其他分布式程序;其次,针对不同的需求,集群的规模可能从几台到几千台不等;此外,对于不同的业务,运行在集群上的应用程序也多种多样.然而,优化Spark的前提是对Spark自身原理和特性的分析和理解,为了避免复杂环境的影响,本文的实验将焦点聚集在Spark本身.考虑到计算节点的访存行为是由Spark本身决定的,不受集群规模影响,因此本文选择了代表Spark的Standalone部署模式的实验环境,采用3台物理机组成HDFS集群和Spark集群,为避免相互干扰进而使观测和分析变得复杂,本文将HDFS集群和Spark集群的工作节点尽量分开,物理机的角色如表1所示:
Table 1 The role of Physical Machines表1 实验所用物理机角色
每台物理机有2个Intel®Xeon®E5620CPU和32 GB物理内存;每个CPU主频为2.4 GHz,包含4个Core,每个Core支持双线程.CPU采用IA32e模式,支持4 KB,2 MB和1 GB大小的页.
为了更好地评测大数据处理程序的性能,很多基准测试程序应运而生,常用的有BigDataBench[25],CloudSuite[26],SparkBench[27]等.Spark拥有活跃的社区,贡献着各种类型的计算模型,目前提供4种类型的应用:机器学习、SQL查询、图计算、流计算[28-29].表2列出了本文中所用的SparkBench的负载程序及输入大小.本文基于这4种类型的应用,以读写RDD的评测结果为例,评测借助SMTT追踪分析不同程序的不同特征的效果.
Table 2 Description and Input of Workloads表2 实验负载及输入
4.2 实验结果
我们追踪了每一个Spark负载中RDD的读写开销情况,由于不同负载业务类型不同,因此在RDD访问方面也呈现出不同的特性,这个特性与负载的具体程序实现方式紧密相关.
Fig.10 Page walk overhead of RDD of single node mode图10 单节点RDD访问引发的页遍历开销
图10显示在只有一个主节点和一个从节点的单节点集群上RDD引发的页遍历读写操作占比.RDD的读操作比例平均为2.20%,写操作比例平均为2.55%,平均有4.75%的周期用于RDD访问.其中,应用程序TC的比例最大:读操作比例为4.87%、写操作为5.21%,累计占到总执行周期的10.08%.PR和TC相似,读操作占4.55%、写操作占4.91%,总共有9.46%的周期在进行RDD读写.此外,PVS读写占比7.22%、RR读写占比6.59%、RR读写占比5.38%,也都高于平均开销.比例最低的负载为MF,读操作比例为0.34%、写操作比例为0.46%,总共花费的执行周期为0.80%.而LR读操作为1.63%、写操作为1.91%,SVM读操作为1.72%、写操作为2.03%,这2个负载虽然读写比例也低于平均值,但其所占总执行周期的比例分别为3.54%和3.75%,相对居中.
由图10可得结论:写操作开销略高于读操作开销,这是因为RDD被设计为只读的,常用的RDD会被持久化到内存中,此后可以多次重复读取,但是“写”只有一次,即它被创建的时候.
图11给出了基于SMTT获得的1个主节点、2个从节点的多节点集群上各Spark负载由RDD访问所花费的周期占总执行周期的比例.平均情况,读操作所占周期比例为2.15%、写操作所占周期比例为2.52%,所以总的RDD读写周期比例为4.67%.首先,开销较大的负载为:TC操作为5.29%、写操作为5.61%,PR读操作为4.18%、写操作为4.55%,SVDPP读操作为3.49%、写操作为3.96%,总的周期比例分别为10.90%,8.73%,7.45%.其次,开销居中的是PVS读操作为2.49%、写操作为2.93%,RR读操作为2.27%、写操作为2.67%,SVM读操作为1.74%、写操作为2.08%,LR读操作为1.57%、读操作为1.90%,所耗周期比例分别为5.42%,4.94%,3.82%,3.47%.最后,MF的开销最小,读操作为0.38%、写操作为0.53%,总比例仅为0.91%.
Fig.11 Page walk overhead of RDD of multiple node mode图11 多节点RDD访问引发的页遍历开销
由图11可得结论:无论是单节点还是多节点,Spark访存引发的页遍历开销普遍较低,这主要是因为Spark迭代计算、连续分配执行内存和存储内存的设计.迭代计算使程序读取初始数据时尽可能复用内存,从而减少内存占用.并且Spark每次从执行内存和存储内存中都分配连续的空间,保证了程序访存的局部性,保持了较高的TLB命中率和较低的页表遍历开销.
单节点和多节点情况下不同负载RDD所占内存比例如图12所示,不同负载的内存占用率差别较大.单节点时,内存使用率的平均值为40.74%.其中,LR使用率为80.10%,SVM使用率为76.55%,SVDPP使用率为71.32%,TC使用率为71.17%,相对较高.PR使用率为56.52%,RR使用率为35.52%,接近平均值.而PVS使用率为13.91%,MF使用率为8.73%,远远低于平均值.
Fig.12 Memory occupation of RDD图12 单节点和多节点下RDD内存占用率
多节点时,内存占用率普遍不高,平均值为32.01%.其中,TC使用率为52.51%,SVM使用率为50.80%,LR使用率为49.73%,SVDPP使用率为46.17%,PR使用率为40.47%,相对较高.PVS使用率为24.58%,RR使用率为22.12%,MF使用率为8.17%,内存利用率最低.
结论1.各负载的RDD内存占用率差别较大,其中,LR,SVM,SVDPP,TC内存占用率较高,PR,RR,PVS内存占用率较低,MF内存占用率最低.
结论2.相比单节点集群,多节点内存利用率较低.
结论3.PVS负载在单节点和多节点情况下的行为差异最大.
5 结论与展望
本文针对大数据计算环境下Spark底层行为与上层应用程序特征之间的关联缺失问题,设计了SMTT工具,打通了Spark层、JVM层和OS层,实现了上层应用程序的语义与底层物理内存信息的对应.针对Spark内存计算的特点,分别针对执行内存和存储内存设计了不同的追踪方案.本文使用SMTT分析了内存使用方式,并分析不同负载用于RDD读/写的开销和内存占用情况,结果显示本文所设计的工具能够有效支持Spark的存储系统分析,为Spark的性能优化奠定了基础.