近两年,Spark技术发展速度惊人,用户越来越多,社区也愈加活跃,生态更加丰富,这些都证明了Spark的魅力。在生态建设上,Spark取得了极大的成功,主要体现在Application、Environment及Data Source三个方面。此外,值得一提的是,Spark的贡献者目前已经超过650人,而另一方面,围绕Spark创业的公司同样随之增多,“Spark as a Service”的概念被越来越多的人接受,Spark的未来值得期待。在本文中,国内最早的Spark研究者与使用者、七牛云存储技术总监陈超将为您解读Spark技术及其生态系统的***进展。(本文已被选登在程序员电子版2015年9月A刊)。
到目前为止,Spark看起来一帆风顺,但事实上,大数据这个领域从来不缺强者,其中***代表性的无疑当属Flink(https://flink.apache.org)。Flink采了MPP的思想,具备很多有意思的设计和特性。本文不准备用过多的篇幅介绍Flink,主要给大家分享Spark最近几个极其重要的改进。注意,下面所提到的改进有些已经实现,而有些尚未完成。
1. Project Tungsten
Spark近期的发展中,最引人瞩目的当属钨丝计划(Project Tungsten)。Kay Ousterhout在名为“Making Sense of Performance in Data Analytics Frameworks”的论文中谈到,类似Spark这样的计算框架,其瓶颈主要在于CPU与内存,而不是大家之前所认为的磁盘IO及网络开销,因为带宽增大、SSD或者磁盘阵列的使用均可以缓解这个问题。但是在序列化、反序列化及Hash等场景下,CPU确实能形成瓶颈,Tungsten的启动就是为了解决这些问题。Tungsten主要包含以下三个方面。
内存管理与二进制处理。
毋庸置疑,JVM确实是个非常优秀的平台,但是短板也非常明显,那就是GC,而Java对象的内存开销同样不能忽视。基于这个问题,Spark选择自己管理内存,所用的工具就是sun.misc.Unsafe。如果大家对细节有兴趣,可以关注BytesToBytesMap结构。需要注意,它是append-only的,并且key与value都是连续的字节区域。自己管理内存不仅缓解了GC的压力,也显著地降低了内存使用。但这里必须提醒大家,Unsafe千万不能滥用,否则后果很严重。
缓存友好的计算。
目前,大家看到缓存似乎都只想到将数据加载到内存就完事了。事实上,更佳的做法应该是CPU级别的缓存。因此,Spark自1.4版本开始便在这个点上发力,其中,最重要的当属在 Aggregations、Joins和Shuffle时可以更有效地排序和哈希。Spark引入了UnsafeShuffleManager这个新的ShuffleManager。它的好处是可以直接对二进制数据进行排序,从而减少了内存占用,也省去了反序列化的过程。这里大家可以注意下UnsafeShuffleExternalSorter,可以称得上整个优化的基础。实际上,CPU反复从内存读取数据在一定程度上阻碍了CPU的Pipeline操作。
代码生成(Code Gen)。
熟悉LLVM的朋友应该能较好地理解这一点,之前的Spark SQL已经在使用该部分了。近日发布的Spark 1.5中,Code Gen得到了更加广泛的应用。需要Code Gen的原因很简单,它能免去昂贵的虚函数调用,当然,也就不存在对Java基本类型装箱之类的操作了。Spark SQL将Code Gen用于表达式的求值,效果显著。值得一提的是,在Spark 1.5中,Spark SQL将Code Gen默认打开了。
Tungsten的部分就先谈到这里,整个项目的完成需要等到1.6版本。不过1.4与1.5已经逐步融入了Tungsten的部分优化,让大家可以及时感受Tungsten带来的各种改进。
2. Dynamic Resource Allocation
严格地讲,动态资源分配(Dynamic Resource Allocation)这个特性在Spark 1.2时就出现了,那时只支持在YARN上对资源做动态分配。在Spark 1.5中,Standalone及Mesos也将支持这个特性,个人认为此举有较大意义,但大家仍需充分了解该特性的使用场景。
在Spark中,动态资源分配的粒度是Executor,通过spark.dynamicAllocation.enabled开启,通过spark.dynamicAllocation.schedulerBacklogTimeout及 spark.dynamicAllocation.sustainedSchedulerBacklogTimeout两个参数进行时间上的控制。另外,Spark对于YARN及Mesos的支持均得到了显著地增强。
3. Adaptive Query Plan
适应查询计划(Adaptive Query Plan)是一个“可能”的特性。“可能”的原因是这一特性可能要等到Spark 1.6版本或者之后的版本才会有。首先,陈述几个问题,如何自动确定并行度(Level of Parallelism);如何自动选择采用Broadcast Join还是Hash Join;Spark如何感知数据的行为。
目前,Spark需要在执行Job前确定Job的DAG,即在执行前,由Operator到DAG的转换就已经完成了,这样显然不够灵活。因此,更好的方案是允许提交独立的DAG stage,同时收集它们执行结果的一些统计信息。基于这些信息,Spark可以动态决定Reduce Task的数量,同时也可以动态地选择是采用Broadcast还是Shuffle。对于Spark SQL来讲,它应该能在聚合时自动设置Reduce Task的数量,并且在Join时自动选择策略。主要的思路是,在决定Reduce Task的数量及采用的Shuffle策略前,先让Map运算,然后输出较大数量的Partition作为Map阶段的结果。接下来,Spark会检查Map Stages输出的Partition的大小(或者其它一些状态),然后基于这些信息做出***选择。
估计大家已经看出,这里其实需要修改DAGScheduler的实现,因为目前的DAGScheduler仅支持接收一张完整的DAG图,而上述讨论的问题要求DAGScheduler支持接收Map Stages,且能收集Map Stages输出结果的相关信息。Shuffle也需要支持能一次Fetch多个Map输出的Partition,而目前的HashShuffleFetcher一次性只能获取1个Partition。当然,这里还会涉及到其它改动,就不一一列出了。Adaptive Query Plan的重要性在于,Spark会替用户确定运行时所需的一些参数及行为,从而用户无需操心。还记得Flink那句宣传语,即“用户对内核唯一需要了解的事就是不需要了解内核”。
结语
概括来讲,Spark的护城河其实有两条——其一是先进的技术,另一条则是丰富的生态系统。从上图可以看出,无论是这段时间在容器领域无比火爆的Kubernete及Docker,还是在NoSQL领域的两面锦旗HBase及Cassandra,亦或是其它如消息队列Kafka、分布式搜索引擎Elasticsearch及各机器学习框架都与Spark产生了联系,并且这样的趋势还在快速蔓延中。这意味着,Spark可能出现在大数据处理的各个领域,并给各个领域带来明显提升。
同时我了解到,很多朋友都在关注Spark在GPU方面的发展。关于这一点,现在业界也有了一些尝试,但仍然有较长的路要走,让我们一起期待在这个领域未来会发生些什么。
逆水行舟,不进则退。Spark在不停地进步着,真诚希望国内能有更多的工程师参与到Spark的开发中,同时也渴望看到更多有意思的Spark应用案例。目前,几乎可以肯定,在大数据领域选择Spark确实为一个明智之举。