Apache Spark项目于2009年诞生于伯克利大学的AMPLab实验室,当初的目的在于将内存内分析机制引入大规模数据集当中。在那个时候,Hadoop MapReduce的关注重点仍然放在那些本质上无法迭代的大规模数据管道身上。想在2009年以MapReduce为基础构建起分析模型实在是件费心费力而又进展缓慢的工作,因此AMPLab设计出Spark来帮助开发人员对大规模数据集执行交互分析、从而运行各类迭代工作负载——也就是对内存中的同一套或者多套数据集进行反复处理,其中最典型的就是机器学习算法。
Spark的意义并不在于取代Hadoop。正相反,它为那些高度迭代的工作负载提供了一套备用处理引擎。通过显著降低面向磁盘的写入强度,Spark任务通常能够在运行速度方面高出Hadoop MapReduce几个数量级。作为“寄生”在Hadoop集群当中的得力助手,Spark利用Hadoop数据层(HDFS、HBase等等)作为数据管道终端,从而实现原始数据读取以及最终结果存储。
编写Spark应用程序
作为由Scala语言编写的项目,Spark能够为数据处理流程提供一套统一化抽象层,这使其成为开发数据应用程序的绝佳环境。Spark在大多数情况下允许开发人员选择Scala、Java以及Python语言用于应用程序构建,当然对于那些最为前沿的层面、只有Scala能够实现大家的一切构想。
Spark当中的突出特性之一在于利用Scala或者Python控制台进行交互式工作。这意味着大家可以在尝试代码运行时,立即查看到其实际执行结果。这一特性非常适合调试工作——大家能够在无需进行编译的前提下变更其中的数值并再次处理——以及数据探索——这是一套典型的处理流程,由大量检查-显示-更新要素所构成。
Spark的核心数据结构是一套弹性分布式数据(简称RDD)集。在Spark当中,驱动程序被编写为一系列RDD转换机制,并附带与之相关的操作环节。顾名思义,所谓转换是指通过变更现有数据——例如根据某些特定指标对数据进行过滤——根据其创建出新的RDD。操作则随RDD自身同步执行。具体而言,操作内容可以是计算某种数据类型的实例数量或者将RDD保存在单一文件当中。
Spark的另一大优势在于允许使用者轻松将一套RDD共享给其它Spark项目。由于RDD的使用贯穿于整套Spark堆栈当中,因此大家能够随意将SQL、机器学习、流以及图形等元素掺杂在同一个程序之内。
熟悉各类其它函数型编程语言——例如LISP、Haskell或者F#——的开发人员会发现,除了API之外、自己能够非常轻松地掌握Spark编程方式。归功于Scala语言的出色收集系统,利用Spark Scala API编写的应用程序能够以干净而且简洁的面貌呈现在开发者面前。在对Spark编程工作进行调整时,我们主要需要考虑这套系统的分布式特性并了解何时需要对对象以及函数进行排序。
拥有其它程序语言,例如Java,知识背景的程序员则往往没办法快速适应Spark项目的函数编程范式。有鉴于此,企业可能会发现找到一位能够切实上手Spark(从这个角度讲,Hadoop也包含其中)的Scala与函数编程人员实在不是件容易的事。
由于Spark的RDD能够实现跨系统共享,因此大家能够随意将SQL、机器学习、流以及图形等元素掺杂在同一个程序之内。
#p#
弹性分布式数据集
对于RDD的使用贯穿于整套堆栈当中,而这也成为Spark如此强大的根基之一。无论是从概念层面还是实施层面,RDD都显得非常简单; RDD类当中的大部分方法都在20行以内。而从核心角度看,RDD属于一套分布式记录集合,由某种形式的持久性存储作为依托并配备一系列转换机制。
RDD是不可变更的。我们无法对RDD进行修改,但却能够轻松利用不同数值创建新的RDD。这种不可变性算得上是分布式数据集的一大重要特性; 这意味着我们用不着担心其它线程或者进程在我们不知不觉中对RDD数值作出了变更——而这正是多线程编程领域的一个老大难问题。这同时意味着我们能够将RDD分发到整个集群当中加以执行,而不必担心该如何在各节点之间对RDD内容变更进行同步。
RDD不可变性在Spark应用程序的容错机制当中同样扮演着重要角色。由于每个RDD都保留有计算至当前数值的全部历史记录、而且其它进程无法对其作出变更,因此在某个节点丢失时对RDD进行重新计算就变得非常轻松——只需要返回原本的持久性数据分区,再根据不同节点重新推导计算即可。(Hadoop当中的大多数分区都具备跨节点持久性。)
RDD能够通过多种数据分区类型加以构成。在大多数情况下,RDD数据来自HDFS,也就是所谓“分区”的书面含义。不过RDD也可以由来自其它持久性存储机制的数据所构成,其中包括HBase、Cassandra、SQL数据库(通过JDBC)、Hive ORC(即经过优化的行列)文件乃至其它能够与Hadoop InputFormat API相对接的存储系统。无论RDD的实际来源如何,其运作机制都是完全相同的。
Spark转换机制的最后一项备注是:此类流程非常懒惰,也就是说直到某项操作要求将一条结果返回至驱动程序,否则此前整个过程不涉及任何计算环节。这样的特性在与Scala shell进行交互时显得意义重大。这是因为RDD在逐步转换的过程当中不会带来任何资源成本——直到需要执行实际操作。到这个时候,所有数值才需要进行计算,并将结果返回给用户。除此之外,由于RDD能够利用内存充当缓存机制,因此频繁使用计算结果也不会造成反复计算或者由此引发的资源消耗。
Spark转换机制非常懒惰,也就是说直到某项操作要求将一条结果返回至用户处,否则此前整个过程不涉及任何计算环节。
执行Spark应用程序
为了将一项Spark任务提交至集群,开发人员需要执行驱动程序并将其与集群管理器(也被称为cluster master)相对接。集群管理器会为该驱动程序提供一套持久性接口,这样同一款应用程序即可在任何受支持集群类型之上实现正常运行。
Spark项目目前支持专用Spark(独立)、Mesos以及YARN集群。运行在集群当中的每个驱动程序以各自独立的方式负责资源分配与任务调度工作。尽管以隔离方式进行应用程序交付,但这种架构往往令集群很难高效实现内存管理——也就是对于Spark而言最为宝贵的资源类型。多个高内存消耗任务在同时提交时,往往会瞬间将内存吞噬殆尽。尽管独立集群管理器能够实现简单的资源调度,但却只能做到跨应用程序FIFO(即先入先出)这种简单的程度,而且无法实现资源识别。
总体而言,Spark开发人员必须更倾向于裸机层面思维,而非利用像Hive或者Pig这样的高级应用程序将数据分析作为思考出发点。举例来说,由于驱动程序充当着调度任务的执行者,它需要最大程度与这些工作节点保持紧密距离、从而避免网络延迟对执行效果造成的负面影响。
驱动程序与集群管理器高可用性这两者都很重要。如果驱动程序停止工作,任务也将立即中止。而如果集群管理器出现故障,新的任务则无法被提交至其中,不过现有任务仍将继续保持执行。在Spark 1.1版本当中,主高可用性机制由独立Spark集群通过ZooKeeper实现,但驱动程序却缺乏与高可用性相关的保障措施。
将一套Spark集群当中的性能最大程度压榨出来更像是一种魔法甚至妖术,因为其中需要涉及对驱动程序、执行器、内存以及内核的自由组合及反复实验,同时根据特定集群管理器对CPU及内存使用率加以优化。目前关于此类运维任务的指导性文档还非常稀缺,而且大家可能需要与同事进行频繁沟通并深入阅读源代码来实现这一目标。
Spark应用程序架构。Spark目前可以被部署在Spark独立、YARN或者Mesos集群当中。请注意,运行在集群当中的每一个驱动程序都会以彼此独立的方式进行资源分配与任务调度。
#p#
监控与运维
每一款驱动程序都拥有自己的一套Web UI,通常为端口4040,其中显示所有实用性信息——包括当前运行任务、调度程度、执行器、阶段、内存与存储使用率、RDD等等。这套UI主要充当信息交付工具,而非针对Spark应用程序或者集群的管理方案。当然,这也是调试以及性能调整之前的基础性工具——我们需要了解的、与应用程序运行密切相关的几乎所有信息都能在这里找到。
虽然算是个不错的开始,但这套Web UI在细节方面仍然显得比较粗糙。举例来说,要想查看任务历史记录、我们需要导航到一台独立的历史服务器,除非大家所使用的是处于独立模式下的集群管理器。不过最大的缺点在于,这套Web UI缺少对于运维信息的管理与控制能力。启动与中止节点运行、查看节点运行状况以及其它一些集群层面的统计信息在这里一概无法实现。总体而言,Spark集群的运行仍然停留在命令行操作时代。
Spark的Web UI提供了与当前运行任务相关的丰富信息,但所有指向集群的管理操作则需要完全通过命令行来实现。
Spark对决Tez
事实上,Spark与Tez都采用有向无环图(简称DAG)执行方式,这两套框架之间的关系就如苹果与桔子般不分轩轾,而最大的差别在于其受众以及设计思路。即使如此,我发现很多IT部门仍然没能分清这两款框架间的差异所在。
Tez是一款应用程序框架,设计目的在于帮助开发人员编写出更为高效的多级MapReduce任务。举例来说,在Hive 0.13版本当中,HQL(即Hive查询语言)由语言编译器负责解析并作为Tez DAG进行渲染,即将数据流映射至处理节点处以实现高效执行。Tez DAG由应用程序以边缘到边缘、顶点到顶点的方式进行构建。用户则完全不需要了解Tez DAG的构建方式,甚至感受不到它的存在。
Spark与Tez之间的真正差异在于二者实现方式的不同。在Spark应用程序当中,同样的工作节点通过跨迭代实现重新使用,这就消除了JVM启动所带来的资源成本。Spark工作节点还能够对变量进行缓存处理,从而消除对数值进行跨迭代重新读取与重新计算的需要。正是借鉴着以上几大特征,Spark才能够在迭代编程当中如鱼得水、充分发力。而由此带来的缺点是,Spark应用程序会消耗大量集群资源、特别是在缓存过期的情况下。我们很难在集群运行着Spark的时候对资源进行优化。
尽管支持多级任务执行机制,Tez仍然不具备任何形式的缓存处理能力。虽然变量能够在一定程度上得到缓存处理,从而保证规划器在可能的情况下保证调度任务从同节点中的上一级处获取必要数值,但Tez当中仍然未能提供任何一种经过妥善规划的跨迭代或者变量广播机制。除此之外,Tez任务还需要反复启动JVM,而这会带来额外的资源开销。因此,Tez更适合处理那些规模极为庞大的数据集,在这种情况下启动时间只占整体任务处理周期的一小部分、几乎可以忽略不计。
在大多数情况下,Hadoop社区对此都拥有很好的移花接木式解决方案,而且其中最出色的部分机制已经能够作用于其它项目。举例来说,YARN-1197将允许Spark执行器以动态方式进行规模调整,这样它们就能够在合适的条件下将资源返还给集群。与之相似,Stinger.next将为Hive等传统Hadoop应用程序带来由跨查询缓存提供的巨大优势。
#p#
一整套集成化分析生态系统
Spark所采用的底层RDD抽象机制构建起整个Spark生态系统的核心数据结构。在机器学习(MLlib)、数据查询(Spark SQL)、图形分析(GraphX)以及流运行(Spark Streaming)等模块的共同支持下,开发人员能够以无缝化方式使用来自任意单一应用程序的库。
举例来说,开发人员可以根据HDFS当中的某个文件创建一个RDD,将该RDD转换为SchemaRDD、利用Spark SQL对其进行查询,而后将结果交付给MLlib库。最后,结果RDD可以被插入到Spark Streaming当中,从而充当消息交付机制的预测性模型。如果要在不使用Spark项目的情况下实现以上目标,大家需要使用多套库、对数据结构进行封包与转换,并投入大量时间与精力对其加以部署。总体而言,将三到上个在最初设计当中并未考虑过协作场景的应用程序整合在一起绝对不是正常人的脆弱心灵所能承受的沉重负担。
堆栈集成机制让Spark在交互式数据探索与同一数据集内的重复性函数应用领域拥有着不可替代的重要价值。机器学习正是Spark项目大展拳脚的理想场景,而在不同生态系统之间以透明方式实现RDD共享的特性更是大大简化了现代数据分析应用程序的编写与部署流程。
然而,这些优势的实现并非全无代价。在1.x系列版本当中,Spark系统在诸多细节上还显得相当粗糙。具体而言,缺乏安全性(Spark无法运行在Kerberised集群当中,也不具备任务控制功能)、缺乏企业级运维功能、说明文档质量糟糕,而且严苛的稀缺性技能要求意味着目前Spark仍然只适合早期实验性部署或者那些有能力满足大规模机器学习模型必需条件且愿意为其构建支付任何投入的大型企业。
到底应不应该部署Spark算是一个“仁者见仁,智者见智”的开放性议题。对于一部分组织而言,Spark这套速度极快的内存内分析引擎能够带来诸多优势,从而轻松为其带来理想的投资回报表现。但对于另一些组织来说,那些虽然速度相对较慢但却更为成熟的工具仍然是其不二之选,毕竟它们拥有适合企业需求的完善功能而且更容易找到有能力对其进行管理与控制的技术人员。
无论如何,我们都要承认Spark的积极意义。Spark项目将一系列创新型思维带入了大数据处理市场,并且表现出极为强劲的发展势头。随着其逐步成熟,可以肯定Spark将最终成为一支不容忽视的巨大力量。
Apache Spark 1.1.0 / Apache软件基金会
总结性描述
作为一套配备精妙API以实现数据处理应用程序创建目标的高速内存内分析引擎,Spark在迭代工作负载这类需要重复访问同一套或者多套数据集的领域——例如机器学习——表现出无可匹敌的竞争优势。
基于Apache 2.0许可的开源项目
优势
• 精妙且具备一致性保障的API帮助开发人员顺利构建起数据处理应用程序
• 支持Hadoop集群上的交互式查询与大规模数据集分析任务
• 在运行迭代工作负载时拥有高出Hadoop几个数量级的速度表现
• 能够以独立配置、YARN、Hadoop MapReduce或者Mesos等方式部署在Hadoop集群当中
• RDD(即弹性分布式数据集)能够在不同Spark项目之间顺利共享,从而允许用户将SQL、机器学习、流运行以及图形等元素掺杂在同一程序当中
• Web UI提供与Spark集群及当前运行任务相关的各类实用性信息
缺点
• 安全性不理想
• 说明文档质量糟糕
• 不具备集群资源管理能力
• 学习曲线不够友好