在所有的 Spark 官方文档中都提到,DataFrame API 和 SQL API 对用户来说是同样可行的选项,因为它们在底层使用相同的实现,所以在性能上是相同的。但在实际应用中,实际情况是否真的如此呢?为了找到答案,我决定进行一个基准测试,来比较这两个 API 的性能,结果非常有意思。
几个月前,我需要在Databricks环境中创建一个批处理任务,将一个表的数据每小时从一个表转换到另一个表。当我开始创建任务时,我立刻决定使用Spark DataFrame API会非常适合这个目的。作为一名软件工程师,我更喜欢用Python而不是SQL编写代码,因此我相信Spark DataFrame API会比纯SQL提供更友好的开发接口。
使用DataFrame API实现这个任务非常简单,很容易。然而,当我们开始定期运行这个批处理作业时,我们开始遇到执行时间超过一小时的情况。这意味着下一个批次会在上一个批次完成之前就开始运行。虽然这本身不算问题,但随着数据量的增长,可能会导致多个批处理作业同时运行,从而互相干扰。为了缓解这种情况,我们决定提升批处理作业的性能,以减少此类问题的出现。
当我们设计策略来提高性能时,我的同事建议把DataFrame API换成SQL API,他说,根据他的经验,SQL在性能上总是比其他API更好。起初,我对这一点表示怀疑,因为Spark的文档始终提到,DataFrame和SQL API在实现和性能上基本上是一样的,仅在风格上有不同,对于那些更喜欢用SQL查询而不是写代码的人来说。
为了验证这个说法——同时,因为我喜欢做性能测试和基准测试——我决定在一个测试环境中比较DataFrame和SQL API。我创建了一个大型数据集,用于两个API,并设计了四个场景,并在两个API中以相同的方式实现了它们。第一个场景是一个简单的WHERE条件和COUNT函数,第二个场景用了一个简单的UDF,第三个场景用了一个复杂的UDF(利用了Mandelbrot集),第四个场景使用了GROUP BY。
例如,第一个场景是关于简单计数的,这里展示的是DataFrame代码:
df.where(col("id") % 2 == 0).count()
df
数据框中筛选出 "id" 列为偶数的行,并计算这些行的数量。
下面就是对应的SQL代码:
spark.sql("SELECT COUNT(*) FROM numbers WHERE id % 2 = 0").collect()[0][0]
# 计算表numbers中id为偶数的记录数
我每种场景都运行了数百次之多,并计算了平均运行时间,都在同一台机器上进行。结果出人意料:
我的测试结果显示,在所有测试的场景中,SQL API 在所有情况下都胜过 DataFrame API。虽然在一些场景中,两者差异不大,SQL 稍占优势,但在其他场景中,SQL 表现明显更佳,速度甚至快了两倍以上!
回到最初的挑战,我们将 DataFrame API 中的任务转换为使用 SQL API,结果令人惊讶:原本通常需要 40 至 70 分钟的任务,现在只需 6 至 15 分钟。
另一个改进是,那些之前不熟悉 Spark 的团队成员现在可以很容易地理解整个任务的流程。对于大多数技术人员来说,SQL 是一种熟悉的技术,而对之前未曾使用过它的开发人员来说,Spark DataFrame API 可能看起来比较复杂。
总体而言,这次经历强调了选择合适工具的重要性。除了性能之外,维护也是一个关键的考虑因素——谁来维护代码,他们是否熟悉Spark DataFrame API?这进一步说明了SQL API的优势,因为SQL被广泛熟知,即使不经常使用它的人也能维护。
更多关于Spark性能的信息,参阅我比较PySpark和Scala Spark的文章:我们是否应该停止使用Python来进行Spark作业?