02 | 性能调优的本质:调优的手段五花八门,该从哪里入手?

你好,我是吴磊。

上节课,我们探讨了性能调优的必要性,结论是:尽管Spark自身运行高效,但作为开发者,我们仍然需要对应用进行性能调优。

那么问题来了,性能调优该怎么做呢?面对成百上千行应用代码、近百个Spark配置项,我们该从哪里入手呢?我认为,要想弄清性能调优怎么入手,必须先得搞明白性能调优的本质是什么。

所以今天这节课,咱们就从一个先入为主的调优反例入手,带你一起探讨并归纳性能调优的本质是什么,最终帮你建立起系统化的性能调优方法论。

先入为主的反例

在典型的ETL场景中,我们经常需要对数据进行各式各样的转换,有的时候,因为业务需求太复杂,我们往往还需要自定义UDF(User Defined Functions)来实现特定的转换逻辑。但是,无论是Databricks的官方博客,还是网上浩如烟海的Spark技术文章,都警告我们尽量不要自定义UDF来实现业务逻辑,要尽可能地使用Spark内置的SQL functions。

在日常的工作中,我发现这些警告被反复地用于Code review中,Code reviewer在审查代码的时候,一旦遇到自定义的UDF,就提示开发的同学用SQL functions去重写业务逻辑,这几乎成了一种条件反射。

甚至,开发的同学也觉得非常有道理。于是,他们花费大量时间用SQL functions重构业务代码。但遗憾的是,这么做之后ETL作业端到端的执行性能并没有什么显著的提升。这种情况就是所谓的投入时间与产出不成正比的窘境:调优的时间没少花,却没啥效果

之所以会出现这种情况,我觉得主要原因在于Code reviewer对于性能调优的理解还停留在照本宣科的层次,没有形成系统化的方法论。要建立系统化的方法论,我们就必须去探究性能调优的本质到底是什么。否则,开发者就像是掉进迷宫的小仓鼠,斗志昂扬地横冲直撞,但就是找不到出口。

既然性能调优的本质这么重要,那么它到底是什么呢?

性能调优的本质

探究任何一个事物的本质,都离不开从个例观察、现象归因到总结归纳的过程。

咱们不妨先来分析分析上面的反例,为什么用SQL functions重构UDF并没有像书本上说的那么奏效?

是因为这条建议本身就不对吗?肯定不是。通过对比查询计划,我们能够明显看到UDF与SQL functions的区别。Spark SQL的Catalyst Optimizer能够明确感知SQL functions每一步在做什么,因此有足够的优化空间。相反,UDF里面封装的计算逻辑对于Catalyst Optimizer来说就是个黑盒子,除了把UDF塞到闭包里面去,也没什么其他工作可做的。

那么,是因为UDF相比SQL functions其实并没有性能开销吗?也不是。我们可以做个性能单元测试,从你的ETL应用中随意挑出一个自定义的UDF,尝试用SQL functions去重写,然后准备单元测试数据,最后在单机环境下对比两种不同实现的运行时间。通常情况下,UDF实现相比SQL functions会慢3%到5%不等。所以你看,UDF的性能开销还是有的。

既然如此,我们在使用SQL functions优化UDF的时候,为什么没有显著提升端到端ETL应用的整体性能呢?

根据木桶理论,最短的木板决定了木桶的容量,因此,对于一只有短板的木桶,其他木板调节得再高也无济于事,最短的木板才是木桶容量的瓶颈。对于ETL应用这支木桶来说,UDF到SQL functions的调优之所以对执行性能的影响微乎其微,根本原因在于它不是那块最短的木板。换句话说,ETL应用端到端执行性能的瓶颈不是开发者自定义的UDF。

结合上面的分析,性能调优的本质我们可以归纳为4点。

  1. 性能调优不是一锤子买卖,补齐一个短板,其他板子可能会成为新的短板。因此,它是一个动态、持续不断的过程。
  2. 性能调优的手段和方法是否高效,取决于它针对的是木桶的长板还是瓶颈。针对瓶颈,事半功倍;针对长板,事倍功半。
  3. 性能调优的方法和技巧,没有一定之规,也不是一成不变,随着木桶短板的此消彼长需要相应的动态切换。
  4. 性能调优的过程收敛于一种所有木板齐平、没有瓶颈的状态。

基于对性能调优本质的理解,我们就能很好地解释日常工作中一些常见的现象。比如,我们经常发现,明明是同样一种调优方法,在你那儿好使,在我这儿却不好使。再比如,从网上看到某位Spark大神呕心沥血总结的调优心得,你拿过来一条一条地试,却发现效果并没有博客中说的那么显著。这也并不意味着那位大神的最佳实践都是空谈,更可能是他总结的那些点并没有触达到你的瓶颈。

定位性能瓶颈的途径有哪些?

你可能会问:“既然调优的关键在于瓶颈,我该如何定位性能瓶颈呢?”我觉得,至少有两种途径:一是先验的专家经验,二是后验的运行时诊断。下面,我们一一来看。

所谓专家经验是指在代码开发阶段、或是在代码Review阶段,凭借过往的实战经验就能够大致判断哪里可能是性能瓶颈。显然,这样的专家并不好找,一名开发者要经过大量的积累才能成为专家,如果你身边有这样的人,一定不要放过他!

但你也可能会说:“我身边要是有这样的专家,就不用来订阅这个专栏了。”没关系,咱们还有第二种途径:运行时诊断。

运行时诊断的手段和方法应该说应有尽有、不一而足。比如:对于任务的执行情况,Spark UI提供了丰富的可视化面板,来展示DAG、Stages划分、执行计划、Executor负载均衡情况、GC时间、内存缓存消耗等等详尽的运行时状态数据;对于硬件资源消耗,开发者可以利用Ganglia或者系统级监控工具,如top、vmstat、iostat、iftop等等来实时监测硬件的资源利用率;特别地,针对GC开销,开发者可以将GC log导入到JVM可视化工具,从而一览任务执行过程中GC的频率和幅度。

对于这两种定位性能瓶颈的途径来说,专家就好比是老中医,经验丰富、火眼金睛,往往通过望、闻、问、切就能一眼定位到瓶颈所在;而运行时诊断更像是西医中的各种检测仪器和设备,如听诊器、X光、CT扫描仪,需要通过量化的指标才能迅速定位问题。二者并无优劣之分,反而是结合起来的效率更高。就像一名医术高超、经验丰富的大夫,手里拿着你的血液化验和B超结果,对于病灶的判定自然更有把握。

你可能会说:“尽管有这两种途径,但是就瓶颈定位来说,我还是不知道具体从哪里切入呀!”结合过往的调优经验,我认为,从硬件资源消耗的角度切入,往往是个不错的选择。我们都知道,从硬件的角度出发,计算负载划分为计算密集型、内存密集型和IO密集型。如果我们能够明确手中的应用属于哪种类型,自然能够缩小搜索范围,从而更容易锁定性能瓶颈。

不过,在实际开发中,并不是所有负载都能明确划分出资源密集类型。比如说,Shuffle、数据关联这些数据分析领域中的典型场景,它们对CPU、内存、磁盘与网络的要求都很高,任何一个环节不给力都有可能形成瓶颈。因此,就性能瓶颈定位来说,除了从硬件资源的视角出发,我们还需要关注典型场景

性能调优的方法与手段

假设,通过运行时诊断我们成功地定位到了性能瓶颈所在,那么,具体该如何调优呢?Spark性能调优的方法和手段丰富而又庞杂,如果一条一条地去罗列,不仅看上去让人昏昏欲睡,更不利于形成系统化的调优方法论。就像医生在治疗某个病症的时候,往往会结合内服、外用甚至是外科手术,这样多管齐下的方式来达到药到病除的目的。

因此,在我看来, Spark的性能调优可以从应用代码和Spark配置项这2个层面展开。

应用代码层面指的是从代码开发的角度,我们该如何取舍,如何以性能为导向进行应用开发。在上一讲中我们已经做过对比,哪怕是两份功能完全一致的代码,在性能上也会有很大的差异。因此我们需要知道,开发阶段都有哪些常规操作、常见误区,从而尽量避免在代码中留下性能隐患

Spark配置项想必你并不陌生,Spark官网上罗列了近百个配置项,看得人眼花缭乱,但并不是所有的配置项都和性能调优息息相关,因此我们需要对它们进行甄别、归类

总的来说,在日常的调优工作中,我们往往从应用代码和Spark配置项这2个层面出发。所谓:“问题从代码中来,解决问题要到代码中去”,在应用代码层面进行调优,其实就是一个捕捉和移除性能BUG的过程。Spark配置项则给予了开发者极大的灵活度,允许开发者通过配置项来调整不同硬件的资源利用率,从而适配应用的运行时负载。

对于应用代码和Spark配置项层面的调优方法与技巧,以及这些技巧在典型场景和硬件资源利用率调控中的综合运用,我会在《性能篇》逐一展开讲解,从不同层面、不同场景、不同视角出发,归纳出一套调优的方法与心得,力图让你能够按图索骥、有章可循地去开展性能调优。

性能调优的终结

性能调优的本质告诉我们:性能调优是一个动态、持续不断的过程,在这个过程中,调优的手段需要随着瓶颈的此消彼长而相应地切换。那么问题来了,性能调优到底什么时候是个头儿呢?就算是持续不断地,也总得有个收敛条件吧?总不能一直这么无限循环下去。

在我看来,性能调优的最终目的,是在所有参与计算的硬件资源之间寻求协同与平衡,让硬件资源达到一种平衡、无瓶颈的状态

我们以大数据服务公司Qubole的案例为例,他们最近在Spark上集成机器学习框架XGBoost来进行模型训练,在相同的硬件资源、相同的数据源、相同的计算任务中对比不同配置下的执行性能。

从下表中我们就可以清楚地看到,执行性能最好的训练任务并不是那些把CPU利用率压榨到100%,以及把内存设置到最大的配置组合,而是那些硬件资源配置最均衡的计算任务。

小结

只有理解Spark性能调优的本质,形成系统化的方法论,我们才能避免投入时间与产出不成正比的窘境。

性能调优的本质,我总结为4点:

  1. 性能调优不是一锤子买卖,补齐一个短板,其他板子可能会成为新的短板。因此,它是一个动态、持续不断的过程;
  2. 性能调优的手段和方法是否高效,取决于它针对的是木桶的长板还是瓶颈。针对瓶颈,事半功倍;针对长板,事倍功半;
  3. 性能调优的方法和技巧,没有一定之规,也不是一成不变,随着木桶短板的此消彼长需要相应地动态切换;
  4. 性能调优的过程收敛于一种所有木板齐平、没有瓶颈的状态。

系统化的性能调优方法论,我归纳为4条:

  1. 通过不同的途径如专家经验或运行时诊断来定位性能瓶颈;
  2. 从不同场景(典型场景)、不同视角(硬件资源)出发,综合运用不同层面(应用代码、Spark配置项)的调优手段和方法;
  3. 随着性能瓶颈的此消彼长,动态灵活地在不同层面之间切换调优方法;
  4. 让性能调优的过程收敛于不同硬件资源在运行时达到一种平衡、无瓶颈的状态。

每日一练

  1. 你还遇到过哪些“照本宣科”的调优手段?

  2. 你认为,对于性能调优的收敛状态,即硬件资源彼此之间平衡、无瓶颈的状态,需要量化吗?如何量化呢?

关于性能调优,你还有哪些看法?欢迎在评论区留言,我们下节课见!

精选留言

  • October

    2021-03-17 15:10:26

    老师讲到可以通过运行时诊断来定位系统瓶颈,可借助于spark ui以及系统级监控工具,但是我依然不清楚怎样查看spark ui的各个指标,怎样查看每个应用程序的各种硬件负载,不知道老师后边的课程有没有相关内容
    作者回复

    好问题,不过专栏最开始的计划没有包含这部分,原因是这部分其实网上都能查到,不过如果后续大家对这块疑问较多,到时候咱们也可以加餐,专门说说监控的事儿。

    2021-03-18 15:08:15

  • seed

    2021-03-17 05:59:14

    1. 还遇到的调优手段:直接从网上copy过来一些参数,在没有理解真正的原理的情况下,先怼上去,跑一下,任务时间缩短了就算调优了
    2. 对于性能调优的收敛状态,需要量化;如何量化这些指标?
    其实就是从我们需要调优的点出发;文中提到:从硬件的角度来看,计算负载划分为计算密集型、内存密集型和 IO 密集型。
    首先需要确认我们的任务属于哪一类型的任务或者说任务的短板在哪一块
    其次从Spark任务执行时长,系统的cpu/memory/io等方面按照任务类型有针对性的进行监控
    然后从应用代码和Spark配置项两个方面入手进行调优
    最后在每次调优后重点关注任务时间是否下降,对比下降前后系统的cpu/memory/io的使用量,就可以做到量化
    作者回复

    说得非常好!

    1的场景非常常见,很多时候都是知其然,而不知其所以然,稀里糊涂就算完事了,交差就行。

    2的回答满分💯,无可挑剔。

    2021-03-17 13:21:57

  • Sandy

    2021-07-07 05:56:51

    老师,已经学完课了,发现没有讲运行时诊断分析析定位性能瓶颈呢?
    作者回复

    好问题,这部分咱们主要是给出了方法论,确实没有介绍具体的监控方法。是这样的,一般定位性能瓶颈,我们都会从硬件的计算资源出发。我们往往把一项操作、或者计算,归类为CPU密集型、内存密集型、I/O密集型和网络密集型。对于不同硬件资源的消耗,我们会推荐使用不同的系统命令,或是可视化的监控工具(如Ganglia、Prometheus等),工具这块咱们文中有举例。

    这块不太好讲,难点在于,我们需要结合非常具体的cases,来介绍具体的方法,但是介绍的方法,不见得适用你日常开发中遇到的cases。所以咱们只抛出了方法论,却没有深入介绍具体的方法。

    不过,其实即便没有这些工具辅助,或者没有案例分析,结合经验,我们也能大概知道哪些操作属于哪种密集型操作,从而有的放矢地去调整不同资源消耗的平衡,比方说:

    CPU密集型:(解)压缩、(反)序列化、hash、排序
    内存密集型:大量的RDD Cache、DataFrame Cache、数据倾斜场景,等等
    磁盘密集型:Shuffle
    网络密集型:Shuffle

    实际上,不止是磁盘与网络,Shuffle会消耗所有的计算资源,因此一般来说,在Spark的应用当中,99%的性能瓶颈,都来自Shuffle。因此,一旦把Shuffle这头拦路虎干掉,性能往往呈数量级地提升。

    东一榔头、西一棒子,说的有点乱,不知道能不能回答你的困惑,有问题再讨论哈~

    2021-07-14 16:09:37

  • 裘元飞

    2021-03-18 16:20:27

    希望老师可以稍微讲一讲例如spark UI等工具如何监控指标等,主要很多时候都不知道有这些工具的存在。
    作者回复

    可以的,后面找机会讲一讲,到时候看大家需要,可以开个直播当面讲,比较直观,图文讲UI很多细节都说不到。

    2021-03-19 09:00:39

  • Geek_32772e

    2021-05-26 21:45:28

    吴老师,我没听说过有文章说不建议使用UDF,您能给我发几个链接证实下吗?
    作者回复

    太多了,可以随便在搜索引擎里面搜“Spark UDF performance”之类的关键字,(用Google或是Bing的国际版),比如这里:https://stackoverflow.com/questions/38296609/spark-functions-vs-udf-performance,非常详尽地介绍了UDF与Spark原生的SQL functions性能差在哪里~

    另外,建议关注Databricks官方的技术博客,里面也有几篇讲到UDF与Spark原生SQL functions之间的性能差异,我稍后翻到了再发给你~

    2021-05-26 23:18:03

  • Shockang

    2021-03-17 19:31:04

    王家林,段智华,夏阳编著的《Spark大数据商业实战三部曲》里面提到——大数据性能调优的本质是什么?答案是基于硬件的调优,即基于CPU、Memory、I/O(Disk/Network)基础上构建算法和性能调优! 无论是Hadoop,还是Spark,还是其他技术,都无法逃脱。 老师也在文章中提到——性能调优的最终目的,是在所有参与计算的硬件资源之间寻求协同与平衡,让硬件资源达到一种平衡、无瓶颈的状态。 我认为有异曲同工之妙!
    作者回复

    没错,性能调优的本质,其实就是用软件驱动硬件,让硬件在运行时能够高效协同,充分压榨每一类硬件资源的“剩余价值”。最早在IBM那会,可能是2011年吧,我们做IBM智能分析系统一体机,就是用软硬搭配的方式做数仓。那会对于硬件资源的协同,我们甚至有量化的比例,我记得是1:4:8,1cpu:4g内存:8块SAS盘。那会很早了,还用RAID阵列来提高I/O效率,当时网络用了最高配置,应该是万兆网我记得。虽然说场景不同,但是以硬件资源平衡为导向的思路,如出一辙。

    2021-03-18 14:59:26

  • 薛峰

    2021-04-09 05:28:58

    第一个估计大家都有过那样的经验
    第二个问题,我用过两种方法:
    1. 用Prometheus+grafana监控系统利用情况。结果意外发现spark不会自动清除应用目录,导致过段时间磁盘都被塞满了。不知道是不是因为spark配置的错误导致的?
    2. 用不同的参数跑同样的数据N次,然后平均处理时间,看看哪种最短。但经常会发现这个运算时间有较大幅度的浮动,不知道是不是跟aws系统整体的忙闲有关。
    作者回复

    1. 对,两个不错的监控工具~ 运行时监控系统利用率。另外,spark应该自动清除临时目录,听上去是个bug。

    2. 后验是最实在的方式,不过这块,就像你说的,要注意系统误差,也就是你得保证n次尝试的运行时环境是一样的,否则的话很难判断,是调优的作用,还是系统变化。

    2021-04-09 15:47:08

  • 浩然

    2021-10-11 00:08:47

    我想讨论一个问题。那个38分钟的执行结果,资源配置上很足,但是执行效率不好。能认为是把CPU用的太多了,资源调度有问题?比如CAS自旋锁以及锁升级?
    作者回复

    好问题,我倒不觉得是资源调度的问题,而是任务调度的问题。可以看到,执行效率不高的几个case,都把并行度设置的很大,并行度高就会存在任务调度开销过大的隐患,从而造成CPU看上去很忙,但忙的都不是“正事”,都忙着去调度任务了,而不是忙着执行任务。后面的CPU利用率那一讲,会详细聊这些内容,不妨关注一下~

    2021-10-11 23:09:37

  • L3nvy

    2021-03-17 11:07:44

    1. 不知道怎么分析性能瓶颈问题,按照自己的理解把业务逻辑Spark SQL重写了一遍,虽然时间下降了,但是引入了数据质量等问题。
    2. 需要监控组件对机器的状态和系统metrics进行收集对比优化效果
    作者回复

    没错,1是典型的盲目调优,2的思路很赞~

    2021-03-17 13:18:44

  • 西南偏北

    2021-05-01 17:02:56

    1. 最初在写Spark应用的时候,网上搜一些调优手段,动不动就是让增加内存、增加CPU,什么问题都是增加内存CPU
    2. 如果是搭建的CDH平台的话,可以结合Spark WebUI和Cloudera Manager Web UI做一些监控资源的对比,比如任务运行时间、cpu使用率、内存使用率等指标。另外,也可以安装Prometheus+Grafana进行更精细化的监控。

    希望老师在之后的篇章中,讲具体调优方法的时候,能多结合一下原理和源码来分析下为什么要这样调优。
    作者回复

    对,第一个是非常的典型case~
    第二个监控也没问题,可以结合你说的这些工具,对硬件资源的消耗情况进行量化监控,现在的UI都做得比较到位了,指标既丰富又直观。

    另外,后面的章节安排,大体上就是按照你说的思路展开的哈~ 😁

    2021-05-02 17:20:03

  • Fendora范东_

    2021-04-02 00:03:38

    磊哥说到了SQL functions指的是sparkSQL内置函数吧。
    还提到了udf和sql functions的性能差异,那如果我用内置函数方式开发udf,理论上性能就是一样的?
    作者回复

    一般来说,sql func会比udf强3%~5%,不等。但这只是他们本身的对比,实际在端到端的作业能提升多少,具体还有看场景。但能用sql func就尽量不用udf指定是没错~

    2021-04-02 09:01:08

  • Gnnn

    2021-03-17 11:09:12

    简单的增加并行度很多时候是没有太大效果的
    作者回复

    没错,并行度需要结合很多东西、尤其是cpu和内存方面的配置一起调整,才能起作用。

    2021-03-17 13:16:24

  • 光羽隼

    2023-10-13 10:46:09

    问题一:以前会从同时代码中直接复制配置项;直接在代码某些地方使用hint
    问题二:性能调优的过程对于每个任务来说,情况不同,可能所使用的调优方式也不同,而且随着任务中使用的数据量变化,调优的方式也会随之变化。量化的点我觉得有两个,运行时长和稳定性。时长是否降低到合适的范围,任务在每天的运行过程中是否稳定
  • 西关八号

    2021-06-10 20:31:52

    希望老师可以稍微讲一讲spark UI等工具如何监控指标
    作者回复

    好的,好的~ 后面会用加餐的形式,把Spark UI这部分补上~

    2021-06-11 10:55:25

  • sparkjoy

    2021-03-23 08:12:56

    老师,spark作业运行的网络性能怎么监控?目前我们只有shuffle读写量和速率相关,还有其他手段吗?谢谢!
    作者回复

    可以考虑结合ganglia做资源监控哈~ UI比较直观

    2021-03-23 12:43:49