在以前的博文中,咱们回顾和总结了2014年Spark在性能提高上所作的努力。本篇博文中,咱们将为你介绍性能提高的下一阶段——Tungsten。在2014年,咱们目击了Spark缔造大规模排序的新世界纪录,同时也看到了Spark整个引擎的大幅度提高——从Python到SQL再到机器学习。html
Tungsten项目将是Spark自诞生以来内核级别的最大改动,以大幅度提高Spark应用程序的内存和CPU利用率为目标,旨在最大程度上压榨新时代硬件性能。Project Tungsten包括了3个方面的努力:java
之因此大幅度聚焦内存和CPU的利用,其主要缘由就在于:对比IO和网络通讯,Spark在CPU和内存上遭遇的瓶颈日益增多。详细信息能够查看最新的大数据负载性能研究(Ousterhout ),而咱们在为Databricks Cloud用户作优化调整时也得出了相似的结论。python
为何CPU会成为新的瓶颈?这里存在多个问题:首先,在硬件配置中,IO带宽提高的很是明显,好比10Gbps网络和SSD存储(或者作了条文化处理的HDD阵列)提供的高带宽;从软件的角度来看,经过Spark优化器基于业务对输入数据进行剪枝,当下许多类型的工做负载已经不会再须要使用大量的IO;在Spark Shuffle子系统中,对比底层硬件系统提供的原始吞吐量,序列化和哈希(CPU相关)成为主要瓶颈。从种种迹象来看,对比IO,Spark当下更受限于CPU效率和内存压力。git
1. Memory Management和Binary Processinggithub
在JVM上的应用程序一般依赖JVM的垃圾回收机制来管理内存。毫无疑问,JVM绝对是一个伟大的工程,为不一样工做负载提供了一个通用的运行环境。然而,随着Spark应用程序性能的不断提高,JVM对象和GC开销产生的影响将很是致命。算法
一直以来,Java对象产生的开销都很是大。在UTF-8编码上,简单如“abcd”这样的字符串也须要4个字节进行储存。然而,到了JVM状况就更糟糕了。为了更加通用,它从新定制了本身的储存机制——使用UTF-16方式编码每一个字符(2字节),与此同时,每一个String对象还包含一个12字节的header,和一个8字节的哈希编码,咱们能够从 Java Object Layout工具的输出上得到一个更清晰的理解:sql
1 java.lang.String object internals: 2 OFFSET SIZE TYPE DESCRIPTION VALUE 3 0 4 (object header) ... 4 4 4 (object header) ... 5 8 4 (object header) ... 6 12 4 char[] String.value [] 7 16 4 int String.hash 0 8 20 4 int String.hash32 0 9 Instance size: 24 bytes (reported by Instrumentation API)
毫无疑问,在JVM对象模型中,一个4字节的字符串须要48字节的空间来存储!express
JVM对象带来的另外一个问题是GC。从高等级上看,一般状况下GC会将对象划分红两种类型:第一种会有很高的allocation/deallocation(年轻代),另外一种的状态很是稳定(年老代)。经过利用年轻代对象的瞬时特性,垃圾收集器能够更有效率地对其进行管理。在GC能够可靠地估算对象的生命周期时,这种机制能够良好运行,可是若是只是基于一个很短的时间,这个机制很显然会遭遇困境,好比对象突然从年轻代进入到年老代。鉴于这种实现基于一个启发和估计的原理,性能能够经过GC调优的一些“黑魔法”来实现,所以你可能须要给JVM更多的参数让其弄清楚对象的生命周期。apache
然而,Spark追求的不只仅是通用性。在计算上,Spark了解每一个步骤的数据传输,以及每一个做业和任务的范围。所以,对比JVM垃圾收集器,Spark知悉内存块生命周期的更多信息,从而在内存管理上拥有比JVM更具效率的可能。缓存
为了扭转对象开销和无效率GC产生的影响,咱们引入了一个显式的内存管理器让Spark操做能够直接针对二进制数据而不是Java对象。它基于sun.misc.Unsafe创建,由JVM提供,一个相似C的内存访问功能(好比explicit allocation、deallocation和pointer arithmetics)。此外,Unsafe方法是内置的,这意味着,每一个方法都将由JIT编译成单一的机器指令。
在某些方面,Spark已经开始利用内存管理。2014年,Databricks引入了一个新的基于Netty的网络传输机制,它使用一个类jemalloc的内存管理器来管理全部网络缓冲。这个机制让Spark shuffle获得了很是大的改善,也帮助了Spark创造了新的世界纪录。
新内存管理的首次亮相将出如今Spark 1.4版本,它包含了一个由Spark管理,能够直接在内存中操做二进制数据的hashmap。对比标准的Java HashMap,该实现避免了不少中间环节开销,而且对垃圾收集器透明。
当下,这个功能仍然处于开发阶段,可是其展示的初始测试行能已然使人兴奋。如上图所示,咱们在3个不一样的途径中对比了聚合计算的吞吐量——开发中的新模型、offheap模型、以及java.util.HashMap。新的hashmap能够支撑每秒超过100万的聚合操做,大约是java.util.HashMap的两倍。更重要的是,在没有太多参数调优的状况下,随着内存利用增长,这个模式基本上不存在性能的衰弱,而使用JVM默认模式最终已被GC压垮。
在Spark 1.4中,这个hashmap能够为DataFracmes和SQL的聚合处理使用,而在1.5中,咱们将为其余操做提供一个让其利用这个特性的数据结构,好比sort和join。毫无疑问,它将应用到大量须要调优GC以得到高性能的场景。
2. Cache-aware computation(缓存友好的计算)
在解释Cache-aware computation以前,咱们首先回顾一下“内存计算”,也是Spark广为业内知晓的优点。对于Spark来讲,它能够更好地利用集群中的内存资源,提供了比基于磁盘解决方案更快的速度。然而,Spark一样能够处理超过内存大小的数据,自动地外溢到磁盘,并执行额外的操做,好比排序和哈希。
相似的状况,Cache-aware computation经过使用 L1/ L2/L3 CPU缓存来提高速度,一样也能够处理超过寄存器大小的数据。在给用户Spark应用程序作性能分析时,咱们发现大量的CPU时间由于等待从内存中读取数据而浪费。在 Tungsten项目中,咱们设计了更加缓存友好的算法和数据结构,从而让Spark应用程序能够花费更少的时间等待CPU从内存中读取数据,也给有用工做提供了更多的计算时间。
咱们不妨看向对记录排序的例子。一个标准的排序步骤须要为记录储存一组的指针,并使用quicksort 来互换指针直到全部记录被排序。基于顺序扫描的特性,排序一般能得到一个不错的缓存命中率。然而,排序一组指针的缓存命中率却很低,由于每一个比较运算都须要对两个指针解引用,而这两个指针对应的倒是内存中两个随机位置的数据。
那么,咱们该如何提升排序中的缓存本地性?其中一个方法就是经过指针顺序地储存每一个记录的sort key。举个例子,若是sort key是一个64位的整型,那么咱们须要在指针阵列中使用128位(64位指针,64位sort key)来储存每条记录。这个途径下,每一个quicksort对比操做只须要线性的查找每对pointer-key,从而不会产生任何的随机扫描。但愿上述解释可让你对咱们提升缓存本地性的方法有必定的了解。
这样一来,咱们又如何将这些优化应用到Spark?大多数分布式数据处理均可以归结为多个操做组成的一个小列表,好比聚合、排序和join。所以,经过提高这些操做的效率,咱们能够从总体上提高Spark。咱们已经为排序操做创建了一个新的版本,它比老版本的速度快5倍。这个新的sort将会被应用到sort-based shuffle、high cardinality aggregations和sort-merge join operator。在2015年末,全部Spark上的低等级算法都将升级为cache-aware,从而让全部应用程序的效率都获得提升——从机器学习到SQL。
3. 代码生成
大约在1年前,Spark引入代码生成用于SQL和DataFrames里的表达式求值(expression evaluation)。表达式求值的过程是在特定的记录上计算一个表达式的值(好比age > 35 && age < 40)。固然,这里是在运行时,而不是在一个缓慢的解释器中为每一个行作单步调试。对比解释器,代码生成去掉了原始数据类型的封装,更重要的是,避免了昂贵的多态函数调度。
在以前的博文中,咱们阐述了代码生成能够加速(接近一个量级)多种TPC-DS查询。当下,咱们正在努力让代码生成能够应用到全部的内置表达式上。此外,咱们计划提高代码生成的等级,从每次一条记录表达式求值到向量化表达式求值,使用JIT来开发更好的做用于新型CPU的指令流水线,从而在同时处理多条记录。
在经过表达式求值优化内部组件的CPU效率以外,咱们还指望将代码生成推到更普遍的地方,其中一个就是shuffle过程当中将数据从内存二进制格式转换到wire-protocol。如以前所述,取代带宽,shuffle一般会因数据系列化出现瓶颈。经过代码生成,咱们能够显著地提高序列化吞吐量,从而反过来做用到shuffle网络吞吐量的提高。
上面的图片对比了单线程对800万复杂行作shuffle的性能,分别使用的是Kryo和代码生成,在速度上后者是前者的2倍以上。
Tungsten和将来的工做
在将来的几个版本中,Tungsten将大幅度提高Spark的核心引擎。它首先将登录Spark 1.4版本,包括了Dataframe API中聚合操做的内存管理,以及定制化序列化器。二进制内存管理的扩展和cache-aware数据结构将出如今Spark 1.5的部分项目(基于DataFrame模型)中。固然若是须要的话,这个提高也会应用到Spark RDD API。
对于Spark,Tungsten是一个长期的项目,所以也存在不少的可能性。值得关注的是,咱们还将考察LLVM或者OpenCL,让Spark应用程序能够利用新型CPU所提供的SSE/SIMD指令,以及GPU更好的并行性来提高机器学习和图的计算。
Spark不变的目标就是提供一个单一的平台,让用户能够从中得到更好的分布式算法来匹配任何类型的数据处理任务。其中,性能一直是主要的目标之一,而Tungsten的目标就是让Spark应用程序达到硬件性能的极限。更多详情能够持续关注Databricks博客,以及6月旧金山的Spark Summit。
转载自 http://www.csdn.net/article/2015-04-30/2824591-project-tungsten-bringing-spark-closer-to-bare-metal
英文原文参见 https://databricks.com/blog/2015/04/28/project-tungsten-bringing-spark-closer-to-bare-metal.html