This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
<audio id="audio" title="01 | 性能调优的必要性Spark本身就很快为啥还需要我调优" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/39/11/3962b64bd80223574cfa5039ddd22511.mp3"></audio>
你好,我是吴磊。
在日常的开发工作中我发现有个现象很普遍。很多开发者都认为Spark的执行性能已经非常强了实际工作中只要按部就班地实现业务功能就可以了没有必要进行性能调优。
你是不是也这么认为呢确实Spark的核心竞争力就是它的执行性能这主要得益于Spark基于内存计算的运行模式和钨丝计划的锦上添花以及Spark SQL上的专注与发力。
但是,真如大家所说,**开发者只要把业务逻辑实现了就万事大吉了吗**?这样,咱们先不急于得出结论,你先跟着我一起看两个日常开发中常见的例子,最后我们再来回答这个问题。
在数据应用场景中ETLExtract Transform Load往往是打头阵的那个毕竟源数据经过抽取和转换才能用于探索和分析或者是供养给机器学习算法进行模型训练从而挖掘出数据深层次的价值。我们今天要举的两个例子都取自典型ETL端到端作业中常见的操作和计算任务。
## 开发案例1数据抽取
第一个例子很简单给定数据条目从中抽取特定字段。这样的数据处理需求在平时的ETL作业中相当普遍。想要实现这个需求我们需要定义一个函数extractFields它的输入参数是Seq[Row]类型也即数据条目序列输出结果的返回类型是Seq[(String, Int)]也就是String, Int对儿的序列函数的计算逻辑是从数据条目中抽取索引为2的字符串和索引为4的整型。
应该说这个业务需求相当简单明了实现起来简直是小菜一碟。在实际开发中我观察到有不少同学一上来就迅速地用下面的方式去实现干脆利落代码写得挺快功能也没问题UT、功能测试都能过。
```
//实现方案1 —— 反例
val extractFields: Seq[Row] =&gt; Seq[(String, Int)] = {
(rows: Seq[Row]) =&gt; {
var fields = Seq[(String, Int)]()
rows.map(row =&gt; {
fields = fields :+ (row.getString(2), row.getInt(4))
})
fields
}
}
```
在上面这个函数体中是先定义一个类型是Seq[(String, Int)]的变量fields变量类型和函数返回类型完全一致。然后函数逐个遍历输入参数中的数据条目抽取数据条目中索引是2和4的字段并且构建二元元组紧接着把元组追加到最初定义的变量fields中。最后函数返回类型是Seq[(String, Int)]的变量fields。
乍看上去这个函数似乎没什么问题。特殊的地方在于尽管这个数据抽取函数很小在复杂的ETL应用里是非常微小的一环但在整个ETL作业中它会在不同地方被频繁地反复调用。如果我基于这份代码把整个ETL应用推上线就会发现ETL作业端到端的执行效率非常差在分布式环境下完成作业需要两个小时这样的速度难免有点让人沮丧。
想要让ETL作业跑得更快我们自然需要做性能调优。可问题是我们该从哪儿入手呢既然extractFields这个小函数会被频繁地调用不如我们从它下手好了看看有没有可能给它“减个肥、瘦个身”。重新审视函数extractFields的类型之后我们不难发现这个函数从头到尾无非是从Seq[Row]到Seq[(String, Int)]的转换函数体的核心逻辑就是字段提取只要从Seq[Row]可以得到Seq[(String, Int)],目的就达到了。
要达成这两种数据类型之间的转换除了利用上面这种开发者信手拈来的过程式编程我们还可以用函数式的编程范式。函数式编程的原则之一就是尽可能地在函数体中避免副作用Side effect副作用指的是函数对于状态的修改和变更比如上例中extractFields函数对于fields变量不停地执行追加操作就属于副作用。
基于这个想法我们就有了第二种实现方式如下所示。与第一种实现相比它最大的区别在于去掉了fields变量。之后为了达到同样的效果我们在输入参数Seq[Row]上直接调用map操作逐一地提取特定字段并构建元组最后通过toSeq将映射转换为序列干净利落一气呵成。
```
//实现方案2 —— 正例
val extractFields: Seq[Row] =&gt; Seq[(String, Int)] = {
(rows: Seq[Row]) =&gt;
rows.map(row =&gt; (row.getString(2), row.getInt(4))).toSeq
}
```
你可能会问“两份代码实现无非是差了个中间变量而已能有多大差别呢看上去不过是代码更简洁了而已。”事实上我基于第二份代码把ETL作业推上线后就惊奇地发现端到端执行性能提升了一倍从原来的两个小时缩短到一个小时。**两份功能完全一样的代码,在分布式环境中的执行性能竟然有着成倍的差别。因此你看,在日常的开发工作中,仅仅专注于业务功能实现还是不够的,任何一个可以进行调优的小环节咱们都不能放过。**
## 开发案例2数据过滤与数据聚合
你也许会说“你这个例子只是个例吧更何况这个例子里的优化仅仅是编程范式的调整看上去和Spark似乎也没什么关系啊”不要紧我们再来看第二个例子。第二个例子会稍微复杂一些我们先来把业务需求和数据关系交代清楚。
```
/**
(startDate, endDate)
e.g. (&quot;2021-01-01&quot;, &quot;2021-01-31&quot;)
*/
val pairDF: DataFrame = _
/**
(dim1, dim2, dim3, eventDate, value)
e.g. (&quot;X&quot;, &quot;Y&quot;, &quot;Z&quot;, &quot;2021-01-15&quot;, 12)
*/
val factDF: DataFrame = _
// Storage root path
val rootPath: String = _
```
在这个案例中我们有两份数据分别是pairDF和factDF数据类型都是DataFrame。第一份数据pairDF的Schema包含两个字段分别是开始日期和结束日期。第二份数据的字段较多不过最主要的字段就两个一个是Event date事件日期另一个是业务关心的统计量取名为Value。其他维度如dim1、dim2、dim3主要用于数据分组具体含义并不重要。从数据量来看pairDF的数据量很小大概几百条记录factDF数据量很大有上千万行。
对于这两份数据来说具体的业务需求可以拆成3步
1. 对于pairDF中的每一组时间对从factDF中过滤出Event date落在其间的数据条目
1. 从dim1、dim2、dim3和Event date 4个维度对factDF分组再对业务统计量Value进行汇总
1. 将最终的统计结果落盘到Amazon S3。
针对这样的业务需求,不少同学按照上面的步骤按部就班地进行了如下的实现。接下来,我就结合具体的代码来和你说说其中的计算逻辑。
```
//实现方案1 —— 反例
def createInstance(factDF: DataFrame, startDate: String, endDate: String): DataFrame = {
val instanceDF = factDF
.filter(col(&quot;eventDate&quot;) &gt; lit(startDate) &amp;&amp; col(&quot;eventDate&quot;) &lt;= lit(endDate))
.groupBy(&quot;dim1&quot;, &quot;dim2&quot;, &quot;dim3&quot;, &quot;event_date&quot;)
.agg(sum(&quot;value&quot;) as &quot;sum_value&quot;)
instanceDF
}
pairDF.collect.foreach{
case (startDate: String, endDate: String) =&gt;
val instance = createInstance(factDF, startDate, endDate)
val outPath = s&quot;${rootPath}/endDate=${endDate}/startDate=${startDate}&quot;
instance.write.parquet(outPath)
}
```
首先他们是以factDF、开始时间和结束时间为形参定义createInstance函数。在函数体中先根据Event date对factDF进行过滤然后从4个维度分组汇总统计量最后将汇总结果返回。定义完createInstance函数之后收集pairDF到Driver端并逐条遍历每一个时间对然后以factDF、开始时间、结束时间为实参调用createInstance函数来获取满足过滤要求的汇总结果。最后以Parquet的形式将结果落盘。
同样地这段代码从功能的角度来说没有任何问题而且从线上的结果来看数据的处理逻辑也完全符合预期。不过端到端的执行性能可以说是惨不忍睹在16台机型为C5.4xlarge AWS EC2的分布式运行环境中基于上面这份代码的ETL作业花费了半个小时才执行完毕。
没有对比就没有伤害在同一份数据集之上采用下面的第二种实现方式仅用2台同样机型的EC2就能让ETL作业在15分钟以内完成端到端的计算任务。**两份代码的业务功能和计算逻辑完全一致,执行性能却差了十万八千里**。
```
//实现方案2 —— 正例
val instances = factDF
.join(pairDF, factDF(&quot;eventDate&quot;) &gt; pairDF(&quot;startDate&quot;) &amp;&amp; factDF(&quot;eventDate&quot;) &lt;= pairDF(&quot;endDate&quot;))
.groupBy(&quot;dim1&quot;, &quot;dim2&quot;, &quot;dim3&quot;, &quot;eventDate&quot;, &quot;startDate&quot;, &quot;endDate&quot;)
.agg(sum(&quot;value&quot;) as &quot;sum_value&quot;)
instances.write.partitionBy(&quot;endDate&quot;, &quot;startDate&quot;).parquet(rootPath)
```
那么问题来了,这两份代码到底差在哪里,是什么导致它们的执行性能差别如此之大。我们不妨先来回顾第一种实现方式,嗅一嗅这里面有哪些不好的代码味道。
我们都知道触发Spark延迟计算的Actions算子主要有两类一类是将分布式计算结果直接落盘的操作如DataFrame的write、RDD的saveAsTextFile等另一类是将分布式结果收集到Driver端的操作如first、take、collect。
显然对于第二类算子来说Driver有可能形成单点瓶颈尤其是用collect算子去全量收集较大的结果集时更容易出现性能问题。因此在第一种实现方式中我们很容易就能嗅到collect这里的调用味道很差。
尽管collect这里味道不好但在我们的场景里pairDF毕竟是一份很小的数据集才几百条数据记录而已全量搜集到Driver端也不是什么大问题。
最要命的是collect后面的foreach。要知道factDF是一份庞大的分布式数据集尽管createInstance的逻辑仅仅是对factDF进行过滤、汇总并落盘但是createInstance函数在foreach中会被调用几百次pairDF中有多少个时间对createInstance就会被调用多少次。对于Spark中的DAG来说在没有缓存的情况下每一次Action的触发都会导致整条DAG从头到尾重新执行。
明白了这一点之后我们再来仔细观察这份代码你品、你细品目不转睛地盯着foreach和createInstance中的factDF你会惊讶地发现有着上千万行数据的factDF被反复扫描了几百次而且是全量扫描哟吓不吓人可不可怕这么分析下来ETL作业端到端执行效率低下的始作俑者是不是就暴露无遗了
反观第二份代码factDF和pairDF用pairDF.startDate &lt; factDF.eventDate &lt;= pairDF.endDate的不等式条件进行数据关联。在Spark中不等式Join的实现方式是Nested Loop Join。尽管Nested Loop Join是所有Join实现方式Merge JoinHash JoinBroadcast Join等中性能最差的一种而且这种Join方式没有任何优化空间但factDF与pairDF的数据关联只需要扫描一次全量数据仅这一项优势在执行效率上就可以吊打第一份代码实现。
## 小结
今天我们分析了两个案例这两个案例都来自数据应用的ETL场景。第一个案例讲的是在函数被频繁调用的情况下函数里面一个简单变量所引入的性能开销被成倍地放大。第二个例子讲的是不恰当的实现方式导致海量数据被反复地扫描成百上千次。
通过对这两个案例进行分析和探讨我们发现对于Spark的应用开发绝不仅仅是完成业务功能实现就高枕无忧了。**Spark天生的执行效率再高也需要你针对具体的应用场景和运行环境进行性能调优**。
而性能调优的收益显而易见:一来可以节约成本,尤其是按需付费的云上成本,更短的执行时间意味着更少的花销;二来可以提升开发的迭代效率,尤其是对于从事数据分析、数据科学、机器学习的同学来说,更高的执行效率可以更快地获取数据洞察,更快地找到模型收敛的最优解。因此你看,性能调优不是一件锦上添花的事情,而是开发者必须要掌握的一项傍身技能。
那么对于Spark的性能调优你准备好了吗生活不止眼前的苟且让我们来一场说走就走的性能调优之旅吧。来吧快上车扶稳坐好系好安全带咱们准备发车了
## 每日一练
1. 日常工作中,你还遇到过哪些功能实现一致、但性能大相径庭的案例吗?
1. 我们今天讲的第二个案例中的正例代码,你觉得还有可能进一步优化吗?
期待在留言区看到你分享,也欢迎把你对开发案例的思考写下来,我们下节课见!

View File

@@ -0,0 +1,115 @@
<audio id="audio" title="02 | 性能调优的本质:调优的手段五花八门,该从哪里入手?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/77/5c/772ff08f7c1eb1c611105e3bbdaee45c.mp3"></audio>
你好,我是吴磊。
上节课我们探讨了性能调优的必要性结论是尽管Spark自身运行高效但作为开发者我们仍然需要对应用进行性能调优。
那么问题来了性能调优该怎么做呢面对成百上千行应用代码、近百个Spark配置项我们该从哪里入手呢我认为要想弄清性能调优怎么入手必须先得搞明白性能调优的本质是什么。
所以今天这节课,咱们就从一个先入为主的调优反例入手,带你一起探讨并归纳性能调优的本质是什么,最终帮你建立起系统化的性能调优方法论。
## 先入为主的反例
在典型的ETL场景中我们经常需要对数据进行各式各样的转换有的时候因为业务需求太复杂我们往往还需要自定义UDFUser 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. 性能调优不是一锤子买卖,补齐一个短板,其他板子可能会成为新的短板。因此,它是一个动态、持续不断的过程。
1. 性能调优的手段和方法是否高效,取决于它针对的是木桶的长板还是瓶颈。针对瓶颈,事半功倍;针对长板,事倍功半。
1. 性能调优的方法和技巧,没有一定之规,也不是一成不变,随着木桶短板的此消彼长需要相应的动态切换。
1. 性能调优的过程收敛于一种所有木板齐平、没有瓶颈的状态。
基于对性能调优本质的理解我们就能很好地解释日常工作中一些常见的现象。比如我们经常发现明明是同样一种调优方法在你那儿好使在我这儿却不好使。再比如从网上看到某位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个层面展开。
应用代码层面指的是从代码开发的角度,我们该如何取舍,如何以性能为导向进行应用开发。在[上一讲](https://time.geekbang.org/column/article/352035)中我们已经做过对比,哪怕是两份功能完全一致的代码,在性能上也会有很大的差异。因此我们需要知道,**开发阶段都有哪些常规操作、常见误区,从而尽量避免在代码中留下性能隐患**。
Spark配置项想必你并不陌生**Spark官网上罗列了近百个配置项看得人眼花缭乱但并不是所有的配置项都和性能调优息息相关因此我们需要对它们进行甄别、归类**。
总的来说在日常的调优工作中我们往往从应用代码和Spark配置项这2个层面出发。所谓“问题从代码中来解决问题要到代码中去”在应用代码层面进行调优其实就是一个捕捉和移除性能BUG的过程。Spark配置项则给予了开发者极大的灵活度允许开发者通过配置项来调整不同硬件的资源利用率从而适配应用的运行时负载。
对于应用代码和Spark配置项层面的调优方法与技巧以及这些技巧在典型场景和硬件资源利用率调控中的综合运用我会在《性能篇》逐一展开讲解从不同层面、不同场景、不同视角出发归纳出一套调优的方法与心得力图让你能够按图索骥、有章可循地去开展性能调优。
## 性能调优的终结
性能调优的本质告诉我们:性能调优是一个动态、持续不断的过程,在这个过程中,调优的手段需要随着瓶颈的此消彼长而相应地切换。那么问题来了,性能调优到底什么时候是个头儿呢?就算是持续不断地,也总得有个收敛条件吧?总不能一直这么无限循环下去。
在我看来,**性能调优的最终目的,是在所有参与计算的硬件资源之间寻求协同与平衡,让硬件资源达到一种平衡、无瓶颈的状态**。
我们以大数据服务公司Qubole的案例为例他们最近在Spark上集成机器学习框架XGBoost来进行模型训练在相同的硬件资源、相同的数据源、相同的计算任务中对比不同配置下的执行性能。
从下表中我们就可以清楚地看到执行性能最好的训练任务并不是那些把CPU利用率压榨到100%,以及把内存设置到最大的配置组合,而是那些硬件资源配置最均衡的计算任务。
<img src="https://static001.geekbang.org/resource/image/33/07/338518a9afaa820e4517868f03aae507.jpeg" alt="">
## 小结
只有理解Spark性能调优的本质形成系统化的方法论我们才能避免投入时间与产出不成正比的窘境。
性能调优的本质我总结为4点
1. 性能调优不是一锤子买卖,补齐一个短板,其他板子可能会成为新的短板。因此,它是一个动态、持续不断的过程;
1. 性能调优的手段和方法是否高效,取决于它针对的是木桶的长板还是瓶颈。针对瓶颈,事半功倍;针对长板,事倍功半;
1. 性能调优的方法和技巧,没有一定之规,也不是一成不变,随着木桶短板的此消彼长需要相应地动态切换;
1. 性能调优的过程收敛于一种所有木板齐平、没有瓶颈的状态。
系统化的性能调优方法论我归纳为4条
1. 通过不同的途径如专家经验或运行时诊断来定位性能瓶颈;
1. 从不同场景典型场景、不同视角硬件资源出发综合运用不同层面应用代码、Spark配置项的调优手段和方法
1. 随着性能瓶颈的此消彼长,动态灵活地在不同层面之间切换调优方法;
1. 让性能调优的过程收敛于不同硬件资源在运行时达到一种平衡、无瓶颈的状态。
## 每日一练
<li>
你还遇到过哪些“照本宣科”的调优手段?
</li>
<li>
你认为,对于性能调优的收敛状态,即硬件资源彼此之间平衡、无瓶颈的状态,需要量化吗?如何量化呢?
</li>
关于性能调优,你还有哪些看法?欢迎在评论区留言,我们下节课见!

View File

@@ -0,0 +1,94 @@
<audio id="audio" title="开篇词 | Spark性能调优你该掌握这些“套路”" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/45/ce/4588d476191410a66fa2e2e1d8b25bce.mp3"></audio>
你好,我是吴磊,欢迎和我一起探索 Spark 应用的性能优化。
2020年6月Spark正式发布了新版本从2.4直接跨越到了3.0。这次大版本升级的亮点就在于性能优化它添加了诸如自适应查询执行AQE、动态分区剪裁DPP、扩展的Join Hints等新特性。
其实在3.0版本发布之前Spark就已然成为了分布式数据处理技术的事实标准。在数据科学与机器学习魔力象限中Gartner更是连续3年2018~2020将DatabricksSpark云原生商业版本提名为Market Leader。
<img src="https://static001.geekbang.org/resource/image/c9/01/c98c4978a48fd52538a99583b48e6601.jpg" alt="">
自然而然地,**Spark也成为了各大头部互联网公司的标配在海量数据处理上扮演着不可或缺的关键角色**。比如字节跳动基于Spark构建的数据仓库去服务几乎所有的产品线包括抖音、今日头条、西瓜视频、火山视频等。再比如百度基于Spark推出BigSQL为海量用户提供次秒级的即席查询。
可以预见的是,**这次版本升级带来的新特性会让Spark在未来5到10年继续雄霸大数据生态圈。**
而说到我和Spark的缘分可以追溯到2014年。一次偶然的机会我参与到了Spark的调研。深入研究之后我对Spark高效的执行性能深深着迷这也让我未来的职业发展都和Spark紧紧地绑在了一起。就这样我从数据分析、数据挖掘领域转移到了现在的商业智能和机器学习领域一直不停地尝试探索数据中蕴含的核心价值。
目前我在FreeWheel带领团队负责机器学习的应用与落地。我们所有已落地和正在启动的项目都在使用Spark进行数据探索、数据处理、数据分析、特征工程和样本工程。除此之外我们还会经常基于Spark在海量数据上进行模型训练和模型推理。
如果你也和我一样正沿着数据规划自己的职业路径那么精通Spark也必定是你的职业目标之一。
## 精通Spark你需要一把叫做“性能调优”的万能钥匙
目前Spark有海量批处理、实时流计算、图计算、数据分析和机器学习这5大应用场景不论你打算朝哪个方向深入性能调优都是你职业进阶必须要跨越的一步。
<img src="https://static001.geekbang.org/resource/image/1b/be/1b271125207a917916f0a45389df75be.jpg" alt="">
为什么这么说呢?原因很简单,**对于这5大场景来说提升执行性能是刚需**。
图计算和机器学习往往需要上百次迭代才能收敛,如果没有性能保障,这类作业不可能完成计算。流计算和数据分析对于响应实时性的要求非常高,没有高效的执行性能,不可能做到在亚秒级完成处理。
相比其他场景批处理对于执行效率的要求是最低的但是在日增数据量以TB、甚至PB为单位计数的当下想要在小时级别完成海量数据处理不做性能调优简直是天方夜谭。
因此我认为这5大场景就像是5扇门每扇门背后都别有洞天而**性能调优就像是一把“万能钥匙”**。有了这把钥匙在手,你才能如入无人之境,去探索更广阔的世界。
## 为什么性能调优不能“照葫芦画瓢”?
其实我身边很多开发人员也都意识到了这一点他们会去网上搜集一些教程进行学习。但是目前关于Spark性能调优的资料大都不是很系统或者只是在讲一些常规的调优技巧和方法。而对于一些大神分享的调优手段我们往往“照葫芦画瓢”做出来的东西也总是达不到预期的效果比如
- 明明都是内存计算为什么我用了RDD/DataFrame Cache性能反而更差了
- 网上吹得神乎其神的调优手段,为啥到了我这就不好使呢?
- 并行度设置得也不低为啥我的CPU利用率还是上不去
- 节点内存几乎全都划给Spark用了为啥我的应用还是OOM
这些问题看似简单,但真不是一两句话就能说得清的。**这需要我们深入Spark的核心原理不断去尝试每一个API、算子设置不同的配置参数最终找出最佳的排列组合。**
那么问题来了,这该怎么做呢?接下来,我就和你分享一下,我是怎么学习性能调优的。
刚刚接触Spark那会儿我觉得它的开发效率是真高啊MapReduce上千行代码才能实现的业务功能Spark几十行代码就搞定了
后来随着客户要求的不断提高以及我个人的“较真行为”为了让应用能跑得更快我几乎把所有的RDD API查了个遍仔细研究每一个算子的含义和运行原理汇总不同算子的适用场景总结哪些算子会引入Shuffle对比同类功能算子的差异与优劣势比如map和mapPartitions再比如groupByKey、reduceByKey和aggregateByKey。
除此之外Spark官网的Configuration页面我也查阅了无数次汇总与性能有关的配置项不停地做对比实验比较不同参数配置下的执行性能。遇到与认知不符的实验结果就再回去反复咀嚼Spark的核心原理从RDD和调度系统到内存管理和存储系统再到内存计算和Shuffle如此往复乐此不疲。
虽然失败的次数非常多,但成倍的性能提升带来的惊喜让我久久不能忘怀。后来,我就把我的经验分享给身边的同事,在帮助他们进行调优的过程中,我也有意识地把我接触到的案例整理了起来,**从点到线、从线到面,**我逐渐摸清了性能调优的脉络,最终总结出一套关于性能调优的方法论,也因此建立起了以性能为导向的开发习惯。
<img src="https://static001.geekbang.org/resource/image/b5/eb/b53099170df81c6dfb629254f5bf82eb.jpg" alt="">
**遵循这套方法论,开发者可以按图索骥地去开展性能调优工作,做到有的放矢、事半功倍。**我希望在这个专栏里把它分享给你。
## 学得快,也要学得好
结合方法论我把专栏划分为了3个部分原理篇、性能篇和实战篇。
原理篇聚焦Spark底层原理打通性能调优的任督二脉
Spark的原理非常多但我会聚焦于那些与性能调优息息相关的核心概念包括**RDD、DAG、调度系统、存储系统和内存管理**。我会力求用最贴切的故事和类比、最少的篇幅让你在最短的时间内掌握这5大概念的核心原理为后续的性能调优打下坚实的基础。
性能篇:实际案例驱动,多角度解读,全方位解析性能调优技巧
我们刚才说了Spark的应用场景非常多主要分为海量批处理、实时流计算、图计算、数据分析和机器学习这5个。但在所有的子框架里Spark对Spark SQL的倾斜和倚重也是有目共睹所以性能篇我主要分两部分来讲。
**一部分是讲解性能调优的通用技巧包括应用开发的基本原则、配置项的设置、Shuffle的优化以及资源利用率的提升。**首先我会从常见的例子入手教你怎么在不改变代码逻辑的情况下快速提升执行能。其次我会带你去归纳与执行效率相关的配置项。接着针对Shuffle、数据关联这些典型场景我们一起去分析有效的应对策略。最后我们再从硬件视角出发带你探讨如何最大化资源利用率在整体上提升Spark的执行性能。
虽然不同应用场景的开发API和运行原理都有所不同但是性能调优的本质和方法论是一样的。因此这类技巧不限定应用场景适用于所有Spark子框架。
**另一部分我会专注于数据分析领域借助如Tungsten、AQE这样的Spark内置优化项和数据关联这样的典型场景来和你聊聊Spark SQL中的调优方法和技巧。**
首先我会带你深入挖掘Tungsten、Catalyst优化器和Spark 3.0发布的诸多新特性充分利用Spark已有的优化机制让性能调优站到一个更高的起点上。接着我会借助数据分析的典型场景案例如数据清洗、数据关联、数据转换等等带你case-by-case地去归纳调优的思路与方法。
值得一提的是随着所有子框架的开发API陆续迁移到DataFrame我们在每个子框架之上开发的应用都将受益于Spark SQL的性能提升。换句话说尽管这部分调优技巧围绕数据分析领域展开但其中的思路和方法也同样适用于其他子框架。
实战篇:打造属于自己的分布式应用
在实战篇为了帮助你实践我们的方法论和调优技巧。我会以2011~2019的《北京市汽油车摇号》数据为例**手把手教你打造一个分布式应用,带你从不同角度洞察汽油车摇号的趋势和走向**。我相信,通过这个实战案例,你对性能调优技巧和思路的把控肯定会有一个“质的飞跃”。
除此之外我还会不定期地针对一些热点话题进行加餐比如和Flink、Presto相比Spark有哪些优势再比如Spark的一些新特性以及业界对于Spark的新探索。这也能帮助我们更好地面对变化把握先机。
<img src="https://static001.geekbang.org/resource/image/88/1e/8886909e1eda59a1d7e7ab45243b991e.jpg" alt="">
最后我想说我一直希望把学习变成一件有趣又轻松的事情所以在这个专栏里我会用一个个小故事和实例来帮助你理解Spark的核心原理引导你建立以性能为导向的开发思维以及从不同视角汇总性能调优的方法和技巧**让你像读小说一样去弄懂Spark**。
我也期望,你能像小说里的主人公一样,利用“调优技巧和方法论”这本武功秘籍,一路过五关、斩六将,打败现实中层出不穷的开发问题,在职业发展中更上一层楼。
最后欢迎你在这里畅所欲言提出你的困惑和疑问也欢迎多多给我留言你们的鼓励是我的动力。让我们一起拿起性能调优这把万能钥匙去开启全新的Spark职业生涯吧