Scala与Spark是天生的一对?

在Spark诞生之初,就有人诟病为什么AMP实验室选了一个如此小众的语言——Scala,很多人还将原因归结为学院派的高冷,但后来事实证明,选择Scala是非常正确的,Scala很多特性与Spark本身理念非常契合,可以说它们是天生一对。Scala背后所代表的函数式编程思想也越来越为人所知。函数式编程思想早在50多年前就被提出,但当时的硬件性能太弱,并不能发挥出这种思想的优势。目前多核CPU大行其道,函数式编程在并发方面的优势也逐渐显示出了威力。这就好像Java在被发明之初,总是有人说消耗内存太多、运行速度太慢,但是随着硬件性能的翻倍,Java无疑是一种非常好的选择。

函数式编程属于声明式编程,与其相对的是命令式编程,命令式编程是按照“程序是一系列改变状态的命令”来建模的一种建模风格,而函数式编程思想是“程序是表达式和变换,以数学方程的形式建立模型,并且尽可能避免可变状态”。函数式编程会有一些类别的操作,如映射、过滤或者归约,每一种都有不同的函数作为代表,如filter、map、reduce。这些函数实现的是低阶变换,而用户定义的函数将作为这些函数的参数来实现整个方程,用户自定义的函数成为高阶变换。

命令式编程将计算机程序看成动作的序列,程序运行的过程就是求解的过程,而函数式编程则是从结果入手,用户通过函数定义了从最初输入到最终输出的映射关系,从这个角度上来说,用户编写代码描述了用户的最终结果(我想要什么),而并不关心(或者说不需要关心)求解过程,因此函数式编程绝对不会去操作某个具体的值,这类似于用户编写的代码:
Scala与Spark是天生的一对?
再来看看函数式(Scala版)的实现:

 
  1. val familyNames = List("ann","bob","c","david")
  2. println(
  3. familyNames.filter(p => p.length() > 1).
  4. map(f => f.capitalize).
  5. reduce((a,b) => a + "," + b).toString()
  6. )

从这个例子我们可以看出,在命令式编程的版本中,只执行了一次循环,在函数式编程的版本里,循环执行了3次(filter、map、reduce),每一次只完成一种逻辑(用户编写的匿名函数),从性能上来说,当然前者更为优秀,这说明了在硬件性能羸弱时,函数式的缺点会被放大,但我们也看到了,在函数式编程的版本不用维护外部状态i,这对于并行计算场景非常友好。

在严格的函数式编程中,所有函数都遵循数学函数的定义,必须有自变量(入参),必须有因变量(返回值)。用户定义的逻辑以高阶函数的形式体现,即用户可以将自定义函数以参数形式传入其他低阶函数中。读者可能对函数作为参数难以理解,其实从数学的角度上来说,这是很自然的,下面是一个数学表达式:

Scala与Spark是天生的一对?

括号中的函数f1 = b作为参数传给函数f2 = Scala与Spark是天生的一对?,这其实是初中的复合函数的用法。相对于高阶函数,函数式语言一般会提供一些低阶函数用于构建整个流程,这些低阶函数都是无副作用的,非常适合并行计算。高阶函数可以让用户专注于业务逻辑,而不需要去费心构建整个数据流。

函数式编程思想因为非常简单,所以特别灵活,用“太极生两仪,两仪生四象,四象生八卦”这句话能很好地反映函数式编程灵活多变的特点,虽然函数式编程语言能显著减少代码行数(其实很多代码由编程语言本身来完成了),但通常让读代码的人苦不堪言。除上述之外,函数式还有很多特性以及有趣之处值得我们去探索。

1.没有变量

在纯粹的函数式编程中,是不存在变量的,所有的值都是不可变(immutable)的,也就是说不允许像命令式编程那样多次给一个变量赋值,比如在命令式编程中我们可以这样写:

+ 1

这是因为x本身就是一个可变状态,但在数学家眼中,这个等式是不成立的。

没有了变量,函数就可以不依赖也不修改外部状态,函数调用的结果不依赖于调用的时间和位置,这样更利于测试和调试。另外,由于多个线程之间不共享状态,因此不需要用锁来保护可变状态,这使得函数式编程能更好地利用多核的计算能力。

2.低阶函数与核心数据结构

如果使用低阶函数与高阶函数来完成我们的程序,这时其实就是将程序控制权让位于语言,而我们专注于业务逻辑。这样做的好处还在于,有利于程序优化,享受免费的性能提升午餐,如语言开发者专注于优化低阶函数,而应用开发者则专注于优化高阶函数。低阶函数是复用的,因此当低阶函数性能提升时,程序不需要改一行代码就能免费获得性能提升。此外,函数式编程语言通常只提供几种核心数据结构,供开发者选择,它希望开发者能基于这些简单的数据结构组合出复杂的数据结构,这与低阶函数的思想是一致的,很多函数式编程语言的特性会着重优化低阶函数与核心数据结构。但这与面向对象的命令式编程是不一样的,在OOP中,面向对象编程的语言鼓励开发者针对具体问题建立专门的数据结构。

3.惰性求值

惰性求值(lazy evaluation)是函数式编程语言常见的一种特性,通常指尽量延后求解表达式的值,这样对于开销大的计算可以做到按需计算,利用惰性求值的特性可以构建无限大的集合。惰性求值可以用闭包来实现。

4.函数记忆

由于在函数式编程中,函数本身是无状态的,因此给定入参,一定能得到一定的结果。基于此,函数式语言会对函数进行记忆或者缓存,以斐波那契数列举例,首先用尾递归来实现求斐波那契数列,Python代码如下:

 
  1. def Fibonacci(n):
  2. if n == 0 :
  3. res = 0
  4. elif num == 1:
  5. res = 1
  6. else:
  7. res = Fibonacci(n - 1) + Fibonacci(n - 2)
  8. return res

当n等于4时,程序执行过程是:

 
  1. Fibonacci(4)
  2. Fibonacci(3)
  3. Fibonacci(2)
  4. Fibonacci(1)
  5. Fibonacci(0)
  6. Fibonacci(1)
  7. Fibonacci(2)
  8. Fibonacci(1)
  9. Fibonacci(0)

为了求Fibonacci (4),我们执行了1次Fibonacci(3)、2次Fibonacci(2)、3次Fibonacci(1)和2次Fibonacci(0),一共8次计算,在函数式语言中,执行过程是这样的:

 
  1. Fibonacci(4)
  2. Fibonacci(3)
  3. Fibonacci(2)
  4. Fibonacci(1)
  5. Fibonacci(0)

一共只用4次计算就可求得Fibonacci(4),对于后面执行的Fibonacci(0)、Fibonacci(1),由于函数式语言已经缓存了结果,因此不会重复计算。

5.副作用很少

函数副作用指的是当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响,例如修改全局变量或修改参数。在函数式编程中,低阶函数本身没有副作用,高阶函数不会(很少)影响其他函数,这对于并发和并行来说非常有用。

函数式编程思想与其他编程思想相比,并没有所谓的优劣之分,还是取决于场景,Spark选择Scala也是由于函数式语言在并行计算下的优势非常契合Spark的使用场景。

函数式编程思想在Spark上的体现

Spark的开发语言是Scala,这是Scala在并行和并发计算方面优势的体现,这是微观层面函数式编程思想的一次胜利。此外,Spark在很多宏观设计层面都借鉴了函数式编程思想,如接口、惰性求值和容错等。

  • 函数式编程接口。前面说到,函数式编程思想的一大特点是低阶函数与核心数据结构,在Spark API中,这一点得到了很好的继承。Spark API同样提供了map、reduce、filter等算子(operator)来构建数据处理管道,用户的业务逻辑以高阶函数的形式定义,用户通过高阶函数与算子之间的组合,像搭积木一样,构建了整个作业的执行计划。此外,从根本上来说,Spark最核心的数据结构只有一种:RDD(Resilient Distributed Dataset,弹性分布式数据集),从API上来说,它和普通集合几乎完全相同,但是它却抽象了分布式文件系统中的文件,对于用户来说,这是透明的,从这个角度上来说,RDD是一个分布式的集合。
  • 惰性求值。Spark的算子分为两类,转换(transform)算子和行动(action)算子,只有行动算子才会真正触发整个作业提交并运行。这样一来,无论用户采用了多少个转换算子来构建一个无比复杂的数据处理管道,只有最后的行动算子才能触发整个作业开始执行。
  • 容错。在Spark的抽象中,处理的每一份数据都是不可变的,它们都是由它所依赖的上游数据集生成出来的,依赖关系由算子定义,在一个Spark作业中,这被称为血统。在考虑容错时,与其考虑如何持久化每一份数据,不如保存血统依赖和上游数据集,从而在下游数据集出现可用性问题时,利用血统依赖和上游数据集重算进行恢复。这是利用了函数(血统依赖)在给定参数(上游数据集)情况下,一定能够得到既定输出(下游数据集)的特性。

本文截选自《Spark海量数据处理 技术详解与平台实战》,范东来 著。
Scala与Spark是天生的一对?

  • 基于Spark新版本编写,包含大量的实例
  • 用一个完整项目贯穿整个学习过程的实用Spark学习指南
  • 层次分明、循序渐进,带你轻松玩转Spark大数据

本书基于Spark发行版2.4.4写作而成,包含大量的实例与一个完整项目,技术理论与实战相结合,层次分明,循序渐进。本书不仅介绍了如何开发Spark应用的基础内容,包括Spark架构、Spark编程、SparkSQL、Spark调优等,还探讨了Structured Streaming、Spark机器学习、Spark图挖掘、Spark深度学习、Alluxio系统等高级主题,同时完整实现了一个企业背景调查系统,借鉴了数据湖与Lambda架构的思想,涵盖了批处理、流处理应用开发,并加入了一些开源组件来满足业务需求。学习该系统可以使读者从实战中巩固所学,并将技术理论与应用实战融会贯通。

本书适合准备学习Spark的开发人员和数据分析师,以及准备将Spark应用到实际项目中的开发人员和管理人员阅读,也适合计算机相关专业的高年级本科生和研究生学习和参考,对于具有一定的Spark使用经验并想进一步提升的数据科学从业者也是很好的参考资料。