在数据领域可用的框架中,只有少数框架在采用和交付方面达到了Spark的水平。显然,该框架已经成为一个赢家,特别是在数据工程方面。本文是对Spark组件的一个非常简单的介绍,其主要目的是提供对Spark架构的一般理解。
本文最初发布于 Towards Data Science 博客,由 InfoQ 中文站翻译并分享。
为什么要了解 Spark?
在数据领域可用的框架中,只有少数框架在采用和交付方面达到了 Spark 的水平。显然,该框架已经成为一个赢家,特别是在数据工程方面。
如果你正在阅读这篇文章,这意味着你已经理解了我这样说的原因,所以我们直接进入主题。
为什么要了解 Spark 的内部构造?
有人可能会说,开车并不需要了解发动机的工作原理,是这样。不过,有人可能会说,了解发动机会让你成为更好的驾驶员,因为你将能够了解整个车辆的性能、局限性和根本问题。
同理,你不需要了解 Spark 的内部构造就可以使用它提供的 API。但是,如果你了解的话,就可以减轻糟糕的性能和隐藏的 Bug 所带来的许多痛苦。此外,你还将掌握在整个分布式系统领域随处可见的概念。
方法
在我看来,学习有两个方面: 知识 和 技术 。前者涉及到通过书本、结构化课程等形式获取知识。它更关注 是什么 。后者与技能有关,即“边做边学”,更侧重于 如何做 。这是我们这里要探讨的。
我们将从每个初学者都能解决的一个简单问题开始,然后逐步演进以说明 Spark 的架构设计。在这个过程中,我们还将了解 HDFS (部分人称为 Hadoop),因为它是一个非常适合 Spark 的平台。
为了做到语言无关,本文使用的所有代码都是伪代码。
问题
你刚入职并分得了一项简单的任务: 数一数数组中有多少个偶数 。
你将从存储在本地文件系统中的 CSV 文件中读取该数组。不用多想,你可能会写出下面这段代码:
新需求一
客户对上述解决方案的巨大成功感到满意,现在,他们认为可以把所有问题都交给你,所以他们要求你 计算这些偶数的平均值 。
你肯定知道 SOLID 原则,特别是 单一责任原则 ,即类或方法应该只因为一个原因更改。然而,你决定打破规则,像下面这样实现:
新需求二
由于你做得快,人们又提出了另一个需求: 返回所有偶数的和 。
这时,你不仅开始考虑 SOLID 原则,而且开始考虑事情进行的方式。你知道,通常情况下,一件事发生了一次并不意味着它会发生两次,但如果它发生了两次,第三次发生就在眼前。因此,你开始考虑实现更容易扩展的东西,并记起了 面向对象编程 中封装的概念。
另外,如果你实现了适当的抽象,那么当另一个需求出现时,你甚至可能不必更改你的实现。
一套可以处理所有这些需求的抽象
你开始考虑,如果他们让你数偶数,那么他们很可能会进一步问你奇数,或者是低于或高于某一个值的数,或在一个范围内的数,等等。因此,即使你是 YAGNI (你并不需要它)应用程序方面的专家,也会决定实现一些能够支持所有这些情况的东西。
最后,你可以得出结论,所有这些操作都与从数组中过滤值有关,因此,你决定提供一个过滤器函数,它可以接收过滤器条件,而不是编写每种可能用到的过滤器。
此外,为了简化设计,你决定在每次调用对象操作时更改其状态。
接受新挑战
你做到了。现在,你不仅实现了所有需求,而且还可以处理从数组中过滤值的新需求。如果客户现在想要奇数而不是偶数,那么它们唯一要做的就是向 filter 方法传递一个新条件,这样就行了。真神奇!但是,你一直在等待的新需求来了: 他们现在需要你处理一个 3TB 的数组 。
你考虑放弃。你自己的硬盘只有 500GB,所以你需要 6 台你这样的机器专门用于存储文件才能开始这项工作。但你的客户喜欢你,也很有说服力,他们给你加薪,还承诺提供 30 台新机器用于解决问题,而不是 6 台。
划分
有了 30 台新机器的使用权,你开始考虑如何解决这个问题。一台机器无法包含整个文件,因此你必须将其切成更小的块,然后分块放到新硬盘中。另外,由于你有足够的资源,作为备份,你还可以将相同的切片存储在多台机器上。也许每个切片两个拷贝,这意味着你可以在三个不同的地方找到这个切片。
你格式化硬盘,开始复制文件。在这个过程中,你认为,将所有切片保存在每台机器上遵循同一规范的父文件夹下是个不错的主意,并且要为每个切片加上一个前缀标识符,这和它属于更大文件的哪一部分有关。你还认为,在至少两台机器中另有一个目录,其中包含一些元数据,描述哪些目录包含切片以及对于每个切片 id 哪些机器中包含其备份,也是一个好主意。
由于有些机器将只包含数据,而有些机器只包含提供方向和名称的元数据,因此,你将前者称为数据机器,后者称为名字机器。但是,由于你实际上是在创建一个网络,所以将机器称为节点更合适,因此,你将数据机器命名为 数据节点 ,将元数据机器命名为 名字节点 。
在给事物命名的过程中,你会意识到,切片与蛋糕和奶酪的联系比与数据块的联系更紧密。你感到非常有启发性和创造性,因此决定给这些切片起一个更好的名字: 分区 。因此,无论你的程序最终变成什么,它都会将整个文件划分为多个分区来处理。
在所有这些命名和决策之后,你会得到这样的结果:
你的第一个分布式文件系统
克服挑战
现在,你已经将文件划分为跨一组节点(从现在开始,我们将创造性地称其为 集群 )的分区,并且有备份和元数据,可以帮助你的程序找到每个分区及其备份。既然移动分区没有任何意义,那么问题就变成了: 在每台机器上执行同一段代码都会得到同一个结果吗?
你应该在每次需要运行程序时将整个程序发送到每台机器上吗?或者,你应该准备好程序的一些部分,只需要把客户编写的部分发给它们?后者听起来更好,所以你选了它。在这个过程中,第一个要求是让你的 ArrayOperator 类在每台机器上都可用,且只发送特定于 main 方法的部分。
你还希望要运行的代码尽可能接近数据,因此,数据节点也必须运行程序。从这个角度来看,节点不仅要存储数据,还要执行实际工作,因此,你决定将它们称为工作者( worker )。
代码的某些部分也可以并行运行。例如,对于上面的程序, average() 、 sum() 和 size() 可以并行执行,因为它们是相互独立的。要实现这一特性,工作者需要支持独立的执行线,所以你决定将每个工作者转换成某种守护进程,由它生成独立执行任务的进程(与此同时,你意识到, 任务 这个名称足以指代每个可以独立执行的单元)。你仍然很受启发,于是决定创造性地将那些执行任务的进程称为执行器( executor )。
现在,你所要做的是设计你的主方法(它会访问客户代码),由它推动将客户代码分割成组成作业的任务,然后询问名字节点哪个数据节点包含该文件的哪个分区,然后并行将任务发送给工作者机器,它会准备好启动执行任务并返回结果的执行器。因为这段代码将推动整个过程,同样,你决定创造性地称它为驱动器( driver )。
驱动器还需要找个方法将所有结果汇总在一起。在本例中,它需要将从每个工作者接收到的所有总数相加。但考虑到目前为止取得的进展,这只是小菜一碟。
总而言之,驱动器将协调任务完成作业。这又是你的想象。要描述一组任务,还有哪个名称比作业( job )更合适?
漂亮的工程设计
作出突破
经过几个晚上,你终于把所有的部件组装在一起了。多么了不起的壮举!经过测试,一切都符合预期。你急切地想要做一个演示,在投入了大量的资金之后,客户也同样渴望看到这样的演示。
演示开始时,你先是表扬了自己(这是应该的),然后继续解释架构。你的客户会更加兴奋。你运行程序,可一切都分崩离析,因为你的 5 台机器离线了,2 台是因为内核崩溃,2 台是因为硬盘故障,还有 1 台是因为一个未测试的特性导致了错误。除了你,每个人都哭了。客户失去了信心,但你毫不动摇。实际上,你又表扬了自己一次,因为你已经把一切都弄清楚了。这些问题并不是偶然的。
你承诺一周内会重新做一次演示。客户离开的时候相当的暴躁,并且有点忧伤,但你坚持了下去。
由于每个分区包含两个其他的副本,并且你有 28 台机器(记住,你为名字节点留了 2 台),如果 5 台机器的故障就导致整个集群宕机,那么你很不走运。
但是,如何利用冗余呢?可以肯定的一点是,它应该在驱动器端启动,因为它负责与所有节点通信。如果你有一个节点失败,驱动器就会首先注意到。为了在作业启动时找出分区的位置,驱动器已经与名字节点建立了联系,因此,它可能还可以从名字节点那里获取失败的工作者/数据节点中的所有副本的位置。有了这些信息,它就可以重新发送要在副本上执行的任务,这样就完成了!
使用前面的方法,你用一种 富有弹性 的方式对 分布式数据 进行了 分布式处理 。
就这样去做吧。
崭新的开始
你打电话给客户,要求重新做一次演示。他们还是假装很沮丧,但几乎无法掩饰他们的兴奋。他们来看你,他们进来,说着关于上次的笑话。而你只听到了“蓝屏”,却不太在意其中的笑点。
在开始之前,你做了一件令人震惊的事情:你要求他们随机关闭两个工作者/数据节点。他们看起来很惊讶,但情绪高昂(看他们带着狡黠的微笑着随意地选择机器,试图打败你,这很有趣)。
去掉两个节点后,你就开始演示了,效果非常好。他们哭了,但这次是不同的眼泪。他们向你祝贺,为不相信你而道歉,再次为你提供加薪,当然,还有一个新需求: 数组现在将包含具有多个属性的对象,而不是数字 。更具体地说,这些记录将包含姓名、年龄和工资,他们想知道叫 Felipe 的人的平均年龄和最高工资是多少。他们还希望保存结果,以便以后可以访问,而不需要再处理。
你一点也不惊讶。
锦上添花
在这个时候,你不必想太多。你一直都在做抽象,所以现在的问题是更上一层楼。
你放弃了以前的设计,改成了下面这个样子:
在新的设计下,你现在可以处理任何类型的记录(这就是为什么你将其名称改为 GeneralOperator )。
这真是太神奇了!想想看。你有一个系统,可以读、写和处理任何类型的数据集,并且是用一个分布式的富有弹性的方式。往大了说,你可以声称自己拥有一个支持任何类型数据集的 分布式弹性 框架。
你感觉到了手中的力量,但你认为,魔法的核心 GeneralOperator 名字不够吸引人,或者至少不是那么一目了然。你没有更好的想法,所以你决定称它为弹性分布式数据集阅读器、写入器和处理器( Resilient and Distributed Datasets Reader, Writer and Processor )。但这太长了。也许可以用缩略词,比如 RDDRWP?哎,这更糟。那么只用 RDD 呢?易于发音,听起来也不错,就这样了。
小结
目前为止,你已经完成了如下工作:
你已经设计了一种基础设施,它以分布式方式存储复制的数据分区,它由保存数据的 数据节点 和包含有关元数据的 名字节点 组成(难道它们不应该有自己的名称吗? HDFS 怎么样?)
你创建了一个称为 弹性分布式数据集 (简称 RDD )的结构,它可以读写和处理存储在Hadoop集群中的数据。
你已经设计了一个基础设施,通过 工作者 (控制执行任务的节点)和 执行器 (实际执行任务)在分布式分区上并行执行 任务 。
你设计了一个 驱动器 应用程序,它可以将客户提供的 作业 分解为多个 任务 ,通过与名字节点通信找出分区的位置,并将任务发送给远程工作者。
伙计,你真棒!但你创造的一切不值得有一个好名字吗?你想法很多,火花一个接一个。是的, Spark !听起来像个名字!
你可以这样推销
扩展
你创造的这些东西当然有很大的价值,但它可能有一个陡峭的学习曲线。另一方面,很长一段时间以来(可能太长了), 结构化查询语言 ( SQL )一直是用来处理数据的语言。把这种能力加入 Spark 怎么样?
让我们和客户聊聊。
注意
上面是对 Spark 组件的一个非常简单的介绍,其主要目的是提供对 Spark 架构的一般理解。简便起见,我有意省略了与 Catalyst、调度、类型转换、Shuffling、计划、资源分配、专用 API 方法等相关的内容。我将在以后的文章中讨论。
查看英文原文:
https://towardsdatascience.com/understand-spark-as-if-you-had-designed-it-c9c13db6ac4b
评论