从 Spark 的数据结构演进说开

搞大数据的都知道 Spark,照例,我不会讲怎么用,也不打算讲怎么优化,而是想从 Spark 的核心数据结构的演进,来看看其中的一些设计和考虑,有什么是值得我们借鉴的。我想这些思想和理念才是更持久和通用的东西。

RDD

和很多流行的开源项目一样,Spark 脱胎于一篇论文(文末阅读原文),而这篇论文的题目就是《Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing》。光从这点就能看出来 RDD 在 Spark 中所处的核心位置。这很正常,正如你在无数场合听到人说数据结构和算法是最基础核心的东西。

先有理论,再去实践。 再简单不过的道理,学术届做的很好,工业界也需要。哪怕不出论文,扎实的理论研究和调研也是必须的。

我们学习一个新东西的时候,首先应该想的,自然是这个东西是什么。紧接着就应该是为什么会有这个东西,想要解决什么问题。

论文里说的很清楚,Spark 起初主要面向两个领域:

  • 迭代算法(iterative algorithms)

  • 交互式数据处理(interactive data mining tools)

这两个领域是当时主流计算框架都做的不好的地方,正好又是大数据重度应用的场景。

其他框架哪里做的不好呢?

主要是慢。

为什么慢呢?

主要是大量的 IO,尤其是磁盘 IO(网络 IO 可以一定程度上通过 data locality 缓解)。

怎么快起来呢?

很简单,也很通用的办法,放内存。

放内存已经有现成的 Redis/Memcache 那些啊,并且大数据可是很大的啊,内存放不下啊。

使用太麻烦,大数据应用通常不需要粒度细到具体某条或者某个数据结构的操作,只要数据整体在内存就好。说白了,希望能封装成自动读写的缓存,对应用层透明。

放不下的问题好解决,分布式起来。嫌成本高就控制总的内存消耗,超过配额的 flush 到磁盘。

但一旦分布式起来,不可回避的一个问题,就是高可用。

高可用的常规实现方法是加副本,大家都耳熟能详。但是副本直接带来成倍增加的成本,而一旦涉及大数据,这个额外开销是非常高的。

高到宁愿重算。但是如果要重算,那就无所谓高可用了。

所以只能折中,要尽可能控制住重算的范围,只算真正丢失的部分,而不要整体重算。

折中,权衡,或者叫 trade-off,是编程领域,尤其是分布式领域,经常要面临的问题。很多时候,没有完美的方案,只能做取舍。

部分重算的关键点有两个,一个是对任务和数据做切分,一个是记住计算的逻辑。

对任务和数据的拆分,这个在分布式系统里也有普遍应用,HDFS 的 block,Hive 的 partition 等,都是很经典的例子。在 Spark 里,把任务拆分成一个个 task,把数据拆分成一个个 partition。这样就能最小粒度的去调度任务和处理数据了。

而记住计算的逻辑,或者叫 pipeline,才知道在局部重算的时候到底该怎么算。Spark 把这个信息作为 RDD 的核心属性固化了下来,可以通过 RDD.dependencies() 方法直接查看这个 RDD 都依赖了哪些父 RDD。

这样,任何一个 task 的任何一个 partition 丢掉了,都能很容易的以最小范围重算出来。以此类推,就能得到一张完整的 DAG 图,描述整个任务的血缘关系(lineage)。

如果重算的代价特别大,Spark 也提供了 checkpoint 的功能,支持保存计算中间状态,失败后可以从 checkpoint 恢复。当然,checkpoint 也是有成本的,不应该随便乱用。这又是一个要做取舍的地方。

Spark 早期的假想敌是 MapReduce,但 Spark 是个很有野心的项目,她要的是一统天下。

现在有了 RDD 这样一个基于内存、可容错的分布式数据结构,在这个坚实的基础之上,很快就能把触角伸到其他细分领域。

  • Spark Core 面向通用的分布式批处理。

  • Spark Streaming 致力于解决流处理问题。

  • Spark MLlib 让机器学习变得更容易。

  • Spark GraphX 把图计算也囊括在内。

小结下,Spark(RDD) 的出现和流行,得益于这些考虑:

  • 解决了实际痛点,这个痛点不仅是应用场景的痛点,也是竞品的痛点。

  • 从解决直接痛点出发,提出了通用型的数据结构,使得能很快的复制扩散到其他应用场景。

  • 在可用性和成本权衡不下的时候,提出了 DAG 这样折中的办法,比较合理的解决了问题。

DataFrame

除了性能上的提升外,RDD 丰富强大的表达能力,又大大降低了开发成本,其实已经能很好的替代 MapReduce 了。但 MapReduce 在很多场景下其实早就有了「替代者」-- Hive。

RDD 的表达能力相比 Hive SQL 并没有什么优势,因此局面很容易从

  • 默认用 Hive

  • Hive 做不了用 MR

变成

  • 默认用 Hive

  • Hive 做不了用 Spark

前面说过,Spark 的野心是要一统天下,那就得支持 SQL,毕竟 SQL 是 Universal Language,是编程领域的英语。

先来思考一个问题,SQL 的核心是什么?

是关系。然后才有基于关系的各种运算,各种主外键,各种 join 等等。

那关系的核心又是什么?

是由具体事物抽象出的对象以及对象间的联系。这里的对象和面向对象里的对象本质上是一样的。面向对象的抽象核心是一个个的 class 及其实例化出来的 object。而关系的抽象核心是一张张表和遵循表的要求插入的一行行数据。

再换一个角度,一堆文本数据生成表的前后,主要的区别是什么?

是每个字段都有了名字和类型。

这两个角度结合起来,站在数据处理的角度,从 RDD 到 SQL,缺少的就是对数据含义和类型的描述,也就是 Schema。

于是有了 DataFrame。简单来说 DataFrame = RDD + Schema。

至于 SQL 语法的支持,只是 API 的转换而已。比如之前分享过的 Apache Calcite,就很容易做到从 SQL 到 DataFrame API 的转换。

当然,Spark 并没有直接用 Calcite,而是自己造了轮子,也就是非常有名的 Catalyst 项目。

另外,SQL 带来的好处,不仅仅是开发成本更低,还有很多通用的性能优化。

说句题外话,SQL 能做大量优化,正是因为她在语法上就只提供了有限的操作类型。这是 SQL 想要降低开发成本的初衷带来的额外好处,也是牺牲灵活性换来的福利。

SQL 性能优化又是个很大的话题了,包括 RBO 和 CBO,这里就不展开了,可以参考文末的相关文章了解,后面我也计划专门写一篇文章讲讲 Spark 的 CBO。

另外,很自然的,SQL 带来的性能提升,又反过来促进了 Spark 的流行。

DataFrame 和 Spark SQL 是如此的理所应当和好用,顺其自然导致了 Spark 在几个细分应用领域从 RDD 到 DataFrame 的变革:

  • Spark Core => DataFrame & Spark SQL

  • Spark Streaming => Spark Structured Streaming

  • Spark MLlib => Spark ML

  • Spark GraphX => 没有官方 DataFrame 实现,而是以第三方包(GraphFrames)的形式提供

小结下,从 RDD 到 DataFrame 和 Spark SQL,Spark 在两方面下了功夫:

  • 补齐了自己相较于竞争对手的短板,对 SQL 的支持,大大降低了开发成本,提高了开发效率,扩展了适用人群

  • 通过 Catalyst 优化器,实现了对执行计划的自动优化,大大提升了性能,降低了调优成本。

DataSet

早些年,有个问题很火:Python 为什么不适合大型项目?其中一个很多支持者的观点就是,Python 是动态语言,缺少类型检查,很多问题会在线上跑了很久之后突然暴露出来。为此需要写大量的测试,甚至很多情况测试也很难覆盖到。

而静态强类型的 Java、C++ 等就不会有这个麻烦。很多问题在编译期就能自动发现。

Spark 不是用 Scala 写的吗,为什么会有类型问题?

问题就出在 DataFrame。

我们在创建一个 RDD 的时候,是能明确知道它的类型的。

但是 DataFrame 的类型是什么?是 Row(org.apache.sql.Row)。这也很好理解,一张表,一个数据集,本来就是一行行数据聚在一起。这个抽象是很贴近现实的。

但是这却为类型检查带来了困难。虽然有 schema,我们很容易通过反射,根据名字得到字段值和类型。但是比如访问了一个不存在的列是不会报错的。很可能前面程序跑了 10 个小时了,突然到下一行,因为写错了列名就前功尽弃了。

这种错误理应在编译时就发现,而不是运行时爆掉。

而要想在编译时发现这些问题,就必须给 DataFrame 带上类型。像 RDD 那样的类型,而不是 Row 这种 generic type。

DataSet 就是指定了 Row 的 type 的 DataFrame。定义一个 Case Class B,再 DataSetA.as[B] 就指定了类型,非常简单。

回过头来看看。RDD 虽然通用,但确实易用性不好,性能也差;DataFrame 虽然易用性好,性能也强,但发现不了编译问题,并且不够通用,总有写 SQL 搞不定的地方。

这个时候,DataSet 横空出世,兼具了 DataFame 的关系属性,又有了 RDD 的强类型属性。

并且很自然的,DataSet 提供了两种类型的 API:typed API 和 untyped API,前者类似 RDD API,是一些通用的方法,返回值是强类型的 DataSet[U];后者类似 DataFrame API,是一些关系类的方法,返回值是无类型的 DataSet[Row]。

但带来的就是一个略显混乱的局面:三种 API,到底该用哪个?

用户不知道该选哪个的时候,应该怎么办?

不让用户选。或者说自动帮用户选。

于是,在 Scala API 里,DataFrame 变成了一种特殊的 DataSet。

type DataFrame = Dataset[Row]

在 Java API 里,甚至没有了 DataFrame 这个类型,直接就是 DataSet<Row>。

从 DataFrame 到 DataSet,我们可以总结出 Spark 的一些考虑:

  • 演进和兼容,而不是推倒重来。

  • 不要给用户太多选择,替用户做选择。  


从 RDD 到 DataFrame,再到 DataSet,这么梳理下来,我们能很清晰的看到 Spark 这个项目在数据结构上的演进过程。

更重要的是,为什么要做这些演进,演进过程中碰到的问题又应该怎么去处理,尤其是有些需要权衡的地方,要怎么去取舍。

如开头所说,我想,这些才是更重要的东西。

640
坚持原创
640?wx_fmt=jpeg
值得分享
640
640

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值