mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 06:03:45 +08:00
mod
This commit is contained in:
153
极客时间专栏/Spark性能调优实战/课前必学/01 | 性能调优的必要性:Spark本身就很快,为啥还需要我调优?.md
Normal file
153
极客时间专栏/Spark性能调优实战/课前必学/01 | 性能调优的必要性:Spark本身就很快,为啥还需要我调优?.md
Normal 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上的专注与发力。
|
||||
|
||||
但是,真如大家所说,**开发者只要把业务逻辑实现了就万事大吉了吗**?这样,咱们先不急于得出结论,你先跟着我一起看两个日常开发中常见的例子,最后我们再来回答这个问题。
|
||||
|
||||
在数据应用场景中,ETL(Extract Transform Load)往往是打头阵的那个,毕竟源数据经过抽取和转换才能用于探索和分析,或者是供养给机器学习算法进行模型训练,从而挖掘出数据深层次的价值。我们今天要举的两个例子,都取自典型ETL端到端作业中常见的操作和计算任务。
|
||||
|
||||
## 开发案例1:数据抽取
|
||||
|
||||
第一个例子很简单:给定数据条目,从中抽取特定字段。这样的数据处理需求在平时的ETL作业中相当普遍。想要实现这个需求,我们需要定义一个函数extractFields:它的输入参数是Seq[Row]类型,也即数据条目序列;输出结果的返回类型是Seq[(String, Int)],也就是(String, Int)对儿的序列;函数的计算逻辑是从数据条目中抽取索引为2的字符串和索引为4的整型。
|
||||
|
||||
应该说这个业务需求相当简单明了,实现起来简直是小菜一碟。在实际开发中,我观察到有不少同学一上来就迅速地用下面的方式去实现,干脆利落,代码写得挺快,功能也没问题,UT、功能测试都能过。
|
||||
|
||||
```
|
||||
//实现方案1 —— 反例
|
||||
val extractFields: Seq[Row] => Seq[(String, Int)] = {
|
||||
(rows: Seq[Row]) => {
|
||||
var fields = Seq[(String, Int)]()
|
||||
rows.map(row => {
|
||||
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] => Seq[(String, Int)] = {
|
||||
(rows: Seq[Row]) =>
|
||||
rows.map(row => (row.getString(2), row.getInt(4))).toSeq
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
你可能会问:“两份代码实现无非是差了个中间变量而已,能有多大差别呢?看上去不过是代码更简洁了而已。”事实上,我基于第二份代码把ETL作业推上线后,就惊奇地发现端到端执行性能提升了一倍!从原来的两个小时缩短到一个小时。**两份功能完全一样的代码,在分布式环境中的执行性能竟然有着成倍的差别。因此你看,在日常的开发工作中,仅仅专注于业务功能实现还是不够的,任何一个可以进行调优的小环节咱们都不能放过。**
|
||||
|
||||
## 开发案例2:数据过滤与数据聚合
|
||||
|
||||
你也许会说:“你这个例子只是个例吧?更何况,这个例子里的优化,仅仅是编程范式的调整,看上去和Spark似乎也没什么关系啊!”不要紧,我们再来看第二个例子。第二个例子会稍微复杂一些,我们先来把业务需求和数据关系交代清楚。
|
||||
|
||||
```
|
||||
/**
|
||||
(startDate, endDate)
|
||||
e.g. ("2021-01-01", "2021-01-31")
|
||||
*/
|
||||
val pairDF: DataFrame = _
|
||||
|
||||
/**
|
||||
(dim1, dim2, dim3, eventDate, value)
|
||||
e.g. ("X", "Y", "Z", "2021-01-15", 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("eventDate") > lit(startDate) && col("eventDate") <= lit(endDate))
|
||||
.groupBy("dim1", "dim2", "dim3", "event_date")
|
||||
.agg(sum("value") as "sum_value")
|
||||
instanceDF
|
||||
}
|
||||
|
||||
pairDF.collect.foreach{
|
||||
case (startDate: String, endDate: String) =>
|
||||
val instance = createInstance(factDF, startDate, endDate)
|
||||
val outPath = s"${rootPath}/endDate=${endDate}/startDate=${startDate}"
|
||||
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("eventDate") > pairDF("startDate") && factDF("eventDate") <= pairDF("endDate"))
|
||||
.groupBy("dim1", "dim2", "dim3", "eventDate", "startDate", "endDate")
|
||||
.agg(sum("value") as "sum_value")
|
||||
|
||||
instances.write.partitionBy("endDate", "startDate").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 < factDF.eventDate <= pairDF.endDate的不等式条件进行数据关联。在Spark中,不等式Join的实现方式是Nested Loop Join。尽管Nested Loop Join是所有Join实现方式(Merge Join,Hash Join,Broadcast Join等)中性能最差的一种,而且这种Join方式没有任何优化空间,但factDF与pairDF的数据关联只需要扫描一次全量数据,仅这一项优势在执行效率上就可以吊打第一份代码实现。
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我们分析了两个案例,这两个案例都来自数据应用的ETL场景。第一个案例讲的是,在函数被频繁调用的情况下,函数里面一个简单变量所引入的性能开销被成倍地放大。第二个例子讲的是,不恰当的实现方式导致海量数据被反复地扫描成百上千次。
|
||||
|
||||
通过对这两个案例进行分析和探讨,我们发现,对于Spark的应用开发,绝不仅仅是完成业务功能实现就高枕无忧了。**Spark天生的执行效率再高,也需要你针对具体的应用场景和运行环境进行性能调优**。
|
||||
|
||||
而性能调优的收益显而易见:一来可以节约成本,尤其是按需付费的云上成本,更短的执行时间意味着更少的花销;二来可以提升开发的迭代效率,尤其是对于从事数据分析、数据科学、机器学习的同学来说,更高的执行效率可以更快地获取数据洞察,更快地找到模型收敛的最优解。因此你看,性能调优不是一件锦上添花的事情,而是开发者必须要掌握的一项傍身技能。
|
||||
|
||||
那么,对于Spark的性能调优,你准备好了吗?生活不止眼前的苟且,让我们来一场说走就走的性能调优之旅吧。来吧!快上车!扶稳坐好,系好安全带,咱们准备发车了!
|
||||
|
||||
## 每日一练
|
||||
|
||||
1. 日常工作中,你还遇到过哪些功能实现一致、但性能大相径庭的案例吗?
|
||||
1. 我们今天讲的第二个案例中的正例代码,你觉得还有可能进一步优化吗?
|
||||
|
||||
期待在留言区看到你分享,也欢迎把你对开发案例的思考写下来,我们下节课见!
|
||||
115
极客时间专栏/Spark性能调优实战/课前必学/02 | 性能调优的本质:调优的手段五花八门,该从哪里入手?.md
Normal file
115
极客时间专栏/Spark性能调优实战/课前必学/02 | 性能调优的本质:调优的手段五花八门,该从哪里入手?.md
Normal 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场景中,我们经常需要对数据进行各式各样的转换,有的时候,因为业务需求太复杂,我们往往还需要自定义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. 性能调优不是一锤子买卖,补齐一个短板,其他板子可能会成为新的短板。因此,它是一个动态、持续不断的过程。
|
||||
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>
|
||||
|
||||
关于性能调优,你还有哪些看法?欢迎在评论区留言,我们下节课见!
|
||||
94
极客时间专栏/Spark性能调优实战/课前必学/开篇词 | Spark性能调优,你该掌握这些“套路”.md
Normal file
94
极客时间专栏/Spark性能调优实战/课前必学/开篇词 | Spark性能调优,你该掌握这些“套路”.md
Normal 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)将Databricks(Spark云原生商业版本)提名为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职业生涯吧!
|
||||
Reference in New Issue
Block a user