mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-11 04:04:34 +08:00
mod
This commit is contained in:
275
极客时间专栏/高楼的性能工程实战课/基准场景/10 | 设计基准场景需要注意哪些关键点?.md
Normal file
275
极客时间专栏/高楼的性能工程实战课/基准场景/10 | 设计基准场景需要注意哪些关键点?.md
Normal file
@@ -0,0 +1,275 @@
|
||||
<audio id="audio" title="10 | 设计基准场景需要注意哪些关键点?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/55/bf/55f692dcfb11fcc9c951bae50f8490bf.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
在前面的课程中我们提到过,在RESAR性能工程中,场景分为四类:基准、容量、稳定性、异常。每一类场景都对应着不同的目标。
|
||||
|
||||
其中,基准场景是为了找到系统中明显的配置及软件Bug,同时也为容量场景提供可对比的基准数据。在RESAR性能工程的逻辑中,基准场景是非常重要、非常重要的部分,而不是随意试验一下场景能不能跑起来,是要有确定的结论的。
|
||||
|
||||
在这节课中,我要给你解释几个基本的问题,比如线程数应该如何确定,压力线程的连续递增的重要性,以及如何将之前所讲的分析思路应用在具体的分析案例中。
|
||||
|
||||
下面我们一起来看一看。
|
||||
|
||||
## 性能场景分类
|
||||
|
||||
在设计性能场景时,我们首先要清楚场景的目标是什么。在一些项目中,我们通常会拿到这样的需求:
|
||||
|
||||
1. 评估一下系统能支持的最大容量。这显然是为了知道当前的系统容量,目标很明确;
|
||||
1. 测试并优化系统以支持线上的业务目标。这个需求显然有了优化的必要;
|
||||
1. 测试并评估未来几年内,性能容量是否可以满足业务发展。这个需求显然是要求测试未来的业务场景。
|
||||
|
||||
这是我们经常拿到的几类性能需求,基于此,我把场景按照目标划分为三类:
|
||||
|
||||
1. 验证:评估当前系统容量;
|
||||
1. 调优:评估并优化当前系统;
|
||||
1. 推算:评估并推算未来系统容量。
|
||||
|
||||
这种分类和我们一直强调的按类型分类(也就是基准、容量、稳定性、异常)是什么关系呢?这里我画一张图说明一下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/01/e9639fbd9a2dd4a99af8ce7b2097c801.jpg" alt="">
|
||||
|
||||
从图中可以明显看出这两种分类之间的关系:我们首先要确定性能场景的目标,然后再设计对应的具体场景。
|
||||
|
||||
你要注意,对于图中的三种目标,位于下方的目标是包含它上方的目标的,比如以调优为目标的场景,包括了以验证为目标的场景。
|
||||
|
||||
有了这些基本的了解后,下面我再给你详细解释一下。
|
||||
|
||||
#### 1. 按目标分类
|
||||
|
||||
对于按目标划分出的这三种性能场景,我们结合RESAR性能过程图具体来看看。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/15/80/155ff7d26a09a1865f473b720f4a7880.jpg" alt="">
|
||||
|
||||
- **性能验证**
|
||||
|
||||
性能验证(测试)是指针对当前的系统、当前的模型、当前的环境,验证一下版本是否有性能的变化。注意,在这个阶段中,我们不做复杂的性能监控,不做性能分析,也不调优。
|
||||
|
||||
在目前的性能市场中,大部分项目都处于性能验证的状态。如果对于一个已经在线上稳定运行了很久的系统来说,我们去做版本更新的验证倒是无可厚非的,只要比对一下数据就可以了。这种项目周期通常在一两周以内,不会更长了,而且也不用更长,除非有大的性能瓶颈。
|
||||
|
||||
对于性能验证的项目,其实很多人一直在做“性能场景执行”和“性能结果/报告”这两个步骤。其他的步骤也不是不做,只是会拿之前的文档做个修改,走个过场,想着反正也没人仔细看。所以,性能验证这个项目就变成了:来一个版本,用同样的脚本、同样的环境、同样的数据,执行一遍就好了。
|
||||
|
||||
当这样的执行多了以后,你就会产生一种误解:原来性能就是这样无聊地一轮一轮执行下去,还是熟悉的姿势、还是熟悉的味道……在我遇到的性能从业人员中,有很多人都是在这样的项目中认识了性能,从而认为自己的技术还挺好的,觉得性能也不怎么难。
|
||||
|
||||
- **性能调优**
|
||||
|
||||
性能调优是指针对当前的系统、当前的模型、当前的环境,做性能监控、性能分析和性能优化,并且要给出具体的结论。这是我们大部分项目都应该做到,但实际上没有做到的。
|
||||
|
||||
如果一个项目需要给出“系统上线后以什么样的容量能力来运行”这样的结论,那么这个场景目标的细化是相当关键的。
|
||||
|
||||
现在,很多性能项目最缺少的就是给出一个明确的结论。什么叫“给出结论”呢?你说我写了TPS是多少、CPU使用率是多少,这叫结论吗?对不起,我觉得这不叫结论。
|
||||
|
||||
**结论应该是有业务含义的**,比如说支持1000万用户在线、支持1万用户并发等等,这才叫结论。
|
||||
|
||||
不管你给出多少TPS,只要老板或是其他人问:“那我上了1000万用户之后,系统会不会死呢?”你会有一种被敲了一闷棍的感觉,不知道该如何回答。而这个时候,给对方的感觉就是,这个性能做得没什么具体的价值。不管你有多累多辛苦,在这种情况下,性能的价值都会被低估。
|
||||
|
||||
前段时间,我跟一个十几年的朋友聊天,就聊到了这个话题:性能如何才能体现出价值。我说,如果是我做的项目,我会给出这样的承诺,那就是在我执行的性能场景范围之内,我要保证线上不会死。如果死了,我觉得这个性能项目就不应该收费了。
|
||||
|
||||
这就像你买了一个手机,回来一用,发现打不了电话,你觉得这时候怎么办?不是退货就是换货,还要生一肚子闷气。
|
||||
|
||||
既然如此,那我们做性能为什么就给不了这样的承诺呢?你想想,如果你做完了一个项目,却不能告诉对方这个系统能不能好好活着,那人家还要你干嘛,直接砍掉这个项目就好,还省了成本。
|
||||
|
||||
此外,从RESAR性能工程的过程图来看,对于性能调优的项目,我们需要完成从“性能需求指标”到“生产运维”的整个过程。注意,这整个过程不是走过场,而是每一步都要精雕细琢。
|
||||
|
||||
- **性能推算**
|
||||
|
||||
性能估算针对的是未来的系统、未来的模型、未来的环境,我们要对此做出严谨的业务增长模型分析,并在场景执行过程中进行性能监控、分析和优化,同时给出具体的结论。很多项目都想做到性能估算,可往往都只走了过场。
|
||||
|
||||
其实,在性能估算的场景目标中,如果要估算的未来时间并不遥远,那我们根据业务的发展趋势的确可以推算得出来,并且这也是合理的场景。就怕遇到那种狮子大开口的需求,一说到估算,就是系统十年不宕机。
|
||||
|
||||
对于性能估算的项目,我们同样需要完成从“性能需求指标”到“生产运维”的整个过程。其中,有两个环节与性能调优项目中的不同,那就是“性能需求指标”和“性能模型”。
|
||||
|
||||
在性能估算项目中,性能需求指标和性能模型一定不是由性能测试人员来决定的,而是由整个团队来决定。上到老板,下到基层员工,都要有统一的认识。要不然等项目做完了之后,你就无法回答老板那个“能不能支持1000万在线“的问题。
|
||||
|
||||
上述就是我们按照目标划分出的三类性能场景,这里我用一张图帮你总结一下,我希望你能对它们有了一个清楚的了解。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d0/de/d0744079dcf8c0908f3fe4b58ee798de.jpg" alt="">
|
||||
|
||||
#### 2. 按过程分类
|
||||
|
||||
我们说,性能场景还可以按照过程分类,而这个“过程”,说的其实就是我们应该怎样执行性能场景、性能场景应该从哪里开始的问题。不知道你记不记得,我之前在[第5讲](https://time.geekbang.org/column/article/357539)中画过这样一张图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/03/7df293f3b57eed500d95acf156c05303.jpg" alt="">
|
||||
|
||||
从图中可以看到,我一直强调的是这四种场景执行过程:
|
||||
|
||||
- 基准场景
|
||||
- 容量场景
|
||||
- 稳定性场景
|
||||
- 异常场景
|
||||
|
||||
请你记住:**性能场景中需要且仅需要这四种场景****。**
|
||||
|
||||
你可能会问,就这么绝对吗?是的,我就是这么固执。
|
||||
|
||||
在正式的性能场景(需要给出结果报告的性能场景)中,我还要再强调两个关键词:“**递增**”和“**连续**”。为了说明这两个关键词有多么重要,我特意用红框红字给你写在下面,希望你能重视。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a9/b6/a9af5f15e7d31108b7af0a080ae119b6.jpg" alt="">
|
||||
|
||||
这两个关键词是我们在性能场景中一定要做到的。因为,在我们的生产环境里没有不连续的情况,并且在我们的生产环境中,用户量肯定是一个由少到多、有起伏变化的过程。而且,也只有这两个关键词能把场景的基调给定下来。所以,我一直在反复反复强调它们。
|
||||
|
||||
也许有人会说,我就是试一下看看场景能不能执行起来,也得这么干吗?嗯……那倒不用,请退出去把门带上。
|
||||
|
||||
下面我们来说一下在基准场景执行过程中,我们要重点关注什么。
|
||||
|
||||
## 基准场景
|
||||
|
||||
在我们对一个系统完全不了解的情况下,我们先要搞清楚系统大概的容量能力是多少,具体要从哪里开始呢?就是从基准场景开始。
|
||||
|
||||
比如说在我们这个电商系统中,我们要测试11个业务。那是不是可以一上来就把这11个业务脚本都做出来,上去压呢?那肯定是不行的,因为我们还不知道每一个业务能跑到多大的TPS,有没有性能瓶颈。如果直接混合去压,会导致多个性能问题一起暴露出来并产生相互的影响,这样的话我们分析起来会比较困难。
|
||||
|
||||
所以,**我们要先做单接口的基准场景**。那具体怎么做呢?我们来看一个例子。首先,我们拿几个用户测试一下登录接口的基本性能(请你注意,这个尝试的过程本身并不是基准场景)。如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/07/d36yy6a2682926799bc2b69031066b07.png" alt="">
|
||||
|
||||
从图中,我们至少可以看出,1个压力线程大概会产生20TPS。
|
||||
|
||||
那单接口的容量达到多少才不影响混合的容量场景呢?很显然,如果这是一个单登录接口,就必须高过50TPS,这是最起码的。而我们现在用的是8C16G的机器,根据CRUD的测试经验,即使不走缓存,这样的操作要达到500TPS应该没什么问题。
|
||||
|
||||
那在一个线程能产生20个TPS的前提下,我们先假设这个接口能达到的最大500TPS都是线性的,那就需要:
|
||||
|
||||
$$线程数 = 500 TPS \div 20 TPS = 25 个线程$$
|
||||
|
||||
同时,因为1个压力线程大概会产生20TPS,从TPS曲线上看还是上升的比较快的,所以我会考虑把Duration(场景的持续时间)放长一点,目的是让压力不要增加得太快,而在这个缓慢增加的过程中观察各类曲线的变化,以判断后续的动作以及最大容量。我会这样来确定场景的加压过程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ee/67/eef19bf93f9a009378444a26da9ab567.png" alt="">
|
||||
|
||||
在图中,我上到了30个线程,这里也可以不要高出那么多,只要高出25个线程就可以了。我把Ramp-up period设置为600秒,也就是20秒上一个线程,这样就会产生一个明显的连续递增的过程。
|
||||
|
||||
现在,我们总结一下整个思路:
|
||||
|
||||
1. 先确定单线程运行时的TPS值;
|
||||
1. 根据系统最大的预估容量设置场景中的线程数、递增参数等。强调一下,如果你不会预估容量,可以直接多加一些线程,然后在递增的过程中查看曲线的变化;
|
||||
1. 确定正式基准场景的压力参数。
|
||||
|
||||
对于其他接口,我们也用这样的思路一个个执行下去。当然,对于这个过程,我们也需要在测试过程中不断地修正。
|
||||
|
||||
现在,我们根据上面讲述的过程,总结一下基准场景的目的:
|
||||
|
||||
<li>
|
||||
**获得单接口最大TPS**:如果单接口最大TPS没有超过容量场景中的要求,那就必须要调优。那如果超过了,是不是就不需要调优了呢?我们接着看第二个目的。
|
||||
</li>
|
||||
<li>
|
||||
**解决单接口基准场景中遇到的性能问题**:也就是说,当我们在做单接口测试时,碰到了性能瓶颈一定要分析,这就涉及到了性能分析逻辑。所以,性能分析基本上可以分为两大阶段:
|
||||
</li>
|
||||
|
||||
<li>
|
||||
第一阶段:硬件资源用完。即在基准场景中,我们要把CPU、内存、网络、IO等资源中的任一个耗尽,因为在这种情况下,我们很容易从全局监控的性能计数器中看到现象,可以接着去跟踪分析。
|
||||
</li>
|
||||
<li>
|
||||
第二阶段:优化到最高TPS。即在基准场景中,我们要把单接口的TPS调到最高,以免成为容量场景中的瓶颈点。
|
||||
</li>
|
||||
|
||||
如果第一阶段的目标达不到,那么不用多想,我们肯定要找瓶颈在哪里。要是硬件资源已经用完了,TPS也满足了容量场景中的要求,那么,从成本的角度来考虑,这个项目就不需要再进行下去了。如果硬件资源用完了,但TPS没有满足容量场景中的要求,那就必须优化。
|
||||
|
||||
下面我们先来执行一个单接口场景,看一下上面的思路如何落地的。
|
||||
|
||||
## 登录接口
|
||||
|
||||
按照上面所讲的基准场景的设计步骤,我们先试运行一下这个接口的基准场景。注意,在基准测试中,试运行的过程只是为了看一下基本的接口响应时间,并不是为了完成基准场景。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/40/d876c3cd1828e331270d8946524fd940.png" alt="">
|
||||
|
||||
啊,满目疮痍呀!
|
||||
|
||||
从图中看,虽然场景执行时间并不长,但是10个线程上来就报了错,响应时间和TPS也都达到了让人伤心的程度,只有12.5TPS。这可怎么办?
|
||||
|
||||
没办法,我们只有分析这个过程了。接下来的内容就是我对问题的分析过程。主要是看一下,我们前面提到的性能分析思路是如何落地的。
|
||||
|
||||
- **问题现象**
|
||||
|
||||
如上图所示,这现象老明显了。
|
||||
|
||||
- **分析过程**
|
||||
|
||||
从我一直提倡的RESAR性能分析逻辑上来说,针对响应时间长的问题,我们首先要做的就是拆分时间。由于这个系统已经部署了SkyWalking,我们自然要果断地用它看看时间浪费在了哪里。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c4/3d/c476c0b16e52c94d8953f4fa31265f3d.png" alt="">
|
||||
|
||||
你看图中,一个Token的SelfDuration居然要5秒多!唉,开源项目的坑还是多呀。看起来功能似乎都实现了,连Star都好几万了,但是完全没性能意识。
|
||||
|
||||
不过这样也好,这下我们可有的玩了。我们做性能分析的人,就是要收拾这样的烂系统,才能快速成长嘛。
|
||||
|
||||
话说回来,既然Token接口响应时间长,我们在SkyWaking中又看不到完整的调用栈,那么接下来就有两个动作可以做:
|
||||
|
||||
1. 打印一个完整的栈,看看调用链路。
|
||||
1. 不打印栈,直接连到Java进程中看方法的时间消耗。
|
||||
|
||||
我们用第一个方法看到调用链路,也还是要跟踪具体方法的耗时,只有这样才能把证据链走下去。在这里,我就直接用第二个方法了。
|
||||
|
||||
在第二个方法中,我们要看方法的时间消耗,可以使用的工具其实有很多,像JDB/JvisualVM/Arthas这些都可以。这里我们用Arthas来跟踪一下。
|
||||
|
||||
首先,我们跟踪一下那个Token的方法。
|
||||
|
||||
```
|
||||
trace com.dunshan.mall.auth.controller.AuthController postAccessToken '#cost > 1000' -n 3
|
||||
trace org.springframework.security.oauth2.provider.endpoint.TokenEndpoint postAccessToken '#cost > 1000' -n 3
|
||||
trace org.springframework.security.oauth2.provider.token.AbstractTokenGranter getOAuth2Authentication '#cost > 1000' -n 3
|
||||
trace org.springframework.security.authentication.AuthenticationManager getOAuth2Authentication '#cost > 500' -n 3
|
||||
trace org.springframework.security.authentication.ProviderManager authenticate '#cost > 500' -n 3
|
||||
trace org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider authenticate '#cost > 500' -n 3
|
||||
|
||||
```
|
||||
|
||||
我们用上面的语句一层层跟踪下去,最终来到了这里:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/3b/6c71c0b24ff382d6291db53012d3a83b.png" alt="">
|
||||
|
||||
请你注意,我们即使不用Arthas,采用其他的工具也可以达到同样的效果。所以,请你不要迷恋工具,要迷恋就迷恋哥。
|
||||
|
||||
既然这个authenticate方法耗时比较长,那我们就打开源代码看看这一段是什么东西。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/26/c1/269c00655a509b4803915a9c898379c1.png" alt="">
|
||||
|
||||
接着,我们调试跟踪进去,看到如下部分:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/3a/198e300b04a133156b1793dafc048d3a.png" alt="">
|
||||
|
||||
原来,这里是一个加密算法BCrypt。
|
||||
|
||||
- **优化方案**
|
||||
|
||||
我解释一下,Bcrypt在加密时,每一次HASH出来的值是不同的,并且特别慢!
|
||||
|
||||
我们跟踪到这里,解决方案其实比较明确了,那就是用更快的加密方式,或者去掉这个加密算法。我们把更换加密方式这个问题留给开发去解决。作为性能分析人员,我决定把这个加密算法直接去掉,咱们先往下走。
|
||||
|
||||
- **优化效果**
|
||||
|
||||
优化效果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/28/e487d4cc5eb67d58b6afc9e11eba7b28.png" alt="">
|
||||
|
||||
从图中可以看到,对于同样的线程数,现在TPS从20涨到了80了。
|
||||
|
||||
从这个简单的分析逻辑中,你可以看到,我们通过响应时间的拆分跟踪,知道了哪个方法慢,再进一步去分析这个方法,确定解决方案。这就是一个最简单的RESAR性能分析七步法的应用,看起来我们似乎在这个分析过程中跳过了七步法中的分析架构图这样的步骤,但实际上在我们分析的过程中,是跳不开的,因为不管是看架构图,还是看调用链,都是要在脑子中有架构逻辑的。
|
||||
|
||||
在基准场景中,我们还会遇到各种问题,后面我都会一一记录下来,希望能给你一些借鉴。
|
||||
|
||||
## 总结
|
||||
|
||||
根据RESAR性能工程理论,在性能场景中,我们按执行过程,可以将场景分为四类:基准场景、容量场景、稳定性场景和异常场景。这些场景各有目的,在这节课中,我们主要描述了基准场景的逻辑,并给出了实例。
|
||||
|
||||
基准场景有两个重要的目的:
|
||||
|
||||
<li>
|
||||
获得单接口最大TPS;
|
||||
</li>
|
||||
<li>
|
||||
解决单接口基准场景中遇到的性能问题。
|
||||
</li>
|
||||
|
||||
这两个目的对我们很重要,都是为了容量场景打基础的。
|
||||
|
||||
在这节课中,我主要想让你感受一下性能分析的过程。当然了,我们最后的这个优化效果其实还没有达到我对性能的要求。不过,你放心,在后面的课程中你将看到更多的分析逻辑。
|
||||
|
||||
来,跟哥往下走。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,请你思考一下:
|
||||
|
||||
1. 为什么RESAR性能工程按过程只分为四类场景?
|
||||
1. 在分析代码时间时,我们如何跟踪Java的执行耗时,有多少种手段?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果这节课让你有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
510
极客时间专栏/高楼的性能工程实战课/基准场景/11 | 打开首页之一:一个案例,带你搞懂基础硬件设施的性能问题.md
Normal file
510
极客时间专栏/高楼的性能工程实战课/基准场景/11 | 打开首页之一:一个案例,带你搞懂基础硬件设施的性能问题.md
Normal file
@@ -0,0 +1,510 @@
|
||||
<audio id="audio" title="11 | 打开首页之一:一个案例,带你搞懂基础硬件设施的性能问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e4/0d/e446ce491f6c2928910d6f6987f9c90d.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
这节课我要带你来看一个完整的性能分析案例的第一部分,用打开首页接口做压力场景,来分析下性能问题。通过这个案例,你将看到各种基础硬件设施层面的性能问题,比如由虚机超分导致的性能问题、CPU运行模式下的性能问题、IO高、硬件资源耗尽但TPS很低的问题等等。
|
||||
|
||||
如果你是从零开始做一个完整的项目,那么这些问题很可能是你首先要去面对的。并且,把它们解决好,是性能分析人员必备的一种能力。同时,你还会看到针对不同计数器采集的数据,我们的分析链路是不同的,而这个分析链路就是我一直强调的证据链,如果你不清楚可以再回顾一下[第3讲](https://time.geekbang.org/column/article/355982)。
|
||||
|
||||
通过这节课,我希望你能明白,有些性能问题其实并没有那么单一,而且不管性能问题出在哪里,我们都必须去处理。
|
||||
|
||||
好,不啰嗦了,下面我们就把打开首页接口的性能瓶颈仔细扒一扒。
|
||||
|
||||
## 看架构图
|
||||
|
||||
在每次分析性能瓶颈之前,我都会画这样一张图,看看这个接口会涉及到哪些服务和技术组件,这对我们后续的性能分析会有很大的帮助。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/8d/6913fb342aa32fae5b46c6f1ecddc58d.png" alt="">
|
||||
|
||||
如果你有工具可以直接展示,那就更好了。如果没有,那我建议你不要自信地认为自己可以记住一个简单的架构。相信我,哪怕是在纸上简单画一画,都会对你后面的分析思路有很大的帮助。
|
||||
|
||||
回到上面这张图,我们可以清楚地看到这个打开首页的逻辑是:User - Gateway(Redis)- Portal - (Redis,MySQL)。
|
||||
|
||||
## 顺便看下代码逻辑
|
||||
|
||||
在做打开首页的基准场景之前,我建议你先看一眼这个接口的代码实现逻辑,从代码中可以看到这个接口在做哪些动作。根据这些动作,我们可以分析它们的后续链路。
|
||||
|
||||
这个代码的逻辑很简单,就是列出首页上的各种信息,然后返回一个JSON。
|
||||
|
||||
```
|
||||
public HomeContentResult contentnew() {
|
||||
HomeContentResult result = new HomeContentResult();
|
||||
if (redisService.get("HomeContent") == null) {
|
||||
//首页广告
|
||||
result.setAdvertiseList(getHomeAdvertiseList());
|
||||
//品牌推荐
|
||||
result.setBrandList(homeDao.getRecommendBrandList(0, 6));
|
||||
//秒杀信息
|
||||
result.setHomeFlashPromotion(getHomeFlashPromotion());
|
||||
//新品推荐
|
||||
result.setNewProductList(homeDao.getNewProductList(0, 4));
|
||||
//人气推荐
|
||||
result.setHotProductList(homeDao.getHotProductList(0, 4));
|
||||
//专题推荐
|
||||
result.setSubjectList(homeDao.getRecommendSubjectList(0, 4));
|
||||
redisService.set("HomeContent", result);
|
||||
}
|
||||
Object homeContent = redisService.get("HomeContent");
|
||||
// result = JSON.parseObject(homeContent.toString(), HomeContentResult.class);
|
||||
result = JSONUtil.toBean(JSONUtil.toJsonPrettyStr(homeContent), HomeContentResult.class);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,这里面一共调用了6个方法,并且这些方法都是直接到数据库里做了查询,如此而已。
|
||||
|
||||
## 确定压力数据
|
||||
|
||||
了解完代码逻辑后,我们上10个线程试运行一下,看看在一个个线程递增的过程中,TPS会有什么样的趋势。
|
||||
|
||||
运行之后,我们得到这样的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/87/0f/876yya208a923dd9c42fe9538063b10f.png" alt="">
|
||||
|
||||
从结果来看,在一开始,一个线程会产生40左右的TPS。这里我们就要思考一下了:**如果想要执行一个场景,<strong><strong>并且这个场景**</strong>可以压出打开首页接口的最大TPS,**<strong>我们**</strong>应该**<strong>怎么**</strong>设置压力工具中的线程数、递增策略**<strong>和**</strong>持续执行策略呢?</strong>
|
||||
|
||||
对此,我们先看看Portal应用节点所在机器的硬件使用情况,了解一下TPS趋势和资源使用率之间的关系。这个机器的情况如下图所示(注意,我跳过了Gateway所在的节点):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/67/d991a5548f72d6f7bcf0257c40da6b67.png" alt="">
|
||||
|
||||
可以看到,当前Portal节点所在的机器是8C16G(虚拟机),并且这个机器基本上没什么压力。
|
||||
|
||||
现在我们先不计算其他资源,只考虑8C16G的配置情况。如果TPS是线性增长的话,那么当该机器的CPU使用率达到 100%的时候,TPS大概就是800左右。因此,我们压力工具中的线程数应该设置为:
|
||||
|
||||
$$ 线程数 = 800 TPS \div 40 TPS = 20 个线程$$
|
||||
|
||||
不过,在压力持续的过程中,TPS和资源使用率之间的等比关系应该是做不到的。因为在压力过程中,各种资源的消耗都会增加一些响应时间,这些也都属于正常的响应时间损耗。
|
||||
|
||||
在确定了压力工具的线程数之后,我们再来看递增策略怎么设置。
|
||||
|
||||
我希望递增时间可以增加得慢一些,以便于我们查看各环节性能数据的反应。根据[第2讲](https://time.geekbang.org/column/article/355019)中的性能分析决策树,在这样的场景中,我们有不少计数器需要分析查看,所以我设置为30秒上一个线程,也就是说递增周期为600秒。
|
||||
|
||||
在确定好压力参数后,我们的试运行场景就可以在JMeter中设置为如下值:
|
||||
|
||||
```
|
||||
<stringProp name="ThreadGroup.num_threads">20</stringProp>
|
||||
<stringProp name="ThreadGroup.ramp_time">600</stringProp>
|
||||
<boolProp name="ThreadGroup.scheduler">true</boolProp>
|
||||
<stringProp name="ThreadGroup.duration">700</stringProp>
|
||||
|
||||
```
|
||||
|
||||
设置好试运行参数后,我们就可以在这样的场景下进一步设置足够的线程来运行,以达到资源使用率的最大化。
|
||||
|
||||
你可能会疑惑:难道不用更高的线程了吗?如果你想做一个正常的场景,那确实不需要用更高的线程了;如果你就是想知道压力线程加多了是什么样子,那你可以试试。我在性能场景执行时,也经常用各种方式压着玩。
|
||||
|
||||
不过,话说回来,确实有一种情况需要我们正儿八经地增加更多的压力,那就是你的响应时间已经增加了,可是增加得又不多,TPS也不再上升。这时候,我们拆分响应时间是比较困难的,特别是当一些系统很快的时候,响应时间可能只是几个毫秒之间。所以,在这种情况下,我们需要多增加一些线程,让响应时间慢的地方更清晰地表现出来,这样也就更容易拆分时间。
|
||||
|
||||
通过压力场景的递增设置(前面算的是只需要20个线程即可达到最大值,而这里,我把压力线程设置为100启动场景,目的是为了看到递增到更大压力时的TPS趋势以及响应时间的增加,这样更容易做时间的拆分),我们看到这个接口的响应时间确实在慢慢增加,并且随着线程数的增加,响应时间很快就上升到了几百毫秒。这是一个明显的瓶颈,我们自然是不能接受的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/b9/b51a9979095ba9bd963e657b96fyy0b9.png" alt="">
|
||||
|
||||
接下来,我们就要好好分析一下这个响应时间究竟消耗到了哪里。
|
||||
|
||||
## 拆分时间
|
||||
|
||||
我们前面提到,打开首页的逻辑是:User - Gateway(Redis)- Portal - (Redis,MySQL),那我们就按照这个逻辑,借助链路监控工具SkyWalking把响应时间具体拆分一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/99/1d3b42340dd5dfdda16bdf1332d34c99.png" alt="">
|
||||
|
||||
- **User —Gateway之间的时间消耗**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/b9/bd8117bdc6124d95893b16c7653be7b9.png" alt="">
|
||||
|
||||
我们看到,User - Gateway之间的时间消耗慢慢上升到了150毫秒左右。
|
||||
|
||||
- **Gateway响应时间**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/73/37/733a7e7f13ea455826aee0bbb2393237.png" alt="">
|
||||
|
||||
gateway上也消耗了150毫秒,这就说明user到gateway之间的网络并没有多少时间消耗,在毫秒级。
|
||||
|
||||
- **Gateway —Portal之间的时间消耗**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f9/fe/f9ac563b64d7a1c6e923d8222fdfyyfe.png" alt="">
|
||||
|
||||
在Portal上,响应时间只消耗了50毫秒左右。我们再到Portal上看一眼。
|
||||
|
||||
- **Portal响应时间**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/72/49/721ae8d7ef027fdc56dd05860cafa849.png" alt="">
|
||||
|
||||
Portal的响应时间是50毫秒左右,和我们上面看到的时间一致。
|
||||
|
||||
通过上述对响应时间的拆分,我们可以确定是Gateway消耗了响应时间,并且这个时间达到了近100毫秒。所以,我们下一步定位的目标就是Gateway了。
|
||||
|
||||
## 定位Gateway上的响应时间消耗
|
||||
|
||||
#### 第一阶段:分析st cpu
|
||||
|
||||
既然Gateway上的响应时间消耗很高,我们自然就要查一下这台主机把时间消耗在了哪里。
|
||||
|
||||
我们的分析逻辑仍然是**先看全局监控,后看定向监控**。全局监控要从整个架构开始看起,然后再确定某个节点上的资源消耗。注意,在看全局监控时,我们要从最基础的查起,而分析的过程中最基础的就是操作系统了。
|
||||
|
||||
通过top命令,我们可以看到Gateway节点上的资源情况,具体如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c5/f5/c5af9b566db8bdeb7fc8d6ea448aa2f5.png" alt="">
|
||||
|
||||
其中,st cpu达到了15%左右。我们知道,st cpu是指虚拟机被宿主机上的其他应用或虚拟机抢走的CPU,它的值这么高显然是不太正常的。所以,我们要进一步查看st cpu异常的原因。
|
||||
|
||||
我们用mpstat命令先来看看宿主机(运行Gateway的虚拟机所在的物理机)上的资源表现:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f4/90/f40e7c2f2a790b289b6d2332dbc47390.png" alt="">
|
||||
|
||||
可以看到,CPU还有20%没有用完,说明宿主机还有空间。不过,宿主机的CPU使用率已经不小了,而消耗这些宿主机的就只有虚拟机里的应用。所以,我们要查一下是不是某个虚拟机的CPU消耗特别高。宿主机上的KVM列表如下:
|
||||
|
||||
```
|
||||
[root@dell-server-3 ~]# virsh list --all
|
||||
Id 名称 状态
|
||||
----------------------------------------------------
|
||||
12 vm-jmeter running
|
||||
13 vm-k8s-worker-8 running
|
||||
14 vm-k8s-worker-7 running
|
||||
15 vm-k8s-worker-9 running
|
||||
|
||||
[root@dell-server-3 ~]#
|
||||
|
||||
```
|
||||
|
||||
可以看到,在这个宿主机上跑了四个虚拟机,那我们就具体看一下这四个虚拟机的资源消耗情况。
|
||||
|
||||
- **vm-jmeter**
|
||||
|
||||
```
|
||||
top - 23:42:49 up 28 days, 8:14, 6 users, load average: 0.61, 0.48, 0.38
|
||||
Tasks: 220 total, 1 running, 218 sleeping, 1 stopped, 0 zombie
|
||||
%Cpu0 : 6.6 us, 3.5 sy, 0.0 ni, 88.5 id, 0.0 wa, 0.0 hi, 0.0 si, 1.4 st
|
||||
%Cpu1 : 6.5 us, 1.8 sy, 0.0 ni, 88.2 id, 0.0 wa, 0.0 hi, 0.4 si, 3.2 st
|
||||
KiB Mem : 3880180 total, 920804 free, 1506128 used, 1453248 buff/cache
|
||||
KiB Swap: 2097148 total, 1256572 free, 840576 used. 2097412 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
7157 root 20 0 3699292 781204 17584 S 27.8 20.1 1:09.44 java
|
||||
9 root 20 0 0 0 0 S 0.3 0.0 30:25.77 rcu_sched
|
||||
376 root 20 0 0 0 0 S 0.3 0.0 16:40.44 xfsaild/dm-
|
||||
|
||||
```
|
||||
|
||||
- **vm-k8s-worker-8**
|
||||
|
||||
```
|
||||
top - 23:43:47 up 5 days, 22:28, 3 users, load average: 9.21, 6.45, 5.74
|
||||
Tasks: 326 total, 1 running, 325 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 20.2 us, 3.7 sy, 0.0 ni, 60.7 id, 0.0 wa, 0.0 hi, 2.9 si, 12.5 st
|
||||
%Cpu1 : 27.3 us, 7.4 sy, 0.0 ni, 50.2 id, 0.0 wa, 0.0 hi, 3.7 si, 11.4 st
|
||||
%Cpu2 : 29.9 us, 5.6 sy, 0.0 ni, 48.5 id, 0.0 wa, 0.0 hi, 4.9 si, 11.2 st
|
||||
%Cpu3 : 31.2 us, 5.6 sy, 0.0 ni, 47.6 id, 0.0 wa, 0.0 hi, 4.5 si, 11.2 st
|
||||
%Cpu4 : 25.6 us, 4.3 sy, 0.0 ni, 52.7 id, 0.0 wa, 0.0 hi, 3.6 si, 13.7 st
|
||||
%Cpu5 : 26.0 us, 5.2 sy, 0.0 ni, 53.5 id, 0.0 wa, 0.0 hi, 4.1 si, 11.2 st
|
||||
%Cpu6 : 19.9 us, 6.2 sy, 0.0 ni, 57.6 id, 0.0 wa, 0.0 hi, 3.6 si, 12.7 st
|
||||
%Cpu7 : 27.3 us, 5.0 sy, 0.0 ni, 53.8 id, 0.0 wa, 0.0 hi, 2.3 si, 11.5 st
|
||||
KiB Mem : 16265688 total, 6772084 free, 4437840 used, 5055764 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 11452900 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
13049 root 20 0 9853712 593464 15752 S 288.4 3.6 67:24.22 java
|
||||
1116 root 20 0 2469728 57932 16188 S 12.6 0.4 818:40.25 containerd
|
||||
1113 root 20 0 3496336 118048 38048 S 12.3 0.7 692:30.79 kubelet
|
||||
4961 root 20 0 1780136 40700 17864 S 12.3 0.3 205:51.15 calico-node
|
||||
3830 root 20 0 2170204 114920 33304 S 11.6 0.7 508:00.00 scope
|
||||
1118 root 20 0 1548060 111768 29336 S 11.3 0.7 685:27.95 dockerd
|
||||
8216 techstar 20 0 2747240 907080 114836 S 5.0 5.6 1643:33 prometheus
|
||||
21002 root 20 0 9898708 637616 17316 S 3.3 3.9 718:56.99 java
|
||||
1070 root 20 0 9806964 476716 15756 S 2.0 2.9 137:13.47 java
|
||||
11492 root 20 0 441996 33204 4236 S 1.3 0.2 38:10.49 gvfs-udisks2-vo
|
||||
|
||||
```
|
||||
|
||||
- **vm-k8s-worker-7**
|
||||
|
||||
```
|
||||
top - 23:44:22 up 5 days, 22:26, 3 users, load average: 2.50, 1.67, 1.13
|
||||
Tasks: 308 total, 1 running, 307 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 4.2 us, 3.5 sy, 0.0 ni, 82.3 id, 0.0 wa, 0.0 hi, 1.7 si, 8.3 st
|
||||
%Cpu1 : 6.2 us, 2.7 sy, 0.0 ni, 82.8 id, 0.0 wa, 0.0 hi, 1.4 si, 6.9 st
|
||||
%Cpu2 : 5.2 us, 2.8 sy, 0.0 ni, 84.0 id, 0.0 wa, 0.0 hi, 1.0 si, 6.9 st
|
||||
%Cpu3 : 4.5 us, 3.8 sy, 0.0 ni, 81.2 id, 0.0 wa, 0.0 hi, 1.4 si, 9.2 st
|
||||
%Cpu4 : 4.4 us, 2.4 sy, 0.0 ni, 83.3 id, 0.0 wa, 0.0 hi, 1.4 si, 8.5 st
|
||||
%Cpu5 : 5.5 us, 2.4 sy, 0.0 ni, 84.5 id, 0.0 wa, 0.0 hi, 1.0 si, 6.6 st
|
||||
%Cpu6 : 3.7 us, 2.7 sy, 0.0 ni, 85.6 id, 0.0 wa, 0.0 hi, 0.7 si, 7.4 st
|
||||
%Cpu7 : 3.1 us, 1.7 sy, 0.0 ni, 84.7 id, 0.0 wa, 0.0 hi, 1.4 si, 9.0 st
|
||||
KiB Mem : 16265688 total, 8715820 free, 3848432 used, 3701436 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 12019164 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
18592 27 20 0 4588208 271564 12196 S 66.9 1.7 154:58.93 mysqld
|
||||
1109 root 20 0 2381424 105512 37208 S 9.6 0.6 514:18.00 kubelet
|
||||
1113 root 20 0 1928952 55556 16024 S 8.9 0.3 567:43.53 containerd
|
||||
1114 root 20 0 1268692 105212 29644 S 8.6 0.6 516:43.38 dockerd
|
||||
3122 root 20 0 2169692 117212 33416 S 7.0 0.7 408:21.79 scope
|
||||
4132 root 20 0 1780136 43188 17952 S 6.0 0.3 193:27.58 calico-node
|
||||
3203 nfsnobo+ 20 0 116748 19720 5864 S 2.0 0.1 42:43.57 node_exporter
|
||||
12089 techstar 20 0 5666480 1.3g 23084 S 1.3 8.5 78:04.61 java
|
||||
5727 root 20 0 449428 38616 4236 S 1.0 0.2 49:02.98 gvfs-udisks2-vo
|
||||
|
||||
```
|
||||
|
||||
- **vm-k8s-worker-9**
|
||||
|
||||
```
|
||||
top - 23:45:23 up 5 days, 22:21, 4 users, load average: 12.51, 10.28, 9.19
|
||||
Tasks: 333 total, 4 running, 329 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 20.1 us, 7.5 sy, 0.0 ni, 43.3 id, 0.0 wa, 0.0 hi, 13.4 si, 15.7 st
|
||||
%Cpu1 : 20.1 us, 11.2 sy, 0.0 ni, 41.4 id, 0.0 wa, 0.0 hi, 11.9 si, 15.3 st
|
||||
%Cpu2 : 23.8 us, 10.0 sy, 0.0 ni, 35.4 id, 0.0 wa, 0.0 hi, 14.2 si, 16.5 st
|
||||
%Cpu3 : 15.1 us, 7.7 sy, 0.0 ni, 49.1 id, 0.0 wa, 0.0 hi, 12.2 si, 15.9 st
|
||||
%Cpu4 : 22.8 us, 6.9 sy, 0.0 ni, 40.5 id, 0.0 wa, 0.0 hi, 14.7 si, 15.1 st
|
||||
%Cpu5 : 17.5 us, 5.8 sy, 0.0 ni, 50.0 id, 0.0 wa, 0.0 hi, 10.6 si, 16.1 st
|
||||
%Cpu6 : 22.0 us, 6.6 sy, 0.0 ni, 45.1 id, 0.0 wa, 0.0 hi, 11.0 si, 15.4 st
|
||||
%Cpu7 : 19.2 us, 8.0 sy, 0.0 ni, 44.9 id, 0.0 wa, 0.0 hi, 9.8 si, 18.1 st
|
||||
KiB Mem : 16265688 total, 2567932 free, 7138952 used, 6558804 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 8736000 avail Mem
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
24122 root 20 0 9890064 612108 16880 S 201.0 3.8 1905:11 java
|
||||
2794 root 20 0 2307652 161224 33464 S 57.7 1.0 1065:54 scope
|
||||
1113 root 20 0 2607908 60552 15484 S 13.8 0.4 1008:04 containerd
|
||||
1109 root 20 0 2291748 110768 39140 S 12.8 0.7 722:41.17 kubelet
|
||||
1114 root 20 0 1285500 108664 30112 S 11.1 0.7 826:56.51 dockerd
|
||||
29 root 20 0 0 0 0 S 8.9 0.0 32:09.89 ksoftirqd/4
|
||||
6 root 20 0 0 0 0 S 8.2 0.0 41:28.14 ksoftirqd/0
|
||||
24 root 20 0 0 0 0 R 8.2 0.0 41:00.46 ksoftirqd/3
|
||||
39 root 20 0 0 0 0 R 8.2 0.0 41:08.18 ksoftirqd/6
|
||||
19 root 20 0 0 0 0 S 7.9 0.0 39:10.22 ksoftirqd/2
|
||||
14 root 20 0 0 0 0 S 6.2 0.0 40:58.25 ksoftirqd/1
|
||||
|
||||
```
|
||||
|
||||
很显然,worker-9的si(中断使用的CPU)和st(被偷走的CPU)都不算低。那这种情况就比较奇怪了,虚拟机本身都没有很高的CPU使用率,为什么st还这么高呢?难道CPU只能用到这种程度?
|
||||
|
||||
来,我们接着查下去。
|
||||
|
||||
#### 第二阶段:查看物理机CPU运行模式
|
||||
|
||||
在这个阶段,我们要查一下服务里有没有阻塞。就像前面提到的,我们要从全局监控的角度,来考虑所查看的性能分析计数器是不是完整,以免出现判断上的偏差。不过,我去查看了线程栈的具体内容,看到线程栈中并没有Blocked啥的,那我们就只能再回到物理机的配置里看了。
|
||||
|
||||
那对于物理机CPU,我们还有什么可看的呢?即使你盖上被子蒙着头想很久,从下到上把所有的逻辑都理一遍,也找不出什么地方会有阻塞。那我们就只有看宿主机的CPU运行模式了。
|
||||
|
||||
```
|
||||
-- 物理机器1
|
||||
[root@hp-server ~]# cpupower frequency-info
|
||||
analyzing CPU 0:
|
||||
driver: pcc-cpufreq
|
||||
CPUs which run at the same hardware frequency: 0
|
||||
CPUs which need to have their frequency coordinated by software: 0
|
||||
maximum transition latency: Cannot determine or is not supported.
|
||||
hardware limits: 1.20 GHz - 2.10 GHz
|
||||
available cpufreq governors: conservative userspace powersave ondemand performance
|
||||
current policy: frequency should be within 1.20 GHz and 2.10 GHz.
|
||||
The governor "conservative" may decide which speed to use
|
||||
within this range.
|
||||
current CPU frequency: 1.55 GHz (asserted by call to hardware)
|
||||
boost state support:
|
||||
Supported: yes
|
||||
Active: yes
|
||||
|
||||
-- 物理机器2
|
||||
[root@dell-server-2 ~]# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
powersave
|
||||
[root@dell-server-2 ~]# cpupower frequency-info
|
||||
analyzing CPU 0:
|
||||
driver: intel_pstate
|
||||
CPUs which run at the same hardware frequency: 0
|
||||
CPUs which need to have their frequency coordinated by software: 0
|
||||
maximum transition latency: Cannot determine or is not supported.
|
||||
hardware limits: 1.20 GHz - 2.20 GHz
|
||||
available cpufreq governors: performance powersave
|
||||
current policy: frequency should be within 1.20 GHz and 2.20 GHz.
|
||||
The governor "powersave" may decide which speed to use
|
||||
within this range.
|
||||
current CPU frequency: 2.20 GHz (asserted by call to hardware)
|
||||
boost state support:
|
||||
Supported: no
|
||||
Active: no
|
||||
2200 MHz max turbo 4 active cores
|
||||
2200 MHz max turbo 3 active cores
|
||||
2200 MHz max turbo 2 active cores
|
||||
2200 MHz max turbo 1 active cores
|
||||
|
||||
-- 物理机器3
|
||||
[root@dell-server-3 ~]# cpupower frequency-info
|
||||
analyzing CPU 0:
|
||||
driver: intel_pstate
|
||||
CPUs which run at the same hardware frequency: 0
|
||||
CPUs which need to have their frequency coordinated by software: 0
|
||||
maximum transition latency: Cannot determine or is not supported.
|
||||
hardware limits: 1.20 GHz - 2.20 GHz
|
||||
available cpufreq governors: performance powersave
|
||||
current policy: frequency should be within 1.20 GHz and 2.20 GHz.
|
||||
The governor "powersave" may decide which speed to use
|
||||
within this range.
|
||||
current CPU frequency: 2.20 GHz (asserted by call to hardware)
|
||||
boost state support:
|
||||
Supported: no
|
||||
Active: no
|
||||
2200 MHz max turbo 4 active cores
|
||||
2200 MHz max turbo 3 active cores
|
||||
2200 MHz max turbo 2 active cores
|
||||
2200 MHz max turbo 1 active cores
|
||||
|
||||
-- 物理机器4
|
||||
[root@lenvo-nfs-server ~]# cpupower frequency-info
|
||||
analyzing CPU 0:
|
||||
driver: acpi-cpufreq
|
||||
CPUs which run at the same hardware frequency: 0
|
||||
CPUs which need to have their frequency coordinated by software: 0
|
||||
maximum transition latency: 10.0 us
|
||||
hardware limits: 2.00 GHz - 2.83 GHz
|
||||
available frequency steps: 2.83 GHz, 2.00 GHz
|
||||
available cpufreq governors: conservative userspace powersave ondemand performance
|
||||
current policy: frequency should be within 2.00 GHz and 2.83 GHz.
|
||||
The governor "conservative" may decide which speed to use
|
||||
within this range.
|
||||
current CPU frequency: 2.00 GHz (asserted by call to hardware)
|
||||
boost state support:
|
||||
Supported: no
|
||||
Active: no
|
||||
|
||||
```
|
||||
|
||||
可以看到,没有一个物理机是运行在performance模式之下的。
|
||||
|
||||
在这里,我们需要对CPU的运行模式有一个了解:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/82/8cd3a3bee80eb77bf348b1a063a90682.jpg" alt="">
|
||||
|
||||
既然我们是性能分析人员,那自然要用performance模式了,所以我们把CPU模式修改如下:
|
||||
|
||||
```
|
||||
-- 物理机器1
|
||||
[root@hp-server ~]# cpupower -c all frequency-set -g performance
|
||||
Setting cpu: 0
|
||||
Setting cpu: 1
|
||||
Setting cpu: 2
|
||||
Setting cpu: 3
|
||||
Setting cpu: 4
|
||||
Setting cpu: 5
|
||||
Setting cpu: 6
|
||||
Setting cpu: 7
|
||||
Setting cpu: 8
|
||||
Setting cpu: 9
|
||||
Setting cpu: 10
|
||||
Setting cpu: 11
|
||||
Setting cpu: 12
|
||||
Setting cpu: 13
|
||||
Setting cpu: 14
|
||||
Setting cpu: 15
|
||||
Setting cpu: 16
|
||||
Setting cpu: 17
|
||||
Setting cpu: 18
|
||||
Setting cpu: 19
|
||||
Setting cpu: 20
|
||||
Setting cpu: 21
|
||||
Setting cpu: 22
|
||||
Setting cpu: 23
|
||||
Setting cpu: 24
|
||||
Setting cpu: 25
|
||||
Setting cpu: 26
|
||||
Setting cpu: 27
|
||||
Setting cpu: 28
|
||||
Setting cpu: 29
|
||||
Setting cpu: 30
|
||||
Setting cpu: 31
|
||||
[root@hp-server ~]# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
performance
|
||||
[root@hp-server ~]#
|
||||
|
||||
-- 物理机器2
|
||||
[root@dell-server-2 ~]# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
powersave
|
||||
[root@dell-server-2 ~]# cpupower -c all frequency-set -g performance
|
||||
Setting cpu: 0
|
||||
Setting cpu: 1
|
||||
Setting cpu: 2
|
||||
Setting cpu: 3
|
||||
Setting cpu: 4
|
||||
Setting cpu: 5
|
||||
Setting cpu: 6
|
||||
Setting cpu: 7
|
||||
Setting cpu: 8
|
||||
Setting cpu: 9
|
||||
Setting cpu: 10
|
||||
Setting cpu: 11
|
||||
Setting cpu: 12
|
||||
Setting cpu: 13
|
||||
Setting cpu: 14
|
||||
Setting cpu: 15
|
||||
[root@dell-server-2 ~]# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
performance
|
||||
[root@dell-server-2 ~]#
|
||||
|
||||
-- 物理机器3
|
||||
[root@dell-server-3 ~]# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
powersave
|
||||
[root@dell-server-3 ~]# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
powersave
|
||||
[root@dell-server-3 ~]# cpupower -c all frequency-set -g performance
|
||||
Setting cpu: 0
|
||||
Setting cpu: 1
|
||||
Setting cpu: 2
|
||||
Setting cpu: 3
|
||||
Setting cpu: 4
|
||||
Setting cpu: 5
|
||||
Setting cpu: 6
|
||||
Setting cpu: 7
|
||||
Setting cpu: 8
|
||||
Setting cpu: 9
|
||||
Setting cpu: 10
|
||||
Setting cpu: 11
|
||||
Setting cpu: 12
|
||||
Setting cpu: 13
|
||||
Setting cpu: 14
|
||||
Setting cpu: 15
|
||||
[root@dell-server-3 ~]# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
performance
|
||||
[root@dell-server-3 ~]#
|
||||
|
||||
-- 物理机器4
|
||||
[root@lenvo-nfs-server ~]# cpupower -c all frequency-set -g performance
|
||||
Setting cpu: 0
|
||||
Setting cpu: 1
|
||||
Setting cpu: 2
|
||||
Setting cpu: 3
|
||||
[root@lenvo-nfs-server ~]# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
|
||||
performance
|
||||
[root@lenvo-nfs-server ~]#
|
||||
|
||||
```
|
||||
|
||||
在我们一顿操作猛如虎之后,性能会怎么样呢?
|
||||
|
||||
结果,性能并没有好起来……这里我就不截图了,因为图和一开始的那张场景运行图一样。
|
||||
|
||||
在这里我们要知道,以上的分析过程说明不止是这个问题点,还有其他资源使用有短板我们没有找到。没办法,我们只能接着查。
|
||||
|
||||
## 总结
|
||||
|
||||
在这节课中,我们通过压力工具中的曲线,判断了瓶颈的存在。然后通过SkyWalking拆分了响应时间。
|
||||
|
||||
在确定了响应时间消耗点之后,我们又开始了两个阶段的分析:第一个阶段的证据链是从现象开始往下分析的,因为st cpu是指宿主机上的其他应用的消耗导致了此虚拟机的cpu资源被消耗,所以,我们去宿主机上去查了其他的虚拟机。这里我们要明确CPU资源应该用到什么样的程度,在发现了资源使用不合理之后,再接着做第二阶段的判断。
|
||||
|
||||
在第二阶段中,我们判断了CPU运行模式。在物理机中,如果我们自己不做主动的限制,CPU的消耗是没有默认限制的,所以我们才去查看CPU的运行模式。
|
||||
|
||||
但是,即便我们分析并尝试解决了以上的问题,TPS仍然没什么变化。可见,在计数器的分析逻辑中,虽然我们做了优化动作,但系统仍然有问题。只能说我们当前的优化手段,只解决了木桶中的最短板,但是其他短板,我们还没有找到。
|
||||
|
||||
请你注意,这并不是说我们这节课的分析优化过程没有意义。要知道,这些问题不解决,下一个问题也不会出现。所以,我们这节课的分析优化过程也非常有价值。
|
||||
|
||||
下节课,我们接着来找打开首页接口的性能瓶颈。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,请你思考一下:
|
||||
|
||||
1. 为什么我们看到虚拟机中st cpu高,就要去查看宿主机上的其他虚拟机?如果在宿主机上看到st cpu高,我们应该做怎样的判断?
|
||||
1. CPU的运行模式在powersave时,CPU的运行逻辑是什么?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果这节课让你有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
328
极客时间专栏/高楼的性能工程实战课/基准场景/12 | 打开首页之二:如何平衡利用硬件资源?.md
Normal file
328
极客时间专栏/高楼的性能工程实战课/基准场景/12 | 打开首页之二:如何平衡利用硬件资源?.md
Normal file
@@ -0,0 +1,328 @@
|
||||
<audio id="audio" title="12 | 打开首页之二:如何平衡利用硬件资源?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/31/4ef08e0a0480a5d668d8e81985829c31.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
针对打开首页接口的性能问题,我们在上节课中确定了是Gateway在消耗响应时间,达到了近100毫秒。于是,我们开始定位Gateway上的响应时间消耗。
|
||||
|
||||
在第一阶段的时候,我们关注了应用所在的主机,同时还了解到,宿主机总共有四台机器;在第二阶段,我们查看了物理机的CPU模式。并尝试通过修改CPU运行模式来优化性能。可是,问题仍然没有解决,TPS没见提升,响应时间依旧很长。
|
||||
|
||||
今天这节课,我们进入第三阶段,继续分析其他的瓶颈点,比如wa cpu、资源均衡使用、网络带宽等问题。其中,**在性能的分析逻辑里,资源均衡使用是一个非常容易被忽略,但又极为重要的方面。**我们通常都盯着计数器给出的数值有什么异常,而不是考虑资源怎么做相应的调配。
|
||||
|
||||
在我们这个案例中,系统是用k8s来管理资源的,所以我们必须要关注资源的均衡使用,避免出现有些服务性能很差,却和性能好的服务分配同样资源的情况。另外,网络资源在k8s中会跨越好几层,我们也要着重关注一下。
|
||||
|
||||
在学习这节课时,我建议你多思考下资源的均衡使用问题。现在,我们就开始今天的课程。
|
||||
|
||||
## 定位gateway上的响应时间消耗
|
||||
|
||||
### 第三阶段:NFS服务器的wa cpu偏高
|
||||
|
||||
根据分析的逻辑,我们仍然是先看全局监控数据,思路依旧是“全局-定向”,这是我一贯的顺序了。
|
||||
|
||||
因此,我们现在再来查一下全局监控计数器,得到下面这样的视图:
|
||||
|
||||
```
|
||||
[root@lenvo-nfs-server ~]# top
|
||||
top - 00:12:28 up 32 days, 4:22, 3 users, load average: 9.89, 7.87, 4.71
|
||||
Tasks: 217 total, 1 running, 216 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 0.0 us, 4.0 sy, 0.0 ni, 34.8 id, 61.2 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu1 : 0.0 us, 4.7 sy, 0.0 ni, 27.8 id, 67.6 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu2 : 0.0 us, 6.1 sy, 0.0 ni, 0.0 id, 93.9 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu3 : 0.0 us, 7.6 sy, 0.0 ni, 3.4 id, 82.8 wa, 0.0 hi, 6.2 si, 0.0 st
|
||||
KiB Mem : 3589572 total, 82288 free, 775472 used, 2731812 buff/cache
|
||||
KiB Swap: 8388604 total, 8036400 free, 352204 used. 2282192 avail Mem
|
||||
|
||||
```
|
||||
|
||||
可以看到,计数器wa的CPU使用率偏高,其中Cpu2的wa已经达到90%以上。我们知道,wa cpu是指CPU在读写的时候,所产生的IO等待时间占CPU时间的百分比。那么,它现在竟然这么高,是因为写操作有很多吗?
|
||||
|
||||
这时候我们就要关注下IO的状态了,因为IO慢绝对是一个性能问题。通过iostat命令,我们看到IO状态如下:
|
||||
|
||||
```
|
||||
[root@lenvo-nfs-server ~]# iostat -x -d 1
|
||||
Linux 3.10.0-693.el7.x86_64 (lenvo-nfs-server) 2020年12月26日 _x86_64_ (4 CPU)
|
||||
..................
|
||||
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
|
||||
sda 0.00 0.00 94.00 39.00 13444.00 19968.00 502.44 108.43 410.80 52.00 1275.59 7.52 100.00
|
||||
|
||||
|
||||
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
|
||||
sda 0.00 18.00 137.00 173.00 17712.00 43056.00 392.05 129.46 601.10 38.80 1046.38 3.74 115.90
|
||||
..................
|
||||
|
||||
```
|
||||
|
||||
你可以看到,IO使用率达到了100%,说明IO的过程实在是太慢了。
|
||||
|
||||
接下来,我们再查查Block Size是多少,算一下当前IO到底是随机读写还是顺序读写。虽然大部分操作系统都默认Block Size是4096,但是,本着不出小错的原则,我们还是查一下比较放心。
|
||||
|
||||
我们先确定磁盘的格式是什么:
|
||||
|
||||
```
|
||||
[root@lenvo-nfs-server ~]# cat /proc/mounts
|
||||
...................
|
||||
/dev/sda5 / xfs rw,relatime,attr2,inode64,noquota 0 0
|
||||
...................
|
||||
[root@lenvo-nfs-server ~]#
|
||||
|
||||
```
|
||||
|
||||
通过上述命令可以知道,这个磁盘是XFS格式。那我们就用下面这个命令来查看Block Size:
|
||||
|
||||
```
|
||||
[root@lenvo-nfs-server ~]# xfs_info /dev/sda5
|
||||
meta-data=/dev/sda5 isize=512 agcount=4, agsize=18991936 blks
|
||||
= sectsz=512 attr=2, projid32bit=1
|
||||
= crc=1 finobt=0 spinodes=0
|
||||
data = bsize=4096 blocks=75967744, imaxpct=25
|
||||
= sunit=0 swidth=0 blks
|
||||
naming =version 2 bsize=4096 ascii-ci=0 ftype=1
|
||||
log =internal bsize=4096 blocks=37093, version=2
|
||||
= sectsz=512 sunit=0 blks, lazy-count=1
|
||||
realtime =none extsz=4096 blocks=0, rtextents=0
|
||||
[root@lenvo-nfs-server ~]#
|
||||
|
||||
```
|
||||
|
||||
结果显示,Block Size是4096。同时,我们也可以看到读写基本上都是顺序的,不是随机。
|
||||
|
||||
那我们就来计算一条数据,确认一下顺序写的能力。如果全部是随机写,那么:
|
||||
|
||||
$次数=(43056\times 1024)\div 4096=10,764次$
|
||||
|
||||
但是,实际上写只有173次,所以确实是顺序写了。
|
||||
|
||||
问题又来了,一次写多少个Block呢?
|
||||
|
||||
$(43056\times1024)\div173\div4096\approx 62个$
|
||||
|
||||
我们得出,一次写62个Block。从这样的数据来看,说明顺序写的能力还是不错的。因为对普通磁盘来说,应用在读写的时候,如果是随机写多,那写的速度就会明显比较慢;如果顺序写多,那么写的速度就可以快起来。
|
||||
|
||||
你发现了吗?虽然当前磁盘的顺序写能力不错,但是等待的时间也明显比较多。所以,接下来,我们得查一下是什么程序写的。这里我们用iotop命令查看:
|
||||
|
||||
```
|
||||
Total DISK READ : 20.30 M/s | Total DISK WRITE : 24.95 M/s
|
||||
Actual DISK READ: 20.30 M/s | Actual DISK WRITE: 8.27 M/s
|
||||
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
|
||||
12180 be/4 root 2.39 M/s 16.01 M/s 0.00 % 35.94 % [nfsd]
|
||||
12176 be/4 root 3.20 M/s 0.00 B/s 0.00 % 32.25 % [nfsd]
|
||||
12179 be/4 root 3.03 M/s 6.43 M/s 0.00 % 32.23 % [nfsd]
|
||||
12177 be/4 root 2.44 M/s 625.49 K/s 0.00 % 31.64 % [nfsd]
|
||||
12178 be/4 root 2.34 M/s 1473.47 K/s 0.00 % 30.43 % [nfsd]
|
||||
12174 be/4 root 2.14 M/s 72.84 K/s 0.00 % 29.90 % [nfsd]
|
||||
12173 be/4 root 2.91 M/s 121.93 K/s 0.00 % 24.95 % [nfsd]
|
||||
12175 be/4 root 1894.69 K/s 27.71 K/s 0.00 % 24.94 % [nfsd]
|
||||
...............
|
||||
|
||||
```
|
||||
|
||||
可以看到,IO都是NFS写过来的。那NFS的流量又是从哪里来的呢?从下面的数据来看,这些流量是从各个挂载了NFS盘的机器写过来的,这是我们一开始部署应用的时候,考虑统一使用NFS来做IO的思路。因为这个机器挂载了一个大容量的磁盘,为了保证磁盘够用,就把多个主机挂载了NFS盘。
|
||||
|
||||
```
|
||||
191Mb 381Mb 572Mb 763Mb 954Mb
|
||||
mqqqqqqqqqqqqqqqqqqqvqqqqqqqqqqqqqqqqqqqvqqqqqqqqqqqqqqqqqqqvqqqqqqqqqqqqqqqqqqqvqqqqqqqqqqqqqqqqqqq
|
||||
172.16.106.119:nfs => 172.16.106.130:multiling-http 1.64Mb 2.04Mb 3.06Mb
|
||||
<= 26.2Mb 14.5Mb 19.8Mb
|
||||
172.16.106.119:nfs => 172.16.106.100:apex-mesh 1.43Mb 2.18Mb 3.79Mb
|
||||
<= 25.5Mb 14.2Mb 14.4Mb
|
||||
172.16.106.119:nfs => 172.16.106.195:vatp 356Kb 1.27Mb 1.35Mb
|
||||
<= 9.71Mb 7.04Mb 7.41Mb
|
||||
172.16.106.119:nfs => 172.16.106.56:815 7.83Kb 4.97Kb 4.81Kb
|
||||
<= 302Kb 314Kb 186Kb
|
||||
172.16.106.119:nfs => 172.16.106.79:device 11.0Kb 7.45Kb 7.57Kb
|
||||
<= 12.4Kb 22.0Kb 28.5Kb
|
||||
172.16.106.119:ssh => 172.16.100.201:cnrprotocol 2.86Kb 2.87Kb 5.81Kb
|
||||
<= 184b 184b 525b
|
||||
169.254.3.2:60010 => 225.4.0.2:59004 2.25Kb 2.40Kb 2.34Kb
|
||||
<= 0b 0b 0b
|
||||
169.254.6.2:60172 => 225.4.0.2:59004 2.25Kb 2.40Kb 2.34Kb
|
||||
<= 0b 0b 0b
|
||||
172.16.106.119:nfs => 172.16.106.149:986 0b 1.03Kb 976b
|
||||
<= 0b 1.26Kb 1.11Kb
|
||||
|
||||
|
||||
qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
|
||||
TX: cum: 37.0MB peak: 31.9Mb rates: 3.44Mb 5.50Mb 8.22Mb
|
||||
RX: 188MB 106Mb 61.8Mb 36.2Mb 41.8Mb
|
||||
TOTAL: 225MB 111Mb 65.2Mb 41.7Mb 50.1Mb
|
||||
|
||||
```
|
||||
|
||||
我们在Total DISK WRITE 中可以看到,读能力才达到20M。没办法,既然wa这个机器的能力不怎么好,那就只有放弃统一写的思路。不过,为了不让机器的IO能力差成为应用的瓶颈点,我们还是再尝试一下这两个动作:
|
||||
|
||||
- 第一,把MySQL的数据文件移走;
|
||||
- 第二,把Log移走。
|
||||
|
||||
接着我们执行场景,希望结果能好。
|
||||
|
||||
可是,在我查看了TPS和RT曲线后,很遗憾地发现,结果并没有改善。TPS依然很低并且动荡非常大:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/b3/dba0181c5de1efcec5108e01cd87bbb3.png" alt="">
|
||||
|
||||
看来我们的努力并没有什么效果,悲剧!就这样,命运让我们不得不来到第四个阶段。
|
||||
|
||||
### 第四阶段:硬件资源耗尽,但TPS仍然很低
|
||||
|
||||
这个阶段我们查什么呢?仍然是全局监控的数据。我们来看一下所有主机的Overview资源:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/c9/206a269a9c10f063f8443f1c01cbb1c9.png" alt="">
|
||||
|
||||
从上图中可以看到,虚拟机k8s-worker-8的CPU使用率已经很高了,达到了95.95%。那我们就登录到这台虚拟机上,看看更详细的全局监控数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/83/6b/832e0138e97ba668d8d0b45bcde9ac6b.png" alt="">
|
||||
|
||||
因为CPU不超分了,所以我们可以很明显地看到,k8s-worker-8中的CPU被耗尽。从进程上来看,CPU是被我们当前正在测试的接口服务消耗的。并且在这台虚拟机上,不止有Portal这一个进程,还有很多其他的服务。
|
||||
|
||||
那我们就把Portal服务调度到一个不忙的worker上去,比如移到worker-3(6C16G)上:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/62/a5143ba93ce9c3408e55d895ebd66e62.png" alt="">
|
||||
|
||||
得到如下结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/38/ca170482d341132a44ebce12byy99238.png" alt="">
|
||||
|
||||
我们看到,TPS已经有所上升了,达到了近300,性能确实变好了一些。但是,这个数据还不如我们一开始不优化的结果,毕竟一开始还能达到300TPS呢。那我们就接着分析当前的瓶颈在哪里。
|
||||
|
||||
我们先来看一下主机的性能数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/01/3d41ef530a6a40c23a35ab1819ca6701.png" alt="">
|
||||
|
||||
其中,worker-8的CPU使用率达到了90.12%。为什么这个CPU还是如此之高呢?我们继续来top一下,看看worker-8的性能数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/34/d8/34156e85f0724e47b23fcb9a1fcf93d8.png" alt="">
|
||||
|
||||
你看,在process table中,排在最上面的是Gateway服务,说明是Gateway进程消耗的CPU最多。既然如此,我们自然要看看这个进程中的线程是不是都在干活。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/17/932490a19dda4bc2f562030e3f393417.png" alt="">
|
||||
|
||||
我们看到上图中全是绿色的,也就是说Gateway中的线程一直处于Runnable状态,看来工作线程确实挺忙的了。而在前面的worker-8性能数据中,si cpu已经达到了16%左右。所以结合这一点,我们来看一下实时的软中断数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/30/abd026dd456e01d21a81b520f99ae430.png" alt="">
|
||||
|
||||
可以看到网络软中断一直在往上跳,这说明确实是网络软中断导致si cpu变高的。网络软中断的变化是我们根据证据链找下来的。证据链如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/03/c37f2f51cfe057f3f071be1d14079703.jpg" alt="">
|
||||
|
||||
我们再看一下网络带宽有多大:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/eb/0f/eb6d22ae621874c387130a3d8144550f.png" alt="">
|
||||
|
||||
可以看到,网络带宽倒是不大。
|
||||
|
||||
从上述Gateway的工作线程、软中断数据和网络带宽情况来看,Gateway只负责转发,并没有什么业务逻辑,也没有什么限制。所以,针对TPS上不去的原因,似乎除了网络转发能力比较差之外,我们再找不到其他解释了。
|
||||
|
||||
这个思路其实是需要一些背景知识的,因为我们通常用网络带宽来判断网络是不是够用,但是这是不够的。你要知道,在网络中当小包过多的时候,网络带宽是难以达到线性流量的。所以,我们这里的网络带宽即便不会很高,也会导致网络软中断的增加和队列的出现。
|
||||
|
||||
既然如此,那我们就把这个Gateway也从worker-8移到worker-2(6C16G)上去,做这一步是为了减少网络软中断的争用。我们再看一下集群的整体性能:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0f/f2/0f62326941b30e6d88f89f00f4da1ff2.png" alt="">
|
||||
|
||||
看起来不错哦,worker-3的CPU使用率降到了70.78%。不过网络带宽有几个地方变红了,这个我们后面再分析。至少我们从这里看到,压力是起来了。
|
||||
|
||||
我们回来看一下压力的情况:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/98/82/98d7f4b019633ce9593e5bce84f1d182.png" alt="">
|
||||
|
||||
TPS已经达到1000左右了!棒棒的,有没有!我们画一个TPS对比图庆祝一下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/20/fae84b95ea49bbc4f43a2af83bb05f20.jpg" alt="">
|
||||
|
||||
其实到这里,打开首页这个接口的基准场景就可以结束了,因为我们已经优化到了比需求还要高的程度。只是从技术角度来说,一个系统优化到最后是会有上限的,所以,我们仍然需要知道这个上限在哪里。
|
||||
|
||||
### 第五阶段:硬件资源还是要用完
|
||||
|
||||
现在压力把worker-3的CPU资源用得最高,用到了70.78%。那么,下面我们就要把这个机器的硬件资源给用完,因为只有将资源都用尽,我们才能判断系统容量的最上限。这也就是我一直强调的,要将**性能优化分为两个阶段:一是把资源用起来;二是把容量调上去。**就算不是CPU资源,把其他的资源用完也可以。
|
||||
|
||||
既然这时候压力已经把worker-3的CPU资源用到了70.78%,那我们就到这个应用中看一下线程把CPU用得怎么样。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e1/a5/e1343ee1e86979dd7008cfbaf9db1da5.png" alt="">
|
||||
|
||||
你看,这里面的线程确实都忙起来了。
|
||||
|
||||
既然如此,那我们把Tomcat和JDBC连接的最大值都改到80,再来看一下TPS的表现(请你注意,这里只是一个尝试,所以改大即可,并没有什么道理。在后续的测试过程中,我们还要根据实际情况来做调整,就是不能让线程太大,也不能不够用)。
|
||||
|
||||
为了让压力能直接压到一个节点上,我们跳过Ingress,用分段的测法直接把压力发到服务上。然后,我们去Pod里设置一个node port把服务代理出来,再修改一下压力脚本。得到结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/74/cd/74ac70cc7f3ab33ec90c63fd860d27cd.png" alt="">
|
||||
|
||||
TPS还是抖动大。那我们接着看全局监控:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/41/6978eefe6bd9acd46c836c46c54f3141.png" alt="">
|
||||
|
||||
看上图就可以知道,有几个主机的带宽都飘红了,而其他的资源使用率并没有特别高。前面我们有说过,分析网络问题,不应该只看网络带宽,还要分析其他的内容,下面我们就得分析一下网络带宽。
|
||||
|
||||
我们到监控工具中看一下网络的流量,你可以看到确实有一些非被测应用在占用带宽,并且占得还不小:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b0/f6/b04997c487a298bc5eb54a3cc8c7ebf6.png" alt="">
|
||||
|
||||
我们再看总体带宽,发现已经用了4G多:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5f/e3/5f21e2761a71550b572145b785e79ee3.png" alt="">
|
||||
|
||||
为了弄清楚那些与被测系统无关的应用,会对带宽消耗产生影响进而影响TPS,我们现在先把影响带宽的应用都删除了,比如Weave Scope、Monitoring的监控工具等,从列表中来看这些应用占了不小的带宽。
|
||||
|
||||
然后我们再次测试,发现TPS有所上升,关键是稳定了很多:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/1f/e55f8dcf308c762226d68a450789a51f.png" alt="">
|
||||
|
||||
我们可以看到,TPS已经上升到了1200左右,可见带宽对TPS还是造成了不小的影响。
|
||||
|
||||
接着,我们查一下网络的队列,发现应用上面已经出现了不小的Recv_Q。
|
||||
|
||||
```
|
||||
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name Timer
|
||||
tcp 759 0 10.100.69.229:8085 10.100.140.32:35444 ESTABLISHED 1/java off (0.00/0/0)
|
||||
tcp 832 0 10.100.69.229:34982 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4871.85/0/0)
|
||||
tcp 1056 0 10.100.69.229:34766 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4789.93/0/0)
|
||||
tcp 832 0 10.100.69.229:35014 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4888.23/0/0)
|
||||
tcp 3408 0 10.100.69.229:34912 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4855.46/0/0)
|
||||
tcp 3408 0 10.100.69.229:35386 10.96.224.111:3306 ESTABLISHED 1/java keepalive (5019.30/0/0)
|
||||
tcp 3392 0 10.100.69.229:33878 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4495.01/0/0)
|
||||
tcp 560 0 10.100.69.229:35048 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4888.23/0/0)
|
||||
tcp 1664 0 10.100.69.229:34938 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4855.46/0/0)
|
||||
tcp 759 0 10.100.69.229:8085 10.100.140.32:35500 ESTABLISHED 1/java off (0.00/0/0)
|
||||
tcp 832 0 10.100.69.229:35114 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4921.00/0/0)
|
||||
tcp 1056 0 10.100.69.229:34840 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4822.69/0/0)
|
||||
tcp 1056 0 10.100.69.229:35670 10.96.224.111:3306 ESTABLISHED 1/java keepalive (5117.60/0/0)
|
||||
tcp 1664 0 10.100.69.229:34630 10.96.224.111:3306 ESTABLISHED 1/java keepalive (4757.16/0/0)
|
||||
|
||||
```
|
||||
|
||||
从这里来看,网络已经成为了下一个瓶颈(关于这一点,我们在后续的课程里会讲)。
|
||||
|
||||
如果你想接着调优,还可以从应用代码下手,让应用处理得更快。不过,对于基准测试来说,一个没有走任何缓存的接口,在一个6C16G的单节点虚拟机上能达到这么高的TPS,我觉得差不多了。
|
||||
|
||||
接下来,我们还要去折腾其他的接口,所以,我们对这个接口的优化到这里就结束了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/34/29beca35bbf7d689bebf27fcf8397634.jpg" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
在打开首页这个接口的基准场景中,涉及到了很多方面的内容。从一开始的信息整理,比如访问路径、查看代码逻辑、场景试运行等,都是在为后面的分析做准备。
|
||||
|
||||
而当我们看到响应时间高,然后做拆分时间这一步,就是我一直在RESAR性能工程中强调的“**分析的起点**”。因为在此之前,我们用的都是压力工具上的数据,只是把它们罗列出来就好了,没有任何分析的部分。
|
||||
|
||||
对于拆分时间,我们能用的手段有多种,你可以用你喜欢的方式,像日志、APM工具,甚至抓包都是可以的。拆分了时间之后,我们就要分析在某个节点上响应时间高的时候,要怎么做。这时就用到了我一直强调的“全局-定向”监控分析思路。
|
||||
|
||||
在每一个阶段,你一定要清楚地定义优化的方向和目标,否则容易迷失方向。特别是对于一些喜欢把鼠标操作得特别快的同学,容易失去焦点,我劝你慢点操作,想清楚下一步再动。
|
||||
|
||||
而我们上述整个过程,都依赖于我说的性能分析决策树。从树顶往下,一层层找下去,不慌不乱,不急不燥。
|
||||
|
||||
只要你想,就能做到。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,我给你留三个思考题。
|
||||
|
||||
<li>
|
||||
当st cpu高的时候,你要去看什么?
|
||||
</li>
|
||||
<li>
|
||||
当wa cpu高的时候,你要去看什么?
|
||||
</li>
|
||||
<li>
|
||||
为什么我们要把硬件资源用完?
|
||||
</li>
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
402
极客时间专栏/高楼的性能工程实战课/基准场景/13 | 用户登录:怎么判断线程中的Block原因?.md
Normal file
402
极客时间专栏/高楼的性能工程实战课/基准场景/13 | 用户登录:怎么判断线程中的Block原因?.md
Normal file
@@ -0,0 +1,402 @@
|
||||
<audio id="audio" title="13 | 用户登录:怎么判断线程中的Block原因?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/db/3a/dbd9452db8dd5b6fba778fe70da4a83a.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
这节课我们接着来“玩”一下用户登录。在[第10讲](https://time.geekbang.org/column/article/362010)的课程中,我们以登录功能为例做了一些分析,来说明基准场景中的一些要点。但是,我们还没有把它优化完,所以这节课还要接着来折腾它。
|
||||
|
||||
用户登录说起来只是一个很普通的功能,不过它的逻辑一点也不简单。因为登录过程要对个人的信息进行对比验证,验证过程中又要调用相应的加密算法,而加密算法是对性能要求很高的一种功能。复杂的加密算法安全性高,但性能就差;不复杂的加密算法性能好,但安全性低,这是一个取舍的问题。
|
||||
|
||||
另外,还有Session存储和同步。对于个大型的系统来说,不管你在哪个系统访问,在调用其他系统时如果需要验证身份就要同步Session信息,并且在做业务时,我们也要把相应的Session信息带上,不然就识别不了。
|
||||
|
||||
你看,登录功能实际上会涉及到很多的业务,它其实一点也不简单。所以,这节课我会带着你好好分析用户登录功能,并带你了解在压力过程中业务逻辑链路和整体TPS之间的关系。同时,也希望你能学会判断线程中的BLOCKED原因。
|
||||
|
||||
## 修改加密算法
|
||||
|
||||
还记得在[第10讲](https://time.geekbang.org/column/article/362010)中,我们在基准场景中对登录业务的测试结果吗?在10个压力线程下,TPS达到了100左右。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/28/e487d4cc5eb67d58b6afc9e11eba7b28.png" alt="">
|
||||
|
||||
同时,在[第10](https://time.geekbang.org/column/article/362010)[讲](https://time.geekbang.org/column/article/362010)中,我们发现了加密算法BCrypt效率低之后,讨论了两种优化方式:一种是用更快的加密方式,另一种是去掉这个加密算法。当时,我选择把加密算法BCrypt直接去掉。在这节课中,我们来试试第一种方式,把它改为MD5,具体有两个动作:
|
||||
|
||||
- 更改加密算法。之前的BCrypt加密算法虽然安全性高,但性能差,所以建议改成MD5。
|
||||
- 加载所有用户到Redis中。
|
||||
|
||||
我们再跑一遍压力场景。注意,在跑这一遍之前,我们只是更改了加密算法,并没有执行加载缓存的动作。我希望一次只做一个动作来判断结果(但是上面两个动作我们都要做哦,请你接着看下去),结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e0/88/e0e107c341982ddc65e9792a35893688.png" alt="">
|
||||
|
||||
从上面的结果来看,性能有些上升了,但是还没达到我想要的样子。我希望性能有突飞猛进的增加,而不是现在这样不温不火的样子,看着就来气。所以,我们还是要继续“收拾收拾”这个接口,使用缓存,看下效果如何。
|
||||
|
||||
## 检验缓存的效果
|
||||
|
||||
为了确定缓存对后续的性能优化产生了效果,我们可以用两个手段来检验效果:
|
||||
|
||||
- 把参数化数据量降下来,只用少量的数据测试一下(请注意,我们只是尝试一下,并不是说用少量的数据来运行场景是对的);
|
||||
- 直接加载全部缓存。
|
||||
|
||||
我们得到这样的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/e9/ac4a9bf86a2242d7bb378823721203e9.png" alt="">
|
||||
|
||||
从曲线上看,登录接口能达到300TPS左右了。但是,我还是觉得不够好,因为从硬件资源上来看,再根据我以往的经验,它应该还能达到更高才对。
|
||||
|
||||
而在分析的过程中,再也没有[第11](https://time.geekbang.org/column/article/362940)[讲](https://time.geekbang.org/column/article/362940)和[第](https://time.geekbang.org/column/article/363736)[12讲](https://time.geekbang.org/column/article/363736)中提到的硬件资源的问题,但是在这里我们通过查看全局监控数据,看到的是us cpu高,说明确实都是业务逻辑在消耗CPU资源了。所以,我们就只有从登陆逻辑入手,来优化这个问题了。
|
||||
|
||||
## 修改登录的逻辑
|
||||
|
||||
通过阅读源代码,我整理了这个系统的原登录逻辑:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1b/e7/1b3425fb8788236b6a812f78ac8bd8e7.jpg" alt="">
|
||||
|
||||
这个逻辑看着比较啰嗦,其中Member服务调auth服务,倒还能理解。可是,Auth服务为什么还要到Member里取用户名呢?自己直接查缓存或DB不香吗?从架构设计的角度来看,为了避免共享数据库,这样的设计似乎也没啥。只是在我们的优化过程中,需要根据实际环境来做判断。
|
||||
|
||||
在我们这个环境中,需要把DB共用,这样Auth服务就可以直接使用数据库,而不用再从Member绕一圈。所以,我们先改成下面这种新的登录逻辑,这样就可以减少一次调用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6f/41/6fe6e7694555c0f7a400a20ae35d4f41.jpg" alt="">
|
||||
|
||||
修改之后,登录TPS如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/af/49/affc3890ee5817d45a7ff67f11d96f49.png" alt="">
|
||||
|
||||
从结果上来看,TPS确实有增加,已经到700以上了。很好。
|
||||
|
||||
这时候是不是就可以结束分析了呢?不是,我们还需要知道当前的瓶颈点在哪,因为根据我的性能理念,每个业务都会有瓶颈点,不管优化到什么程度,除非一直把硬件资源耗光。所以,我们继续接着分析。
|
||||
|
||||
## 看架构图
|
||||
|
||||
还是一样,在分析性能瓶颈之前,我们先来看架构图,了解用户登录接口会涉及到哪些服务和技术组件。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fe/80/fe3783b643a5c121eba4b64a2b123880.png" alt="">
|
||||
|
||||
从这个架构图中可以看到,登录操作跨了Gateway/Member/Auth三个服务,连接了Redis/MySQL两个组件。图中的MongoDB虽然看上去有线,但实际上登录并没有用上。
|
||||
|
||||
了解这些信息之后,我们按照分析逻辑,一步步来分析问题。
|
||||
|
||||
## 拆分时间
|
||||
|
||||
我们前面提到,修改登录逻辑后的TPS如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/78/32/7857609f9d0432fdf3a5c66a15480f32.png" alt="">
|
||||
|
||||
可以看到,响应时间已经上升到了100ms左右,所以,我们现在要找出这个时间消耗在了哪里。你可能已经注意到,图中的用户增加到了150。这是为了把响应时间拉大,便于我们分析。下面我们把这个响应时间拆分一下,看看问题出在哪里。
|
||||
|
||||
- **Gateway服务上的时间**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/bd/9f25bb77c2c5b7f72bc05b2f1eyyabbd.png" alt="">
|
||||
|
||||
- **Member服务上的时间**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/d7/b578360e3dec474700b631753f78aed7.png" alt="">
|
||||
|
||||
- **Auth服务上的时间**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/c7/dc8d2yy46b0671ffb593f5d6d022bfc7.png" alt="">
|
||||
|
||||
我们看到,Member服务上的时间消耗是150ms左右,Auth服务上的时间消耗有60ms左右。Member服务是我们要着重分析的,因为它的响应时间更长。而Auth上虽然时间不太长,但是也达到了60ms左右,从经验上来说,我觉得还是有点稍长了,最好平均能到50ms以下,所以我们也要稍微关心一下。
|
||||
|
||||
## 全局监控
|
||||
|
||||
我们的分析逻辑雷打不动,依旧是**先看全局监控,后看定向监控**。从下面这张全局监控图的数据来看,worker-7和worker-8的CPU使用率比其他的要高。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c1/50/c124a94a6b5529561e50546b552b1750.png" alt="">
|
||||
|
||||
既然worker-7/8的CPU使用率要高一点,那我们就要查一下这两个节点上跑着什么样的服务。所以我们来看一下POD的分布,大概看一下每个POD在哪个worker节点上,以便后面分析POD相互之间的影响:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/d9/e54ea48b51f6cb68a291ed759624ded9.png" alt="">
|
||||
|
||||
原来,在worker-7和worker-8上,分别运行着响应时间稍高的Auth服务和Member服务。对于这两个服务,我们都要分析,只是得一个一个来,那我们就从auth服务开始。
|
||||
|
||||
你可能会问:为什么要先从Auth服务下手呢?没啥原因,就是看它的CPU更红一点。你还可能奇怪:图中其他地方也红了,为什么不关注呢?我来逐一给你分析一下。
|
||||
|
||||
<li>
|
||||
图中的worker-1和worker-2,内存使用率相对较大,达到了70%以上。从经验上来说,我几乎没有怎么关心过Linux的内存使用率,除非出现大量的page faults。因为Linux内存在分配给应用程序使用之后,是会体现在Cache当中的。被应用程序Cache住的内存在操作系统上来看都是被使用的,但实际上可能并未真的被使用,这时操作系统会把这部分Cache内存计算到available内存当中,所以说,我们直接看操作系统级别的空闲内存是分析不出问题来的。
|
||||
</li>
|
||||
<li>
|
||||
在worker-2上,我们看到TCP的Time Wait达到近3万,不过这也不是我关心的点,因为Time Wait是正常的TCP状态,只有端口不够用、内存严重不足,我才会稍微看一眼。
|
||||
</li>
|
||||
<li>
|
||||
至于worker-1和worker-2的上下行带宽,看起来真是不大。在内网结构中,我们在测试的时候,内网带宽达到过好几Gbps,这点带宽还不足以引起我们的重视。
|
||||
</li>
|
||||
|
||||
所以,我们要“收拾”的还是worker-7和worker-8。
|
||||
|
||||
既然Auth服务在worker-7上,member服务在worker-8上,就像前面说的,我们不如就从Auth服务开始。
|
||||
|
||||
## Auth服务定向分析
|
||||
|
||||
对于Auth服务,我们从哪里开始分析呢?其实,我们可以按部就班。既然是Auth服务导致worker-7的CPU使用率偏高,那我们就可以走下面这个证据链:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/6a/20f64daa0f9fc17fd4c82bec3756756a.jpg" alt="">
|
||||
|
||||
按照这个证据链,我们应该先看进程。不过,仗着傻小子火气壮(俗称:艺高人胆大),我直接就去看线程状态了,想看看能不能凭经验蒙对一把。于是,我打开了Spring Boot Admin的线程页面:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8b/c6/8b7e968fa5343d494a46ce460cc487c6.png" alt="">
|
||||
|
||||
有没有满目疮痍的感觉?人生就是这样,到处都有惊吓。
|
||||
|
||||
在我颤抖着手点开一些红色的地方之后,看到了类似这样的信息:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/08/288524e6f68225b08d4425e121c49308.png" alt="">
|
||||
|
||||
可以看到,阻塞数非常大,达到了842。此外,锁拥有者ID是86676,锁拥有者名称是线程823。
|
||||
|
||||
我们抓两段栈出来看一下,找一下锁之间的关系:
|
||||
|
||||
```
|
||||
-- 第一处:
|
||||
"http-nio-8401-exec-884" #86813 daemon prio=5 os_prio=0 tid=0x00007f2868073000 nid=0x559e waiting for monitor entry [0x00007f2800c6d000]
|
||||
java.lang.Thread.State: BLOCKED (on object monitor
|
||||
at java.security.Provider.getService(Provider.java:1035)
|
||||
- waiting to lock <0x000000071ab1a5d8> (a sun.security.provider.Sun)
|
||||
at sun.security.jca.ProviderList.getService(ProviderList.java:332)
|
||||
.....................
|
||||
at com.dunshan.mall.auth.util.MD5Util.toMD5(MD5Util.java:11)
|
||||
at com.dunshan.mall.auth.config.MyPasswordEncoder.matches(MyPasswordEncoder.java:23)
|
||||
.....................
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
|
||||
at com.dunshan.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
|
||||
.....................
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
|
||||
.....................
|
||||
|
||||
|
||||
-- 第二处:
|
||||
"http-nio-8401-exec-862" #86728 daemon prio=5 os_prio=0 tid=0x00007f28680d6000 nid=0x553a waiting for monitor entry [0x00007f2802b8c000]
|
||||
java.lang.Thread.State: BLOCKED (on object monitor
|
||||
at sun.security.rsa.RSACore$BlindingParameters.getBlindingRandomPair(RSACore.java:404)
|
||||
- waiting to lock <0x000000071ddad410> (a sun.security.rsa.RSACore$BlindingParameters)
|
||||
at sun.security.rsa.RSACore.getBlindingRandomPair(RSACore.java:443)
|
||||
.....................
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46)
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
|
||||
at com.dunshan.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
|
||||
.....................
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
|
||||
.....................
|
||||
|
||||
```
|
||||
|
||||
这两个栈的内容并不是同一时刻出现的,说明这个BLOCKED一直存在。但是不管怎么样,这个栈在做RSA加密,它和Token部分有关。
|
||||
|
||||
其中,线程http-nio-8401-exec-884是BLOCKED状态,那就说明有其他线程持有这个锁,所以我们自然要看一下线程栈中的waiting to lock <0x000000071ab1a5d8>。其实,如果你有经验的话,一下子就能知道这里面是什么问题。不过,我们做性能分析的人要讲逻辑。
|
||||
|
||||
我在这里啰嗦几句,**当你碰到这种锁问题,又不知道具体原因的时候,要下意识地去打印一个完整的栈来看,而不是再到Spring Boot Admin里胡乱点**。为什么不建议你这么做?原因有这么几个:
|
||||
|
||||
- 由于线程太多,点着看逻辑关系比较累;
|
||||
- 不断在刷,眼晕;
|
||||
- 我不喜欢。
|
||||
|
||||
所以,对于前面遇到的锁问题,我们首先要做的就是到容器中的jstack里打印一下栈,把它下载下来,然后祭出工具打开看一眼。
|
||||
|
||||
你可能会问,为什么不用Arthas之类的工具直接在容器里看?主要是因为Arthas的Dashboard在Thread比较多的时候,看起来真心累。
|
||||
|
||||
下面这张图就是jstack打印出来的栈,在下载之后用工具打开的效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/7a/4bd559def066b9319b080f8183c4a77a.png" alt="">
|
||||
|
||||
是不是有种买彩票的感觉?看起来有那么多的BLOCKED状态的线程(多达842个),居然一个都没蒙到!我本来想抓BLOCKED状态的线程,并且线程描述是“Waiting on monitor”,但是,从上面的线程描述统计来看,一个也没见。哼,真生气。
|
||||
|
||||
这时候,身为一个做性能分析的人,我们一定要记得倒杯茶,静静心,默默地把jstack连续再执行几遍。我在这里就连续执行了10遍,然后再找每个栈的状态。
|
||||
|
||||
终于,Waiting on monitor来了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/bd/ac6bd08f67a8979e6d9e66yy038526bd.png" alt=""><img src="https://static001.geekbang.org/resource/image/9f/38/9ff82b785d0f1f5f46113f0bf5eca938.png" alt="">
|
||||
|
||||
看起来有得玩了!接下来让我们看看究竟是谁阻塞住了上面的线程。
|
||||
|
||||
我们先在相应的栈里,找到对应的持有锁的栈。下面是栈中的阻塞关系。
|
||||
|
||||
- **第一个栈**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/02/9396a601d0bb81678bec5958eca75b02.png" alt="">
|
||||
|
||||
- **第二个栈**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/34/8c47a54d11800d8bd7a016315ae83534.png" alt="">
|
||||
|
||||
你要注意,这是两个栈文件。所以,我们要分别从这两个栈文件里找到各自的对应等待关系。下面这段代码就对应了上面的Waiting线程。
|
||||
|
||||
```
|
||||
-- 第一处
|
||||
"http-nio-8401-exec-890" #86930 daemon prio=5 os_prio=0 tid=0x00007f28680a5800 nid=0x561d waiting for monitor entry [0x00007f2800263000]
|
||||
java.lang.Thread.State: BLOCKED (on object monitor
|
||||
at java.security.Provider.getService(Provider.java:1035)
|
||||
- locked <0x000000071ab1a5d8> (a sun.security.provider.Sun)
|
||||
at sun.security.jca.ProviderList.getService(ProviderList.java:332)
|
||||
.....................
|
||||
at com.dunshan.mall.auth.util.MD5Util.toMD5(MD5Util.java:11)
|
||||
at com.dunshan.mall.auth.config.MyPasswordEncoder.matches(MyPasswordEncoder.java:23)
|
||||
.....................
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46)
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
|
||||
at com.dunshan.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
|
||||
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
|
||||
|
||||
|
||||
-- 第二处
|
||||
"http-nio-8401-exec-871" #86739 daemon prio=5 os_prio=0 tid=0x00007f28681d6800 nid=0x5545 waiting for monitor entry [0x00007f2801a7b000]
|
||||
java.lang.Thread.State: BLOCKED (on object monitor
|
||||
at sun.security.rsa.RSACore$BlindingParameters.getBlindingRandomPair(RSACore.java:404)
|
||||
- locked <0x000000071ddad410> (a sun.security.rsa.RSACore$BlindingParameters)
|
||||
at sun.security.rsa.RSACore.getBlindingRandomPair(RSACore.java:443)
|
||||
.....................
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46)
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
|
||||
at com.dunshan.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
|
||||
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
|
||||
at com.dunshan.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
|
||||
|
||||
```
|
||||
|
||||
你看上面locked这一行的锁ID,既然找到了这两处持有锁的栈,那我们就通过上面的栈,到源码中找到这两处栈的代码:
|
||||
|
||||
```
|
||||
-- 第一处同步代码块
|
||||
public synchronized Service getService(String type, String algorithm) {
|
||||
checkInitialized()
|
||||
// avoid allocating a new key object if possible
|
||||
ServiceKey key = previousKey
|
||||
if (key.matches(type, algorithm) == false) {
|
||||
key = new ServiceKey(type, algorithm, false);
|
||||
previousKey = key
|
||||
|
||||
if (serviceMap != null) {
|
||||
Service service = serviceMap.get(key)
|
||||
if (service != null) {
|
||||
return service;
|
||||
|
||||
|
||||
ensureLegacyParsed()
|
||||
return (legacyMap != null) ? legacyMap.get(key) : null;
|
||||
|
||||
|
||||
|
||||
-- 第二处同步代码块
|
||||
// return null if need to reset the parameters
|
||||
BlindingRandomPair getBlindingRandomPair(
|
||||
BigInteger e, BigInteger d, BigInteger n) {
|
||||
|
||||
|
||||
if ((this.e != null && this.e.equals(e)) ||
|
||||
(this.d != null && this.d.equals(d))) {
|
||||
|
||||
|
||||
BlindingRandomPair brp = null;
|
||||
synchronized (this) {
|
||||
if (!u.equals(BigInteger.ZERO) &&
|
||||
!v.equals(BigInteger.ZERO))
|
||||
|
||||
|
||||
brp = new BlindingRandomPair(u, v);
|
||||
if (u.compareTo(BigInteger.ONE) <= 0 ||
|
||||
v.compareTo(BigInteger.ONE) <= 0) {
|
||||
|
||||
|
||||
// need to reset the random pair next time
|
||||
u = BigInteger.ZERO
|
||||
v = BigInteger.ZERO
|
||||
} else {
|
||||
u = u.modPow(BIG_TWO, n)
|
||||
v = v.modPow(BIG_TWO, n)
|
||||
|
||||
} // Otherwise, need to reset the random pair.
|
||||
|
||||
return brp;
|
||||
|
||||
|
||||
|
||||
return null;
|
||||
|
||||
```
|
||||
|
||||
你可以看到,第一处是JDK中提供的getService类采用了全局同步锁定,导致的分配key时产生争用,这个其实在JDK的Bug List中有过描述,详见[JDK-7092821](https://bugs.openjdk.java.net/browse/JDK-7092821)。准确来说,它不算是Bug,如果你想改的话,可以换一个库。
|
||||
|
||||
第二处是JDK中提供的RSA方法,是为了防范时序攻击特意设计成这样的。RSA中有大素数的计算,为了线程安全,RSA又加了锁。关于RSA的逻辑,你可以去看下源代码的/sun/security/rsa/RSACore.java中的逻辑。
|
||||
|
||||
不过,RSA是一种低效的加密方法,当压力发起来的时候,这样的synchronized类必然会导致BLOCKED出现。对此,在源码中有下面这样一段注释,其中建议先计算u/v,可以提高加密效率。
|
||||
|
||||
```
|
||||
* Computing inverses mod n and random number generation is slow, s
|
||||
* it is often not practical to generate a new random (u, v) pair for
|
||||
* each new exponentiation. The calculation of parameters might even be
|
||||
* subject to timing attacks. However, (u, v) pairs should not be
|
||||
* reused since they themselves might be compromised by timing attacks,
|
||||
* leaving the private exponent vulnerable. An efficient solution to
|
||||
* this problem is update u and v before each modular exponentiation
|
||||
* step by computing:
|
||||
*
|
||||
* u = u ^ 2
|
||||
* v = v ^ 2
|
||||
|
||||
* The total performance cost is small
|
||||
|
||||
```
|
||||
|
||||
既然我们已经知道了这两个BLOCKED产生的原因,那下一步的操作就比较简单了。
|
||||
|
||||
- 针对第一处锁:实现自己的方法,比如说实现一个自己的分布式锁。
|
||||
- 针对第二处锁:换一个高效的实现。
|
||||
|
||||
至此,我们就找到了应用中BLOCKED的逻辑。因为我们这是一个性能专栏,所以我就不再接着整下去了。如果你是在一个项目中,分析到这里就可以把问题扔给开发,然后去喝茶了,让他们伤脑筋去,哈哈。
|
||||
|
||||
不过,这只是一句玩笑而已,你可别当真。作为性能分析人员,我们要给出合情合理并且有证据链的分析过程,这样我们和其他团队成员沟通的时候,才会更加简单、高效。
|
||||
|
||||
## Member服务定向分析
|
||||
|
||||
分析完Auth服务后,我们再来看看Member服务的性能怎么样。因为全局监控数据前面我们已经展示了,所以这里不再重复说明,我们直接来拆分一下对Member服务调用时的响应时间。
|
||||
|
||||
- **Gateway上的响应时间**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/ea/ea227a75847cbbbdc5918449fd0ecbea.png" alt="">
|
||||
|
||||
- **Member上的响应时间**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/3f/5ed1408625eec5ef5beb6772b42f463f.png" alt="">
|
||||
|
||||
- **Auth上的响应时间**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/71/d15560e97228f70db9fd7d31a4c56971.png" alt="">
|
||||
|
||||
从上面的信息来看,这几段都有不同的时间消耗:Member服务上有80毫秒左右,Auth服务上已经有60毫秒左右,明显是有点高了。
|
||||
|
||||
我们登录到Member这个服务中,先看整体的资源使用情况。这里我用了最经典传统的top命令:
|
||||
|
||||
```
|
||||
%Cpu0 : 63.8 us, 12.4 sy, 0.0 ni, 9.2 id, 0.0 wa, 0.0 hi, 14.2 si, 0.4 st
|
||||
%Cpu1 : 60.3 us, 11.7 sy, 0.0 ni, 11.0 id, 0.0 wa, 0.0 hi, 16.6 si, 0.3 st
|
||||
%Cpu2 : 59.4 us, 12.0 sy, 0.0 ni, 14.1 id, 0.0 wa, 0.0 hi, 13.8 si, 0.7 st
|
||||
%Cpu3 : 59.8 us, 12.1 sy, 0.0 ni, 11.7 id, 0.0 wa, 0.0 hi, 15.7 si, 0.7 st
|
||||
|
||||
```
|
||||
|
||||
从CPU使用分布上来看,其他计数器都还正常,只是si有点高。这是一个网络中断的问题,虽然有优化的空间,但是受基础架构所限,性能提升得不太多,这也是为什么现在很多企业都放弃了虚拟化,直接选择容器化的一个原因。
|
||||
|
||||
针对这个网络中断的问题,我将在后面的课程中仔细给你扒一扒,这节课我们暂且不做过多的讲解。
|
||||
|
||||
## 总结
|
||||
|
||||
这节课我用登录功能给你串了一个完整的性能分析场景。
|
||||
|
||||
在前面代码修改的部分,性能分析过程是比较快的,我们就是看看哪里的代码逻辑会消耗更多的时间。这个思路就是前面提到的us cpu的证据链。
|
||||
|
||||
而接下来我们在分析Auth服务的时候,是先从拆分时间开始一步步走到代码里的,其中最核心的部分是从CPU到栈,再到BLOCKED的判断。当我们看到栈上有BLOCKED的时候,要记得打印栈信息。但是因为有些锁会非常快速地获取和释放,所以就可能会出现打印栈时,看到等某个锁的栈信息,但是整个栈文件中却没有这把锁的情况。这个时候,你就要注意了,**我们一定要去连续地多打几次栈,直到抓到对应的锁。**
|
||||
|
||||
这是分析栈中锁的一个关键,因为我们经常会看到等锁的栈信息,看不到持有锁的栈信息。而连续多打几次栈,就是为了把持有锁和等待锁的栈同时打印出来,否则我们就找不出分析的逻辑了。
|
||||
|
||||
接着,当我们看到了持有锁的栈之后,就根据自己业务代码的调用逻辑,一层层地去找是哪里加的锁。至于这个锁加的合理不合理,就和业务逻辑有关了。作为性能分析人员,这个时候我们就可以把开发、业务、架构等人拉到一起讨论。这个锁要不要改,不是做性能的人说了算,而是大家一起说了算。
|
||||
|
||||
通过上述的分析,相信你可以看到,在我的性能分析逻辑中,从现象到原理,都需要搞清楚。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,我给你留几个思考题来巩固今日所学。
|
||||
|
||||
1. 为什么看到BLOCKED的栈时要连续多打几次栈信息?
|
||||
1. 为什么从性能分析中要从现象到原理?
|
||||
1. 低效的代码有什么优化思路?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
245
极客时间专栏/高楼的性能工程实战课/基准场景/14 | 用户信息查询:如何解决网络软中断瓶颈问题?.md
Normal file
245
极客时间专栏/高楼的性能工程实战课/基准场景/14 | 用户信息查询:如何解决网络软中断瓶颈问题?.md
Normal file
@@ -0,0 +1,245 @@
|
||||
<audio id="audio" title="14 | 用户信息查询:如何解决网络软中断瓶颈问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/dd/2f/dd3d8ea77438c1ec3eab314189d3a42f.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
这节课我们接着来整另一个接口:用户信息查询。通过这个接口,我们一起来看看,当网络软中断过高时,会对TPS产生什么样的影响。其实对于这一点的判断,在很多性能项目中都会出现,而其中的难点就在于,很多人都无法将软中断跟响应时间慢和TPS所受到的影响关联起来。今天我就带你来解决这个问题。
|
||||
|
||||
同时,我也会给你讲解如何根据硬件配置及软件部署情况,做纯网络层的基准验证,以确定我们判断方向的正确性,进而提出具有针对性的优化方案。而我们最终优化的效果,会通过TPS对比来体现。
|
||||
|
||||
## 压力数据
|
||||
|
||||
我们先来看用户信息查询的压力数据情况如何。因为我们现在测试的是单接口,而用户信息查询又需要有登录态,所以我们要先跑一部分Token数据出来,再执行用户信息查询接口。
|
||||
|
||||
准备好Token数据后,第一次用户信息查询如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fd/7b/fd2eb1c3a0fa2b5520a1bc93f986a37b.png" alt="">
|
||||
|
||||
这个步骤只是试验一下,持续时间长是为了查找问题。从上图来看,这个接口的起点不错,已经达到750左右。
|
||||
|
||||
不过,性能瓶颈也比较明显:响应时间随着压力线程的增加而增加了,TPS也达到了上限。对于这样的接口,我们可以调优,也可以不调优,因为这个接口当前的TPS可以达到我们的要求。只不过,本着“**活着不就是为了折腾****”**的原则,我们还是要分析一下这个接口的瓶颈到底在哪里。
|
||||
|
||||
还是按照我们之前讲过的分析思路,下面我们来分析这个问题。
|
||||
|
||||
## 看架构图
|
||||
|
||||
从链路监控工具中,我们拉出架构图来,这样简单直接,又不用再画图了,真的是懒人必知技能。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/b9/5c44dfd49128498753cd1e7dfbdb28b9.png" alt="">
|
||||
|
||||
从上图可以知道,用户信息查询的路径是User - Gateway - Member - MySQL。
|
||||
|
||||
你也许会问,图中不是还有Redis、MongoDB、Monitor吗?是的,这些我们也要记在脑子里。这个接口用到了Redis,如果接口有问题,变慢了,我们也要分析;MongoDB并没有用上,所以我们不管它;Monitor服务是Spring Boot Admin服务,我们也暂且不管它,后面需要用到的时候再说。
|
||||
|
||||
注意,这一步是分析的铺垫,是为了让我们后面分析时不会混乱。
|
||||
|
||||
## 拆分响应时间
|
||||
|
||||
在场景数据中,我们明显看到响应时间慢了,那我们就要知道慢在了哪里。我们根据上面的架构图知道了用户信息查询接口的路径,现在就要拆分这个响应时间,看一看每一段消耗了多长时间。
|
||||
|
||||
如果你有APM工具,那可以直接用它查看每一段消耗的时间。如果你没有,也没关系,只要能把架构图画出来,并把时间拆分了就行,不管你用什么招。
|
||||
|
||||
另外,我啰嗦一句,请你不要过分相信APM工具厂商的广告,咱们还是要看疗效。在追逐技术的同时,我们也需要理智地判断到底是不是需要。
|
||||
|
||||
具体的拆分时间如下:
|
||||
|
||||
- User -Gateway
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/07/f2bf1bd9788d1e76f8e35622ea848907.png" alt="">
|
||||
|
||||
- Gateway上消耗的时间
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5f/20/5f64cyy3efd8f411ebdfcb293dd5d920.png" alt="">
|
||||
|
||||
- Gateway -Member
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8b/y4/8b992f2331135edacf0e4b93a9d19yy4.png" alt=""><img src="https://static001.geekbang.org/resource/image/43/f4/432ed240c57dcd81c1c13a23yy750cf4.png" alt="">
|
||||
|
||||
- Member上消耗的时间
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ae/4b/ae85fb6af7324a24d2abf2f8e679664b.png" alt="">
|
||||
|
||||
- Member到DB的时间
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/e0/dac8d5b2c1eb5fb2c628d118567bfce0.png" alt="">
|
||||
|
||||
我把上述拆分后的时间都整理到我们的架构图中:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bb/13/bb3676e3b24053b5b489b95012dd0613.png" alt="">
|
||||
|
||||
看到这张图,思路变得特别清晰了,有没有?根据上图的时间拆分,我们明显看到Member服务上消耗时间更多一点,所以下一步我们去关注Member服务。
|
||||
|
||||
## 全局监控分析
|
||||
|
||||
还是一样,我们先看全局监控:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7e/22/7e24929e3bb2baeaa00be336d6545d22.png" alt="">
|
||||
|
||||
其中,worker-8的CPU用得最多,我们先从这里下手。
|
||||
|
||||
这里我要跟你强调一下,**在全局监控的思路中,不是说我们看了哪些数据,而是我们要去看哪些数据**。这时候,你就必须先有一个全局计数器的东西。比如说在Kubernetes里,我们就要有这样的思路:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7c/e8/7c76b4796a4b28f2165b7fa4366d42e8.jpg" alt="">
|
||||
|
||||
也就是说,**我们要先把全局监控的计数器都罗列出来,然后再一个一个查去**。
|
||||
|
||||
其实,这里面不止是罗列那么简单,它还要有相应的逻辑。那怎么弄懂这个逻辑呢?这就要依赖于性能分析人员的基础知识了。我经常说,要想做全面的性能分析,就必须具备计算机基础知识,而这个知识的范围是很大的。之前我画过一张图,现在我做了一些修正,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/49/08978ac7ecc8ba84045d15c48f5b7e49.jpg" alt="">
|
||||
|
||||
图中这些内容,都是我们做性能分析时会遇到的东西。有人可能会说,这些已经远远超出性能工程师的技能范围了。所以我要再强调一下,我讲的一直都是性能工程。在整个项目的性能分析中,我并不限定技术的范围,只要是用得上,我们都需要拿出来分析。
|
||||
|
||||
前面我们说是worker-8上的CPU资源用得最多,所以我们来查一下被测的服务,也就是Member服务,是不是在worker-8上。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ef/9f/ef618ccfedfcde3c16eaafd68710e89f.png" alt="">
|
||||
|
||||
从上图看,Member服务确实是在worker-8上。
|
||||
|
||||
那下一步我们就要进到这个节点查看一下。查看之后,如果全是us cpu消耗,那我觉得这个基准测试就可以结束了。因为对于一个应用来说,us cpu高本来就是非常合理的情况。
|
||||
|
||||
之前一个做第三方测试的人跑过来跟我说,甲方爸爸不喜欢看到CPU使用率太高,让他想尽一切办法把CPU降下来。可是他没有什么招,所以就来问我该怎么办。
|
||||
|
||||
我问他测试的目标是什么。他回答客户并不关心TPS啥的,只说要把CPU降下来。我说这简单,你把压力降下来,CPU不就降下来了吗?本来以为只是一句调侃的话,结果他真去做了,并且还被客户接受了!后来我反思了一下,因为自己错误引导了性能行业的发展方向。
|
||||
|
||||
从职业的角度来说,我们对一些不懂的客户,最好要有一个良好的沟通,用对方能听懂的语言来解释。不过,在不该让步的时候,我们也不能让步。这才是专业的价值,不能是客户要什么,我们就给什么。
|
||||
|
||||
现在我们来看一下这个节点的top数据:
|
||||
|
||||
```
|
||||
[root@k8s-worker-8 ~]# top
|
||||
top - 02:32:26 up 1 day, 13:56, 3 users, load average: 26.46, 22.37, 14.54
|
||||
Tasks: 289 total, 1 running, 288 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 73.9 us, 9.4 sy, 0.0 ni, 3.5 id, 0.0 wa, 0.0 hi, 12.5 si, 0.7 st
|
||||
%Cpu1 : 69.8 us, 12.5 sy, 0.0 ni, 4.3 id, 0.0 wa, 0.0 hi, 12.8 si, 0.7 st
|
||||
%Cpu2 : 71.5 us, 12.7 sy, 0.0 ni, 4.2 id, 0.0 wa, 0.0 hi, 10.9 si, 0.7 st
|
||||
%Cpu3 : 70.3 us, 11.5 sy, 0.0 ni, 6.1 id, 0.0 wa, 0.0 hi, 11.5 si, 0.7 st
|
||||
KiB Mem : 16266296 total, 3803848 free, 6779796 used, 5682652 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 9072592 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
30890 root 20 0 7791868 549328 15732 S 276.1 3.4 23:17.90 java -Dapp.id=svc-mall-member -javaagent:/opt/skywalking/agent/sky+
|
||||
18934 root 20 0 3716376 1.6g 18904 S 43.9 10.3 899:31.21 java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitFor+
|
||||
1059 root 20 0 2576944 109856 38508 S 11.1 0.7 264:59.42 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-
|
||||
1069 root 20 0 1260592 117572 29736 S 10.8 0.7 213:48.18 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.
|
||||
15018 root 20 0 5943032 1.3g 16496 S 6.9 8.6 144:47.90 /usr/lib/jvm/java-1.8.0-openjdk/bin/java -Xms2g -Xmx2g -Xmn1g -Dna+
|
||||
4723 root 20 0 1484184 43396 17700 S 5.9 0.3 89:53.45 calico-node -felix
|
||||
|
||||
```
|
||||
|
||||
在这个例子中,我们看到si cpu(软中断消耗的CPU)有10%多,其实这只是一个瞬间值,在不断跳跃的数据中,有很多次数据都比这个值大,说明si消耗的CPU有点高了。对于这种数据,我们就要关心一下了。
|
||||
|
||||
## 定向监控分析
|
||||
|
||||
我们进一步来看软中断的变化,既然软中断消耗的CPU高,那必然是要看一下软中断的计数器了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/5d/459c1yy79b39824f7b35d7e2c61ca85d.png" alt="">
|
||||
|
||||
上图是一张瞬间的截图,而在实际的观察过程中,我们是要多看一会儿时间的。请你注意图中这些有白底的数字,在观察中,这些数值增加的越大说明中断越高。而我在观察的过程中,看到的是NET_RX变化的最大。
|
||||
|
||||
现在,从si cpu高到NET_RX中断多的逻辑基本上清楚了:因为NET_RX都是网络的接收,所以NET_RX会不断往上跳。
|
||||
|
||||
不过,请你注意,这个中断即使是正常的,也需要不断增加。我们要判断它合理不合理,一定要结合si cpu一起来看。并且在网络中断中,不止是软中断,硬中断也会不断增加。
|
||||
|
||||
从上图来看,网络中断已经均衡了,没有单队列网卡的问题。我们再看一下网络带宽。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cf/09/cfe3cfb6835a90f97d64589090616b09.png" alt="">
|
||||
|
||||
总共用了50Mb的带宽,中断就已经达到10%,也就是说带宽没有完全用满,可是中断已经不低了,这说明我们的数据包中还是小包居多。
|
||||
|
||||
于是我们做如下调整。调整的方向 就是增加队列长度和缓冲区大小,让应用可以接收更多的数据包。
|
||||
|
||||
```
|
||||
-- 增加网络的队列长度
|
||||
net.core.netdev_max_backlog = 10000 (原值:1000)
|
||||
- 增加tomcat的队列长度为10000(原值1000)
|
||||
server:
|
||||
port: 8083
|
||||
tomcat
|
||||
accept-count: 10000
|
||||
-- 改变设备一次可接收的数据包数量
|
||||
net.core.dev_weight = 128 (原值64)
|
||||
-- 控制socket 读取位于等待设备队列中数据包的微秒数
|
||||
net.core.busy_poll = 100 (原值0)
|
||||
-- 控制 socket 使用的接收缓冲区的默认大小
|
||||
net.core.rmem_default = 2129920 (原值:212992)
|
||||
net.core.rmem_max = 2129920 (原值:212992)
|
||||
-- 繁忙轮询
|
||||
net.core.busy_poll = 100
|
||||
这个参数是用来控制了socket 读取位于等待设备队列中数据包的微秒数
|
||||
|
||||
```
|
||||
|
||||
一顿操作猛如虎之后,原本满怀希望,然而再次查了TPS曲线之后,发现并没有什么卵用,让我们把一首《凉凉》唱出来。
|
||||
|
||||
我仔细想了一遍发送和接收数据的逻辑。既然上层应用会导致us cpu高,而si cpu高是因为网卡中断多引起的,那我们还是要从网络层下手。所以,我做了网络带宽能达到多高的验证。我先列一下当前的硬件配置。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/5f/5695abb87f22c463a5f3ffef5475015f.jpg" alt="">
|
||||
|
||||
我们通过iperf3直接测试网络,试验内容如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/36/fb30edf2e6e5768631e7fa15d40e6a36.jpg" alt="">
|
||||
|
||||
从上面的数据可以看到,在不同的层面进行纯网络测试,si是有很大区别的。当网络流量走了KVM+Kubernetes+Docker的结构之后,网络损失居然这么高,si cpu也上升了很多。
|
||||
|
||||
这也解释了为什么现在很多企业放弃虚拟化,直接用物理机来跑Kubernetes了。
|
||||
|
||||
由于当前K8s用的是Calico插件中的IPIP模式,考虑到BGP模式的效率会高一些,我们把IPIP模式改为BGP。这一步也是为了降低网络接收产生的软中断。
|
||||
|
||||
那IPIP和BGP到底有什么区别呢?对于IPIP来说,它套了两次IP包,相当于用了一个IP层后,还要用另一个IP层做网桥。在通常情况下,IP是基于MAC的,不需要网桥;而BGP是通过维护路由表来实现对端可达的,不需要多套一层。但是BGP不是路由协议,而是矢量性协议。关于IPIP和BGP更多原理上的区别,如果你不清楚,我建议你自学一下相关的网络基础知识。
|
||||
|
||||
我们把IPIP修改为BGP模式之后,先测试下纯网络的区别,做这一步是为了看到在没有应用压力流量时,网络本身的传输效率如何:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b7/6a/b7a3aec6f1f2630e50f1a8958cd3486a.png" alt="">
|
||||
|
||||
根据上面的测试结果,将带宽在不同的网络模式和包大小时的具体数值整理如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b2/2b/b2578dc107ef96f9111380d3a4eacc2b.jpg" alt="">
|
||||
|
||||
可以看到,BGP的网络能力确实要强一些,差别好大呀。
|
||||
|
||||
我们再接着回去测试下接口,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f9/eb/f982b49f5f5d4d8fb654a49770aa0eeb.png" alt="">
|
||||
|
||||
再看软中断,看一下BGP模式下的软中断有没有降低:
|
||||
|
||||
```
|
||||
top - 22:34:09 up 3 days, 55 min, 2 users, load average: 10.62, 6.18, 2.76
|
||||
Tasks: 270 total, 2 running, 268 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 51.6 us, 11.5 sy, 0.0 ni, 30.0 id, 0.0 wa, 0.0 hi, 6.6 si, 0.3 st
|
||||
%Cpu1 : 54.4 us, 9.4 sy, 0.0 ni, 28.2 id, 0.0 wa, 0.0 hi, 7.7 si, 0.3 st
|
||||
%Cpu2 : 55.9 us, 11.4 sy, 0.0 ni, 26.9 id, 0.0 wa, 0.0 hi, 5.9 si, 0.0 st
|
||||
%Cpu3 : 49.0 us, 12.4 sy, 0.0 ni, 32.8 id, 0.3 wa, 0.0 hi, 5.2 si, 0.3 st
|
||||
KiB Mem : 16266296 total, 7186564 free, 4655012 used, 4424720 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 11163216 avail Mem
|
||||
|
||||
```
|
||||
|
||||
## 优化效果
|
||||
|
||||
通过上面的调整结果,我们看到了软中断确实降低了不少,但是我们还是希望这样的优化体现到TPS上来,所以我们看一下优化之后TPS的效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/f3/9cfdc1287577f637e4e5912961f64df3.png" alt="">
|
||||
|
||||
si cpu有降低:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/04/2074ec8982f130c9ee8b6cf572ff7b04.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
当我们看到一个接口已经满足了业务要求时,从成本上来说,我们不应该花时间再去收拾它。但是,**从技术上来说,我们对每一个接口的性能结果,都要达到“知道最终瓶颈在哪里”的程度**。这样才方便我们在后续的工作中继续优化。
|
||||
|
||||
在这节课的例子中,我们从si cpu开始分析,经过软中断查找和纯网络测试,定位到了Kubernetes的网络模式,进而我们选择了更加合理的网络模式。整个过程穿过了很长的链路,而这个思维也是在我在宣讲中一贯提到的“**证据链**”。
|
||||
|
||||
最后,我还是要强调一遍,**性能分析一定要有证据链,没有证据链的性能分析就是耍流氓。**我们要做正派的老司机。
|
||||
|
||||
## 课后作业
|
||||
|
||||
我给你留两道题,请你思考一下:
|
||||
|
||||
1. 为什么看到NET_RX中断高的时候,我们会想到去测试一下纯网络带宽?
|
||||
1. 你能总结一下,这节课案例的证据链吗?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
461
极客时间专栏/高楼的性能工程实战课/基准场景/15 | 查询商品:资源不足有哪些性能表现?.md
Normal file
461
极客时间专栏/高楼的性能工程实战课/基准场景/15 | 查询商品:资源不足有哪些性能表现?.md
Normal file
@@ -0,0 +1,461 @@
|
||||
<audio id="audio" title="15 | 查询商品:资源不足有哪些性能表现?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c9/7c/c9f8721d71357ece5f5aa8c8791ef37c.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
这节课,我们来收拾“查询商品”这个接口。虽然这次的现象同样是TPS低、响应时间长,但是,这个接口走的路径和之前的不一样,所以在分析过程中会有些新鲜的东西,你将看到在资源真的不足的情况下,我们只有增加相应节点的资源才能提升性能。
|
||||
|
||||
在我的项目中,我一直都在讲,**不要轻易给出资源不足的结论。因为但凡有优化的空间,我们都要尝试做优化,而不是直接告诉客户加资源。而给出“增加资源”这个结论,也必须建立在有足够证据的基础上**。在这节课中,你也将看到这一点。
|
||||
|
||||
话不多说,我们直接开始今天的内容。
|
||||
|
||||
## 压力场景数据
|
||||
|
||||
对于查询商品接口,我们第一次试执行性能场景的结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/75/a2/75ef8d040cdbcef4556bbfa7fd7098a2.png" alt="">
|
||||
|
||||
你看,TPS只有250左右,并且响应时间也明显随着压力的增加而增加了,看起来瓶颈已经出现了,对吧?根据哥的逻辑,下一步就是看架构图啦。
|
||||
|
||||
## 先看架构图
|
||||
|
||||
我们用APM工具来看看这个接口的架构。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/70/6bcbe3236d91fd7628fa5a6186243870.png" alt="">
|
||||
|
||||
你看,从压力机到Gateway服务、到Search服务、再到ES-Client,这个APM工具也只能帮我们到这里了。因为我们用的是ElasticSearch 7来做的搜索服务的支撑,而这个skywalking工具也没有对应的Agent,所以后面并没有配置skywalking。
|
||||
|
||||
在这里,我要多啰嗦几句。现在的APM工具大多是基于应用层来做的,有些运维APM采集的数据会更多一些,但也是响应时间、吞吐量等这样的信息。对于性能分析而言,现在的APM工具有减少排查时间的能力,但是在组件级的细化定位上还有待提高。虽然AI OPS也被提到了台面,但是也没见过哪个公司上了AIOPS产品后,就敢不让人看着。
|
||||
|
||||
总之,从细化分析的角度,我们在定位问题的根本原因时,手头有什么工具就可以用什么工具,即使什么工具都没有,撸日志也是照样能做到的,所以我建议你不要迷信工具,要“迷信”思路。
|
||||
|
||||
下面我们来拆分下这个接口的响应时间,看看这个案例的问题点在哪里。
|
||||
|
||||
## 拆分响应时间
|
||||
|
||||
**“在RESAR性能分析逻辑中,拆分响应时间只是一个分析的起点**。”这是我一直在强调的一句话。如果性能工程师连这个都不会做,就只能好好学习天天向上了。
|
||||
|
||||
根据架构图,我们拆分响应时间如下。
|
||||
|
||||
- Gateway服务上的响应时间:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/0d/dc64c9e8ace689c86d6584b64f9f230d.png" alt="">
|
||||
|
||||
- Search服务上的响应时间:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/c9/a6dc83d1b51445464d3db3e5421798c9.png" alt="">
|
||||
|
||||
- ES Client的响应时间:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0b/81/0b168cdd73f30b5565c52a6d97b45081.png" alt="">
|
||||
|
||||
一层层看过之后,我们发现查询商品这个接口的响应时间消耗在了ES client上面。而在我们这个查询的路径上,在gateway/search服务上,我们并没有做什么复杂的动作。
|
||||
|
||||
既然知道了响应时间消耗在哪里,下面我们就来定位它,看能不能把TPS优化起来。
|
||||
|
||||
## 全局监控
|
||||
|
||||
根据高老师的经验,我们还是从全局监控开始,看全局监控可以让我们更加有的放矢。在分析的过程中,经常有人往下走了几步之后,就开始思维混乱、步伐飘逸。因为数据有很多,所以分析时很容易从一个数据走到不重要的分支上去了。而这时候,如果你心里有全局监控数据,思路就会更清晰,不会在无关的分支上消耗时间。
|
||||
|
||||
回到我们这个例子中,从下面的k8s worker(也就是k8s中的node,在我们的环境中我习惯叫成worker,就是为了体现:在我的地盘,我爱叫啥就叫啥)的数据上来看,似乎没有一个worker的资源使用率是特别高的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/e5/25bb0386561be19b9615a4b0130fe4e5.png" alt="">
|
||||
|
||||
请你注意,在k8s中看资源消耗,一定不要只看worker这个层面,因为这个层面还不够,一个worker上可能会运行多个pod。从上图来看,由于worker层面没有资源消耗,但是时间又明显是消耗在ES client上的,所以,接下来我们要看一下每一个pod的资源使用情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/13/f3/13f919d42a29f2d37f699036658006f3.png" alt="">
|
||||
|
||||
咦,有红色。你看,有两个与ES相关的POD,它们的CPU都飘红了,这下可有得玩了。既然是与ES相关的POD,那我们就把ES所有的POD排个序看看。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/bd/624d7bc2fe25f731aa42037d8d9f29bd.png" alt="">
|
||||
|
||||
从上图的数据来看,有一个ES Client 消耗了67%的CPU,有两个ES Data消耗了99%的CPU,ES本来就是吃CPU的大户,所以我们接下来要着重分析它。
|
||||
|
||||
这里我再说明一点,我们从前面的worker资源使用率一步一步走到这里,在分析方向上是合情合理的,因为这些都是属于我提到的全局监控的内容。
|
||||
|
||||
## 定向分析
|
||||
|
||||
现在我们就来扒一扒ES,看看它在哪个worker节点上。罗列Pod信息如下:
|
||||
|
||||
```
|
||||
[root@k8s-master-1 ~]# kubectl get pods -o wide | grep elasticsearch
|
||||
elasticsearch-client-0 1/1 Running 0 6h43m 10.100.230.2 k8s-worker-1 <none> <none>
|
||||
elasticsearch-client-1 1/1 Running 0 6h45m 10.100.140.8 k8s-worker-2 <none> <none>
|
||||
elasticsearch-data-0 1/1 Running 0 7h8m 10.100.18.197 k8s-worker-5 <none> <none>
|
||||
elasticsearch-data-1 1/1 Running 0 7h8m 10.100.5.5 k8s-worker-7 <none> <none>
|
||||
elasticsearch-data-2 1/1 Running 0 7h8m 10.100.251.67 k8s-worker-9 <none> <none>
|
||||
elasticsearch-master-0 1/1 Running 0 7h8m 10.100.230.0 k8s-worker-1 <none> <none>
|
||||
elasticsearch-master-1 1/1 Running 0 7h8m 10.100.227.131 k8s-worker-6 <none> <none>
|
||||
elasticsearch-master-2 1/1 Running 0 7h8m 10.100.69.206 k8s-worker-3 <none> <none>
|
||||
[root@k8s-master-1 ~]#
|
||||
|
||||
```
|
||||
|
||||
现在就比较清晰了,可以看到,在整个namespace中有两个ES client,三个ES data,三个ES master。
|
||||
|
||||
我们来画一个细一点的架构图,以便在脑子里记下这个逻辑:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/0f/2e8a4fe8b5e214a8463d918dea8cyy0f.jpg" alt="">
|
||||
|
||||
再结合我们在全局分析中看到的资源使用率图,现在判断至少有两个问题:
|
||||
|
||||
1. ES client请求不均衡;
|
||||
1. ES data CPU 高。
|
||||
|
||||
下面我们一个一个来分析。
|
||||
|
||||
### ES client请求不均衡
|
||||
|
||||
从上面的架构图中可以看到,search服务连两个ES client,但是只有一个ES client的CPU使用率高。所以,我们需要查一下链路,看看ES的service:
|
||||
|
||||
```
|
||||
[root@k8s-master-1 ~]# kubectl get svc -o wide | grep search
|
||||
elasticsearch-client NodePort 10.96.140.52 <none> 9200:30200/TCP,9300:31614/TCP 34d app=elasticsearch-client,chart=elasticsearch,heritage=Helm,release=elasticsearch-client
|
||||
elasticsearch-client-headless ClusterIP None <none> 9200/TCP,9300/TCP 34d app=elasticsearch-client
|
||||
elasticsearch-data ClusterIP 10.96.16.151 <none> 9200/TCP,9300/TCP 7h41m app=elasticsearch-data,chart=elasticsearch,heritage=Helm,release=elasticsearch-data
|
||||
elasticsearch-data-headless ClusterIP None <none> 9200/TCP,9300/TCP 7h41m app=elasticsearch-data
|
||||
elasticsearch-master ClusterIP 10.96.207.238 <none> 9200/TCP,9300/TCP 7h41m app=elasticsearch-master,chart=elasticsearch,heritage=Helm,release=elasticsearch-master
|
||||
elasticsearch-master-headless ClusterIP None <none> 9200/TCP,9300/TCP 7h41m app=elasticsearch-master
|
||||
svc-mall-search ClusterIP 10.96.27.150 <none> 8081/TCP 44d app=svc-mall-search
|
||||
[root@k8s-master-1 ~]#
|
||||
|
||||
```
|
||||
|
||||
你看,整个namespace中有一个client service(解析出来的是VIP,访问此服务时不会绕过K8s的转发机制),还有一个client-headless service(解析出来的是POD IP,访问这个服务时会绕过K8s的转发机制)。
|
||||
|
||||
接下来,我们查一下为什么会出现访问不均衡的情况。
|
||||
|
||||
通过查看search服务的ES配置,我们看到如下信息:
|
||||
|
||||
```
|
||||
elasticsearch:
|
||||
rest:
|
||||
uris: elasticsearch-client:9200
|
||||
username: elastic
|
||||
password: admin@123
|
||||
|
||||
```
|
||||
|
||||
看到我们这里是用的elasticsearch-client:9200,我们再来看一下client service的配置:
|
||||
|
||||
```
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
meta.helm.sh/release-name: elasticsearch-client
|
||||
meta.helm.sh/release-namespace: default
|
||||
creationTimestamp: '2020-12-10T17:34:19Z'
|
||||
labels:
|
||||
app: elasticsearch-client
|
||||
app.kubernetes.io/managed-by: Helm
|
||||
chart: elasticsearch
|
||||
heritage: Helm
|
||||
release: elasticsearch-client
|
||||
managedFields:
|
||||
- apiVersion: v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
'f:metadata': {}
|
||||
'f:spec':
|
||||
'f:ports': {}
|
||||
manager: Go-http-client
|
||||
operation: Update
|
||||
time: '2020-12-10T17:34:19Z'
|
||||
name: elasticsearch-client
|
||||
namespace: default
|
||||
resourceVersion: '4803428'
|
||||
selfLink: /api/v1/namespaces/default/services/elasticsearch-client
|
||||
uid: 457e962e-bee0-49b7-9ec4-ebfbef0fecdd
|
||||
spec:
|
||||
clusterIP: 10.96.140.52
|
||||
externalTrafficPolicy: Cluster
|
||||
ports:
|
||||
- name: http
|
||||
nodePort: 30200
|
||||
port: 9200
|
||||
protocol: TCP
|
||||
targetPort: 9200
|
||||
- name: transport
|
||||
nodePort: 31614
|
||||
port: 9300
|
||||
protocol: TCP
|
||||
targetPort: 9300
|
||||
selector:
|
||||
app: elasticsearch-client
|
||||
chart: elasticsearch
|
||||
heritage: Helm
|
||||
release: elasticsearch-client
|
||||
sessionAffinity: None
|
||||
type: NodePort
|
||||
|
||||
```
|
||||
|
||||
从上面的配置来看,sessionAffinity也配置为None了,也就是说这个service不以客户端的IP来保持session。因为在这个环境配置中,Type为NodePort,而我们在k8s中配置的转发规则是iptables。所以说,service是依赖iptables的规则来做后端转发的。
|
||||
|
||||
接下来,我们检查一下iptables的转发规则。
|
||||
|
||||
我们先来看iptables中关于ES client的规则:
|
||||
|
||||
```
|
||||
[root@k8s-master-1 ~]# iptables -S KUBE-SERVICES -t nat|grep elasticsearch-client|grep 9200
|
||||
-A KUBE-SERVICES ! -s 10.100.0.0/16 -d 10.96.140.52/32 -p tcp -m comment --comment "default/elasticsearch-client:http cluster IP" -m tcp --dport 9200 -j KUBE-MARK-MASQ
|
||||
-A KUBE-SERVICES -d 10.96.140.52/32 -p tcp -m comment --comment "default/elasticsearch-client:http cluster IP" -m tcp --dport 9200 -j KUBE-SVC-XCX4XZ2WPAE7BUZ4
|
||||
[root@k8s-master-1 ~]#
|
||||
|
||||
```
|
||||
|
||||
可以看到,service的规则名是KUBE-SVC-XCX4XZ2WPAE7BUZ4,那我们再去查它对应的iptables规则:
|
||||
|
||||
```
|
||||
[root@k8s-master-1 ~]# iptables -S KUBE-SVC-XCX4XZ2WPAE7BUZ4 -t nat
|
||||
-N KUBE-SVC-XCX4XZ2WPAE7BUZ4
|
||||
-A KUBE-SVC-XCX4XZ2WPAE7BUZ4 -m comment --comment "default/elasticsearch-client:http" -j KUBE-SEP-LO263M5QW4XA6E3Q
|
||||
[root@k8s-master-1 ~]#
|
||||
[root@k8s-master-1 ~]# iptables -S KUBE-SEP-LO263M5QW4XA6E3Q -t nat
|
||||
-N KUBE-SEP-LO263M5QW4XA6E3Q
|
||||
-A KUBE-SEP-LO263M5QW4XA6E3Q -s 10.100.227.130/32 -m comment --comment "default/elasticsearch-client:http" -j KUBE-MARK-MASQ
|
||||
-A KUBE-SEP-LO263M5QW4XA6E3Q -p tcp -m comment --comment "default/elasticsearch-client:http" -m tcp -j DNAT --to-destination 10.100.227.130:9200
|
||||
|
||||
```
|
||||
|
||||
问题来了,这里好像没有负载均衡的配置(没有probability参数),并且根据iptables规则也只是转发到了一个ES client上。到这里,其实我们也就能理解,为什么在全局监控的时候,我们只看到一个ES client有那么高的CPU使用率,而另一个ES client却一点动静都没有。
|
||||
|
||||
但是,这里的iptables规则并不是自己来配置的,而是在部署k8s的时候自动刷进去的规则。现在只有一条规则了,所以只能转发到一个POD上去。
|
||||
|
||||
那我们就再刷一遍ES的POD,重装一下ES的POD,看k8s自己能不能刷出来负载均衡的iptables规则。重来一遍之后,我们再来看iptables规则:
|
||||
|
||||
```
|
||||
[root@k8s-master-1 ~]# iptables -S KUBE-SVC-XCX4XZ2WPAE7BUZ4 -t nat
|
||||
-N KUBE-SVC-XCX4XZ2WPAE7BUZ4
|
||||
-A KUBE-SVC-XCX4XZ2WPAE7BUZ4 -m comment --comment "default/elasticsearch-client:http" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-IFM4L7YNSTSJP4YT
|
||||
-A KUBE-SVC-XCX4XZ2WPAE7BUZ4 -m comment --comment "default/elasticsearch-client:http" -j KUBE-SEP-5RAP6F6FATXC4DFL
|
||||
[root@k8s-master-1 ~]#
|
||||
|
||||
```
|
||||
|
||||
哟,现在刷出来两条iptables规则了,看来之前在我们不断折腾的部署过程中,ES client一直是有问题的。
|
||||
|
||||
在上面的iptables规则里,那两条iptables的上一条中有一个关键词“——probability 0.50000000000”。我们知道,iptables的匹配规则是从上到下的,既然上一条的匹配是随机0.5,也就是说只有50%的请求会走第一条规则,那下一条自然也是随机0.5了,因为总共只有两条规则嘛。这样一来就均衡了。
|
||||
|
||||
我们再接着做这个接口的压力场景,看到如下信息:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/67/408592f11401e1yyb1c06ef48f3f8167.png" alt="">
|
||||
|
||||
看起来ES client均衡了,对不对?
|
||||
|
||||
它对应的TPS,如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/61/99/614e849f975cf983747e080329c0d699.png" alt="">
|
||||
|
||||
明显TPS提升了60左右。
|
||||
|
||||
ES client请求不均衡的问题解决了,现在,我们还要来看一下ES data单节点CPU高的问题。
|
||||
|
||||
### ES Data CPU使用率高
|
||||
|
||||
- **第一阶段:加一个CPU**
|
||||
|
||||
在TPS提升之后,我们再来看一下全局监控数据。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/79/2ec7cce680e81f57717c642cf7834279.png" alt="">
|
||||
|
||||
看起来比一开始好多了。基于前面分析ES client的经验,我们就先来查一下ES data的iptables规则:
|
||||
|
||||
```
|
||||
-- 查看下有哪些ES data的POD
|
||||
[root@k8s-master-1 ~]# kubectl get pods -o wide | grep data
|
||||
elasticsearch-data-0 1/1 Running 0 10h 10.100.18.197 k8s-worker-5 <none> <none>
|
||||
elasticsearch-data-1 1/1 Running 0 10h 10.100.5.5 k8s-worker-7 <none> <none>
|
||||
elasticsearch-data-2 1/1 Running 0 10h 10.100.251.67 k8s-worker-9 <none> <none>
|
||||
|
||||
|
||||
-- 查看ES data对应的iptables规则
|
||||
[root@k8s-master-1 ~]# iptables -S KUBE-SERVICES -t nat|grep elasticsearch-data
|
||||
-A KUBE-SERVICES ! -s 10.100.0.0/16 -d 10.96.16.151/32 -p tcp -m comment --comment "default/elasticsearch-data:http cluster IP" -m tcp --dport 9200 -j KUBE-MARK-MASQ
|
||||
-A KUBE-SERVICES -d 10.96.16.151/32 -p tcp -m comment --comment "default/elasticsearch-data:http cluster IP" -m tcp --dport 9200 -j KUBE-SVC-4LU6GV7CN63XJXEQ
|
||||
-A KUBE-SERVICES ! -s 10.100.0.0/16 -d 10.96.16.151/32 -p tcp -m comment --comment "default/elasticsearch-data:transport cluster IP" -m tcp --dport 9300 -j KUBE-MARK-MASQ
|
||||
-A KUBE-SERVICES -d 10.96.16.151/32 -p tcp -m comment --comment "default/elasticsearch-data:transport cluster IP" -m tcp --dport 9300 -j KUBE-SVC-W4QKPGOO4JGYQZDQ
|
||||
|
||||
|
||||
-- 查看9200(外部通信)对应的规则
|
||||
[root@k8s-master-1 ~]# iptables -S KUBE-SVC-4LU6GV7CN63XJXEQ -t nat
|
||||
-N KUBE-SVC-4LU6GV7CN63XJXEQ
|
||||
-A KUBE-SVC-4LU6GV7CN63XJXEQ -m comment --comment "default/elasticsearch-data:http" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-ZHLKOYKJY5GV3ZVN
|
||||
-A KUBE-SVC-4LU6GV7CN63XJXEQ -m comment --comment "default/elasticsearch-data:http" -m statistic --mode random --probability 1 -j KUBE-SEP-6ILKZEZS3TMCB4VJ
|
||||
-A KUBE-SVC-4LU6GV7CN63XJXEQ -m comment --comment "default/elasticsearch-data:http" -j KUBE-SEP-JOYLBDPA3LNXKWUK
|
||||
|
||||
|
||||
-- 查看以上三条规则的转发目标
|
||||
[root@k8s-master-1 ~]# iptables -S KUBE-SEP-ZHLKOYKJY5GV3ZVN -t nat
|
||||
-N KUBE-SEP-ZHLKOYKJY5GV3ZVN
|
||||
-A KUBE-SEP-ZHLKOYKJY5GV3ZVN -s 10.100.18.197/32 -m comment --comment "default/elasticsearch-data:http" -j KUBE-MARK-MASQ
|
||||
-A KUBE-SEP-ZHLKOYKJY5GV3ZVN -p tcp -m comment --comment "default/elasticsearch-data:http" -m tcp -j DNAT --to-destination 10.100.18.197:9200
|
||||
[root@k8s-master-1 ~]# iptables -S KUBE-SEP-6ILKZEZS3TMCB4VJ -t nat
|
||||
-N KUBE-SEP-6ILKZEZS3TMCB4VJ
|
||||
-A KUBE-SEP-6ILKZEZS3TMCB4VJ -s 10.100.251.67/32 -m comment --comment "default/elasticsearch-data:http" -j KUBE-MARK-MASQ
|
||||
-A KUBE-SEP-6ILKZEZS3TMCB4VJ -p tcp -m comment --comment "default/elasticsearch-data:http" -m tcp -j DNAT --to-destination 10.100.251.67:9200
|
||||
[root@k8s-master-1 ~]# iptables -S KUBE-SEP-JOYLBDPA3LNXKWUK -t nat
|
||||
-N KUBE-SEP-JOYLBDPA3LNXKWUK
|
||||
-A KUBE-SEP-JOYLBDPA3LNXKWUK -s 10.100.5.5/32 -m comment --comment "default/elasticsearch-data:http" -j KUBE-MARK-MASQ
|
||||
-A KUBE-SEP-JOYLBDPA3LNXKWUK -p tcp -m comment --comment "default/elasticsearch-data:http" -m tcp -j DNAT --to-destination 10.100.5.5:9200
|
||||
[root@k8s-master-1 ~]
|
||||
|
||||
```
|
||||
|
||||
Everythins is perfect!!规则很合理。ES Data总共有三个pod,从逻辑上来看,它们各占了三分之一。
|
||||
|
||||
在前面的ES client分析中,我们讲到,第一个POD是0.5,下一条自然也只剩下0.5,这很容易理解。现在,ES data的部分有三条iptables规则,我们来简单说明一下。
|
||||
|
||||
>
|
||||
通常我们理解的iptables就是一个防火墙。不过,要是从根本上来讲,它不算是一个防火墙,只是一堆规则列表,而通过iptables设计的规则列表,请求会对应到netfilter框架中去,而这个netfilter框架,才是真正的防火墙。其中,netfilter是处于内核中的,iptables就只是一个用户空间上的配置工具而已。
|
||||
|
||||
|
||||
>
|
||||
我们知道iptables有四表五链。四表是:fileter表(负责过滤)、nat表(负责地址转换)、mangle表(负责解析)、raw表(关闭nat表上启用的连接追踪);五链是:prerouting链(路由前)、input链(输入规则)、forward链(转发规则)、output链(输出规则)、postrouting链(路由后)。
|
||||
|
||||
|
||||
>
|
||||
而在这一部分,我们主要是看nat表以及其上的链。对于其他的部分,如果你想学习,可自行查阅iptables相关知识。毕竟,我还时刻记得自己写的是一个性能专栏,而不是计算机基础知识专栏,哈哈。
|
||||
|
||||
|
||||
从上面的信息可以看到,我们的这个集群中有三个ES data服务,对应着三条转发规则,其中,第一条规则的匹配比例是:0.33333333349;第二条比例:0.50000000000;第三条是1。而这三条转发规则对应的POD IP和端口分别是:10.100.18.197:9200、10.100.251.67:9200、10.100.5.5:9200,这也就意味着,通过这三条iptables规则可以实现负载均衡,画图理解如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/88/5e9c08e967c62d9e9a84084d8c2de888.jpg" alt="">
|
||||
|
||||
我们假设有30个请求进来,那ES Data 0上就会有30x0.33333333349=10个请求;对于剩下的20个请求,在ES Data 1上就会有20x0.50000000000=10个请求;而最后剩下的10个请求,自然就到了ES Data 2上。这是一个非常均衡的逻辑,只是在iptables规则中,我看着这几个数据比例,实在是觉得别扭。
|
||||
|
||||
既然明白了这个逻辑,下面我们还是把查询商品接口的场景压起来看一下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/06/49/06eb3b24a57226cd9a6717cd1e3f6049.png" alt="">
|
||||
|
||||
从数据上来看,经常会出现ES data 某个节点消耗CPU高的情况。可是,对应到我们前面看到的全局worker监控界面中,并没有哪个worker的CPU很高。所以,在这里,我们要查一下ES Data中的cgroup配置,看它的限制是多少。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8y/3c/8yyd469d83c9a346aff8c659f6c4ae3c.png" alt="">
|
||||
|
||||
也就是说,ES data的每个POD都是配置了一颗CPU,难怪CPU使用率动不动就红了。
|
||||
|
||||
还有一点你要记住,前面我们在查看data列表的时候发现,ES data 0 在worker-5上,ES data 1 在worker-7上,ES data 2 在worker-9上。而我们现在看到的却是,它们都各自分到了一个CPU。既然如此,那我们就再添加一个CPU,然后再回去看一下worker-5/7/9的反应。为什么只加一个CPU呢?因为从worker-7上来看,现在的CPU使用率已经在50%左右了,要是加多了,我怕它吃不消。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f5/01/f57acfca05yy43bd882a57404ea50d01.png" alt="">
|
||||
|
||||
看一下压力场景执行的效果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/9c/299bbd39759ca90bdc4953334bc0c39c.png" alt="">
|
||||
|
||||
似乎……不怎么样?TPS并没有增加。
|
||||
|
||||
- **第二阶段:加副本**
|
||||
|
||||
我们再看加了CPU之后的全局POD监控:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/92/94/925d14923a3bfac75ef84b9b21622294.png" alt="">
|
||||
|
||||
还是只有一个ES data的CPU使用率高,所以我想查一下ES中的数据分布。因为负载均衡的问题解决了,并且知道有三个ES data节点。现在我们就要知道是不是每个节点都被访问到了。
|
||||
|
||||
```
|
||||
pms 0 p 10.100.18.199 _w 32 17690 18363 6.7mb 7820 true true 8.5.1 false
|
||||
pms 0 p 10.100.18.199 _15 41 2110 0 465.7kb 5500 true true 8.5.1 true
|
||||
pms 0 p 10.100.18.199 _16 42 21083 30255 9.5mb 5900 true true 8.5.1 false
|
||||
pms 0 p 10.100.18.199 _17 43 2572 0 568kb 5500 true true 8.5.1 true
|
||||
pms 0 p 10.100.18.199 _18 44 1403 0 322.9kb 5500 true true 8.5.1 true
|
||||
pms 0 p 10.100.18.199 _19 45 1856 0 414.1kb 5500 true true 8.5.1 true
|
||||
pms 0 p 10.100.18.199 _1a 46 1904 0 423kb 5500 true true 8.5.1 true
|
||||
|
||||
```
|
||||
|
||||
为啥数据都在一个节点上(都是10.100.18.199)?看起来只有一个数据副本的原因了。
|
||||
|
||||
```
|
||||
green open pms A--6O32bQaSBrJPJltOLHQ 1 0 48618 48618 55.1mb 18.3mb
|
||||
|
||||
```
|
||||
|
||||
所以,我们先把副本数加上去,因为我们有三个data节点,所以这里加三个副本:
|
||||
|
||||
```
|
||||
PUT /pms/_settings
|
||||
{
|
||||
"number_of_replicas": 3
|
||||
}
|
||||
|
||||
我们再次查看ES中的数据分布,如下所示:
|
||||
|
||||
pms 0 r 10.100.18.200 _w 32 17690 18363 6.7mb 7820 true true 8.5.1 false
|
||||
pms 0 r 10.100.18.200 _15 41 2110 0 465.7kb 5500 true true 8.5.1 true
|
||||
pms 0 r 10.100.18.200 _16 42 21083 30255 9.5mb 5900 true true 8.5.1 false
|
||||
pms 0 r 10.100.18.200 _17 43 2572 0 568kb 5500 true true 8.5.1 true
|
||||
pms 0 r 10.100.18.200 _18 44 1403 0 322.9kb 5500 true true 8.5.1 true
|
||||
pms 0 r 10.100.18.200 _19 45 1856 0 414.1kb 5500 true true 8.5.1 true
|
||||
pms 0 r 10.100.18.200 _1a 46 1904 0 423kb 5500 true true 8.5.1 true
|
||||
pms 0 p 10.100.251.69 _w 32 17690 18363 6.7mb 7820 true true 8.5.1 false
|
||||
pms 0 p 10.100.251.69 _15 41 2110 0 465.7kb 5500 true true 8.5.1 true
|
||||
pms 0 p 10.100.251.69 _16 42 21083 30255 9.5mb 5900 true true 8.5.1 false
|
||||
pms 0 p 10.100.251.69 _17 43 2572 0 568kb 5500 true true 8.5.1 true
|
||||
pms 0 p 10.100.251.69 _18 44 1403 0 322.9kb 5500 true true 8.5.1 true
|
||||
pms 0 p 10.100.251.69 _19 45 1856 0 414.1kb 5500 true true 8.5.1 true
|
||||
pms 0 p 10.100.251.69 _1a 46 1904 0 423kb 5500 true true 8.5.1 true
|
||||
pms 0 r 10.100.140.10 _w 32 17690 18363 6.7mb 7820 true true 8.5.1 false
|
||||
pms 0 r 10.100.140.10 _15 41 2110 0 465.7kb 5500 true true 8.5.1 true
|
||||
pms 0 r 10.100.140.10 _16 42 21083 30255 9.5mb 5900 true true 8.5.1 false
|
||||
pms 0 r 10.100.140.10 _17 43 2572 0 568kb 5500 true true 8.5.1 true
|
||||
pms 0 r 10.100.140.10 _18 44 1403 0 322.9kb 5500 true true 8.5.1 true
|
||||
pms 0 r 10.100.140.10 _19 45 1856 0 414.1kb 5500 true true 8.5.1 true
|
||||
pms 0 r 10.100.140.10 _1a 46 1904 0 423kb 5500 true true 8.5.1 true
|
||||
|
||||
```
|
||||
|
||||
我们接着压起来,看看POD的资源:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/cc/11ayy743164f36a8d0b04bc76af52ccc.png" alt="">
|
||||
|
||||
现在看着是不是开心多了?data节点的CPU都用起来了。
|
||||
|
||||
我们再看一下worker的资源:
|
||||
|
||||
```
|
||||
[root@k8s-master-1 ~]# kubectl get pods -o wide | grep data
|
||||
elasticsearch-data-0 1/1 Running 0 16m 10.100.18.199 k8s-worker-5 <none> <none>
|
||||
elasticsearch-data-1 1/1 Running 0 17m 10.100.251.68 k8s-worker-9 <none> <none>
|
||||
elasticsearch-data-2 1/1 Running 0 18m 10.100.140.9 k8s-worker-2 <none> <none>
|
||||
|
||||
```
|
||||
|
||||
现在ES Data的POD分布到2、5、9三这个worker上去了,我们查看下全局监控:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/53/b1/53a522a2754d7fcea550fde05cd791b1.png" alt="">
|
||||
|
||||
嗯,不错,ES data的POD把资源用起来了。其实这里要想继续调,还可以把CPU加大,ES本来就是吃CPU、内存的大户。不过,我们前面在配置的时候,给ES data的CPU也确实太小了。这个问题,并不是我故意设计出来的,而是当时在部署的时候,没考虑到这些。
|
||||
|
||||
最后,我们来看优化后的效果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1f/24/1fb93ff5yycfc2bb6049244820e73b24.png" alt="">
|
||||
|
||||
呀呀呀,你看TPS压都压不住呀,很快涨到900左右了!这个优化结果很好。
|
||||
|
||||
现在回过头来看第一个阶段,我们加CPU没有效果,主要还是因为副本数量太少。其实,在ES的优化中,还有很多细节可以玩。只不过,在我们这个课程中,我希望给你的是一个整体的分析思路和逻辑,而不是纠结于每个细节上的参数。所以,在这里,我们就不再说具体参数的调整了。
|
||||
|
||||
如果你想在ES上做更多的优化,可以在分析完业务之后,确定一下ES的架构、数据索引、分片等信息,然后再来设计一个合理的ES部署。
|
||||
|
||||
## 总结
|
||||
|
||||
在这节课中,我们看到APM工具也有无能为力的地方。所以说,当我们分析到一个具体组件之后,要想再往下分析,就得靠自己的技术功底了。
|
||||
|
||||
在出现请求不均衡的时候,我们一定要先去看负载均衡的逻辑有没有问题。当看到ES client不均衡时,我们去看了iptables的原理,在发现iptables只有一个转发规则的时候,接下来要做的当然就是重刷转发规则了。
|
||||
|
||||
在ES client转发均衡了之后,我们在ES data单节点上又看到CPU使用率过高。由于ES data在POD当中,我们自然就要想到去看cgroup的限制。
|
||||
|
||||
而在添加了CPU之后,我们发现TPS并没有提高,这时候就要去看ES的逻辑了。ES的强大之处就在于多副本多分片的查询能力,所以,我们增加了副本之后,CPU就用起来了,这是一个合理的优化结果,TPS也自然提高了。
|
||||
|
||||
经过一系列的动作,我们终于把资源给用起来了。这也是我一直在强调的,**性能优化第一个阶段的目标,就是把资源给用起来,然后再考虑更细节的优化**。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,我给你留两道题,请你思考一下:
|
||||
|
||||
1. 当负载出现不均衡时,主要的分析方向是什么?
|
||||
1. 什么时候才需要去看组件内部的实现逻辑?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
505
极客时间专栏/高楼的性能工程实战课/基准场景/16 | 商品加入购物车:SQL优化和压力工具中的参数分析.md
Normal file
505
极客时间专栏/高楼的性能工程实战课/基准场景/16 | 商品加入购物车:SQL优化和压力工具中的参数分析.md
Normal file
@@ -0,0 +1,505 @@
|
||||
<audio id="audio" title="16 | 商品加入购物车:SQL优化和压力工具中的参数分析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3a/5c/3a451ffb16843c002866c1880fbe095c.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
今天这节课,我用商品加入购物车接口,来给你讲一讲SQL优化和压力工具中的参数分析。
|
||||
|
||||
对于SQL的优化,很多人一看到数据库资源使用率高,就猜测是SQL有问题。这个方向看起来没错,但是,具体是哪个SQL有问题,以及有什么样的问题,往往回答不出来。因此,这节课我会教你怎么根据资源使用率高,快速定位到有问题的SQL,并做出相应的调整。此外,你还将看到,当压力工具的参数使用不合理时,我们应该如何处理由此产生的数据库锁的问题。
|
||||
|
||||
现在,我们就开始这节课的分析。
|
||||
|
||||
## 压力数据
|
||||
|
||||
对于商品加入购物车这个接口,我们第一次运行的性能场景结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f8/8c/f871d42d3e4528def48fe1202e219a8c.png" alt="">
|
||||
|
||||
看着有一种想哭的感觉,有没有?从这张图来看,问题不止一个。我用自己在有限的职业生涯中吸收的天地之灵气,打开天眼一看,感觉这里有两个问题:
|
||||
|
||||
- TPS即使在峰值的时候,也不够高,才50左右;
|
||||
- TPS在峰值的时候,有大量的错误产生。
|
||||
|
||||
那哪个问题更重要呢?有人可能说,明显应该处理错误呀,有错误看着不眼晕吗?如果你是有强迫症的人,那没办法,可以先处理错误。
|
||||
|
||||
不过,在我看来,先处理TPS不高的问题也是可以的。因为虽然有错误产生,但并不是全错呀,只有5%的错,你着个啥急。
|
||||
|
||||
可是,不管怎么着,我们都要走性能分析决策树的思路。
|
||||
|
||||
## 看架构图
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e0/e0/e0035b408239e31deb2c31771f9acbe0.png" alt="">
|
||||
|
||||
这个接口的逻辑清晰明了:压力工具 - Gateway - Cart - Member。
|
||||
|
||||
我打算先分析TPS不高、响应时间变长的问题,这个问题可以在压力曲线图的前半段中看出来。所以,接下来,我们的分析就从拆分响应时间开始。
|
||||
|
||||
如果你想在这样的场景中先处理错误 ,那就从查日志开始。其实,这些错误是容易处理的,因为它们给出了非常明确的方向指示。
|
||||
|
||||
## 分析的第一阶段
|
||||
|
||||
### 拆分响应时间
|
||||
|
||||
这次我们截小图。
|
||||
|
||||
- User - Gateway:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/39/a18fyyf8936d3b906bbe4ccecf661539.png" alt="">
|
||||
|
||||
- Gateway - Cart:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c6/c5/c6c3fe9faed90d3476844b293fbc86c5.png" alt="">
|
||||
|
||||
- Cart - Member :
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7f/b5/7f5eb3a78a4efcb35855a54bfd71fbb5.png" alt="">
|
||||
|
||||
- Cart - MySQL:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/2a/acfc5fc9a600a6b2d29dc1e0c980d82a.png" alt="">
|
||||
|
||||
- Member - MySQL:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/71/117406c267046fe0771ba8109f801871.png" alt="">
|
||||
|
||||
从响应时间上来看,我们需要先收拾MySQL,并且是和Cart服务相关的SQL,因为Cart - MySQL之间的响应时间有点长。
|
||||
|
||||
### 全局分析
|
||||
|
||||
按照我们的惯例,还是得来看一下全局监控。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/a3/42f4042fa1100d96df9989b57f69c9a3.png" alt="">
|
||||
|
||||
既然worker-1上的CPU使用率很高,那我们就去看看worker-1上运行着什么服务。
|
||||
|
||||
你也许会问,网络的下载带宽也飘红了啊,已经达到100Mb以上了。这就涉及到怎么理解计数器的问题了。这里的网络虽然飘红了,但也只有100多Mb,它飘红只是因为Grafana DashBoard的阈值设置问题。如果你不想让它飘红,也可以把阈值设置得高一点。并且对于网络来说,100多Mb,真的不算大。
|
||||
|
||||
我们来看一下worker-1上有什么。
|
||||
|
||||
```
|
||||
[root@k8s-master-2 ~]# kubectl get pods -o wide|grep k8s-worker-1
|
||||
elasticsearch-data-1 1/1 Running 1 11d 10.100.230.57 k8s-worker-1 <none> <none>
|
||||
elasticsearch-master-0 1/1 Running 0 3d11h 10.100.230.60 k8s-worker-1 <none> <none>
|
||||
mysql-min-d564fc4df-vs7d6 1/1 Running 0 22h 10.100.230.1 k8s-worker-1 <none> <none>
|
||||
[root@k8s-master-2 ~]#
|
||||
|
||||
```
|
||||
|
||||
你看,这个worker-1上不止有MySQL,还有ES data,这是一个吃网络的大户。不过,现在问题并没有指向它。
|
||||
|
||||
我们在前面看到的是MySQL的响应时间长,所以我们再到worker-1上,接着看全局监控的数据。
|
||||
|
||||
```
|
||||
[root@k8s-worker-1 ~]# top
|
||||
top - 23:08:21 up 3 days, 11:30, 5 users, load average: 29.90, 28.54, 23.00
|
||||
Tasks: 309 total, 1 running, 307 sleeping, 0 stopped, 1 zombie
|
||||
%Cpu0 : 94.1 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 2.9 si, 2.9 st
|
||||
%Cpu1 : 94.1 us, 2.9 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 2.9 si, 0.0 st
|
||||
%Cpu2 : 90.9 us, 3.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 6.1 st
|
||||
%Cpu3 : 89.7 us, 3.4 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 3.4 si, 3.4 st
|
||||
%Cpu4 : 87.9 us, 6.1 sy, 0.0 ni, 3.0 id, 0.0 wa, 0.0 hi, 0.0 si, 3.0 st
|
||||
%Cpu5 : 87.9 us, 9.1 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 3.0 st
|
||||
KiB Mem : 16265992 total, 1176564 free, 8436112 used, 6653316 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 7422832 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
21344 27 20 0 8222204 628452 12892 S 331.4 3.9 141:36.72 /opt/rh/rh-mysql57/root/usr/libexec/mysqld --defaults-file=/etc/my.cnf
|
||||
5128 techstar 20 0 5917564 1.4g 21576 S 114.3 8.8 233:09.48 /usr/share/elasticsearch/jdk/bin/java -Xshare:auto -Des.networkaddress.cache+
|
||||
5127 techstar 20 0 14.1g 3.5g 25756 S 40.0 22.8 1647:28 /usr/share/elasticsearch/jdk/bin/java -Xshare:auto -Des.networkaddress.cache+
|
||||
1091 root 20 0 1145528 108228 29420 S 25.7 0.7 263:51.49 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
|
||||
1078 root 20 0 2504364 106288 38808 S 14.3 0.7 429:13.57 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.co+
|
||||
17108 root 20 0 164472 2656 1712 R 14.3 0.0 0:00.66 top
|
||||
|
||||
```
|
||||
|
||||
从上面的数据中,我们也能看到MySQL的进程消耗的CPU比较多,这说明我们现在走的证据链是正确的。既然走到了数据库,那我们主要看什么呢?当然是看MySQL的全局监控了。所以,我打印了MySQL Report,过滤掉一些没问题的数据之后得到如下结果(不然内容就太长了):
|
||||
|
||||
```
|
||||
__ Questions ___________________________________________________________
|
||||
Total 637.05k 8.0/s
|
||||
DMS 293.57k 3.7/s %Total: 46.08
|
||||
Com_ 235.02k 2.9/s 36.89
|
||||
.............................
|
||||
Slow 20 ms 119.50k 1.5/s 18.76 %DMS: 40.70 Log:
|
||||
DMS 293.57k 3.7/s 46.08
|
||||
SELECT 224.80k 2.8/s 35.29 76.57
|
||||
UPDATE 51.86k 0.6/s 8.14 17.66
|
||||
INSERT 16.92k 0.2/s 2.66 5.76
|
||||
REPLACE 0 0/s 0.00 0.00
|
||||
DELETE 0 0/s 0.00 0.00
|
||||
.............................
|
||||
|
||||
|
||||
__ SELECT and Sort _____________________________________________________
|
||||
Scan 137.84k 1.7/s %SELECT: 61.32
|
||||
.............................
|
||||
|
||||
```
|
||||
|
||||
从上面的数据我们可以看到,在Total的部分中,DMS(Data Manipulation Statements ,数据维护语句)占比46.08%。而在DMS中,SELECT占比76.57%。所以,我们要把后续分析的重点放在SELECT语句上。
|
||||
|
||||
通过Slow这一行,看到慢日志也已经出现,因为我把慢日志阈值设置的比较低,只有20ms,所以,你能看到每秒产生了1.5个慢日志。我之所以把慢日志阈值设的比较低,主要是想把稍微慢一点的SQL都记录下来。不过,在你的应用中,要根据实际的情况来,不要设置过大,也不要过小,不然都是泪。
|
||||
|
||||
### 定向分析
|
||||
|
||||
下面就是看慢日志喽。请你记住,在看MySQL慢日志之前,最好先把日志清一遍,让这个日志只记录压力场景执行时间段内的慢SQL,不然受影响的数据会很多。
|
||||
|
||||
```
|
||||
[root@7dgroup1 gaolou]# pt-query-digest slow-query.log
|
||||
|
||||
|
||||
# 7.2s user time, 70ms system time, 36.78M rss, 106.05M vsz
|
||||
# Current date: Wed Dec 30 23:30:14 2020
|
||||
# Hostname: 7dgroup1
|
||||
# Files: slow-query.log
|
||||
# Overall: 36.60k total, 7 unique, 89.06 QPS, 17.17x concurrency _________
|
||||
# Time range: 2020-12-30T15:22:00 to 2020-12-30T15:28:51
|
||||
# Attribute total min max avg 95% stddev median
|
||||
# ============ ======= ======= ======= ======= ======= ======= =======
|
||||
# Exec time 7055s 20ms 1s 193ms 501ms 160ms 128ms
|
||||
# Lock time 7s 0 39ms 194us 247us 696us 125us
|
||||
# Rows sent 35.45k 0 1 0.99 0.99 0.09 0.99
|
||||
# Rows examine 2.33G 0 112.76k 66.71k 112.33k 46.50k 112.33k
|
||||
# Query size 14.26M 6 1016 408.53 592.07 195.17 202.40
|
||||
|
||||
|
||||
# Profile
|
||||
# Rank Query ID Response time Calls R/Call V/M It
|
||||
# ==== ============================= =============== ===== ====== ===== ==
|
||||
# 1 0xB8BDB35AD896842FAC41202B... 5744.3322 81.4% 18420 0.3119 0.07 SELECT pms_sku_stock
|
||||
# 2 0xC71984B4087F304BE41AC8F8... 1309.1841 18.6% 18138 0.0722 0.03 SELECT oms_cart_item
|
||||
# MISC 0xMISC 1.4979 0.0% 46 0.0326 0.0 <5 ITEMS>
|
||||
|
||||
|
||||
# Query 1: 44.82 QPS, 13.98x concurrency, ID 0xB8BDB35AD896842FAC41202BB9C908E8 at byte 6504041
|
||||
# This item is included in the report because it matches --limit.
|
||||
# Scores: V/M = 0.07
|
||||
# Time range: 2020-12-30T15:22:00 to 2020-12-30T15:28:51
|
||||
# Attribute pct total min max avg 95% stddev median
|
||||
# ============ === ======= ======= ======= ======= ======= ======= =======
|
||||
# Count 50 18420
|
||||
# Exec time 81 5744s 76ms 1s 312ms 580ms 148ms 279ms
|
||||
# Lock time 47 3s 70us 37ms 184us 224us 673us 119us
|
||||
# Rows sent 50 17.99k 1 1 1 1 0 1
|
||||
# Rows examine 85 1.98G 112.76k 112.76k 112.76k 112.76k 0 112.76k
|
||||
# Query size 26 3.72M 212 212 212 212 0 212
|
||||
# String:
|
||||
# Hosts 10.100.5.54
|
||||
# Users reader
|
||||
# Query_time distribution
|
||||
# 1us
|
||||
# 10us
|
||||
# 100us
|
||||
# 1ms
|
||||
# 10ms #
|
||||
# 100ms ################################################################
|
||||
# 1s #
|
||||
# 10s+
|
||||
# Tables
|
||||
# SHOW TABLE STATUS LIKE 'pms_sku_stock'\G
|
||||
# SHOW CREATE TABLE `pms_sku_stock`\G
|
||||
# EXPLAIN /*!50100 PARTITIONS*/
|
||||
select
|
||||
|
||||
|
||||
id, product_id, sku_code, price, stock, low_stock, pic, sale, promotion_price, lock_stock,
|
||||
sp_data
|
||||
|
||||
from pms_sku_stock
|
||||
|
||||
|
||||
WHERE ( sku_code = '202008270027906' )\G
|
||||
|
||||
|
||||
# Query 2: 44.13 QPS, 3.19x concurrency, ID 0xC71984B4087F304BE41AC8F82A88B245 at byte 20901845
|
||||
# This item is included in the report because it matches --limit.
|
||||
# Scores: V/M = 0.03
|
||||
# Time range: 2020-12-30T15:22:00 to 2020-12-30T15:28:51
|
||||
# Attribute pct total min max avg 95% stddev median
|
||||
# ============ === ======= ======= ======= ======= ======= ======= =======
|
||||
# Count 49 18138
|
||||
# Exec time 18 1309s 20ms 419ms 72ms 148ms 43ms 59ms
|
||||
# Lock time 52 4s 76us 39ms 205us 260us 719us 138us
|
||||
# Rows sent 49 17.45k 0 1 0.99 0.99 0.12 0.99
|
||||
# Rows examine 14 356.31M 19.96k 20.22k 20.12k 19.40k 0 19.40k
|
||||
# Query size 73 10.51M 604 610 607.81 592.07 0 592.07
|
||||
# String:
|
||||
# Hosts 10.100.5.54
|
||||
# Users reader
|
||||
# Query_time distribution
|
||||
# 1us
|
||||
# 10us
|
||||
# 100us
|
||||
# 1ms
|
||||
# 10ms ################################################################
|
||||
# 100ms ##################
|
||||
# 1s
|
||||
# 10s+
|
||||
# Tables
|
||||
# SHOW TABLE STATUS LIKE 'oms_cart_item'\G
|
||||
# SHOW CREATE TABLE `oms_cart_item`\G
|
||||
# EXPLAIN /*!50100 PARTITIONS*/
|
||||
select
|
||||
|
||||
|
||||
id, product_id, product_sku_id, member_id, quantity, price, product_pic, product_name,
|
||||
product_sub_title, product_sku_code, member_nickname, create_date, modify_date, delete_status,
|
||||
product_category_id, product_brand, product_sn, product_attr
|
||||
from oms_cart_item
|
||||
WHERE ( member_id = 381920
|
||||
and product_id = 317
|
||||
and delete_status = 0
|
||||
and product_sku_id = 317 )\G
|
||||
|
||||
```
|
||||
|
||||
从上面的数据来看,我们的优化方向比较简单明了:占用总时间最长的两个SQL需要收拾,其中,一个占用了总时间的81.4%,另一个占用了18.6%。
|
||||
|
||||
我们先来看最慢的那个SQL:
|
||||
|
||||
```
|
||||
select
|
||||
id, product_id, sku_code, price, stock, low_stock, pic, sale, promotion_price, lock_stock,
|
||||
sp_data
|
||||
from pms_sku_stock
|
||||
WHERE ( sku_code = '202008270027906' )\G
|
||||
|
||||
```
|
||||
|
||||
要想知道一个语句哪里慢,就得来看一下执行计划:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/17/b0/17a4e2d2df0bebda224068869262feb0.png" alt="">
|
||||
|
||||
在执行计划中,type这一列的参数值为ALL,说明这个SQL没有用到索引。你想想,一个有where条件的语句,又没有用到索引,那它上方的索引到底合不合理呢?我们不妨检查一下这个索引:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/2c/9fb5d843506d87c40660058bab5e4d2c.png" alt="">
|
||||
|
||||
通过检查索引,我们看到只有一个ID列,也就是一个主键索引,并没有where条件中的sku_code列。所以,我们先给sku_code加一个索引来实现精准查询,这样就不用扫描整表的数据了:
|
||||
|
||||
```
|
||||
ALTER TABLE pms_sku_stock ADD INDEX sku_code_index (sku_code);
|
||||
|
||||
```
|
||||
|
||||
修改之后,我们再来看一下此时的执行计划:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/c3/ba4c981ce540dd1fbaf74547483fa7c3.png" alt="">
|
||||
|
||||
现在,type列的参数值变为了ref,说明where条件确实走了索引了。那我们再把场景执行起来,看看效果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bb/d8/bbbe15d314ab1fab1798bf4f7bc229d8.png" alt="">
|
||||
|
||||
从结果来看,TPS从50增加到了150以上。响应时间也从750ms左右降到250ms以下。效果显著。
|
||||
|
||||
收拾完了第一个SQL后,我们再来收拾另一个SQL。同样地,我们先看它的执行计划:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/61/9ab86c87e0496699628d4d50a244d161.png" alt="">
|
||||
|
||||
type列的参数值为ALL,表明where条件没有使用索引。但是,第二个语句用了好几个where条件,所以,我们直接加一个组合索引,让where条件可以走到索引这里:
|
||||
|
||||
```
|
||||
ALTER TABLE oms_cart_item ADD INDEX mix_index (member_id,product_id,product_sku_id);
|
||||
|
||||
```
|
||||
|
||||
加了组合索引后,这个SQL的执行计划如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/5c/a1dd2eabfa7e4903f33013241f145d5c.png" alt="">
|
||||
|
||||
还是一样,我们再次把场景跑起来,看看优化了这两个最慢的SQL之后,效果如何。
|
||||
|
||||
### 优化效果
|
||||
|
||||
优化效果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/3c/dcfc2e05c8f4a767b284a9a6eb82313c.png" alt="">
|
||||
|
||||
优化前后的对比图如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/10/50/100f25cbb012cd7d8902byy884528850.png" alt="">
|
||||
|
||||
建议你在写报告的时候,画这种对比图,用它来说明优化效果是非常直接明显的。
|
||||
|
||||
## 分析的第二阶段
|
||||
|
||||
现在我们就要来分析错误了,反正也忽悠不过去。
|
||||
|
||||
### 压力数据
|
||||
|
||||
下面是对应的错误图,我把图截多一点,可以看到趋势如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/37/ec/372bfab0665388228fe36b1e167c0cec.png" alt="">
|
||||
|
||||
你看,**TPS中有对的,也有错的,并且TPS越高的时候,错误率也越高**。这一点很重要,希望你能记住。
|
||||
|
||||
紧接着,我们来拆分响应时间。
|
||||
|
||||
### 拆分响应时间
|
||||
|
||||
先设置skywalking的时间段:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c2/38/c2a3c6e60cd14b69980cbaf8207be638.png" alt="">
|
||||
|
||||
请你注意,在看性能计数器的时候,每一个工具上的时间窗口一定要对应上。
|
||||
|
||||
- User - Gateway:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/64/71/64abda597f27856747bc2b7afaa76371.png" alt="">
|
||||
|
||||
- Gateway - Cart:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fe/6b/febd0fc79be360bf4b6b477afc26e06b.png" alt=""><br>
|
||||
<img src="https://static001.geekbang.org/resource/image/75/71/75764747266a66d64cfcaf648e93aa71.png" alt="">
|
||||
|
||||
- Cart - Member:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ec/aa/ec6b494ca14142f920861c198a3d08aa.png" alt=""><img src="https://static001.geekbang.org/resource/image/d1/b9/d15c9575a080cd2066ba962b7c5517b9.png" alt="">
|
||||
|
||||
- Cart - MySQL:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6d/c8/6d9f71f3eeab6705389cf7e7b0ded8c8.png" alt="">
|
||||
|
||||
- Member - MySQL:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/96/9fcaef4474c9696ea5a8ffe83315f796.png" alt="">
|
||||
|
||||
罗列了一堆信息之后……并没有什么发现。
|
||||
|
||||
你可能会奇怪,为什么说没有发现呢,Cart上的响应时间不是比较长吗?这里你就要注意了,我们现在分析的问题是错误,而不是响应时间,所以时间长就长呗。**在分析的过程中,你一定要时刻记得自己查的是什么问题,不要走到半路就走岔了,那样会陷入混乱的状态。**
|
||||
|
||||
### 全局分析
|
||||
|
||||
通常情况下,我们的全局分析都是从资源开始的对吧,也就是从性能分析决策树中一层层查下去。对应我们第4节课讲的内容,你可以把所有的第一层计数器查一遍。
|
||||
|
||||
而在我们的这个问题的分析中,其实不用那么麻烦,因为在前面看到压力数据的时候,已经看到了大量的报错了,要想分析错误,肯定得先知道错误在哪,所以,这里我们直接查日志相关的内容就可以。查到日志的时候,我们看到下面这些错误信息:
|
||||
|
||||
```
|
||||
2020-12-30 23:44:06.754 ERROR 1 --- [io-8086-exec-41] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DeadlockLoserDataAccessException:
|
||||
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
|
||||
### The error may involve com.dunshan.mall.mapper.OmsCartItemMapper.updateByPrimaryKey-Inline
|
||||
### The error occurred while setting parameters
|
||||
### SQL: update oms_cart_item set product_id = ?, product_sku_id = ?, member_id = ?, quantity = ?, price = ?, product_pic = ?, product_name = ?, product_sub_title = ?, product_sku_code = ?, member_nickname = ?, create_date = ?, modify_date = ?, delete_status = ?, product_category_id = ?, product_brand = ?, product_sn = ?, product_attr = ? where id = ?
|
||||
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
|
||||
; Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction] with root cause
|
||||
...................................
|
||||
|
||||
```
|
||||
|
||||
这个错误已经给了我们明确的指向:死锁。可是为什么会死锁呢?
|
||||
|
||||
在性能分析中,你要记得,死锁其实是相对容易分析的内容。**有争用才有锁,而死锁,就是<strong><strong>说锁被**</strong>争得死死的。</strong>
|
||||
|
||||
下面我们开始定向分析为什么会产生锁。
|
||||
|
||||
### 定向分析
|
||||
|
||||
首先,我们找到商品加入购物车业务对应的代码:
|
||||
|
||||
```
|
||||
/**
|
||||
* 增加购物车
|
||||
* @param productSkuCode 库存商品编号
|
||||
* @param quantity 商品数量
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public int addCart(String productSkuCode, Integer quantity) {
|
||||
.........................................
|
||||
OmsCartItem existCartItem = getCartItem(cartItem);
|
||||
if (existCartItem == null) {
|
||||
cartItem.setCreateDate(new Date());
|
||||
count = cartItemMapper.insert(cartItem);
|
||||
} else {
|
||||
cartItem.setModifyDate(new Date());
|
||||
existCartItem.setQuantity(existCartItem.getQuantity() + cartItem.getQuantity());
|
||||
count = cartItemMapper.updateByPrimaryKey(existCartItem);
|
||||
}
|
||||
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
引用这段代码的事务如下:
|
||||
|
||||
```
|
||||
@Transactional
|
||||
int addCart(String productSkuCode, Integer quantity);
|
||||
|
||||
```
|
||||
|
||||
根据上面的关系,对于商品加入购物车来说,什么能引起死锁呢?你看,在代码中有一个update,它对应的也就是前面日志中的update语句。所以,要是发生死锁的话,那指定就是ID冲突了,而这个ID对应的就是会员ID。也就是说,有多个线程同时想更新同一个会员的购物车,这怎么能行!
|
||||
|
||||
既然是会员ID冲突了,那是谁给的会员信息呢?想都不用想,这个会员信息肯定是从脚本中传过来的呀,所以我们要查查脚本。
|
||||
|
||||
对应的脚本如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/79/a7/7986171dedc7108ac44eb8f216fdc9a7.png" alt="">
|
||||
|
||||
你看,这里有一个productSkuCode参数,共用了1000行数据量。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/12/08bb30112213e9450cd77ee698yy3812.png" alt="">
|
||||
|
||||
上面的图对应的JMeter脚本是这样的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/33/4c/331479527f3ef875426ce77d899d724c.png" alt="">
|
||||
|
||||
我们来看JMeter脚本中的这三个参数:
|
||||
|
||||
```
|
||||
quotedData: false
|
||||
recycle: true
|
||||
stopThread: false
|
||||
|
||||
```
|
||||
|
||||
这意味着,我们所有的线程都在共用这1000条数据,并且在不断循环。这会导致数据使用重复,也就是说,如果有两个以上的线程用到了相同的用户数据,就会更新同一个购物车,于是产生冲突报错。
|
||||
|
||||
我们现在把上面三个参数改一下:
|
||||
|
||||
```
|
||||
quotedData: true
|
||||
recycle: false
|
||||
stopThread: true
|
||||
|
||||
```
|
||||
|
||||
这样就保证了每个线程可以分到不同的数据。
|
||||
|
||||
可是,另一个问题来了:我们做这样处理的话,1000条数据是不够用的,怎么办呢?那我们就只有把用户数据加大,等生成更多的Token之后,我们再来执行场景。
|
||||
|
||||
通过一晚上的造数,时间来到了第二天。
|
||||
|
||||
### 优化效果
|
||||
|
||||
于是,我们得到了如下结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/4b/188e028b8ef2f7c3994ba4a1f483bb4b.png" alt="">
|
||||
|
||||
从数据上来看,报错没有了,这是一个合理的结果。
|
||||
|
||||
## 总结
|
||||
|
||||
现在,我们总结一下这节课。
|
||||
|
||||
“哎,哎,你先别总结呀,问题都没解决完,你看这不是还有TPS掉下来的情况吗?”
|
||||
|
||||
“年轻人,别捉急,饭都得一口一口吃,问题自然要一个一个解决了。这个问题,我会放在后面的课程中解决。”
|
||||
|
||||
在这节课中,我们从TPS不高开始,一直分析到了具体的SQL,看似是两个简单的索引就搞定的事情,逻辑也并不复杂,但是,这个分析思路非常重要。
|
||||
|
||||
对于第二个问题,我们从错误数据查到了日志中出现的死锁信息,这一点大部分人应该都可以做得到。只不过,能立即想到参数冲突的,就是有经验的人了。
|
||||
|
||||
此外,这里还有一个重点就是,**参数化数据一定要符合真实场景**!高老师已经反复强调很多遍了,希望你能记得住。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,我给你留两道题,请你思考一下:
|
||||
|
||||
1. 除了用本节课中的手段,你还有什么方法可以快速定位到SQL语句慢的问题?
|
||||
1. 你能画出在第二阶段分析中的逻辑吗?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
536
极客时间专栏/高楼的性能工程实战课/基准场景/17 | 查询购物车:为什么铺底参数一定要符合真实业务特性?.md
Normal file
536
极客时间专栏/高楼的性能工程实战课/基准场景/17 | 查询购物车:为什么铺底参数一定要符合真实业务特性?.md
Normal file
@@ -0,0 +1,536 @@
|
||||
<audio id="audio" title="17 | 查询购物车:为什么铺底参数一定要符合真实业务特性?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1b/1a/1b7963b258a0e2b0ea65118dyy92a91a.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
今天我们来看看查询购物车接口。
|
||||
|
||||
到现在为止,这是我们分析的第六个接口了。不过,我希望你能明白,我们分析每个接口,并不是为了搞清楚这个接口本身的逻辑,而是通过不同接口的基准测试,来分析不同的性能问题,争取给你带来更多的分析案例。
|
||||
|
||||
现在很多人在性能场景执行过程中,仍然会问出“当铺底数据不符合生产环境时,该怎么办”这样的疑问,其实答案也挺简单,那就是模拟不出生产环境中的问题。
|
||||
|
||||
所以,在这节课中,你将看到当铺底数据不合理时,会对TPS产生什么样具体的影响。由此,你会进一步理解为什么我一直在跟你强调**铺底数据要符合生产环境逻辑**。
|
||||
|
||||
此外,我们还会分析另一个问题,这个问题可能会让你看着比较郁闷,你会发现我们分析了很久,逻辑看似非常合理,但是结果并不如人意。面对这样的情况,那我们该怎么处理呢?这里留个悬念,我们直接开始今天的分析。
|
||||
|
||||
## 压力数据
|
||||
|
||||
对于查询购物车这个接口,还是一样,我们先来看第一次运行的性能场景结果。这是一个一开始就足以让人心碎的性能数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/e3/6be835b024ebcdyyb8b37yy716bee1e3.png" alt="">
|
||||
|
||||
你看,线程数在增加的过程中,TPS只达到40,而响应时间从一开始就不断地增加。
|
||||
|
||||
这可怎么办呢?根据我们RESAR性能分析逻辑,第一步仍然是看架构图,接着是拆分响应时间。因为响应时间在不断增加,所以我们想要拆分响应时间非常容易。
|
||||
|
||||
## 架构图
|
||||
|
||||
在拆分响应时间之前,我们看一下架构图。在这一步,你只需要把架构图记个大概就行了。因为后面还要反复回来看多次。
|
||||
|
||||
##
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/3c/4d60c5c09byy55e72b058c5871e3bf3c.png" alt="">
|
||||
|
||||
## 第一阶段分析
|
||||
|
||||
### 拆分响应时间
|
||||
|
||||
我们反反复复在讲,**做性能分析的时候,首先就是拆分时间**。
|
||||
|
||||
别人在问我问题的时候,经常会这样描述:TPS不高,响应时间长,瓶颈在哪呢?一看到这种问题,我通常会反问:响应时间长在哪呢?然后,经典的对话结束语就出现了——我不知道呀。我也很想帮助对方解决问题,但是,对于这样的描述,我根本无从下手。
|
||||
|
||||
一个做性能的人,怎么能只描述响应时间长呢?你至少要告诉别人慢在哪里。这就是为什么我一直在强调要画架构图。因为有了图,才有拆分时间的手段,这样一来,我们自然就不会盲目,除非你啥都没有。
|
||||
|
||||
在拆分时间的时候,你还要注意一点,**要找准时间段**。根据我的经验,一般是看响应时间的趋势,如果一直都长的话,倒是简单,看哪一段响应时间都行。要是有的时候长,有的时候短,那你就要注意了,在拆分响应时间的时候,要注意把监控工具中的时间段选择好。
|
||||
|
||||
在这里,我们选择SkyWalking时间段:`2021-01-02 13:53:00 - 2021-01-02 13:54:00`。具体拆分时间如下:
|
||||
|
||||
- User - Gateway:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/82/4d7a06yy402957608011dd69b8e8fd82.png" alt="">
|
||||
|
||||
- Gateway:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/3b/0d466c07da6d2db5fff60abc32e0ec3b.png" alt="">
|
||||
|
||||
- Gateway - Cart:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/ff/5c0fbf24959d98c5787f64def5026fff.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ce/49/ced9b3920d9d00ab06b3d443bbae8649.png" alt="">
|
||||
|
||||
- Cart:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/b5/bayy3fc2a1e2958789e5f3410f6498b5.png" alt="">
|
||||
|
||||
- Cart - MySQL:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d2/b2/d22c0a2ee7180ecde6f97505e467e9b2.png" alt="">
|
||||
|
||||
通过上面抓取的数据,你明显可以看到,是购物车服务Cart那一段的响应时间长。
|
||||
|
||||
我们要注意,有些数据抓取工具由于工具本身的问题,会存在不小的数据偏差,比如说对于上面的SkyWalking时间段,我们看到Gateway - Cart之间的服务端平均响应时间是829.25。但是,在Cart上却是984.50。同样的一段时间,这里就出现了一些偏差。
|
||||
|
||||
在每一个监控工具上,都或多或少存在性能数据偏差,就比如docker stats,我简直是不想看。所以,我们有时候要结合多个工具来对比数据。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
拆分完响应时间后,我们不再从全局分析开始,而是直接跳到了定向监控。因为对于查询购物车这个接口,我们已经知道Cart服务是慢的,所以,我们就直接进去查看对应的慢的方法在哪里。
|
||||
|
||||
这个接口的调用方法如下所示:
|
||||
|
||||
```
|
||||
/**
|
||||
* 根据会员id查询购物车数据
|
||||
*
|
||||
* @param memberId 会员id
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<OmsCartItem> list(Long memberId) {
|
||||
if (memberId == null) {
|
||||
return null;
|
||||
}
|
||||
OmsCartItemExample example = new OmsCartItemExample();
|
||||
example.createCriteria().andDeleteStatusEqualTo(0).andMemberIdEqualTo(memberId);
|
||||
return cartItemMapper.selectByExample(example);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通过上面的代码,我们知道了方法名,那我们直接用Arthas来Trace这个接口就好了,命令如下:
|
||||
|
||||
```
|
||||
trace com.dunshan.mall.cart.service.imp.CartItemServiceImpl list -v -n 5 --skipJDKMethod false '1==1'
|
||||
|
||||
```
|
||||
|
||||
于是,我们得到了如下的信息:
|
||||
|
||||
```
|
||||
[arthas@1]$ trace com.dunshan.mall.cart.service.imp.CartItemServiceImpl list -v -n 5 --skipJDKMethod false '1==1'
|
||||
Condition express: 1==1 , result: true
|
||||
`---ts=2021-01-02 14:59:53;thread_name=http-nio-8086-exec-556;id=10808;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@18c26588
|
||||
`---[999.018045ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl$$EnhancerBySpringCGLIB$$e110d1ef:list()
|
||||
`---[998.970849ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #57
|
||||
|
||||
|
||||
Condition express: 1==1 , result: true
|
||||
`---ts=2021-01-02 14:59:54;thread_name=http-nio-8086-exec-513;id=107d3;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@18c26588
|
||||
`---[1095.593933ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl$$EnhancerBySpringCGLIB$$e110d1ef:list()
|
||||
`---[1095.502983ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #57
|
||||
|
||||
|
||||
Condition express: 1==1 , result: true
|
||||
`---ts=2021-01-02 14:59:53;thread_name=http-nio-8086-exec-505;id=1078b;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@18c26588
|
||||
`---[2059.097767ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl$$EnhancerBySpringCGLIB$$e110d1ef:list()
|
||||
`---[2059.013275ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #57
|
||||
|
||||
|
||||
Condition express: 1==1 , result: true
|
||||
`---ts=2021-01-02 14:59:54;thread_name=http-nio-8086-exec-541;id=107f6;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@18c26588
|
||||
`---[1499.559298ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl$$EnhancerBySpringCGLIB$$e110d1ef:list()
|
||||
`---[1499.498896ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #
|
||||
|
||||
通过上面的数据可以看到list()的响应时间确实是长了,但是这个接口并不复杂,就是一个select语句而已。对应的select语句的Mapper内容如下:
|
||||
|
||||
<select id="selectByExample" parameterType="com.dunshan.mall.model.OmsCartItemExample" resultMap="BaseResultMap">
|
||||
select
|
||||
<if test="distinct">
|
||||
distinct
|
||||
</if>
|
||||
<include refid="Base_Column_List" />
|
||||
from oms_cart_item
|
||||
<if test="_parameter != null">
|
||||
<include refid="Example_Where_Clause" />
|
||||
</if>
|
||||
<if test="orderByClause != null">
|
||||
order by ${orderByClause}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
```
|
||||
|
||||
这个Mapper对应到数据库中,具体的SQL就是:
|
||||
|
||||
```
|
||||
SELECT id, product_id, product_sku_id, member_id, quantity, price, product_pic, product_name, product_sub_title, product_sku_code, member_nickname, create_date, modify_date, delete_status, product_category_id, product_brand, product_sn, product_attr FROM oms_cart_item WHERE ( delete_status = 0 AND member_id = 597427 )
|
||||
|
||||
```
|
||||
|
||||
既然是一个select语句消耗的时间长,那我们就到数据库里,根据相应的SQL来看对应表的数据直方图。命令如下:
|
||||
|
||||
```
|
||||
select member_id,count(*) from oms_cart_item_202101021530 GROUP BY 1 ORDER BY 2 DESC;
|
||||
|
||||
```
|
||||
|
||||
结果如下,我们截取了直方图的部分数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/95/bf/954ca65681d0f21602f6c477c1e5e6bf.png" alt="">
|
||||
|
||||
从上述数据库中的数据来看,一个会员ID的下面已经加了不少数据。虽然select是通过会员ID查的,但是没做分页处理。这是最简单直接的SQL问题了,分析过程也非常简单。当我们一看到SQL时间长的时候,就要查看一下执行计划:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/c0/7d1726dd3ea450bef207b96261b883c0.png" alt="">
|
||||
|
||||
既然上面的type值是ALL,说明走的是全表扫描,那我们就要根据SQL中的where条件来确定一下要创建什么索引;如果where条件中的查询结果是多条结果,并且数据较多,那就需要做分页。分析到这里,其实也比较容易想到对应的解决方案,有两个动作要做:
|
||||
|
||||
1. 创建索引:创建索引是为了查询的时候可以精准查询。
|
||||
1. 做分页:是为了避免返回到前端的数据太多。
|
||||
|
||||
### 优化效果
|
||||
|
||||
我们虽然讲的是“优化效果”,但,准确来说只是“验证效果”。因为上面的两个动作都是为了提高SQL的查询效果,确切来说就为了减少查询出来的数据。那我们现在就直接把数据给降下来,来验证我们的判断是不是正确。
|
||||
|
||||
为了验证我们的分析过程是正确的,这里我先直接把表给TRUNCATE掉,先看看响应时间能不能上来。如果能上来,那就是这里的问题了。
|
||||
|
||||
可如果不是呢?那我们只能回到角落默默流泪了。这么简单的问题都找不到,我不是一个合格的性能分析人员。
|
||||
|
||||
不管怎么说,来,我们看下结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/f1/c0c511412da730a61d7ff0d166d1f1f1.png" alt="">
|
||||
|
||||
可以看到,TPS一下子上升了很多,在场景不间断的情况下,这个比对如此喜人。看来,我还能继续干这一行。
|
||||
|
||||
不过,我们的分析到这里并没有结束,屋漏偏逢连夜雨,我在接着做压力的过程中,又出现了状况,这让我们不得不进入第二个阶段的分析。
|
||||
|
||||
## 第二阶段分析
|
||||
|
||||
到底又出现了什么问题呢?具体情况如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/48/02/486a53a0d5d9af7f985e61f91fecd702.png" alt="">
|
||||
|
||||
What? 那是TPS曲线吗?那是掉下来了吗?掉的还这么多吗?同样是场景不间断啊。我的职业生涯难道要就此断送了吗?
|
||||
|
||||
这个问题有点复杂。但是从响应时间曲线上看,明显是响应时间增加了,TPS下来了。既然这样,仍然走我们拆分响应时间的思路就好了,这里不再赘述。
|
||||
|
||||
通过拆分时间,我们知道响应时间长的问题出在了Gateway上。下面我们就根据RESAR性能分析逻辑,老老实实开始分析。
|
||||
|
||||
### 全局监控分析
|
||||
|
||||
我们从系统级的资源上可以明显看到,所有的worker节点都无压力。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/d4/e351d44ec9be9300597431ca6b28c1d4.png" alt="">
|
||||
|
||||
我们再从Pod角度来看一下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f3/f0/f355e78f2869e77b1625b4b57555c2f0.png" alt="">
|
||||
|
||||
你看,有些Pod消耗的CPU已经达到了100%。我把所有的Pod排个序,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/ed/9123f41165957b2761ecb02e781133ed.png" alt="">
|
||||
|
||||
虽然我们看到了像node_exporter、ES相关的Pod资源都用得不低,但是这些CPU使用率高的节点Pod的资源也都限制了。同时,你要注意,这个资源占用率高的Pod中并没有我们的应用节点,也就是说我们应用节点的CPU资源并没有用完。
|
||||
|
||||
我本来想去看一下在这段时间内,应用所在的worker上的内存消耗具体是怎样的。但是,在这段时间内却没了数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/70/4d8c352b4762747fe30480e43180e170.png" alt="">
|
||||
|
||||
你看,中间的数据已经断掉了,node_exporter已经不传数了。没有办法,我们只有放弃看 worker上的内存消耗了。
|
||||
|
||||
既然如此,那我们先查一下Gateway在哪个worker上,同时也来看一下这个worker上有多少Pod。走这一步是因为在整个Kubernetes中,所有的namespace都用worker主机的资源。所以,从资源使用的角度来看,我们要考虑到所有命名空间中的Pod。
|
||||
|
||||
所有namespace在应用节点上的所有Pod如下:
|
||||
|
||||
```
|
||||
- 先查询gateway所在的worker节点名
|
||||
|
||||
[root@k8s-master-2 ~]# kubectl get pods --all-namespaces -o wide | grep gateway
|
||||
default gateway-mall-gateway-6567c8b49c-pc7rf 1/1 Running 0 15h 10.100.140.2 k8s-worker-2 <none> <none>
|
||||
|
||||
- 再查询对应worker上的所有POD
|
||||
[root@k8s-master-2 ~]# kubectl get pods --all-namespaces -o wide | grep k8s-worker-2
|
||||
default elasticsearch-client-1 1/1 Running 4 20d 10.100.140.28 k8s-worker-2 <none> <none>
|
||||
default elasticsearch-data-2 1/1 Running 0 4d2h 10.100.140.35 k8s-worker-2 <none> <none>
|
||||
default elasticsearch-master-2 1/1 Running 4 20d 10.100.140.30 k8s-worker-2 <none> <none>
|
||||
default gateway-mall-gateway-6567c8b49c-pc7rf 1/1 Running 0 15h 10.100.140.2 k8s-worker-2 <none> <none>
|
||||
kube-system calico-node-rlhcc 1/1 Running 0 2d5h 172.16.106.149 k8s-worker-2 <none> <none>
|
||||
kube-system coredns-59c898cd69-sfd9w 1/1 Running 4 36d 10.100.140.31 k8s-worker-2 <none> <none>
|
||||
kube-system kube-proxy-l8xf9 1/1 Running 6 36d 172.16.106.149 k8s-worker-2 <none> <none>
|
||||
monitoring node-exporter-mjsmp 2/2 Running 0 4d17h 172.16.106.149 k8s-worker-2 <none> <none>
|
||||
nginx-ingress nginx-ingress-nbhqc 1/1 Running 0 5d19h 10.100.140.34 k8s-worker-2 <none> <none>
|
||||
[root@k8s-master-2 ~]#
|
||||
|
||||
```
|
||||
|
||||
从上面的结果可以看到,我们的worker节点上有9个Pod。
|
||||
|
||||
不过我们一开始看全局资源信息的时候,并没有发现整个worker节点的资源使用率很高。这是因为我们已经在Pod里限制了资源。所以我们列一下每个Pod的资源限制:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/11/42e9797ac167e0d058f585c12b762b11.jpg" alt="">
|
||||
|
||||
对于那些其他资源占用不高的Pod,我们就不看了。
|
||||
|
||||
既然资源有限制,那我们还要把目光转回到Gateway上面来。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
通过查看链路时间,我们也能知道是Gateway上消耗的时间较长:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/a7/0153e9a21e1c4428d6f602bcb11a1da7.png" alt="">
|
||||
|
||||
但是,这个sendRequest是干嘛的?不知道。
|
||||
|
||||
那我们就做一个试验,看看跳过Gateway之后的TPS是多少。:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/16/31/1694f3dd8195b96cf0d0fe57254bb331.png" alt="">
|
||||
|
||||
可见,走Gateway,TPS只能有400多;不走Gateway,TPS能达到800多。所以,问题确实出在了Gateway上。
|
||||
|
||||
看到这里,有一个环节我们是缺失的,那就是查看Kubernetes容器里的Java进程的健康状态。因为我们在前面查了worker,也查了worker上的Pod,所以现在就到了第三层,也就是Pod中的Java应用。
|
||||
|
||||
对此,你也不用有负担,你想想对于一个Java应用来说,能有个啥?无非就是堆、栈一顿看。来,我们打印个Gateway的栈看一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/48/3f/48930f2e0cd5464def2c0169b7bcdd3f.png" alt="">
|
||||
|
||||
从栈上,啥也没看出来,整个状态似乎都挺合理的。 注意,在这里我不是只看一个截图哦,我已经把整个栈都撸了一遍。由于CPU也不高,我们在分析栈的时候,主要看一下有没有锁等待。从上图可以看到,并没有锁,等待也都合理。
|
||||
|
||||
看完栈之后,接下来该看堆了。我们得想尽办法,把Kubernetes的Java进程堆拿出来看看:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/81/9035c1762a408b1a56650a01b175aa81.png" alt="">
|
||||
|
||||
看到没!如此规则的关联关系:TPS和Gateway的GC趋势是完全一致的。
|
||||
|
||||
不过,这样看还是不够具体,我们还需要更细的数据。所以,我们进去看一下GC状态:
|
||||
|
||||
```
|
||||
[root@gateway-mall-gateway-6567c8b49c-pc7rf /]# jstat -gcutil 1 1000 1000
|
||||
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
|
||||
0.00 55.45 45.33 52.96 94.74 92.77 38427 1953.428 94 113.940 2067.368
|
||||
57.16 0.00 26.86 53.24 94.74 92.77 38428 1954.006 94 113.940 2067.946
|
||||
0.00 54.30 15.07 53.65 94.74 92.77 38429 1954.110 94 113.940 2068.050
|
||||
39.28 0.00 18.39 53.84 94.74 92.77 38430 1954.495 94 113.940 2068.435
|
||||
39.28 0.00 81.36 53.84 94.74 92.77 38430 1954.495 94 113.940 2068.435
|
||||
0.00 26.13 68.79 53.84 94.74 92.77 38431 1954.597 94 113.940 2068.537
|
||||
39.18 0.00 59.75 53.84 94.74 92.77 38432 1954.683 94 113.940 2068.624
|
||||
0.00 24.70 76.28 53.84 94.74 92.77 38433 1954.794 94 113.940 2068.734
|
||||
|
||||
```
|
||||
|
||||
你看,一次YGC大概需要100ms,一秒一次YGC,这样YGC就占了10%左右,这个时间有点多了。
|
||||
|
||||
既然YGC消耗CPU较高,那我们就考虑优化Java参数。先来看一下Java参数:
|
||||
|
||||
```
|
||||
[root@gateway-mall-gateway-6567c8b49c-pc7rf /]# jinfo -flags 1
|
||||
Attaching to process ID 1, please wait...
|
||||
Debugger attached successfully.
|
||||
Server compiler detected.
|
||||
JVM version is 25.242-b08
|
||||
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=262144000 -XX:+ManagementServer -XX:MaxHeapSize=4164943872 -XX:MaxNewSize=1388314624 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=87359488 -XX:OldSize=174784512 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
|
||||
Command line: -Dapp.id=svc-mall-gateway -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.service_name=svc-mall-gateway -Dskywalking.collector.backend_service=skywalking-oap:11800 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=1100 -Dcom.sun.management.jmxremote.rmi.port=1100 -Djava.rmi.server.hostname=localhost -Dspring.profiles.active=prod -Djava.security.egd=file:/dev/./urandom
|
||||
[root@gateway-mall-gateway-6567c8b49c-pc7rf /]#
|
||||
|
||||
```
|
||||
|
||||
从上面的参数中就可以看到,我在Kubernetes的Java进程中并没有配置GC回收相关的参数。所以,这里我们加上相关的参数。
|
||||
|
||||
在下面的参数中,我加了PrintGC相关的参数以及ParNew参数:
|
||||
|
||||
```
|
||||
[root@gateway-mall-gateway-6c6f486786-mnd6j /]# jinfo -flags 1
|
||||
Attaching to process ID 1, please wait...
|
||||
Debugger attached successfully.
|
||||
Server compiler detected.
|
||||
JVM version is 25.261-b12
|
||||
Non-default VM flags: -XX:CICompilerCount=2 -XX:CompressedClassSpaceSize=1065353216 -XX:+HeapDumpOnOutOfMemoryError -XX:InitialHeapSize=2147483648 -XX:+ManagementServer -XX:MaxHeapSize=2147483648 -XX:MaxMetaspaceSize=1073741824 -XX:MaxNewSize=1073741824 -XX:MetaspaceSize=1073741824 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=1073741824 -XX:OldSize=1073741824 -XX:ParallelGCThreads=6 -XX:+PrintGC -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParNewGC
|
||||
Command line: -Dapp.id=svc-mall-gateway -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.service_name=svc-mall-gateway -Dskywalking.collector.backend_service=skywalking-oap:11800 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=1100 -Dcom.sun.management.jmxremote.rmi.port=1100 -Djava.rmi.server.hostname=localhost -Xms2g -Xmx2g -XX:MetaspaceSize=1g -XX:MaxMetaspaceSize=1g -Xmn1g -XX:+UseParNewGC -XX:ParallelGCThreads=6 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCDetails -Xloggc:gc.log -Dspring.profiles.active=prod -Djava.security.egd=file:/dev/./urandom
|
||||
[root@gateway-mall-gateway-6c6f486786-mnd6j /]#
|
||||
|
||||
```
|
||||
|
||||
本来指望ParNew能有啥用,然而并没有什么用。
|
||||
|
||||
既然加参数不是能很快见效的,那我们就得看一下YGC的时候回收了什么,然后再来决定从哪里下手收拾Java进程内存的消耗问题。所以,我们打印一下jmap histo信息,来看一下对象消耗内存的变化,如下所示:
|
||||
|
||||
```
|
||||
[root@gateway-mall-gateway-6c6f486786-mnd6j /]# jmap -histo 1 | head -20
|
||||
|
||||
|
||||
num #instances #bytes class name
|
||||
----------------------------------------------
|
||||
1: 2010270 124874960 [C
|
||||
2: 787127 91014984 [I
|
||||
3: 601333 42467920 [Ljava.lang.Object;
|
||||
4: 1534551 36829224 java.lang.String
|
||||
5: 420603 31107504 [B
|
||||
6: 21891 21972896 [Ljava.util.concurrent.ConcurrentHashMap$Node;
|
||||
7: 186170 11914880 java.util.regex.Matcher
|
||||
8: 228807 10982736 java.util.StringTokenizer
|
||||
9: 291025 9312800 java.util.concurrent.ConcurrentHashMap$Node
|
||||
10: 274253 8804936 [Ljava.lang.String;
|
||||
11: 179524 8617152 org.springframework.web.util.pattern.PathPattern$MatchingContext
|
||||
12: 210473 8418920 java.util.LinkedHashMap$Entry
|
||||
13: 154562 6182480 io.netty.handler.codec.DefaultHeaders$HeaderEntry
|
||||
14: 191349 6123168 java.util.LinkedList
|
||||
15: 126218 6058464 java.util.TreeMap
|
||||
16: 68528 6030464 java.lang.reflect.Method
|
||||
17: 98411 5363408 [Ljava.util.HashMap$Node;
|
||||
[root@gateway-mall-gateway-6c6f486786-mnd6j /]#
|
||||
|
||||
```
|
||||
|
||||
在这里,我们需要把这个命令多执行几次,看看对象消耗内存的变化。前面我们看到YGC过于频繁,但是从内存上来看,对象的内存回收得挺好。
|
||||
|
||||
所以,对于这种YGC很高,但从对象内存的消耗又看不出什么有效信息的问题,只有一种可能,那就是对象创建得快,销毁也快。那么,我们只有一个地方可以准确查找对象内存的消耗了,那就是对象的delta。我们连上JvisualVM,看下内存对象delta变量:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/57/31/572cf090832be1da53f0fd04f8e27e31.png" alt="">
|
||||
|
||||
(注:这张图上的字之所以这么小,是因为我连的是远程Windows桌面,分辨率不高,实在没有办法。不过,你要是仔细看的话,还是能看到最上面那个HashMap。)
|
||||
|
||||
我比较喜欢用这种视图来看delta值。从这里可以看到,增加和销毁都很快。
|
||||
|
||||
在前面我们加了打印GC log的参数,所以我们把GC log拿出来分析一下,得到结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/86/01/86783a376c32ca492f6b0e7efed5d101.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/11/69a1628494465a42dc29361bd1bc2811.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/41/937e064499296cfa20442bf6d3aaec41.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a9/f2/a948d415bd3b32d437edbd0816757ff2.png" alt="">
|
||||
|
||||
从上面的分析来看,主要是YGC在消耗响应时间。这与我们前面的分析吻合,但是我们仍旧没有找到具体的问题点。
|
||||
|
||||
在这个问题的分析过程中,我不断在做应用的修改、重启等动作。结果,没想到性能问题没解决,又遇到了两个其他问题,特地记录在这里。
|
||||
|
||||
之所以记录这样的问题,是想告诉你:**在我们的分析过程中,什么样的问题都有可能存在。而我们虽说是做性能分析的人,但也不是只分析性能问题,而是见到问题就要去解决,要不然,你就走不下去**。
|
||||
|
||||
#### 支线问题一
|
||||
|
||||
我在查找宿主机日志时发现如下信息:
|
||||
|
||||
```
|
||||
[3594300.447892] ACPI Exception: AE_AML_BUFFER_LIMIT, Evaluating _PMM (20130517/power_meter-339)
|
||||
[3594360.439864] ACPI Error: SMBus/IPMI/GenericSerialBus write requires Buffer of length 66, found length 32 (20130517/exfield-389)
|
||||
[3594360.439874] ACPI Error: Method parse/execution failed [\_SB_.PMI0._PMM] (Node ffff8801749b05f0), AE_AML_BUFFER_LIMIT (20130517/psparse-536)
|
||||
|
||||
```
|
||||
|
||||
从错误信息来看,这是一个ACPI缓存区大小的问题。这个缓存大小在BIOS和内核之间没有协商一致,也就是说请求的缓存区大小是66字节,而给的却是32字节。所以,电源监控的管理模块就报了异常。
|
||||
|
||||
这是缺少内核模块引起的,因为这个内核模块在我所用的这个内核版本中不会自动更新。对应的解决方法倒也简单:
|
||||
|
||||
```
|
||||
echo "blacklist acpi_power_meter" >> /etc/modprobe.d/hwmon.conf
|
||||
modprobe ipmi_si
|
||||
modprobe acpi_ipmi
|
||||
|
||||
```
|
||||
|
||||
其中,第一条命令是为了不让这个错误再次出现。当然了,这不是解决问题,只是不想看到这个报错而心里烦燥。后面两条命令是手动加载模块,但前提是你要更新内核版本。
|
||||
|
||||
#### 支线问题二
|
||||
|
||||
再回到我们分析的主线上,前面提到一个Java的YGC消耗的CPU比较高,但是业务逻辑又没有什么问题。所以,我尝试换一个最简单的Demo程序,先来测试一下整个集体是不是正常的。这个Demo程序没有任何业务逻辑,只返回247B的示例程序。
|
||||
|
||||
我简单说明一下,我之所以把这个测试过程放在这个支线问题中来描述,是想让我的行为更加有条理。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/65/d8c0cbd80d3b2de6bb1c7a0cca0ea165.png" alt="">
|
||||
|
||||
在这个测试过程中,我执行了两次。上图的前半部分走了Ingress,后面没有走Ingress,可是后面TPS并没有掉下来。这时,问题就基本清楚了。
|
||||
|
||||
我这里列个表格梳理一下到现在看到的信息,理理思路。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/91/5eca82b2dc67e069edbddfc946e8f991.jpg" alt="">
|
||||
|
||||
从以上数据可以判断出,TPS掉下来和Ingress有绝对的关系。那我们就来看看Ingress的日志:
|
||||
|
||||
```
|
||||
root@nginx-ingress-m9htx:/var/log/nginx# ls -lrt
|
||||
total 0
|
||||
lrwxrwxrwx 1 root root 12 Sep 10 2019 stream-access.log -> /proc/1/fd/1
|
||||
lrwxrwxrwx 1 root root 12 Sep 10 2019 error.log -> /proc/1/fd/2
|
||||
lrwxrwxrwx 1 root root 12 Sep 10 2019 access.log -> /proc/1/fd/1
|
||||
root@nginx-ingress-m9htx:/proc/1/fd# ls -lrt
|
||||
total 0
|
||||
lrwx------ 1 root root 64 Jan 7 18:00 7 -> 'socket:[211552647]'
|
||||
lrwx------ 1 root root 64 Jan 7 18:00 4 -> 'anon_inode:[eventpoll]'
|
||||
lrwx------ 1 root root 64 Jan 7 18:00 3 -> 'socket:[211552615]'
|
||||
l-wx------ 1 root root 64 Jan 7 18:00 2 -> 'pipe:[211548854]'
|
||||
l-wx------ 1 root root 64 Jan 7 18:00 1 -> 'pipe:[211548853]'
|
||||
lrwx------ 1 root root 64 Jan 7 18:00 0 -> /dev/null
|
||||
root@nginx-ingress-m9htx:/proc/1/fd# find ./ -inum 212815739
|
||||
root@nginx-ingress-m9htx:/proc/1/fd# find ./ -inum 212815740
|
||||
|
||||
```
|
||||
|
||||
悲怆的感觉!你看,日志直接重定向到标准输出和标准错误了,而标准输出和标准错误默认都是屏幕。那我们就到Kubernetes管理工具中去追踪日志。可是,结果是啥也没有。唉,这可怎么办呢?
|
||||
|
||||
从下面这张图我们也可以看到,当压力经过这个Ingress时,报错是必然的,压力越大,报错越多。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/3c/ea69119818e9e0e014c979a137efc23c.png" alt="">
|
||||
|
||||
可是分析到这里,我们再没有其他可以分析的日志了。没什么办法,只能查一下Ingress的版本了,结果发现,当前的Ingress已经有了新的版本。
|
||||
|
||||
为了避免去踩Ingress本身存在的一些坑,我把它的版本从1.5.5换到1.9.1之后,得到如下结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/61/e9/61dd625bd75c44e2d2dd1f56678202e9.png" alt="">
|
||||
|
||||
你看图中没有报错了,看来那些错误是Ingress版本导致的。
|
||||
|
||||
然而,即便如此,我们还是没有解决TPS会掉的问题。你可能会说,上面这张图里的TPS不是没有掉吗?其实,这只是假象。在上面的场景中,我们只是为了验证Ingress的问题,所以,执行时间并不长。
|
||||
|
||||
请你注意,我们到这里并没有解决前面所说的TPS会掉的问题。应该说,我们这里可能有两个问题,一个是Ingress,而另一个可能是在其他地方,但是我们还没有去验证。因此,我们要回到主线上,继续来分析它。
|
||||
|
||||
#### 回到主线
|
||||
|
||||
经过一翻折腾,你是不是感觉脑袋已经晕了?当我们被一些技术细节深深拖进去的时候,一定要保持清醒。
|
||||
|
||||
根据我的经验,这个时候我们可以在纸上画一下架构图。并不是说前面已经有架构图,我们就不用画了。画架构图是为了帮我们梳理思路。并且我们还要画得再细一点:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/54/a7/5404ce9653d310167bc0d0b4fabc91a7.jpg" alt="">
|
||||
|
||||
经过梳理,我采用分段测法来判断问题与哪一层相关:因为Cart服务需要通过外部调用走网关,那我在这里直接调用Cart服务,不走网关。并且我也跳过Ingress,直接用NodePort来提供服务,看看TPS有没有调下来。
|
||||
|
||||
首先,我直接从cart服务的NodePort压进去,得到这样的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/44/7e/445d5c2da479e860ca9813b9f4124a7e.png" alt="">
|
||||
|
||||
也就是说,Cart服务本身就会导致TPS降下来,看起来也并不规律。
|
||||
|
||||
那我们就修改Tomcat参数,把线程数、连接数调大,再来一次。你可能奇怪,为什么要这样调呢?这是因为在查看应用线程健康状态的过程中,我注意到Spring Boot里的Tomcat线程很忙。
|
||||
|
||||
在我这样反复验证了几次之后,发现TPS并没有掉下去。
|
||||
|
||||
为了进一步验证TPS和上面的线程数、连接数等参数有关,我又特意把配置改回去,再看是不是Tomcat参数的问题。
|
||||
|
||||
结果,TPS掉下去的情况没有复现!
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/1a/e5b0fee657d8d6d84f0bc57yy755421a.png" alt="">
|
||||
|
||||
气得我不得不吃份麻辣烫发泄一下。本来我已经看到了TPS掉下来和GC有关。并且,我们在GC中经过一顿分析发现,Servlet的hashmap$node在快速地创建和回收,说明YGC消耗资源多和压力大小有关,所以调了Tomcat相关的参数。可是,现在在同样的压力下,问题竟然不能复现,也真是醉了。
|
||||
|
||||
像这种随机的问题是比较难整的。不知道TPS稳定的假象是不是和中间有过重启有关。话说重启大法,在技术领域中真是绝对的大招。
|
||||
|
||||
既然这个问题没有复现,现场也没有了,我们也只能放弃。
|
||||
|
||||
虽然针对这个问题,我们从前到后的分析逻辑都非常合理,但是仍然没有找到问题点在哪里。如果它是一个随机的问题,那就是我们没有在合适的时机抓到问题的原因。
|
||||
|
||||
对于一个项目来说,如果出现的随机问题对业务造成的影响是不能接受的,那我们就必须花大精力去解决。如果影响不大,那也可以先放一放。但是每一个问题都有出现的必然性,也就是说,那些看似随机的问题,其实在技术中都有着绝对的必然性。
|
||||
|
||||
那这个问题到底是什么呢?在这里,我先留一个悬念,因为再继续分析下去,我们这节课就太长了,你看着也很累。下节课我们接着分析。
|
||||
|
||||
## 总结
|
||||
|
||||
在这节课中,我们讲了两个阶段的性能分析。
|
||||
|
||||
第一个阶段比较简单,就是一个查询的问题。对于查询来说,在实时交易的过程中,最好能够精准查找。如果出现范围查询,那就必须要有分页。
|
||||
|
||||
不过,如果是大范围的查询,那不仅会对网络造成压力,同时还会对应用、数据库等各层都产生非常明显的压力。所以,在出现范围查询时,我们必须做好技术选型。当业务必须做这样的范围查询时,你可以考虑换组件,像大数据这样的思路就可以用起来了。
|
||||
|
||||
第二个阶段有点麻烦,虽然我们花了很多时间精力,但是到最后没有找到根本原因。不过,我们分析的方向和思路都是没有问题的。
|
||||
|
||||
对于这种看似很随机的问题,在实际的项目中也经常出现。我们分析到最后可能会发现这是一个非常简单的问题,让人气得直跺脚。至于这个问题的根本原因是什么,我们下节课再做说明。
|
||||
|
||||
无论如何,在这节课中,我们仍然把分析的逻辑描述完整了,希望能给到你一些完整的思路。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,请你思考一下:
|
||||
|
||||
1. 在实时交易中,如何快速判断数据库的数据量所引发的性能问题?定向分析的证据链是什么?
|
||||
1. 如何从CPU使用高定位到GC效率引发的性能问题?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下这节课再见!
|
||||
437
极客时间专栏/高楼的性能工程实战课/基准场景/18 | 购物车信息确定订单:为什么动态参数化逻辑非常重要?.md
Normal file
437
极客时间专栏/高楼的性能工程实战课/基准场景/18 | 购物车信息确定订单:为什么动态参数化逻辑非常重要?.md
Normal file
@@ -0,0 +1,437 @@
|
||||
<audio id="audio" title="18 | 购物车信息确定订单:为什么动态参数化逻辑非常重要?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bc/6f/bc3yyb86d3c5d129e41257bbe28e7a6f.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
我们今天来看一下购物车信息确定订单这个接口的性能怎么样,有哪些需要优化的地方。
|
||||
|
||||
在这节课中,我将给你展示如何进行方法级的跟踪,来判断参数的问题。而这个参数,并不是我们这个接口直接用到的,它有不同的使用层次。
|
||||
|
||||
直接的参数化我们都能理解,对吧。但是当一个参数产生新的数据,而新的数据又会在后续的动作中用到时,你就得注意了,因为我们有可能在第一层数据中没有发现问题,但是在后续的动作中会遇到问题。所以,我们一定要关注参数化的变化,也就是动态的参数化的数据。
|
||||
|
||||
此外,在这节课中,我还将带你一起来看看在应用有多个节点的情况下,某个节点消耗资源过多导致的复杂问题该怎么处理。
|
||||
|
||||
话不多说,我们开始今天的分析吧!
|
||||
|
||||
## 场景运行数据
|
||||
|
||||
对于购物车信息确定订单这个接口,我们第一次运行的性能场景结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5f/70/5fbbac9cbfa5f14d55b60a5ec2e15970.png" alt="">
|
||||
|
||||
在图中,响应时间随着压力的增加而增加,而TPS只到了160多,还是有点低了,我们现在就得考虑把TPS提升。
|
||||
|
||||
注意,**这是一个典型的TPS不高,响应时间不断增加的性能问题。**
|
||||
|
||||
按照RESAR性能分析逻辑,我们看一下这个接口的架构图。
|
||||
|
||||
## 看架构图
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7a/6e/7ae3703b51010e62aeb9b0928b0d096e.png" alt="">
|
||||
|
||||
可以看到,这个接口涉及到的服务比较多,架构图也比之前其他接口的要复杂一些。
|
||||
|
||||
紧接着,我们就来拆分响应时间。
|
||||
|
||||
## 拆分响应时间
|
||||
|
||||
- Gateway:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/70/90/7043a3e4fbf19c3c4292d15f1d30d390.png" alt="">
|
||||
|
||||
- Order:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/95/293933826488236680ae2cc01a634995.png" alt="">
|
||||
|
||||
- Member:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4c/0b/4caf56a16fba5334f5ca89d264cc750b.png" alt="">
|
||||
|
||||
- Cart:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/0a/0dd52c35102e5c2ffa6yy8820d6ffa0a.png" alt="">
|
||||
|
||||
- Portal:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/yy/5cddd9c4a31a7d832057e31b2d0075yy.png" alt="">
|
||||
|
||||
从上面的时间拆分来看,Cart消耗了最长的时间。所以,我们先分析Cart。
|
||||
|
||||
我们再顺手点一下Cart和MySQL之间的时间消耗,看看是什么情况:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/43/b0/433ea4910e4d00yy144b47c0d0c3eab0.png" alt="">
|
||||
|
||||
这个Cart和MySQL之间的时间看起来不长,那我们就不用考虑数据库的SQL时间消耗了。
|
||||
|
||||
接下来,我们就来分析响应时间长的Cart服务。
|
||||
|
||||
## 第一阶段
|
||||
|
||||
### 全局分析
|
||||
|
||||
按照惯例,我们来看一下worker层面的资源消耗情况:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/aa/1d64ede0a356a10767ce714e0b16a0aa.png" alt="">
|
||||
|
||||
从上图来看,worker-3上消耗的资源较多。那我们就来查看一下worker-3上有什么服务。
|
||||
|
||||
```
|
||||
[root@k8s-master-2 ~]# kubectl get pods -o wide | grep k8s-worker-3
|
||||
cloud-nacos-registry-685b8957d7-vskb6 1/1 Running 0 2d11h 10.100.69.199 k8s-worker-3 <none> <none>
|
||||
cloud-redis-7f7db7f45c-t5g46 2/2 Running 0 2d8h 10.100.69.196 k8s-worker-3 <none> <none>
|
||||
elasticsearch-master-2 1/1 Running 0 3h28m 10.100.69.209 k8s-worker-3 <none> <none>
|
||||
svc-mall-cart-558d787dc7-g6qgh 1/1 Running 0 2d11h 10.100.69.201 k8s-worker-3 <none> <none>
|
||||
svc-mall-order-fbfd8b57c-kbczh 1/1 Running 0 2d11h 10.100.69.202 k8s-worker-3 <none> <none>
|
||||
svc-mall-portal-846d9994f8-m7jbq 1/1 Running 0 38h 10.100.69.207 k8s-worker-3 <none> <none>
|
||||
svc-mall-search-c9c8bc847-h7sgv 1/1 Running 0 161m 10.100.69.210 k8s-worker-3 <none> <none>
|
||||
[root@k8s-master-2 ~]#
|
||||
|
||||
```
|
||||
|
||||
可以看到,worker-3上有8个服务,哪个服务消耗的资源最多呢?现在我们进入worker-3,查看下top:
|
||||
|
||||
```
|
||||
[root@k8s-worker-3 ~]# top
|
||||
top - 01:51:35 up 2 days, 12:18, 2 users, load average: 19.48, 18.40, 17.07
|
||||
Tasks: 319 total, 1 running, 318 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 68.6 us, 6.4 sy, 0.0 ni, 19.9 id, 0.0 wa, 0.0 hi, 5.1 si, 0.0 st
|
||||
%Cpu1 : 66.7 us, 5.8 sy, 0.0 ni, 22.8 id, 0.0 wa, 0.0 hi, 4.8 si, 0.0 st
|
||||
%Cpu2 : 66.4 us, 6.1 sy, 0.0 ni, 22.7 id, 0.0 wa, 0.0 hi, 4.7 si, 0.0 st
|
||||
%Cpu3 : 65.7 us, 5.4 sy, 0.0 ni, 23.6 id, 0.0 wa, 0.0 hi, 5.4 si, 0.0 st
|
||||
%Cpu4 : 66.6 us, 5.7 sy, 0.0 ni, 22.0 id, 0.0 wa, 0.0 hi, 5.7 si, 0.0 st
|
||||
%Cpu5 : 67.6 us, 5.8 sy, 0.0 ni, 22.5 id, 0.0 wa, 0.0 hi, 4.1 si, 0.0 st
|
||||
KiB Mem : 16265992 total, 2525940 free, 7015104 used, 6724948 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 8848464 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
32216 root 20 0 8878548 658820 16980 S 280.5 4.1 375:31.82 java -Dapp.id=svc-mall-cart -javaagent:/opt/skywalking/agent/skywalking-agen+
|
||||
32589 root 20 0 8839408 589196 15892 S 84.1 3.6 171:16.88 java -Dapp.id=svc-mall-order -javaagent:/opt/skywalking/agent/skywalking-age+
|
||||
24119 root 20 0 8798548 549804 15892 S 65.9 3.4 115:52.74 java -Dapp.id=svc-mall-portal -javaagent:/opt/skywalking/agent/skywalking-ag+
|
||||
1089 root 20 0 2438956 105708 37776 S 6.3 0.6 248:21.71 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.co+
|
||||
5470 root 20 0 1154816 14992 1816 S 3.6 0.1 20:15.93 redis-server 0.0.0.0:6379
|
||||
|
||||
```
|
||||
|
||||
从以上数据来看,的确是Cart服务消耗的CPU比较高。不过,它**还没有把6个CPU都用完**,这一点我们要记一下。
|
||||
|
||||
下面开始定向分析。
|
||||
|
||||
### 定向分析
|
||||
|
||||
既然Cart服务消耗的CPU多,那我们当然要看一下Cart中的线程都在干啥。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/65/ab/654fae31cc93e59a4ef32b8146bf34ab.png" alt="">
|
||||
|
||||
这些线程状态基本都在绿色的Runnable状态,看起来都比较繁忙,有可能是因为线程数配置的太低了,我们查看下配置:
|
||||
|
||||
```
|
||||
server:
|
||||
port: 8086
|
||||
tomcat:
|
||||
accept-count: 1000
|
||||
threads:
|
||||
max: 20
|
||||
min-spare: 5
|
||||
max-connections: 500
|
||||
|
||||
```
|
||||
|
||||
知道了Spring Boot内置的Tomcat线程数配置,我们拆分一下在Cart上正在执行的方法,看看我们的定位方法是不是合理:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/74/5d/7470ab205c982e07bd36a38d1bca405d.png" alt="">
|
||||
|
||||
看这张图的时候,你要注意消耗时间长的位置,也就是图中右侧线段比较长的地方。这里面有两个环节的问题:
|
||||
|
||||
1. MySQL的执行时间长。你要注意哦,虽然这里的MySQL/JDBI/PreparedStatement/execute并没有消耗很长的时间,但是它的下一步Balance/Promotion/Cart/CartPromotion消耗的时间是长的;
|
||||
1. Promotionnew方法本身的时间长。
|
||||
|
||||
由于慢的节点和MySQL有关,我们创建一个,mysqlreport来看MySQL整体的监控数据:
|
||||
|
||||
```
|
||||
__ Connections _________________________________________________________
|
||||
Max used 152 of 151 %Max: 100.66
|
||||
Total 540 0.0/s
|
||||
|
||||
```
|
||||
|
||||
原来是连接用完了!我们赶紧改一下,从151改到500。
|
||||
|
||||
不过,重测之后响应时间还是没有变化,那我们就只能接着跟踪Cart上的方法了。
|
||||
|
||||
#### 方法级跟踪
|
||||
|
||||
于是,我们不得不来到方法级跟踪, 看一下我们关注的方法Promotionnew慢在哪里。
|
||||
|
||||
由上面那张调用视图,我们可以编写下面这样的跟踪语句:
|
||||
|
||||
trace -E com.dunshan.mall.cart.controller.CartItemController listPromotionnew -n 5 -v --skipJDKMethod false '1==1'
|
||||
|
||||
然后得到了下面这个结果:
|
||||
|
||||
```
|
||||
`---ts=2021-01-16 15:08:58;thread_name=http-nio-8086-exec-34;id=f8;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@56887c8f
|
||||
`---[97.827186ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl$$EnhancerBySpringCGLIB$$ac8f5a97:listPromotion()
|
||||
`---[97.750962ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #57
|
||||
`---[97.557484ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl:listPromotion()
|
||||
+---[72.273747ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl:list() #166
|
||||
+---[0.003516ms] cn.hutool.core.collection.CollUtil:isNotEmpty() #172
|
||||
+---[0.004207ms] java.util.List:stream() #173
|
||||
+---[0.003893ms] java.util.stream.Stream:filter() #57
|
||||
+---[0.003018ms] java.util.stream.Collectors:toList() #57
|
||||
+---[0.060052ms] java.util.stream.Stream:collect() #57
|
||||
+---[0.002017ms] java.util.ArrayList:<init>() #177
|
||||
+---[0.003013ms] org.springframework.util.CollectionUtils:isEmpty() #179
|
||||
`---[25.152532ms] com.dunshan.mall.cart.feign.CartPromotionService:calcCartPromotion() #181
|
||||
|
||||
```
|
||||
|
||||
可以看到,在我们跟踪的方法com.dunshan.mall.cart.service.imp.CartItemServiceImpl:listPromotion()中,有两处listPromotion和calcCartPromotion时间消耗较大,分别是:
|
||||
|
||||
1. com.dunshan.mall.cart.service.imp.CartItemServiceImpl:list()
|
||||
1. com.dunshan.mall.cart.feign.CartPromotionService:calcCartPromotion()
|
||||
|
||||
#### 跟踪List函数
|
||||
|
||||
我们在Arthas中执行trace跟踪语句如下:
|
||||
|
||||
```
|
||||
trace com.dunshan.mall.cart.service.imp.CartItemServiceImpl list -v -n 5 --skipJDKMethod false '1==1'
|
||||
|
||||
```
|
||||
|
||||
然后得到这样的结果:
|
||||
|
||||
```
|
||||
`---ts=2021-01-16 15:19:45;thread_name=http-nio-8086-exec-65;id=23ce;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@56887c8f
|
||||
`---[70.158793ms] com.dunshan.mall.cart.service.imp.CartItemServiceImpl:list()
|
||||
+---[0.003501ms] com.dunshan.mall.model.OmsCartItemExample:<init>() #150
|
||||
+---[0.002642ms] com.dunshan.mall.model.OmsCartItemExample:createCriteria() #151
|
||||
+---[0.002932ms] com.dunshan.mall.model.OmsCartItemExample$Criteria:andDeleteStatusEqualTo() #57
|
||||
+---[0.00304ms] com.dunshan.mall.model.OmsCartItemExample$Criteria:andMemberIdEqualTo() #57
|
||||
`---[70.078976ms] com.dunshan.mall.mapper.OmsCartItemMapper:selectByExample() #152
|
||||
|
||||
```
|
||||
|
||||
在一阵无聊的trace之后,看到一个select语句消耗时间较长,这个select语句是:
|
||||
|
||||
```
|
||||
select id, product_id, product_sku_id, member_id, quantity, price, product_pic, product_name, product_sub_title, product_sku_code, member_nickname, create_date, modify_date, delete_status, product_category_id, product_brand, product_sn, product_attr from oms_cart_item WHERE ( delete_status = ? and member_id = ? )
|
||||
|
||||
```
|
||||
|
||||
一个简单的select语句,怎么会耗时这么久呢?我们先不管为什么会这样,先来看看这个oms_cart_item的数据有多少。我连上数据库后一查,发现在oms_cart_item里面有10612条数据,这个数据量并不大。
|
||||
|
||||
此外,我还查看了一下索引,也是有的,并且执行计划也走到了索引这里。那为什么会慢呢?到这里,我们得考虑一下是不是和数据量有关了。所以,我们来看看这个select语句究竟查出了多少条数据。
|
||||
|
||||
在我补全SQL后一查,发现一个member_id对应500多条记录,这是一个人一次买了500个东西?
|
||||
|
||||
既然在购物车信息里,同一个人有这么多记录,那一定是在商品加入购物车时加进去的。而要往一个人的购物车里加东西,显然是在性能脚本执行的过程中添加,因为每个用户的购物车一开始都是空的。所以,我们要去查一下商品加入购物车的脚本是怎么回事。
|
||||
|
||||
商品加入购物车的脚本很简单,就是一个post,加上商品ID,在HTTP协议的请求头里面有一个Token来标识是哪个用户。
|
||||
|
||||
在这里,我们要查的就是token有没有用重,JMeter中的Token参数化配置如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/4d/e9697bc38ed69b485656591c013b7e4d.png" alt="">
|
||||
|
||||
看起来挺好,我们在这里设计了不重用数据,所以在设置上Token不会被重用。那么,只有一种可能,就是Token重了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/14/d9/14d832cb3a32b51cb3a979c4043a66d9.png" alt=""><img src="https://static001.geekbang.org/resource/image/d2/0a/d213650cffee80d88b495cfc0bfe010a.png" alt="">
|
||||
|
||||
在随机检查了几条Token之后,我发现有大量的Token重复。这也就解释了,为什么我们会在一个人的购物车里看到那么多商品数据。
|
||||
|
||||
可是,这个逻辑就有问题了。你想想,我们设置了参数化中数据不重复使用,但实际上确实有大量的Token被重用,这就说明Token的参数化文件本身就重复了。
|
||||
|
||||
那怎么办呢?我们只有把所有的Token全部清掉,让Token数据在商品加入购物车的时候不会被重用,以此来避免在一个人的购物车中加入太多商品的情况。
|
||||
|
||||
接着怎么办?只有一招了,就是把所有的数据都清掉,然后用所有的用户创建合理的购物车数据。于是,我在这里又花了几个小时,造出了130多万数据。现在,我们再回归一下场景:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/bd/d995e490eba886f2cd1b1eb96e27d4bd.png" alt="">
|
||||
|
||||
你看,TPS增加到了300!
|
||||
|
||||
本来这是一个美好到可以喝下午茶的结果,然而……天不随人愿,我在场景持续执行的过程中,又发现了问题,这让我们不得不开启第二阶段的分析。
|
||||
|
||||
## 第二阶段
|
||||
|
||||
### 场景运行数据
|
||||
|
||||
是什么问题呢?我在压力运行的数据中,竟然看到了这种TPS曲线:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/86/1c/866308ac100a2f7fe4aff4cb545baa1c.png" alt="">
|
||||
|
||||
你看,TPS相当规律地往下掉,不仅会掉下去,而且还会恢复回去,形成一个明显的锯齿状,并且这锯齿还挺大。
|
||||
|
||||
怎么办?根据高老师的思路,现在我们就得按照RESAR性能分析逻辑来收拾这个问题了。我们在前面已经看过架构图,所以,现在直接来拆分响应时间。
|
||||
|
||||
### 拆分响应时间
|
||||
|
||||
- Gateway:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cf/ee/cfd701a0577144f42b5be4834ebce7ee.png" alt="">
|
||||
|
||||
- Order:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/32/c1/32c9cb7568648c06e7eb969bbd0d3bc1.png" alt="">
|
||||
|
||||
- Cart:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/18/0d2e5444036c7f48e8006469b5788f18.png" alt="">
|
||||
|
||||
- Portal:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/27/50baae9e1f86a718c21e470d9da8ea27.png" alt="">
|
||||
|
||||
从上面的数据来看,似乎每一层都有时间消耗,性能都不怎么样。
|
||||
|
||||
### 全局分析
|
||||
|
||||
那我们就查看下当前的全局监控数据,可以看到worker-3上的CPU消耗最多:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/15/381218081130641a582b92e7849ea515.png" alt="">
|
||||
|
||||
因此,我们来查一下worker-3上有哪些Pod:
|
||||
|
||||
```
|
||||
[root@k8s-master-3 ~]# kubectl get pods -o wide | grep k8s-worker-3
|
||||
cloud-nacos-registry-685b8957d7-vskb6 1/1 Running 0 3d7h 10.100.69.199 k8s-worker-3 <none> <none>
|
||||
cloud-redis-7f7db7f45c-t5g46 2/2 Running 1 3d4h 10.100.69.196 k8s-worker-3 <none> <none>
|
||||
elasticsearch-master-2 1/1 Running 0 23h 10.100.69.209 k8s-worker-3 <none> <none>
|
||||
svc-mall-cart-79c667bf56-j76h6 1/1 Running 0 20h 10.100.69.213 k8s-worker-3 <none> <none>
|
||||
svc-mall-order-fbfd8b57c-kbczh 1/1 Running 0 3d7h 10.100.69.202 k8s-worker-3 <none> <none>
|
||||
svc-mall-portal-846d9994f8-m7jbq 1/1 Running 0 2d10h 10.100.69.207 k8s-worker-3 <none> <none>
|
||||
svc-mall-search-c9c8bc847-h7sgv 1/1 Running 0 23h 10.100.69.210 k8s-worker-3 <none> <none>
|
||||
[root@k8s-master-3 ~]#
|
||||
|
||||
```
|
||||
|
||||
居然有这么多服务都在worker-3上。
|
||||
|
||||
我们现在登录到worker-3上,看一下top资源。其实,我在这里主要想看的是process table,因为我想先确定一下哪个服务消耗的资源最高,然后再决定收拾哪个服务。
|
||||
|
||||
```
|
||||
[root@k8s-worker-3 ~]# top
|
||||
top - 22:13:01 up 3 days, 8:39, 3 users, load average: 40.34, 30.03, 18.02
|
||||
Tasks: 326 total, 6 running, 320 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 74.5 us, 13.4 sy, 0.0 ni, 7.7 id, 0.0 wa, 0.0 hi, 4.4 si, 0.0 st
|
||||
%Cpu1 : 66.3 us, 12.1 sy, 0.0 ni, 16.5 id, 0.0 wa, 0.0 hi, 4.7 si, 0.3 st
|
||||
%Cpu2 : 49.7 us, 32.4 sy, 0.0 ni, 14.9 id, 0.0 wa, 0.0 hi, 2.7 si, 0.3 st
|
||||
%Cpu3 : 73.2 us, 9.7 sy, 0.0 ni, 12.4 id, 0.0 wa, 0.0 hi, 4.7 si, 0.0 st
|
||||
%Cpu4 : 76.4 us, 10.5 sy, 0.0 ni, 8.8 id, 0.0 wa, 0.0 hi, 4.1 si, 0.3 st
|
||||
%Cpu5 : 62.4 us, 16.4 sy, 0.0 ni, 16.1 id, 0.0 wa, 0.0 hi, 4.7 si, 0.3 st
|
||||
KiB Mem : 16265992 total, 211212 free, 9204800 used, 6849980 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 6650068 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
32485 root 20 0 8895760 700564 16896 S 101.6 4.3 723:03.52 java -Dapp.id=svc-mall-cart -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.service_name=svc-ma+
|
||||
32589 root 20 0 8845576 778684 15896 S 93.6 4.8 427:04.44 java -Dapp.id=svc-mall-order -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.service_name=svc-m+
|
||||
24119 root 20 0 8825208 600956 15888 S 67.9 3.7 262:00.83 java -Dapp.id=svc-mall-portal -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.service_name=svc-+
|
||||
............
|
||||
|
||||
```
|
||||
|
||||
在上述top资源中,我们主要来看几个吃CPU的大户。不难发现,Cart/Order/Portal这三个服务在购物车信息确定订单的业务中都有用到,并且都在同一个worker上。同时,我们也可以看到,在这个6C的worker中,现在的CPU队列已经达到40了。
|
||||
|
||||
### 定向分析
|
||||
|
||||
从系统上来看,CPU队列长的问题主要是由于资源的争用,因为线程在不断地唤醒,通过start_thread就能看出来:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/15/1a/15cae37yy8db5883c6cf2fd4b944ed1a.png" alt="">
|
||||
|
||||
现在我们要做的就是把线程降下去。
|
||||
|
||||
怎么降呢?有两种手段:
|
||||
|
||||
1. 把服务移走,先一个个收拾,分而治之。
|
||||
1. 把服务的线程减少。
|
||||
|
||||
这两种方式都是能减少资源争用,但是会带来不同的影响。其中,第一种手段比较合理,只是会消耗更多的整体资源;第二种手段虽然会减少争用,但是会导致响应时间增加。
|
||||
|
||||
我这么跟你一讲,你可能已经发现了,这两种手段都不能解释TPS不稳定的问题。那为什么TPS一会儿掉下去,一会儿又恢复呢?现在我们还不知道答案,不过基于我们“全局-定向”的分析思路,我们先看一下worker-3的资源消耗:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/94/8a/94635d74fa68485c9b5f240efe25448a.png" alt="">
|
||||
|
||||
在同一时间段,我也查看了同一台物理机上的其他worker的资源消耗情况,发现worker-8的资源消耗有点不太正常。请你注意,我此时的查看逻辑,仍然依据的是[第3](https://time.geekbang.org/column/article/355982)[讲](https://time.geekbang.org/column/article/355982)中描述的逻辑,以及对应[第4](https://time.geekbang.org/column/article/356789)[讲](https://time.geekbang.org/column/article/356789)中的性能分析决策树。希望你不要觉得这里有跳跃,实际上我们还是在全局监控的第一层计数器上。
|
||||
|
||||
我们具体来看一下worker-8的资源消耗:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5f/7d/5ffbabcbaab9c75bbd9702e8c6bc397d.png" alt="">
|
||||
|
||||
再来看一下压力场景的执行数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/53/22/53yy0beb312b256b881aa7f2b13d5422.png" alt="">
|
||||
|
||||
从上面worker-8的资源使用率来看,确实有很高的时候。考虑到同一物理机上资源的争用问题,我们现在把cart移到Worker-7上去,把order移到worker-9上去,再来看TPS:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/c7/da51bcc4719030dfca91d6ayyd4bafc7.png" alt="">
|
||||
|
||||
花花绿绿,起起伏伏,真是好看……我们先不管这样做有没有增加TPS,光看这个趋势,就足以让人心碎了。既然结果还是这样,那我们就用老套路,继续拆分时间来看看。
|
||||
|
||||
- Gateway:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/15/38/15e776926d852b45437d162cbb3e7d38.png" alt="">
|
||||
|
||||
- Order:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/35/8c9dbf53ba48148205dda9056c576a35.png" alt="">
|
||||
|
||||
- Cart:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/03/d9/03e79a09e74234a155cba30b83b9e4d9.png" alt="">
|
||||
|
||||
- Portal:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3f/77/3f887ef22afd45f5aaa1dcfd8c9b0b77.png" alt="">
|
||||
|
||||
- Member:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d7/e2/d7dbe6944ea0efd7db8eee2999c917e2.png" alt="">
|
||||
|
||||
从上面的时间来看,Gateway消耗的时间比较长,这就奇怪了,这都换了到了Gateway服务上有问题了。所以,我们到Gateway机器上看一下到底有哪些服务:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/28/18653de1d1182584c7792bd0e46ecd28.png" alt="">
|
||||
|
||||
呀呀呀,那个占第一位的是个啥?原来是ES的一个进程,这个进程消耗了多个CPU。看到这,我想起来前几天为了增加ES的能力,我们给ES data和ES client增加过CPU。当时考虑到它们都是吃CPU的大户,只能用一个CPU实在太委屈它们了,所以增加了CPU数量,想让它们的性能好一些。
|
||||
|
||||
可是没想到,ES data和ES client对应用的影响有这么大。
|
||||
|
||||
我当时改ES的CPU,是因为我们架构中的一个搜索服务用到了它,而当时的CPU给的是一个C,这导致Search服务的TPS很低。关于这一点,我在[第](https://time.geekbang.org/column/article/366020)[15](https://time.geekbang.org/column/article/366020)[讲](https://time.geekbang.org/column/article/366020)中有描述,你如果不清楚,可以再看看。
|
||||
|
||||
同时,我们还要注意一点,ES data和ES client都不是单一的节点,而是有几个节点。由此产生的问题就是,任意一个ES节点出现资源消耗过多的时候,都会影响到它所在的worker机器资源,进而影响到这个ES节点所在的整个物理机。
|
||||
|
||||
既然ES的进程消耗资源占居首位,那我们该怎么办呢?为了验证问题,我先把ES给停掉,看看TPS能不能上来,如果能上来,我们再考虑怎么限制ES的资源。
|
||||
|
||||
停了ES之后,TPS如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/43/c6/4399eb3a375812e4d08a26680e8978c6.png" alt="">
|
||||
|
||||
看到没有?TPS增加了一倍,并且也没有掉下来!非常理想!
|
||||
|
||||
所以,接下来,我们就要考虑把ES限制到固定的worker上,让它不影响现在的应用。
|
||||
|
||||
## 总结
|
||||
|
||||
在这节课中,我们有两个阶段的分析。
|
||||
|
||||
在第一个阶段中,我们定位了数据问题。对于性能来说,**数据是非常重要的基础资源,而数据的合理性直接影响了测试的结果。**
|
||||
|
||||
经常有初入性能行业的人讨论:性能脚本中的数据到底应该用多少?我觉得这是一个非常明确的问题,**在所有的性能场景中,使用的资源都应该按真实发生的业务逻辑来确定,有且只有这样,才能保证结果是有效的**。
|
||||
|
||||
在第二阶段中,我们定位了一个有意思的问题,而这个问题的复杂性在于整体的架构。因为我们是用KVM、Kubernetes和Docker作为基础设施的,而我们选择的应用原本也不是真正的微服务,是对一个开源系统架构做了更改,把它改成了真正的微服务。
|
||||
|
||||
在这样的环境中,如果一个应用有问题,那在修改重启的时候,应用会被Kubernetes调度到哪个节点上,是不确定的。也正是出于这样的原因,我们一会儿看到这里有问题,一会儿看到那里有问题,定位的逻辑全都对,但是就是层面不对。这也是[上节课](https://time.geekbang.org/column/article/368125)中随机问题出现的原因。
|
||||
|
||||
所以,根据我们在[第4讲](https://time.geekbang.org/column/article/356789)中提到的性能分析决策树,我们仍然需要有全局监控、定向监控的思路,并且还要找到计数器的相关性。这样一来,当看到相关计数器有问题的时候,我们就能知道它们之间的关联关系了。
|
||||
|
||||
希望你在这节课中,能把性能分析的逻辑记在心中。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,请你思考一下:
|
||||
|
||||
1. 性能脚本中的参数应该如何设计?
|
||||
1. 如何定位TPS会掉下来的情况?大概描述一下你的思路。
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下这节课再见!
|
||||
424
极客时间专栏/高楼的性能工程实战课/基准场景/19 | 生成订单信息之一:应用JDBC池优化和内存溢出分析.md
Normal file
424
极客时间专栏/高楼的性能工程实战课/基准场景/19 | 生成订单信息之一:应用JDBC池优化和内存溢出分析.md
Normal file
@@ -0,0 +1,424 @@
|
||||
<audio id="audio" title="19 | 生成订单信息之一:应用JDBC池优化和内存溢出分析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a3/95/a3ac9f9d18126ba413a7230a1bc1c295.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
在这节课中,我们来看一下生成订单接口的基准场景是什么结果。
|
||||
|
||||
你将看到一些重复的问题,比如SQL的问题定位,虽然具体的问题不同,但我们的分析逻辑没有区别,我会简单带过。同时,你也会看到一些新的问题,比如JDBC池增加之后,由于数据量过大导致JVM内存被消耗光;批量业务和实时业务共存导致的锁问题等。这节课,我们重点来看看这样的问题如何进一步优化。
|
||||
|
||||
话不多说,开整!
|
||||
|
||||
## 场景运行数据
|
||||
|
||||
对于生成订单接口,我们第一次试执行性能场景的结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/6a/dcbed4b3bc53be11f7d18280b8f1d16a.png" alt="">
|
||||
|
||||
从场景执行的结果来看。40个压力线程只跑出来50多的TPS,响应时间也蹭蹭蹭地跑了近700ms。这显然是一个非常慢的接口了。
|
||||
|
||||
从这样的接口来看,我们选择这个项目算是选择对了,因为到处都是性能问题。
|
||||
|
||||
下面我们就来一步步分析一下。
|
||||
|
||||
## 架构图
|
||||
|
||||
前面我们做过多次描述,画架构图是为了知道分析的路径。所以按照惯例,我们仍然把架构图列在这里。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/89/5f/8973byyed69dec259cb6f7d4bf3f4b5f.png" alt="">
|
||||
|
||||
由于这个接口比较复杂,架构图看起来有点乱,我又整了一个简化版本:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c9/aa/c95c8daab26ca52fb2df688986a1c0aa.png" alt="">
|
||||
|
||||
Order服务是这个接口的核心,因此,你可以看到我把Order相关的服务都筛选了出来,这样我们就能很清楚地知道它连接了哪些东西。
|
||||
|
||||
下面我们来拆分响应时间。
|
||||
|
||||
## 拆分响应时间
|
||||
|
||||
因为在场景运行的时候,我们看到响应时间比较长,所以我们用APM工具来拆分一下:
|
||||
|
||||
- Gateway :
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b7/5c/b7f6b87b608cc0fc9b8f6e05baf90c5c.png" alt="">
|
||||
|
||||
从上图我们就可以看到Gateway上的时间在700ms左右,这与前面的场景数据是可以对上的。
|
||||
|
||||
我说明一下,这张小图的采样间隔是分钟,因此,你可能会发现这个曲线和压力工具给出的TPS曲线,在一些细节上对应不起来。不过这没关系,我们更应该关注整体的趋势。
|
||||
|
||||
- Order:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c9/01/c9aaa22298a00d7b68a6d89f7cef7501.png" alt="">
|
||||
|
||||
我们前面提到,Order是生产订单信息这个接口的重点,并且它的业务逻辑也非常复杂,因此,我们要多关注这个服务。
|
||||
|
||||
从数据上来看,Order的是时间消耗在350毫秒左右,占到整个响应时间的一半。这是我们着重要分析的,而Gateway的转发能力也是要考虑的问题点,只是Gateway上没有逻辑,只做转发,如果是因为数据量大而导致的Gateway转发慢,那我们解决了Order的问题之后,Gateway的问题也就会被解决。所以,我们先分析Order的问题。
|
||||
|
||||
所以,我们现在就来分析一下。
|
||||
|
||||
## 第一阶段
|
||||
|
||||
### 全局监控分析
|
||||
|
||||
我们先看全局监控:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/e8/584fc960c184d9c4bba12fae9b6c58e8.png" alt="">
|
||||
|
||||
一眼扫过去,啥也没有。既没有明显的CPU资源消耗,也没有明显的网络资源、IO资源瓶颈。
|
||||
|
||||
**遇到这种情况,我们一定要留意整个链路上有限制的点**。什么是有限制的点?比如说,各种池(连接池、等)、栈中的锁、数据库连接、还有数据库的锁之类。其实,总结下来就是一个关键词:**阻塞**。
|
||||
|
||||
我们只要分析出阻塞的点,就能把链路扩宽,进而把资源都用起来。
|
||||
|
||||
当然,也有可能在你分析了一圈之后,发现没有任何有阻塞的点,可是资源就是用不上去。这种情况只有一种可能,那就是你分析得还不够细致。因为可能存在阻塞的地方实在太多了,我们只能一步步拆解。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
正所谓“心中常备决策树,让你分析不迷路”。到了定向监控分析这里,按照[第4讲](https://time.geekbang.org/column/article/356789)中强调的性能分析决策树,我们先来分析Order服务:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3e/84/3e2cd8928022bccf83fe69a71f004784.jpg" alt="">
|
||||
|
||||
在我分析Order的线程栈信息时,发现在Order的栈中,有大量这样的内容:
|
||||
|
||||
```
|
||||
"http-nio-8086-exec-421" Id=138560 WAITING on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@a268a48
|
||||
at sun.misc.Unsafe.park(Native Method)
|
||||
- waiting on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@a268a48
|
||||
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
|
||||
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
|
||||
at com.alibaba.druid.pool.DruidDataSource.takeLast(DruidDataSource.java:1899)
|
||||
at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1460)
|
||||
at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1255)
|
||||
at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:5007)
|
||||
at com.alibaba.druid.filter.stat.StatFilter.dataSource_getConnection(StatFilter.java:680)
|
||||
at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:5003)
|
||||
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1233)
|
||||
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1225)
|
||||
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:90)
|
||||
..........................
|
||||
at com.dunshan.mall.order.service.impl.PortalOrderServiceImpl$$EnhancerBySpringCGLIB$$f64f6aa2.generateOrder(<generated>)
|
||||
at com.dunshan.mall.order.controller.PortalOrderController.generateOrder$original$hak2sOst(PortalOrderController.java:48)
|
||||
at com.dunshan.mall.order.controller.PortalOrderController.generateOrder$original$hak2sOst$accessor$NTnIbuo7(PortalOrderController.java)
|
||||
at com.dunshan.mall.order.controller.PortalOrderController$auxiliary$MTWkGopH.call(Unknown Source)
|
||||
..........................
|
||||
at com.dunshan.mall.order.controller.PortalOrderController.generateOrder(PortalOrderController.java)
|
||||
..........................
|
||||
|
||||
```
|
||||
|
||||
你看,栈信息中有很多getConnection,这明显是Order服务在等数据库连接池。所以,我们要做的就是把JDBC池加大:
|
||||
|
||||
```
|
||||
原配置:
|
||||
initial-size: 5 #连接池初始化大小
|
||||
min-idle: 10 #最小空闲连接数
|
||||
max-active: 20 #最大连接数
|
||||
|
||||
|
||||
修改为:
|
||||
initial-size: 20 #连接池初始化大小
|
||||
min-idle: 10 #最小空闲连接数
|
||||
max-active: 40 #最大连接数
|
||||
|
||||
```
|
||||
|
||||
你可以看到,我在这里并没有把JDBC池一次性修改得太大,主要是因为我不想为了维护连接池而产生过多的CPU消耗。我也建议你在增加资源池的时候,先一点点增加,看看有没有效果,等有了效果后再接着增加。
|
||||
|
||||
修改JDBC池后,我们再来看一下压力场景的执行数据:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a8/bb/a8e672d9f5f829509a68216ed0146ebb.png" alt="">
|
||||
|
||||
从数据上看,TPS有上升的趋势,并且一度达到了150以上。可是紧接着,TPS就掉下来了,这个时候的响应时间倒是没有明显增加。而且你看,TPS不仅掉下来了,而且还断断续续的,极为不稳定。
|
||||
|
||||
此外,我们还可以发现,在后续的压力中不仅有错误信息产生,响应时间也在上升。与此同时,我查看了全局监控的资源,并没有发现太大的资源消耗。既然有错误产生,没二话,我们只能整它!
|
||||
|
||||
## 第二阶段
|
||||
|
||||
### 全局监控分析
|
||||
|
||||
因为我们在前面修改了Order的JDBC池,所以在出现新的问题之后,我们先来看一下Order服务的健康状态。在查看Order服务的top时,看到如下信息:
|
||||
|
||||
```
|
||||
top - 01:28:17 up 19 days, 11:54, 3 users, load average: 1.14, 1.73, 2.27
|
||||
Tasks: 316 total, 1 running, 315 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu1 : 3.0 us, 2.7 sy, 0.0 ni, 93.6 id, 0.0 wa, 0.0 hi, 0.3 si, 0.3 st
|
||||
%Cpu2 : 3.4 us, 3.4 sy, 0.0 ni, 93.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu3 : 3.7 us, 2.8 sy, 0.0 ni, 93.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu4 : 3.6 us, 2.1 sy, 0.0 ni, 93.6 id, 0.0 wa, 0.0 hi, 0.3 si, 0.3 st
|
||||
%Cpu5 : 2.8 us, 1.8 sy, 0.0 ni, 95.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
KiB Mem : 16265992 total, 2229060 free, 9794944 used, 4241988 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 6052732 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
29349 root 20 0 8836040 4.3g 16828 S 99.7 27.4 20:51.90 java
|
||||
1089 root 20 0 2574864 98144 23788 S 6.6 0.6 2066:38 kubelet
|
||||
|
||||
```
|
||||
|
||||
悲催的数据还是来了,你看,有一个us cpu达到了100%!这是啥情况?
|
||||
|
||||
我通过 top -Hp和jstack -l 1 两个命令查看进程后发现,原来是VM Thread线程占用了CPU,这个线程是做垃圾回收(GC)的。 既然如此,那我们就来看一下内存的回收状态,查看jstat如下:
|
||||
|
||||
```
|
||||
[root@svc-mall-order-7fbdd7b85f-ks828 /]# jstat -gcutil 1 1s
|
||||
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 93 652.664 681.486
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 93 652.664 681.486
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 93 652.664 681.486
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 93 652.664 681.486
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 93 652.664 681.486
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 93 652.664 681.486
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 93 652.664 681.486
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 94 659.863 688.685
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 94 659.863 688.685
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 94 659.863 688.685
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 94 659.863 688.685
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 94 659.863 688.685
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 94 659.863 688.685
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 94 659.863 688.685
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 95 667.472 696.294
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 95 667.472 696.294
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 95 667.472 696.294
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 95 667.472 696.294
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 95 667.472 696.294
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 95 667.472 696.294
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 95 667.472 696.294
|
||||
0.00 100.00 100.00 100.00 94.86 93.15 168 28.822 95 667.472 696.294
|
||||
0.00 100.00 100.00 100.00 94.85 93.14 168 28.822 96 674.816 703.638
|
||||
0.00 100.00 100.00 100.00 94.85 93.14 168 28.822 96 674.816 703.638
|
||||
0.00 100.00 100.00 100.00 94.85 93.14 168 28.822 96 674.816 703.638
|
||||
0.00 100.00 100.00 100.00 94.85 93.14 168 28.822 96 674.816 703.638
|
||||
0.00 100.00 100.00 100.00 94.85 93.14 168 28.822 96 674.816 703.638
|
||||
0.00 100.00 100.00 100.00 94.85 93.14 168 28.822 96 674.816 703.638
|
||||
0.00 100.00 100.00 100.00 94.85 93.14 168 28.822 96 674.816 703.638
|
||||
|
||||
```
|
||||
|
||||
从上面的数据来看,FullGC在不断出现,但是又回收不了内存,这个问题就严重了。
|
||||
|
||||
你要注意,对于这种情况,我们正常的判断逻辑应该是:一**个实时的业务系统就算是有FullGC,也应该是每次都回收到正常的状态。如果HEAP内存确实不够用,那我们可以增加。但是如果HEAP一直在减少,直到FullGC也回收不了,那就有问题了。**
|
||||
|
||||
因此,对于这样的问题,我们要做两方面的分析:
|
||||
|
||||
1. 内存确实在被使用,所以,FullGC回收不了。
|
||||
1. 内存有泄露,并且已经泄露完,所以,FullGC无法回收。
|
||||
|
||||
那么接下来,我们在做定向监控分析时就要从这两个角度来思考。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
既然内存已经满了,我们就执行一下jmap -histo:live 1|head -n 50,来看看占比相对较多的内存是什么:
|
||||
|
||||
```
|
||||
[root@svc-mall-order-7fbdd7b85f-ks828 /]# jmap -histo:live 1|head -n 50
|
||||
|
||||
|
||||
num #instances #bytes class name
|
||||
----------------------------------------------
|
||||
1: 74925020 2066475600 [B
|
||||
2: 2675397 513676056 [[B
|
||||
3: 2675385 85612320 com.mysql.cj.protocol.a.result.ByteArrayRow
|
||||
4: 2675386 42806176 com.mysql.cj.protocol.a.MysqlTextValueDecoder
|
||||
5: 246997 27488016 [C
|
||||
6: 80322 16243408 [Ljava.lang.Object;
|
||||
7: 14898 7514784 [Ljava.util.HashMap$Node;
|
||||
8: 246103 5906472 java.lang.String
|
||||
9: 109732 3511424 java.util.concurrent.ConcurrentHashMap$Node
|
||||
10: 37979 3342152 java.lang.reflect.Method
|
||||
11: 24282 2668712 java.lang.Class
|
||||
12: 55296 2654208 java.util.HashMap
|
||||
13: 15623 2489384 [I
|
||||
14: 81370 1952880 java.util.ArrayList
|
||||
15: 50199 1204776 org.apache.skywalking.apm.agent.core.context.util.TagValuePair
|
||||
16: 36548 1169536 java.util.HashMap$Node
|
||||
17: 566 1161296 [Ljava.util.concurrent.ConcurrentHashMap$Node;
|
||||
18: 28143 1125720 java.util.LinkedHashMap$Entry
|
||||
19: 13664 1093120 org.apache.skywalking.apm.agent.core.context.trace.ExitSpan
|
||||
20: 23071 922840 com.sun.org.apache.xerces.internal.dom.DeferredTextImpl
|
||||
21: 35578 853872 java.util.LinkedList$Node
|
||||
22: 15038 842128 java.util.LinkedHashMap
|
||||
23: 52368 837888 java.lang.Object
|
||||
24: 17779 711160 com.sun.org.apache.xerces.internal.dom.DeferredAttrImpl
|
||||
25: 11260 630560 com.sun.org.apache.xerces.internal.dom.DeferredElementImpl
|
||||
26: 18743 599776 java.util.LinkedList
|
||||
27: 26100 598888 [Ljava.lang.Class;
|
||||
28: 22713 545112 org.springframework.core.MethodClassKey
|
||||
29: 712 532384 [J
|
||||
30: 6840 492480 org.apache.skywalking.apm.agent.core.context.trace.LocalSpan
|
||||
31: 6043 483440 org.apache.skywalking.apm.dependencies.net.bytebuddy.pool.TypePool$Default$LazyTypeDescription$MethodToken
|
||||
32: 7347 352656 org.aspectj.weaver.reflect.ShadowMatchImpl
|
||||
33: 6195 297360 org.springframework.core.ResolvableType
|
||||
34: 6249 271152 [Ljava.lang.String;
|
||||
35: 11260 270240 com.sun.org.apache.xerces.internal.dom.AttributeMap
|
||||
36: 3234 258720 java.lang.reflect.Constructor
|
||||
37: 390 255840 org.apache.skywalking.apm.dependencies.io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueue
|
||||
38: 7347 235104 org.aspectj.weaver.patterns.ExposedState
|
||||
39: 5707 228280 java.lang.ref.SoftReference
|
||||
40: 3009 216648 org.apache.skywalking.apm.agent.core.context.TracingContext
|
||||
41: 13302 212832 org.apache.ibatis.scripting.xmltags.StaticTextSqlNode
|
||||
42: 8477 203448 org.apache.skywalking.apm.dependencies.net.bytebuddy.pool.TypePool$Default$LazyTypeDescription$MethodToken$ParameterToken
|
||||
43: 5068 162176 java.util.concurrent.locks.ReentrantLock$NonfairSync
|
||||
44: 2995 143760 org.apache.skywalking.apm.agent.core.context.trace.TraceSegmentRef
|
||||
45: 2426 135856 java.lang.invoke.MemberName
|
||||
46: 3262 130480 java.util.WeakHashMap$Entry
|
||||
47: 1630 130400 org.apache.skywalking.apm.agent.core.context.trace.EntrySpan
|
||||
[root@svc-mall-order-7fbdd7b85f-ks828 /]#
|
||||
|
||||
```
|
||||
|
||||
在分析内存时,我们可以过滤掉java自己的对象,只看和业务相关的对象。从上面的第3、4条可以看出,com.mysql.cj.protocol和SQL相关,那我们就到innodb_trx表中去查一下,看看有没有执行时间比较长的SQL。
|
||||
|
||||
在查询过程中,我们看到了这样一条SQL:
|
||||
|
||||
```
|
||||
select id, member_id, coupon_id, order_sn, create_time, member_username, total_amount pay_amount, freight_amount, promotion_amount, integration_amount, coupon_amount discount_amount, pay_type, source_type, status, order_type, delivery_company, delivery_sn auto_confirm_day, integration, growth, promotion_info, bill_type, bill_header, bill_content bill_receiver_phone, bill_receiver_email, receiver_name, receiver_phone, receiver_post_code receiver_province, receiver_city, receiver_region, receiver_detail_address, note, confirm_status delete_status, use_integration, payment_time, delivery_time, receive_time, comment_time modify_time from oms_order WHERE ( id = 0 and status = 0 and delete_status = 0 )
|
||||
|
||||
```
|
||||
|
||||
进而我又查询了这个语句,发现涉及到的数据有4358761条,这显然是代码写的有问题。那我们就去查看一下在代码中,哪里调用了这个SQL。
|
||||
|
||||
通过查看代码,看到如下逻辑:
|
||||
|
||||
```
|
||||
example.createCriteria().andIdEqualTo(orderId).andStatusEqualTo(0).andDeleteStatusEqualTo(0);
|
||||
List<OmsOrder> cancelOrderList = orderMapper.selectByExample(example);
|
||||
|
||||
```
|
||||
|
||||
这段代码对应的select语句是:
|
||||
|
||||
```
|
||||
<select id="selectByExample" parameterType="com.dunshan.mall.model.OmsOrderExample" resultMap="BaseResultMap">
|
||||
select
|
||||
<if test="distinct">
|
||||
distinct
|
||||
</if>
|
||||
<include refid="Base_Column_List" />
|
||||
from oms_order
|
||||
<if test="_parameter != null">
|
||||
<include refid="Example_Where_Clause" />
|
||||
</if>
|
||||
<if test="orderByClause != null">
|
||||
order by ${orderByClause}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
```
|
||||
|
||||
这是一个典型的语句没过滤的问题。像这样的开发项目,也最多就是做个Demo用。要是在真实的线上项目中,早就不知道伤害了多少人。
|
||||
|
||||
我们在这里直接修改代码加上limit,不让它一次性查询出所有的数据。
|
||||
|
||||
然后,我们看一下优化效果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/5c/42d0c383e03acf38b31e4fc7879c2f5c.png" alt="">
|
||||
|
||||
你看,没有出现TPS断裂的情况了,优化效果还是有的,说明那条SQL语句不会再查出太多数据把内存给占满了。
|
||||
|
||||
不过,TPS值并没有增加多少,所以我们必须做第三阶段的分析。
|
||||
|
||||
## 第三阶段
|
||||
|
||||
这次我们不从全局监控数据来看了,有了前面的经验,我们直接来做定向监控分析。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
因为我们在前面改了SQL,所以在执行SQL之后,我们要去查一下innodb_trx表,看看还有没有慢的SQL。 结果,看到了如下SQL:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cb/73/cb7cd20de10b8d55b3c94f47c2576273.png" alt="">
|
||||
|
||||
把这个SQL拿出来,看看它的执行计划:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/54/67785136ea6b3748d5ab7c148445ff54.png" alt="">
|
||||
|
||||
又是一个典型的全表扫描,并且是由一个update使用的。看到这里,你是不是有种想把开发拉出去祭旗的冲动?
|
||||
|
||||
由于生成订单信息是一个复杂的接口,我们不急着收拾这个SQL,先把slow log全都拿出来分析一遍。
|
||||
|
||||
请你注意,有时候项目执行的场景多了,数据相互之间的影响就会很大,容易导致我们分析的方向不准确。所以,我们最好把slow log都清一遍。反正我通常都会这么干,因为我不想看到乱七八糟的数据。
|
||||
|
||||
在清理完慢SQL、重新执行场景之后,我又把slow log拿出来,用pt-digest-query分析了一遍(关于这一点,我们在[第16讲](https://time.geekbang.org/column/article/367285)中讲过,如果你不记得的话,建议你再回顾一下),看到如下的数据:
|
||||
|
||||
```
|
||||
# Profile
|
||||
# Rank Query ID Response time Calls R/Call V/M I
|
||||
# ==== ============================ =============== ===== ======== ===== =
|
||||
# 1 0x2D9130DB1449730048AA1B5... 1233.4054 70.5% 3 411.1351 2.73 UPDATE oms_order
|
||||
# 2 0x68BC6C5F4E7FFFC7D17693A... 166.3178 9.5% 2677 0.0621 0.60 INSERT oms_order
|
||||
# 3 0xB86E9CC7B0BA539BD447915... 91.3860 5.2% 1579 0.0579 0.01 SELECT ums_member
|
||||
# 4 0x3135E50F729D62260977E0D... 61.9424 3.5% 4 15.4856 0.30 SELECT oms_order
|
||||
# 5 0xAE72367CD45AD907195B3A2... 59.6041 3.4% 3 19.8680 0.13 SELECT oms_order
|
||||
# 6 0x695C8FFDF15096AAE9DBFE2... 49.1613 2.8% 1237 0.0397 0.01 SELECT ums_member_receive_address
|
||||
# 7 0xD732B16862C1BC710680BB9... 25.5382 1.5% 471 0.0542 0.01 SELECT oms_cart_item
|
||||
# MISC 0xMISC 63.2937 3.6% 1795 0.0353 0.0 <9 ITEMS>
|
||||
|
||||
```
|
||||
|
||||
通过上面的Profile信息我们看到,第一个语句消耗了总时间的70.5%,第二个语句消耗了总时间的9.5%。我们说要解决性能问题,其实解决的就是这种消耗时间长的语句。而后面的SQL执行时间短,我们可以暂时不管。
|
||||
|
||||
通常在这种情况下,你可以只解决第一个语句,然后再回归测试看看效果,再来决定是否解决第二个问题。我先把这两个完整的SQL语句列在这里:
|
||||
|
||||
```
|
||||
1. UPDATE oms_order SET member_id = 260869, order_sn = '202102030100205526', create_time = '2021-02-03 01:05:56.0', member_username = '7dcmppdtest15176472465', total_amount = 0.00, pay_amount = 0.00, freight_amount = 0.00, promotion_amount = 0.00, integration_amount = 0.00, coupon_amount = 0.00, discount_amount = 0.00, pay_type = 0, source_type = 1, STATUS = 4, order_type = 0, auto_confirm_day = 15, integration = 0, growth = 0, promotion_info = '', receiver_name = '6mtf3', receiver_phone = '18551479920', receiver_post_code = '66343', receiver_province = '北京', receiver_city = '7dGruop性能实战', receiver_region = '7dGruop性能实战区', receiver_detail_address = '3d16z吉地12号', confirm_status = 0, delete_status = 0 WHERE id = 0;
|
||||
|
||||
|
||||
2. insert into oms_order (member_id, coupon_id, order_sn, create_time, member_username, total_amount, pay_amount, freight_amount, promotion_amount, integration_amount, coupon_amount, discount_amount, pay_type, source_type, status, order_type, delivery_company, delivery_sn, auto_confirm_day, integration, growth, promotion_info, bill_type, bill_header, bill_content, bill_receiver_phone, bill_receiver_email, receiver_name, receiver_phone, receiver_post_code, receiver_province, receiver_city, receiver_region, receiver_detail_address, note, confirm_status, delete_status, use_integration, payment_time, delivery_time, receive_time, comment_time, modify_time)values (391265, null, '202102030100181755', '2021-02-03 01:01:03.741', '7dcmpdtest17793405657', 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, null, null, 15, 0, 0, '', null, null, null, null, null, 'belod', '15618648303', '93253', '北京', '7dGruop性能实战', '7dGruop性能实战区', 'hc9r1吉地12号', null, 0, 0, null, null, null, null, null, null);
|
||||
|
||||
```
|
||||
|
||||
我们先来看第一个语句。这个update语句虽然被调用的次数不多,但是特别慢。这显然不应该是实时接口在调用,那我们就要查一下到底是什么业务调用了这个语句。你看,在这个语句中,update更新的是where条件中ID为0的数据,这看上去就是一个批量业务。
|
||||
|
||||
我们再来看第二个语句。第二个insert语句调用次数多,应该是实时交易的SQL。通常,我们会通过批量插入数据来优化insert,所以,就需要调整bulk_insert_buffer_size参数(默认是8M)来实现这一点。因为bulk_insert_buffer_size就是在批量插入数据时提高效率的。我去查询了一下这个参数,确实没有优化过,还是默认值。
|
||||
|
||||
这里你要注意一点,在生产环境中,因为Order表中要加索引,所以在架构设计时也最好是主从分离,让update、insert和select不会相互影响。
|
||||
|
||||
分析完这两个SQL语句,我们先来查找第一个SQL的来源。通过查找代码,可以看到这里调用了该语句:
|
||||
|
||||
```
|
||||
orderMapper.updateByPrimaryKeySelective(cancelOrder);
|
||||
|
||||
```
|
||||
|
||||
但是,请注意,这个updateByPrimaryKeySelective方法是批量任务中的,而批量任务应该和实时交易分开才是。如果你是作为性能团队的人给架构或开发提优化建议,那你可以这样给建议:
|
||||
|
||||
<li>
|
||||
读写分离;
|
||||
</li>
|
||||
<li>
|
||||
批量业务和实时业务分离。
|
||||
</li>
|
||||
|
||||
在这里,我先把这个批量业务给分离开,并且也不去调用它。但是,在真实的业务逻辑中,你可不能这么干。我之所以这么做,是为了看后续性能优化的效果和方向。
|
||||
|
||||
做了上述修改之后,TPS如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/3d/20e420c49edcd201cc68c0698e7ddf3d.png" alt="">
|
||||
|
||||
从效果上来看,TPS能达到300左右了,响应时间看起来也稳定了。我们终于可以进入正常的性能分析阶段了,哈哈。
|
||||
|
||||
不过,到这里,我们的工作并没有结束,从上图来看,系统资源并没有完全用起来,这个接口显然还有优化的空间。所以,在下节课中,我们接着来唠。
|
||||
|
||||
## 总结
|
||||
|
||||
在这节课中,我们做了三个阶段的分析优化。
|
||||
|
||||
在第一阶段中,我们修改了JDBC池,虽然TPS有上升的趋势,但是,新问题也同样出现了:TPS非常不稳定,还有断断续续的情况。
|
||||
|
||||
在第二阶段中,我们分析了内存溢出的问题,定位出了原因并优化了内存问题。虽然我们在TPS曲线上明显看到了优化的效果,但仍然没有达到理想的程度。
|
||||
|
||||
在第三阶段中,我们分析定位了SQL的问题,这是非常合乎逻辑的。因为我们在第二阶段中修改了SQL,所以到了第三阶段,就要直接到数据库中做相应的定位。从结果上看,效果不错,TPS已经有明显正常的趋势了。不过,你要注意的是,当批量业务和实时业务同时出现在同一个数据库中,并且是对同样的表进行操作,这时,你就得考虑一下架构设计是否合理了。
|
||||
|
||||
总之,在这节课中你可以看到,当SQL查询出来的数据到了应用内存的时候,导致了内存的增加。而应用内存的增加也增加了GC的次数,进而消耗了更多的CPU资源。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,请你思考两个问题:
|
||||
|
||||
1. 为什么JDK中看到VM Thread线程消耗CPU高,会去查看内存消耗是否合理?
|
||||
1. 在MySQL中分析SQL问题时为什么要先查询innodb_trx表?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
441
极客时间专栏/高楼的性能工程实战课/基准场景/20 | 生成订单信息之二:业务逻辑复杂,怎么做性能优化?.md
Normal file
441
极客时间专栏/高楼的性能工程实战课/基准场景/20 | 生成订单信息之二:业务逻辑复杂,怎么做性能优化?.md
Normal file
@@ -0,0 +1,441 @@
|
||||
<audio id="audio" title="20 | 生成订单信息之二:业务逻辑复杂,怎么做性能优化?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1a/c1/1af1dcb5cf233a25f706acab4ecf1fc1.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
在上节课中,我们针对生成订单信息这个接口做了三个阶段的分析定位和优化动作,让TPS变得正常了一些。不过,系统资源并没有完全用起来,这个接口显然还有优化的空间。因为高老师说很过多遍,**在性能优化的过程中,我们要把资源都用起来。**
|
||||
|
||||
关于“把资源用起来”这一理论,我希望你能明白的是,**我们在性能环境中做优化,把资源用起来是为了看系统的最大容量在哪里。这并不意味着,你可以在生产环境中让硬件使用到这种程度**。
|
||||
|
||||
对于一个不可控的系统容量来说,资源使用率高,极有可能导致各种问题出现。所以,安全稳妥起见,很多生产环境的资源利用率都是非常低的,倘若用得超过了20%,运维都得半夜惊出一身冷汗。
|
||||
|
||||
而我们在性能环境中的测试结果,要想给生产环境配置一个比较明确并且可以借鉴的结论,就必须先去分析生产的业务容量,然后再来确定当生产业务容量达到峰值的时候,相应的硬件资源用到多少比较合理。
|
||||
|
||||
不过,在我们的优化环境中,我们可以通过把一个系统用起来,来判断软件的容量能力。所以,我们接着上节课的内容,再进入到第四阶段。你将看到在业务逻辑复杂的情况下,我们该怎么做优化。
|
||||
|
||||
闲言少叙,直接开整。
|
||||
|
||||
## 第四阶段
|
||||
|
||||
在解决了前面三个不正经的问题之后,我们现在可以正常分析时间消耗到哪去了,只要解决了快慢的问题,我们才能进而解决资源没有用起来的问题。所以,我们先来拆分响应时间,同样,我们也不做全局监控分析,因为.......哥嫌累。
|
||||
|
||||
### 拆分响应时间
|
||||
|
||||
之前很多次我们都在用APM来拆分响应时间,感觉没什么新意,这次我用日志来拆分一下时间。
|
||||
|
||||
- Gateway:
|
||||
|
||||
```
|
||||
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 151 ms
|
||||
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 147 ms
|
||||
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 141 ms
|
||||
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 122 ms
|
||||
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 125 ms
|
||||
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 150 ms
|
||||
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 177 ms
|
||||
|
||||
|
||||
```
|
||||
|
||||
- Order:
|
||||
|
||||
```
|
||||
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 72 ms 72 ms
|
||||
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 94 ms 93 ms
|
||||
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 76 ms 76 ms
|
||||
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 95 ms 95 ms
|
||||
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 90 ms 90 ms
|
||||
|
||||
```
|
||||
|
||||
我们先不用看后面的服务,因为从这个接口往后就直接到数据库了,我们先来看一下应用本身有没有问题。
|
||||
|
||||
为了让你看得清楚一点,这里我只截取了部分数据,但并不是说我们只看这些就够了。在项目中的话,你可以通过写脚本或其他的方式自己做响应时间的统计。
|
||||
|
||||
从上面的信息可以看到,这个接口的整个响应时间是150ms左右,而在order服务上就消耗了90毫秒。所以,下面我们要分析:为什么在order上会消耗这么久的时间。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
要想知道Order服务的时间消耗,那显然,我们得知道Order应用中的线程都在做什么动作,所以我们先直接来分析Order的栈。
|
||||
|
||||
通过Spring Boot Admin,我们可以查看到线程的整体状态:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fe/d7/fe51f9e6ec935017ccf71a3fa4d834d7.png" alt="">
|
||||
|
||||
你看,线程确实比较繁忙。至于这些线程在做什么,我们通过栈的内容可以知道,然后再进一步确定优化的方向。
|
||||
|
||||
但是,由于系统资源还没有用到上限,我们得先调整一下Tomcat的线程数,把它加大一些,争取让Order应用把硬件资源用起来。
|
||||
|
||||
```
|
||||
原值:
|
||||
max: 20
|
||||
修改为:
|
||||
max: 100
|
||||
|
||||
```
|
||||
|
||||
我们看一下调整后的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3c/c4/3c9493c583ecf38ed9e0b96fe18494c4.png" alt="">
|
||||
|
||||
没想到,性能更差了……这乱七八糟的曲线和想像中的优美曲线完全不一致呀!
|
||||
|
||||
事实证明,偷懒是绕不过去坑的,我们只有再次查看响应时间消耗到了哪里。
|
||||
|
||||
于是,通过用各个服务的日志拆分响应时间,我发现在Member服务上有这样的日志(为了让你看清楚点,我截取了一些时间消耗比较大的日志,请注意一下哦,这是我们改了Order的Tomcat线程池之后的结果):
|
||||
|
||||
```
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:15 +0800] "GET /sso/feign/info HTTP/1.1" 200 814 "-" "okhttp/3.14.8" 2348 ms 2348 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:17 +0800] "GET /sso/feign/info HTTP/1.1" 200 816 "-" "okhttp/3.14.8" 4155 ms 4155 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:17 +0800] "GET /sso/feign/info HTTP/1.1" 200 817 "-" "okhttp/3.14.8" 4968 ms 1813 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:15 +0800] "GET /sso/feign/info HTTP/1.1" 200 810 "-" "okhttp/3.14.8" 2333 ms 2333 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:17 +0800] "GET /sso/feign/info HTTP/1.1" 200 815 "-" "okhttp/3.14.8" 5206 ms 4970 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:20 +0800] "GET /sso/feign/info HTTP/1.1" 200 818 "-" "okhttp/3.14.8" 6362 ms 6362 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:20 +0800] "GET /sso/feign/info HTTP/1.1" 200 818 "-" "okhttp/3.14.8" 6710 ms 6710 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:20 +0800] "GET /sso/feign/info HTTP/1.1" 200 817 "-" "okhttp/3.14.8" 6696 ms 6587 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:21 +0800] "GET /sso/feign/info HTTP/1.1" 200 813 "-" "okhttp/3.14.8" 7987 ms 7976 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] "GET /sso/feign/info HTTP/1.1" 200 814 "-" "okhttp/3.14.8" 8784 ms 8784 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] "GET /sso/feign/info HTTP/1.1" 200 817 "-" "okhttp/3.14.8" 9100 ms 8764 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] "GET /sso/feign/info HTTP/1.1" 200 834 "-" "okhttp/3.14.8" 9126 ms 9013 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] "GET /sso/feign/info HTTP/1.1" 200 817 "-" "okhttp/3.14.8" 9058 ms 9058 ms
|
||||
10.100.69.248 - - [04/Feb/2021:00:37:23 +0800] "GET /sso/feign/info HTTP/1.1" 200 820 "-" "okhttp/3.14.8" 9056 ms 9056 ms
|
||||
|
||||
```
|
||||
|
||||
显然,这个Member服务的响应时间太长了。而在生成订单信息这个接口中,也确实调用了Member服务,因为要使用Token嘛。既然是Order的Tomcat线程池加大了,导致Member服务响应如此之慢,那我们就有理由作出判断:Order之所以消耗时间长,是因为Member服务不能提供Order请求时的快速响应。通俗点讲,就是Member的性能差。
|
||||
|
||||
要想分析Member性能为什么差,我们其实可以直接到Member上打印栈信息来看看,这是高老师有时候偷懒的做法。
|
||||
|
||||
而我们一直在讲,完整的分析逻辑应该是先看全局监控数据,再看定向监控数据。所以,高老师在这里,勤快一点。我们通过全局监控数据来看看整体的资源消耗:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/64/78/6405e04c8bd17315060485857336d078.png" alt="">
|
||||
|
||||
worker-8的CPU资源居然用到了这么高!这说明我们在前面增加Order的Tomcat线程数是有价值的。现在,瓶颈点到了另一个地方,也就是我们的Member服务。
|
||||
|
||||
既然worker-8的资源使用率高,那我们就来看看它上面有什么Pod,不难看出Member就在worker8上:
|
||||
|
||||
```
|
||||
[root@k8s-master-2 ~]# kubectl get pods -o wide | grep k8s-worker-8
|
||||
elasticsearch-client-0 1/1 Running 0 38h 10.100.231.233 k8s-worker-8 <none> <none>
|
||||
monitor-mall-monitor-d8bb58fcb-kfbcj 1/1 Running 0 23d 10.100.231.242 k8s-worker-8 <none> <none>
|
||||
skywalking-oap-855f96b777-5nxll 1/1 Running 6 37h 10.100.231.235 k8s-worker-8 <none> <none>
|
||||
skywalking-oap-855f96b777-6b7jd 1/1 Running 5 37h 10.100.231.234 k8s-worker-8 <none> <none>
|
||||
svc-mall-admin-75ff7dcc9b-8gtr5 1/1 Running 0 17d 10.100.231.208 k8s-worker-8 <none> <none>
|
||||
svc-mall-demo-5584dbdc96-fskg9 1/1 Running 0 17d 10.100.231.207 k8s-worker-8 <none> <none>
|
||||
svc-mall-member-5fc984b57c-bk2fd 1/1 Running 0 12d 10.100.231.231 k8s-worker-8 <none> <none>
|
||||
[root@k8s-master-2 ~]#
|
||||
|
||||
```
|
||||
|
||||
同时,我们还能发现,这个节点上有不少服务,而这些服务都是比较吃CPU的,并且在压力过程中,还出现了sy cpu消耗很高的情况,我截两个瞬间的数据给你看看,一个是sy cpu高的情况,一个是us cpu高的情况,具体如下所示:
|
||||
|
||||
```
|
||||
- sys cpu高的情况
|
||||
[root@k8s-worker-8 ~]# top
|
||||
top - 00:38:51 up 28 days, 4:27, 3 users, load average: 78.07, 62.23, 39.14
|
||||
Tasks: 275 total, 17 running, 257 sleeping, 1 stopped, 0 zombie
|
||||
%Cpu0 : 4.2 us, 95.4 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.4 st
|
||||
%Cpu1 : 1.8 us, 98.2 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu2 : 2.1 us, 97.9 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu3 : 1.0 us, 99.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
KiB Mem : 16266296 total, 1819300 free, 7642004 used, 6804992 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 8086580 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
12902 root 20 0 1410452 32280 17744 S 48.1 0.2 751:39.59 calico-node -felix
|
||||
9 root 20 0 0 0 0 R 34.8 0.0 131:14.01 [rcu_sched]
|
||||
3668 techstar 20 0 4816688 1.3g 23056 S 33.9 8.5 111:17.12 /usr/share/elasticsearch/jdk/bin/java -Xshare:auto -Des.networkaddress.cache.ttl=60 -Des.networkaddress+
|
||||
26105 root 20 0 119604 6344 2704 R 25.8 0.0 0:02.36 runc --root /var/run/docker/runtime-runc/moby --log /run/containerd/io.containerd.runtime.v1.linux/moby+
|
||||
26163 root 20 0 19368 880 636 R 25.2 0.0 0:00.95 iptables-legacy-save -t nat
|
||||
26150 root 20 0 18740 3136 1684 R 21.6 0.0 0:01.18 runc init
|
||||
26086 root 20 0 18744 5756 2376 R 20.3 0.0 0:03.10 runc --root /var/run/docker/runtime-runc/moby --log /run/containerd/io.containerd.runtime.v1.linux/moby+
|
||||
410 root 20 0 0 0 0 S 19.4 0.0 42:42.56 [xfsaild/dm-1]
|
||||
14 root 20 0 0 0 0 S 14.8 0.0 54:28.76 [ksoftirqd/1]
|
||||
6 root 20 0 0 0 0 S 14.2 0.0 50:58.94 [ksoftirqd/0]
|
||||
26158 root 20 0 18740 1548 936 R 14.2 0.0 0:00.90 runc --version
|
||||
31715 nfsnobo+ 20 0 129972 19856 9564 S 11.3 0.1 12:41.98 ./kube-rbac-proxy --logtostderr --secure-listen-address=[172.16.106.56]:9100 --tls-cipher-suites=TLS_EC+
|
||||
10296 root 20 0 3402116 113200 39320 S 10.3 0.7 2936:50 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubern+
|
||||
22 root rt 0 0 0 0 S 8.7 0.0 3:18.08 [watchdog/3]
|
||||
26162 root 20 0 139592 2792 2508 R 8.4 0.0 0:00.39 /opt/cni/bin/calico
|
||||
6843 root 20 0 965824 110244 30364 S 7.7 0.7 1544:20 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
|
||||
24 root 20 0 0 0 0 S 7.4 0.0 49:03.89 [ksoftirqd/3]
|
||||
3636 techstar 20 0 4368 364 280 S 6.8 0.0 0:12.19 /tini -- /usr/local/bin/docker-entrypoint.sh eswrapper
|
||||
26159 root 20 0 18740 760 552 R 6.5 0.0 0:00.28 runc --version
|
||||
1755 root 20 0 411108 5836 4416 S 4.8 0.0 35:39.97 /usr/libexec/packagekitd
|
||||
|
||||
|
||||
- us cpu高的情况
|
||||
[root@k8s-worker-8 ~]# top
|
||||
top - 00:43:01 up 28 days, 4:31, 3 users, load average: 72.51, 68.20, 47.01
|
||||
Tasks: 263 total, 2 running, 260 sleeping, 1 stopped, 0 zombie
|
||||
%Cpu0 : 77.2 us, 15.7 sy, 0.0 ni, 2.2 id, 0.0 wa, 0.0 hi, 4.8 si, 0.0 st
|
||||
%Cpu1 : 77.0 us, 15.7 sy, 0.0 ni, 2.3 id, 0.0 wa, 0.0 hi, 5.0 si, 0.0 st
|
||||
%Cpu2 : 70.3 us, 20.9 sy, 0.0 ni, 2.9 id, 0.0 wa, 0.0 hi, 5.9 si, 0.0 st
|
||||
%Cpu3 : 76.6 us, 12.2 sy, 0.0 ni, 5.1 id, 0.0 wa, 0.0 hi, 6.1 si, 0.0 st
|
||||
KiB Mem : 16266296 total, 1996620 free, 7426512 used, 6843164 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 8302092 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
20072 root 20 0 7944892 689352 15924 S 137.1 4.2 3127:04 java -Dapp.id=svc-mall-member -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.+
|
||||
29493 root 20 0 3532496 248960 17408 S 98.3 1.5 0:06.70 java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Dmode=no-init -Xmx2g -Xms2g -cl+
|
||||
28697 root 20 0 3711520 1.0g 18760 S 61.6 6.7 124:41.08 java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Dmode=no-init -Xmx2g -Xms2g -cl+
|
||||
25885 root 20 0 3716560 1.2g 18908 S 59.3 7.6 183:12.97 java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Dmode=no-init -Xmx2g -Xms2g -cl+
|
||||
6843 root 20 0 965824 109568 30364 S 7.6 0.7 1544:49 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
|
||||
3668 techstar 20 0 4816688 1.3g 23056 S 6.6 8.5 111:58.56 /usr/share/elasticsearch/jdk/bin/java -Xshare:auto -Des.networkaddress.cache.ttl=60 -Des.networkaddress+
|
||||
10296 root 20 0 3402372 111692 39320 S 6.6 0.7 2937:43 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubern+
|
||||
18 root rt 0 0 0 0 S 5.0 0.0 5:57.54 [migration/2]
|
||||
6 root 20 0 0 0 0 S 2.6 0.0 51:21.52 [ksoftirqd/0]
|
||||
410 root 20 0 0 0 0 D 2.6 0.0 43:08.23 [xfsaild/dm-1]
|
||||
28310 root 20 0 7807048 565740 15924 S 2.6 3.5 1036:53 java -Dapp.id=svc-mall-admin -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.s+
|
||||
29741 root 20 0 7749608 540376 15848 S 2.3 3.3 304:41.47 java -Dapp.id=svc-mall-monitor -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent+
|
||||
12902 root 20 0 1410452 30368 17744 S 2.0 0.2 752:30.32 calico-node -felix
|
||||
16712 root 0 -20 0 0 0 S 2.0 0.0 1:56.16 [kworker/2:0H]
|
||||
6381 root 20 0 7782400 491476 15928 S 1.7 3.0 441:08.96 java -Dapp.id=svc-mall-demo -
|
||||
|
||||
```
|
||||
|
||||
从sy cpu高的top数据来看,这个节点显然在不断地调度系统资源,通过top中的rcu_sched/softirq等进程就可以知道,这种情况显然是因为Kubernetes在这个节点上过多地安排了任务。所以,我先把Member服务移到另一个worker上,然后看到TPS如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2a/5a/2a0dbc3d50efdb6993a7839c8a7fb35a.png" alt="">
|
||||
|
||||
你看,TPS增加到400多了,也就是说我们的方向是对的。
|
||||
|
||||
那为什么我们之前修改Order服务的Tomcat线程数没有看到效果呢?这是因为压力已经到了Member服务上,这让Member服务所在的worker节点资源使用率增加,导致Member服务无法正常响应请求。因此,整个TPS看起来没有什么优化效果。现在,我们移走了Member服务,看到效果明显增加,这说明我们的方向还在正确的道路上。
|
||||
|
||||
我们再回来看一下整体的资源监控:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/94/7e/94e40d9e66ca3dd35dd1140fa5fdfb7e.png" alt="">
|
||||
|
||||
现在,没有一个worker的资源用满或者接近用满,完全不符合我们“把资源都用起来”的目标,这显然是不可接受的。
|
||||
|
||||
在长时间的压力中,我们发现资源怎么也用不上去。而且在上节课第三阶段的最后一个图中,你也能清楚地看到这一点。
|
||||
|
||||
到这里为止,我们查看了一次次的性能分析决策树,也调整了一些参数,比如Spring Boot中的Tomcat连接池、JDBC池、Jedis池、MQ池等,调整之后TPS似乎有增加的趋势,但是非常不明显。所以,我们只能开始新一轮的定位。
|
||||
|
||||
## 第五阶段
|
||||
|
||||
### 定位时间消耗
|
||||
|
||||
在上一个阶段的分析中,我们用日志拆分了响应时间,是想让你看到我们用不同的手段都可以把响应时间拆出来。这也是我一直强调的:你不要在意用什么手段,而要在意你想要的是什么。
|
||||
|
||||
在这一阶段中,我们再换一个思路:跟踪方法的执行过程来判断时间消耗。我想让你看到:在优化过程中,唯有思路不变,手段任你选择。
|
||||
|
||||
这个方法和我们用日志拆分时间的逻辑其实是一样的。我们可以直接用Arthas来定位方法的时间消耗。请你记住,除了Arthas之外,还有很多其他工具也是可以的,比如JvisualVM/JMC/BTrace等。
|
||||
|
||||
我们已经知道接口方法是com.dunshan.mall.order.service.impl.PortalOrderServiceImpl中的generateOrder,所以,我们直接trace(跟踪)它就可以了。
|
||||
|
||||
你要注意,在这一步中,我们需要反复trace多次,这是为了保证判断方向的正确性。不得不承认,这是一种耗时又枯燥的工作,有一种数羊睡觉的感觉。不过,有的人能数睡着,有的人却是越数越兴奋。
|
||||
|
||||
现在,我们来看一下反复trace后的结果。由于跟踪的栈实在太长了,我把多次跟踪的内容做了简化,其中重要的部分如下所示:
|
||||
|
||||
```
|
||||
+---[91.314104ms] com.dunshan.mall.order.feign.MemberService:getCurrentMember() #150
|
||||
....................
|
||||
+---[189.777528ms] com.dunshan.mall.order.feign.CartItemService:listPromotionnew() #154
|
||||
....................
|
||||
+---[47.300765ms] com.dunshan.mall.order.service.impl.PortalOrderServiceImpl:sendDelayMessageCancelOrder() #316
|
||||
|
||||
```
|
||||
|
||||
为什么说这几个方法重要呢?这里我要说明一下,对于跟踪的内容,我们主要判断的是:**消耗时间的方法是不是固定的**。如果时间不是消耗在了固定的方法上,那就有些麻烦了,因为这说明不是方法本身的问题,而是其他的资源影响了方法的执行时间;如果时间一直消耗在了固定的方法上,就比较容易了,我们只要接着去跟踪这个方法就好了。
|
||||
|
||||
而我反复跟踪了多次之后,总是发现上面几个方法都比较消耗时间。既然已经知道了方法的时间消耗,那全局监控已经救不了我们了,只有在定向监控中来分析了。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
我先说明一下,根据我们的分析思路,我在定向监控分析之前,反复分析了全局监控计数器,没觉得有什么资源使用上的问题。并且从压力工具到数据库,我也没发现有什么阻塞点,整条大路都很宽敞。
|
||||
|
||||
但是,上面我们也确实看到了响应时间消耗在了几个具体的方法上,并且这几个方法并不是一直都消耗这么长的时间,而是有快有慢。
|
||||
|
||||
经过反复确认后,我觉得有必要来看一下业务逻辑了。因为对于一个复杂的业务来说,如果业务代码逻辑太长,那我们不管怎么优化,都不会有什么效果,最后只能按照扩容的思路来加机器了。
|
||||
|
||||
不过,在我的逻辑中,即便是加机器,我们也要给出加机器的逻辑。如果业务可优化,那我们更要尽力一试。因为从成本上来说,优化代码是一个更优的选择。
|
||||
|
||||
在这里,我多说几句闲话。我看到过很多企业连一些简单的优化都没有做,就从寻找心理安全感的角度去增加机器,结果耗费了大量的成本,这是非常不理智的。从技术的角度来说,花不多的时间成本就可以节省大量的资源成本,这显然是很划算的。可是,受一些社会不良思维的误导,有些企业就觉得只要能通过加机器解决的问题,都不是啥大问题。
|
||||
|
||||
对于这种思路,我们就得放到成本上来算一算了。大部分打工人可能会觉得,反正用的又不是自己的钱,管老板花多少钱加机器干嘛?没意义。但是,从节能减排的全球大局观来看,一个该做的优化没有做,不仅浪费公司的成本,还一点儿都不环保!呃.......好像扯的有点远了。
|
||||
|
||||
我们回到正题,既然我们想优化业务,就得先知道业务的调用逻辑是个啥样子。所以我们打开idea,找到generateOrder方法,然后把sequence diagram(idea的一个插件)打开,就看到了这样一张很长的业务逻辑图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7a/ac/7aa61af1bf6a89c2d525d6a4538e25ac.png" alt="">
|
||||
|
||||
如果你看不懂这张图,也没有关系。我在这里给你大致描述一下这张图里有什么东西:
|
||||
|
||||
1. 获取用户名;
|
||||
1. 获取购物车列表;
|
||||
1. 获取促销活动信息;
|
||||
1. 判断库存;
|
||||
1. 判断优惠卷;
|
||||
1. 判断积分;
|
||||
1. 计算金额;
|
||||
1. 转订单并插库;
|
||||
1. 获取地址信息;
|
||||
1. 计算赠送积分和成长值
|
||||
1. 插入订单表;
|
||||
1. 更新优惠卷状态;
|
||||
1. 扣积分;
|
||||
1. 删除购物车商品;
|
||||
1. 发送取消订单消息;
|
||||
1. 返回结果;
|
||||
|
||||
是不是有种很复杂的感觉?通过我大概列出来的这些内容,你就能知道下订单这个动作有多长了。对这样的复杂接口,如果业务逻辑要求必须是这样的,那我们在接口上就没有什么优化空间了。在前面,我们已经把TPS优化到了400多,在这样的硬件机器上,也基本上就这样了。
|
||||
|
||||
在这节课中,我们不是要去设计一个下订单的业务逻辑,因为每个企业的下订单逻辑,都会有不同的业务限制。做为性能工程师,我们没有对业务逻辑的设计说改就改的权利,因为修改业务逻辑需要所有的相关人员一起商讨确定。不过,我们可以通过分析的结果给出优化的建议。
|
||||
|
||||
在这里,我把优惠卷、积分、发送延时取消订单信息的步骤都从下订单的步骤中删掉。有人可能会问这样改合适吗?我强调一下,不是我要这样改业务逻辑,而是想看看这样改了之后,TPS有没有增加。如果增加了,就说明我们的方向是对的,也就是说,这个业务逻辑需要再和各方商量一下,重新设计。
|
||||
|
||||
我们来看修改之后的TPS图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9e/c3/9ecd0a5d86f6d9c9d3723e943b8e14c3.png" alt="">
|
||||
|
||||
可以看到,这样的修改确实有效果,那我们后续的优化建议就比较清晰了。如果你在项目中遇到这样的接口,优化建议就可以这样来提:
|
||||
|
||||
1. 分库分表;
|
||||
1. 利用缓存;
|
||||
1. 异步处理非关键步骤;
|
||||
1. 大接口拆成小接口。
|
||||
|
||||
但是,建议终归是建议,通常在一个企业中,对于这样的接口,技术团队会根据具体的业务逻辑做长时间的技术分析,来判断如何实现。如果确实没办法在技术上做优化,那就只能上最后一招:扩容!这个扩容就不再是扩某一段了,而是一整条链路上涉及到的服务。
|
||||
|
||||
还有一点,在一个业务链路中,每个企业通常都是根据发展的速度做相应的技术沉淀。如果技术团队太追潮流了,学习成本大,不见得是好事;如果太陈旧了,维护的成本大,也不见得是好事。因此,我们只有根据实际的业务发展不断地演进业务流程和技术实现,才是正道。
|
||||
|
||||
我们优化到这里,看似就可以结束收工了,但是并没有,因为天不随人愿的定律从来都没有被打破过,我们只得来到第六个阶段。
|
||||
|
||||
## 第六阶段
|
||||
|
||||
### 定位TPS会降下来的问题
|
||||
|
||||
具体是什么原因呢?我在接着压的时候,又出现了这样的问题:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f3/16/f3caebb33d1b4274df8bbdf696fbed16.png" alt="">
|
||||
|
||||
你看,TPS掉呀掉的,心都碎了……虽说在每个项目的优化过程中,都会出现各种意外的事情,但是,这个接口的意外也太多了点。没办法,我们接着查吧。
|
||||
|
||||
仍然是按照高老师强调的性能分析决策树(如果你不清楚,可以再看看[第4讲](https://time.geekbang.org/column/article/356789)),我们一个个计数器看过去,最后在mysqlreport中看到了下面这些数据:
|
||||
|
||||
```
|
||||
__ InnoDB Lock _________________________________________________________
|
||||
Waits 889 0.1/s
|
||||
Current 77
|
||||
Time acquiring
|
||||
Total 36683515 ms
|
||||
Average 41263 ms
|
||||
Max 51977 ms
|
||||
|
||||
```
|
||||
|
||||
显然当前的锁有点多,并且这锁的时间还挺长。要想查锁,就得先知道当前正在运行的是什么样的事务,所以我们就去查一下innodb_trx表,因为MySQL在这个表中会记录所有正在执行的事务。在数据库中,我们发现了大量的lock_wait(锁等待):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ef/e8/ef514de33b59cc1eca4fb29bb39eaee8.png" alt="">
|
||||
|
||||
既然有锁等待,那我们自然要查一下锁关系,看看是什么在等待锁。在看具体的锁关系之前,我们也来查看一下应用日志。
|
||||
|
||||
为什么要看应用日志呢?因为对于数据库来说,锁是为了保护数据的一致性,而产生锁的事务自然是从应用中来的。按照这样的逻辑,我们在MySQL中和在应用中看到的事务SQL,应该是对应的。而现在我们只看到了MySQL中的锁,还不知道在应用中会是什么样子,所以,我们要看一下应用的日志。
|
||||
|
||||
这里温馨提醒一句:**在这个时候,<strong><strong>我们**</strong>还**<strong>需要注意,不要用**</strong>重压力工具中的某些具有唯一性的参数化数据</strong>**。****因为当参数化数据用重了,在数据库中执行update语句也照样会出现锁。**
|
||||
|
||||
在查看了应用日志之后,我们看到如下信息:
|
||||
|
||||
```
|
||||
[2021-02-06 00:46:59.059] [org.apache.juli.logging.DirectJDKLog] [http-nio-8086-exec-72] [175] [ERROR] Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.CannotAcquireLockException:
|
||||
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
|
||||
### The error may involve com.dunshan.mall.mapper.OmsOrderMapper.insert-Inline
|
||||
### The error occurred while setting parameters
|
||||
### SQL: insert into oms_order (member_id, coupon_id, order_sn, create_time, member_username, total_amount, pay_amount, freight_amount, promotion_amount, integration_amount, coupon_amount, discount_amount, pay_type, source_type, status, order_type, delivery_company, delivery_sn, auto_confirm_day, integration, growth, promotion_info, bill_type, bill_header, bill_content, bill_receiver_phone, bill_receiver_email, receiver_name, receiver_phone, receiver_post_code, receiver_province, receiver_city, receiver_region, receiver_detail_address, note, confirm_status, delete_status, use_integration, payment_time, delivery_time, receive_time, comment_time, modify_time) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
|
||||
; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction] with root cause
|
||||
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
|
||||
|
||||
```
|
||||
|
||||
你看,连一个insert都会报lock_wait,这显然是出现表级锁了。因为insert本身是不会出现表级锁的,所以应该还有其他的信息。我们接着看日志,果然,又看到如下信息:
|
||||
|
||||
```
|
||||
[2021-02-06 01:00:51.051] [org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler] [scheduling-1] [95] [ERROR] Unexpected error occurred in scheduled task
|
||||
org.springframework.dao.CannotAcquireLockException:
|
||||
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
|
||||
### The error may involve defaultParameterMap
|
||||
### The error occurred while setting parameters
|
||||
### SQL: update oms_order set status=? where id in ( ? )
|
||||
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
|
||||
; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
|
||||
|
||||
```
|
||||
|
||||
你看,其中有update语句,这样一来,逻辑就成立了:我们知道,update是会锁数据的,但是,MySQL用的是InnoDB的引擎。如果update的条件是精确查找,那就应该不会出现表级锁。
|
||||
|
||||
可是,如果update的范围比较大,就会有问题了,因为这会导致insert语句被阻塞。过一会儿之后,你就会看到如下内容:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/68/f9/6877fa4a920aa988a08b3db3b1a63bf9.png" alt="">
|
||||
|
||||
我们看到,所有的insert都在LOCK WAIT状态了,这就是表级锁对insert产生的影响。如果你再查一下锁和锁等待的话,就会看到如下信息:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4f/4d/4f790e6fa173ddbf2172ee11ef42c74d.png" alt="">
|
||||
|
||||
不难看出,lock_mode这一列的值全是X,意思是X锁。我们知道,排他锁(X锁),又叫写锁。图中的锁类型(lock_type)全是RECORD,锁住的是索引,并且索引是GEN_CLUST_INDEX,说明这个锁等待是因为innodb创建的隐藏的聚集索引。
|
||||
|
||||
当一个SQL没有走任何索引时,就会在每一条聚集索引后面加X锁,这和表级锁的现象是一样的,只是在原理上有所不同而已。为了方便描述,我们仍然用“表级锁”来描述。
|
||||
|
||||
要查锁,就得看看是谁持有锁。经过仔细查看上面的INNODB_LOCK_WAIT后,我们确定了这个锁对应的事务ID是157710723,它对应的SQL是:
|
||||
|
||||
```
|
||||
update oms_order set status=4 where id in ( 0 );
|
||||
|
||||
```
|
||||
|
||||
我们去代码中查一下这段update代码:
|
||||
|
||||
```
|
||||
/**
|
||||
* 批量修改订单状态
|
||||
*/
|
||||
int updateOrderStatus(@Param("ids") List<Long> ids,@Param("status") Integer status);
|
||||
|
||||
```
|
||||
|
||||
原来这是一个批量任务的调用,具体逻辑如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8b/9c/8b4d2ea1d59309d1467be3c236f2039c.png" alt="">
|
||||
|
||||
这个批量任务的问题在于,在一个订单表中做批量更新的操作,并且这个批量查询的内容还挺多。因为上面的ID是0,表示订单是未支付的,而未支付的订单在这个表中有很多,所以,在更新时做大范围的查找,会进而导致表级锁的出现。
|
||||
|
||||
这个批量任务的设计明显有问题。你想想,要在订单表中做更新这么大的动作,那也应该是做精准更新,而不是范围更新。其实对于订单的更新逻辑,我们可以选择其他的实现方式。
|
||||
|
||||
锁的原因找到了,我们现在要把范围更新改为非常精准的更新,让它不产生表级锁。修改之后,重新执行场景的结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/68/0d63c40c3a5c25177807fa3612437d68.png" alt="">
|
||||
|
||||
从优化效果来看,TPS已经达到700以上了。对这样一个复杂的接口来说,这已经非常不错了。
|
||||
|
||||
其实,这个订单业务还有很多的优化空间,比如说:
|
||||
|
||||
1. 异步生成订单序列号,然后存放到Redis里,随用随取。
|
||||
1. 批量业务需要重新设计。
|
||||
1. 读写分离之后,对业务代码也做相应更新。
|
||||
1. ……
|
||||
|
||||
由于订单逻辑是电商中的非常复杂的一步,我就不再展开说了,因为再说就超出了性能的范畴。
|
||||
|
||||
## 总结
|
||||
|
||||
在这个接口中,我们遇到了好几个问题。先抛开问题和复杂度不说,我想表达的是,在性能优化过程中,问题是像洋葱一样一个个剥开的。虽然有可能一个优化动作就可以产生很好的效果,但是我们一定不要着急,要慢慢分析一个个问题。
|
||||
|
||||
回顾一下我们对这个接口的所有分析优化过程。在第一阶段中,我们修改线程池产生了效果,但也出现了新问题;在第二阶段中,我们解决了查询大量数据导致内存被耗光的问题;在第三阶段,我们解决了索引的问题;在第四阶段中,我们重新调配了资源,让系统的调度更加合理。
|
||||
|
||||
在第五阶段中,我们定位了方法的时间消耗问题,这一步你要注意,一定要在分析了业务逻辑之后再做相应的优化,不要因一味追求性能的优化效果而纠结。
|
||||
|
||||
在第六阶段中,我们定位了批量任务设计不合理的问题。在正经的批量任务中,批量产生的表级锁和insert的功能点,一定要分开。
|
||||
|
||||
总之,在分析的过程中,我们不要界定问题的边界,遇到什么问题就解决什么问题,不急不燥,不卑不亢。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,请你思考两个问题:
|
||||
|
||||
1. 如何快速定位内存被消耗光的情况?
|
||||
1. 如何快速定位业务逻辑导致的TPS上不去、资源也用不上的情况?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
306
极客时间专栏/高楼的性能工程实战课/基准场景/21 | 支付前查询订单列表:如何分析优化一个固定的技术组件?.md
Normal file
306
极客时间专栏/高楼的性能工程实战课/基准场景/21 | 支付前查询订单列表:如何分析优化一个固定的技术组件?.md
Normal file
@@ -0,0 +1,306 @@
|
||||
<audio id="audio" title="21 | 支付前查询订单列表:如何分析优化一个固定的技术组件?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/00/1e/008bc7cb8406aecbc220b28e6757a11e.mp3"></audio>
|
||||
|
||||
今天我们来分析支付前查询订单列表接口。
|
||||
|
||||
在这节课中,我将带你来看一下对于一个固定的技术组件,分析优化思路应该是怎样的,也就是说组件不是我们开发的,但是又要分析优化它,我们该怎么办?
|
||||
|
||||
此外,我们还会遇到一个问题,就是当数据库的CPU并没有全部用完,而是只用了几颗的时候,我们应该如何具体定向?对此,我们将用到查看数据库本身线程栈的方法,这和前面直接看trx表有所不同。
|
||||
|
||||
下面,我们一起进入今天的内容。
|
||||
|
||||
## 场景运行数据
|
||||
|
||||
对于支付前查询订单列表接口,我们先来看第一次运行的性能场景结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0f/d2/0f78445ef87c78dc2bb856cb25f8d4d2.png" alt="">
|
||||
|
||||
从运行的场景数据来看,这个接口的TPS一开始还是挺高的,达到了800多。但是,响应时间也增加了,瓶颈已经出现。我们只要知道瓶颈在哪,就能知道这个接口有没有优化空间。
|
||||
|
||||
根据高老师的分析逻辑,在正式分析之前,我们看一下架构图。
|
||||
|
||||
## 架构图
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/64/04/64df3dd5f9ae4e5d8e4d4a1e33130d04.png" alt="">
|
||||
|
||||
这张架构图是非常清楚的,可以看到,当前接口的逻辑为:Gateway - Order - Member,其中也使用到了MySQL和Redis。
|
||||
|
||||
下面我们来看看,响应时间消耗到哪里去了。
|
||||
|
||||
## 拆分响应时间
|
||||
|
||||
<li>
|
||||
<p>Gateway:<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/a9/a9/a977967f392a21e349a4190864cf5fa9.png" alt=""></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Order:<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/1b/b3e37c3ed659a04d3673ce8371edd11b.png" alt=""></p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Member:<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/1a/fb52b79c98b57408482dc8fc43d1e61a.png" alt=""></p>
|
||||
</li>
|
||||
|
||||
从响应时间的分布来看,Gateway(网关)上消耗的时间要长一些。所以,我们接下来得从Gateway下手,分析一下到底是哪里消耗了时间。
|
||||
|
||||
## 第一阶段
|
||||
|
||||
### 全局监控分析
|
||||
|
||||
按照“**先看全局监控,后看定向监控**”的逻辑,我们先看这个接口的全局监控:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/e8/5895f1645bc9524f69374c0cd7e06fe8.png" alt="">
|
||||
|
||||
由于Gateway消耗的响应时间长,我们看过全局监控视图之后,要判断出Gateway在哪个worker上:
|
||||
|
||||
```
|
||||
[root@k8s-master-2 ~]# kubectl get pods -o wide | grep gateway
|
||||
gateway-mall-gateway-757659dbc9-tdwnm 1/1 Running 0 3d16h 10.100.79.96 k8s-worker-4 <none> <none>
|
||||
[root@k8s-master-2 ~]#
|
||||
|
||||
```
|
||||
|
||||
这个Gateway服务在worker-4上,同时,在全局监控图上可以看到,虽然Gateway只消耗了70%的CPU,但它还是消耗了最多的响应时间。既然这样,我们就要关注一下Gateway的线程状态,看看它在处理什么。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
在做定向监控时,我们先来看一下线程的CPU消耗:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/74/fb/745fa375415efdbe60cbe04efacb0dfb.png" alt="">
|
||||
|
||||
通过上图可以看到,在Gateway中有两类重要的工作线程,分别是reactor-http-epoll和boundedElastic。
|
||||
|
||||
在官方的说明中提到,reactor-http-epoll线程的设置最好与CPU个数一致。我们当前的reactor-http-epoll线程是4个,而这个worker有6C,所以还能增加两个,增加多了意义也不大。至于boundedElastic,它是有边界的弹性线程池,默认为CPU核x10,也没啥可优化的。
|
||||
|
||||
我们再持续看一会儿Gateway服务中的线程所消耗的时间比例,看一下方法级的时间消耗有没有异常的情况,也就是比例非常高的,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0c/e2/0cff8cd7771508150e0d341cee1b9de2.png" alt="">
|
||||
|
||||
你看,当前的执行方法也都没啥异常的。
|
||||
|
||||
现在我们就把线程增加到6个,看能不能把CPU用高一点。如果CPU用多了之后,仍然是Gateway消耗的时间长,那我们就只有再继续加CPU了。
|
||||
|
||||
请你注意,在性能项目中,不要轻易给出加CPU这样的建议。一定要在你分析了逻辑之后,确定没有其他优化空间了,再给这样的建议。
|
||||
|
||||
### 优化效果
|
||||
|
||||
我们来看一下优化效果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/32/a2/32faf723ede915a7ca66547c591522a2.png" alt="">
|
||||
|
||||
通过回归测试,我们看到TPS有一点增加,只是在图的后半段(由于在测试过程中,Gateway重启过,前面的TPS就当是预热了)增加的并不明显,大概有50多TPS的样子。不过,也算是有了效果。
|
||||
|
||||
我们优化到这里并没有结束,因为在查看各个Worker的过程中,我还发现一个奇怪的现象,那就是数据库里有两个CPU的使用率非常高。下面我们来扒一扒。
|
||||
|
||||
## 第二阶段
|
||||
|
||||
### 全局监控分析
|
||||
|
||||
因为前面优化的效果并不怎么样,所以我们要重新开始分析。让我们从全局监控开始:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8b/21/8bf119287eab532f8f01bf09ffff6721.png" alt="">
|
||||
|
||||
看起来倒是没啥,数据库所在的worker-1也不见有什么大的资源消耗。
|
||||
|
||||
请你注意,我在文章中经常用这个界面来看全局监控的数据。但这并不是说,我只看这个界面。当我在这个界面中看不到明显的问题点时,我也会去看一些命令,像top/vmstat等,这和我一直说的全局监控的完整计数器有关。因此,你的脑袋里要有全局监控计数器的视图,然后才能真正看全第一层的计数器。
|
||||
|
||||
我们再来看数据库所在的worker上的top数据,发现了这样的现象:
|
||||
|
||||
```
|
||||
bash-4.2$ top
|
||||
top - 09:57:43 up 3 days, 17:54, 0 users, load average: 4.40, 3.57, 3.11
|
||||
Tasks: 11 total, 1 running, 9 sleeping, 1 stopped, 0 zombie
|
||||
%Cpu0 : 8.0 us, 4.7 sy, 0.0 ni, 84.3 id, 0.0 wa, 0.0 hi, 2.2 si, 0.7 st
|
||||
%Cpu1 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu2 : 6.5 us, 4.4 sy, 0.0 ni, 85.5 id, 0.0 wa, 0.0 hi, 2.2 si, 1.5 st
|
||||
%Cpu3 : 7.8 us, 5.7 sy, 0.0 ni, 83.7 id, 0.0 wa, 0.0 hi, 2.1 si, 0.7 st
|
||||
%Cpu4 : 96.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 4.0 si, 0.0 st
|
||||
%Cpu5 : 7.0 us, 4.0 sy, 0.0 ni, 84.9 id, 0.0 wa, 0.0 hi, 2.6 si, 1.5 st
|
||||
KiB Mem : 16265992 total, 1203032 free, 6695156 used, 8367804 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 9050344 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
1 mysql 20 0 8272536 4.7g 13196 S 248.8 30.5 6184:36 mysqld
|
||||
|
||||
```
|
||||
|
||||
可以非常明显地看到,有两个CPU的使用率高,那我们就来定向分析下数据库。
|
||||
|
||||
在此之前,我们不妨心中默念10遍“只要思路不乱,任何问题都是一盘菜”,因为保持思路清晰非常重要。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
我们要定向分析数据库,可是在数据库上又不是所有的CPU使用率都高,所以,我们要来看一下数据库线程到底在做什么动作。有了上面的进程信息之后,我们再深入到线程级:
|
||||
|
||||
```
|
||||
bash-4.2$ top -Hp 1
|
||||
top - 09:56:40 up 3 days, 17:53, 0 users, load average: 3.05, 3.30, 3.01
|
||||
Threads: 92 total, 2 running, 90 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 5.4 us, 2.9 sy, 0.0 ni, 89.2 id, 0.0 wa, 0.0 hi, 2.2 si, 0.4 st
|
||||
%Cpu1 : 99.7 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.3 st
|
||||
%Cpu2 : 5.4 us, 3.2 sy, 0.0 ni, 88.2 id, 0.0 wa, 0.0 hi, 2.5 si, 0.7 st
|
||||
%Cpu3 : 6.3 us, 4.2 sy, 0.0 ni, 87.0 id, 0.0 wa, 0.0 hi, 2.1 si, 0.4 st
|
||||
%Cpu4 : 96.3 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 3.7 si, 0.0 st
|
||||
%Cpu5 : 4.0 us, 2.5 sy, 0.0 ni, 91.0 id, 0.0 wa, 0.0 hi, 1.8 si, 0.7 st
|
||||
KiB Mem : 16265992 total, 1205356 free, 6692736 used, 8367900 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 9052664 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
311 mysql 20 0 8272536 4.7g 13196 R 99.9 30.5 18:20.34 mysqld
|
||||
241 mysql 20 0 8272536 4.7g 13196 R 99.7 30.5 1906:40 mysqld
|
||||
291 mysql 20 0 8272536 4.7g 13196 S 3.3 30.5 15:49.21 mysqld
|
||||
319 mysql 20 0 8272536 4.7g 13196 S 3.0 30.5 11:50.34 mysqld
|
||||
355 mysql 20 0 8272536 4.7g 13196 S 3.0 30.5 13:01.53 mysqld
|
||||
265 mysql 20 0 8272536 4.7g 13196 S 2.7 30.5 18:17.48 mysqld
|
||||
307 mysql 20 0 8272536 4.7g 13196 S 2.7 30.5 16:47.77 mysqld
|
||||
328 mysql 20 0 8272536 4.7g 13196 S 2.7 30.5 15:34.92 mysqld
|
||||
335 mysql 20 0 8272536 4.7g 13196 S 2.7 30.5 8:55.38 mysqld
|
||||
316 mysql 20 0 8272536 4.7g 13196 S 2.3 30.5 14:38.68 mysqld
|
||||
350 mysql 20 0 8272536 4.7g 13196 S 2.3 30.5 10:37.94 mysqld
|
||||
233 mysql 20 0 8272536 4.7g 13196 S 2.0 30.5 14:19.32 mysqld
|
||||
279 mysql 20 0 8272536 4.7g 13196 S 2.0 30.5 19:51.80 mysqld
|
||||
318 mysql 20 0 8272536 4.7g 13196 S 2.0 30.5 11:34.62 mysqld
|
||||
331 mysql 20 0 8272536 4.7g 13196 S 2.0 30.5 11:46.94 mysqld
|
||||
375 mysql 20 0 8272536 4.7g 13196 S 2.0 30.5 1:29.22 mysqld
|
||||
300 mysql 20 0 8272536 4.7g 13196 S 1.7 30.5 17:45.26 mysqld
|
||||
380 mysql 20 0 8272536 4.7g 13196 S 1.7 30.5 1:24.32 mysqld
|
||||
|
||||
```
|
||||
|
||||
你看,只有两个MySQL的线程在使用CPU。到了这一步,你可能会想,接下来去查SQL!虽然可能就是SQL的问题,但我还是建议你**找到相应的证据。**
|
||||
|
||||
由于MySQL是用C语言写的,那我们就用gstack(这是一个装了GDB之后就会有的命令)打印一下这两个MySQL的栈看看具体的函数。我们把那两个PID(311、241)的栈拿出来之后,看到如下信息:
|
||||
|
||||
```
|
||||
Thread 59 (Thread 0x7f1d60174700 (LWP 241)):
|
||||
#0 0x000055a431fefea9 in JOIN_CACHE::read_record_field(st_cache_field*, bool) ()
|
||||
#1 0x000055a431ff01ca in JOIN_CACHE::read_some_record_fields() ()
|
||||
#2 0x000055a431ff070f in JOIN_CACHE::get_record() ()
|
||||
#3 0x000055a431ff2a92 in JOIN_CACHE_BNL::join_matching_records(bool) ()
|
||||
#4 0x000055a431ff18f0 in JOIN_CACHE::join_records(bool) ()
|
||||
#5 0x000055a431e397c0 in evaluate_join_record(JOIN*, QEP_TAB*) ()
|
||||
#6 0x000055a431e3f1a5 in sub_select(JOIN*, QEP_TAB*, bool) ()
|
||||
#7 0x000055a431e37a90 in JOIN::exec() ()
|
||||
#8 0x000055a431eaa0ba in handle_query(THD*, LEX*, Query_result*, unsigned long long, unsigned long long) ()
|
||||
#9 0x000055a43194760d in execute_sqlcom_select(THD*, TABLE_LIST*) ()
|
||||
#10 0x000055a431e6accf in mysql_execute_command(THD*, bool) ()
|
||||
#11 0x000055a431e6d455 in mysql_parse(THD*, Parser_state*) ()
|
||||
#12 0x000055a431e6e3b6 in dispatch_command(THD*, COM_DATA const*, enum_server_command) ()
|
||||
#13 0x000055a431e6fc00 in do_command(THD*) ()
|
||||
#14 0x000055a431f33938 in handle_connection ()
|
||||
#15 0x000055a4320e66d4 in pfs_spawn_thread ()
|
||||
#16 0x00007f1e8f1fcdd5 in start_thread () from /lib64/libpthread.so.0
|
||||
#17 0x00007f1e8d3cc02d in clone () from /lib64/libc.so.6
|
||||
Thread 41 (Thread 0x7f1d585e0700 (LWP 311)):
|
||||
#0 0x000055a4319dbe44 in Item_field::val_int() ()
|
||||
#1 0x000055a4319fb839 in Arg_comparator::compare_int_signed() ()
|
||||
#2 0x000055a4319fbd9b in Item_func_eq::val_int() ()
|
||||
#3 0x000055a431ff24ab in JOIN_CACHE::check_match(unsigned char*) ()
|
||||
#4 0x000055a431ff26ec in JOIN_CACHE::generate_full_extensions(unsigned char*) ()
|
||||
#5 0x000055a431ff2ab4 in JOIN_CACHE_BNL::join_matching_records(bool) ()
|
||||
#6 0x000055a431ff18f0 in JOIN_CACHE::join_records(bool) ()
|
||||
#7 0x000055a431e397c0 in evaluate_join_record(JOIN*, QEP_TAB*) ()
|
||||
#8 0x000055a431e3f1a5 in sub_select(JOIN*, QEP_TAB*, bool) ()
|
||||
#9 0x000055a431e37a90 in JOIN::exec() ()
|
||||
#10 0x000055a431eaa0ba in handle_query(THD*, LEX*, Query_result*, unsigned long long, unsigned long long) ()
|
||||
#11 0x000055a43194760d in execute_sqlcom_select(THD*, TABLE_LIST*) ()
|
||||
#12 0x000055a431e6accf in mysql_execute_command(THD*, bool) ()
|
||||
#13 0x000055a431e6d455 in mysql_parse(THD*, Parser_state*) ()
|
||||
#14 0x000055a431e6e3b6 in dispatch_command(THD*, COM_DATA const*, enum_server_command) ()
|
||||
#15 0x000055a431e6fc00 in do_command(THD*) ()
|
||||
#16 0x000055a431f33938 in handle_connection ()
|
||||
#17 0x000055a4320e66d4 in pfs_spawn_thread ()
|
||||
#18 0x00007f1e8f1fcdd5 in start_thread () from /lib64/libpthread.so.0
|
||||
#19 0x00007f1e8d3cc02d in clone () from /lib64/libc.so.6
|
||||
|
||||
```
|
||||
|
||||
很明显,是两个execute_sqlcom_select函数,也就是两个select语句。我们接着往上看栈,还可以看到是JOIN函数。既然是select语句中的JOIN,那我们直接去找SQL语句就好了。
|
||||
|
||||
因此,我们直接去查innodb_trx表,看看正在执行SQL有没有消耗时间长的。你也许会执行show processlist之类的命令,但是为了看全SQL,我还是建议你直接查trx表。由于我们使用的thread_handling是默认的one-thread-per-connection,操作系统的线程和mysql里的线程都是一一对应的。所以,我们在这里直接查trx表不会有什么误判。
|
||||
|
||||
通过查找innodb_trx表,我们看到了这样两个SQL消耗时间较长,列在这里:
|
||||
|
||||
```
|
||||
-- sql1
|
||||
SELECT
|
||||
count(*)
|
||||
FROM
|
||||
oms_order o
|
||||
LEFT JOIN oms_order_item ot ON o.id = ot.order_id
|
||||
WHERE
|
||||
o. STATUS = 0
|
||||
AND o.create_time < date_add(NOW(), INTERVAL - 120 MINUTE)
|
||||
LIMIT 0,
|
||||
1000
|
||||
|
||||
|
||||
-- sql2:
|
||||
SELECT
|
||||
o.id,
|
||||
o.order_sn,
|
||||
o.coupon_id,
|
||||
o.integration,
|
||||
o.member_id,
|
||||
o.use_integration,
|
||||
ot.id ot_id,
|
||||
ot.product_name ot_product_name,
|
||||
ot.product_sku_id ot_product_sku_id,
|
||||
ot.product_sku_code ot_product_sku_code,
|
||||
ot.product_quantity ot_product_quantity
|
||||
FROM
|
||||
oms_order o
|
||||
LEFT JOIN oms_order_item ot ON o.id = ot.order_id
|
||||
WHERE
|
||||
o. STATUS = 0
|
||||
AND o.create_time < date_add(NOW(), INTERVAL - 120 MINUTE)
|
||||
|
||||
```
|
||||
|
||||
我们提到多次,要想看SQL慢,就得看SQL对应的执行计划(在MySQL中,如果执行计划看得不清楚,还可以看Profile信息)。这两个SQL对应的执行计划如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9e/61/9e41ba801dd20e46ff00386067670f61.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/95/31/95b6efa2ddda492a8bf61abe8e483831.png" alt="">
|
||||
|
||||
依然是常见的全表扫描。看到这里,你是不是有一种索然无味的感觉?但是,我们还是需要知道这两个语句为什么会产生。
|
||||
|
||||
其实,支付前查询订单列表这个接口并没有用到这两个SQL。于是,我到代码中看了一下这两个SQL的生成过程,反向查找到如下代码:
|
||||
|
||||
```
|
||||
@Scheduled(cron = "0 0/20 * ? * ?")
|
||||
private void cancelTimeOutOrder(){
|
||||
Integer count = portalOrderService.cancelTimeOutOrder();
|
||||
LOGGER.info("取消订单释放锁定库存:{}",count);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
很显然,这是一个定时计划,每20分钟执行一次。到这里,问题就很清楚了,原来是定时任务调用了这两个批量的查询语句,导致了两个CPU使用率达到100%,并且也持续了一段时间。
|
||||
|
||||
像这样的定时任务,我们要格外关注一下,注意把它和实时业务分开部署和处理,减少批量业务对实时业务的资源争用。如果放在一起处理,那就要控制好要批量查询的数据量级,让SQL的查询变得合理。
|
||||
|
||||
由于数据库可用的CPU比较多,这个定时任务对我们的TPS并没有产生什么明显的影响,在这里我们不用做什么处理,以后注意分开就好了。
|
||||
|
||||
## 总结
|
||||
|
||||
在这节课中,虽然我们的优化并没有让TPS明显增加,但是因为分析的技术细节不一样,我也非常完整地记录了整个分析过程。
|
||||
|
||||
在第一阶段的分析中,我们运用的还是之前提到的分析思路。不同点在于,对于一个非常成熟的固定组件,我们要想优化它,就要去了解它的架构,找到它的相关性能参数。因为在实际的性能项目中,面对这样的组件,我们往往没有时间去纠结内部的实现,需要非常快速地作出判断。如果时间允许,你倒是可以慢慢折腾。
|
||||
|
||||
其实理解一个技术组件的原理,并没有想像中的那么高不可攀、深不可测,只要耐心看下去,你总会成长。
|
||||
|
||||
在第二阶段的分析中,我们由某几个CPU高的现象分析到了具体的SQL问题。这个过程虽然简单,但是从这个问题上,我们可以看出这个系统还有很多的优化空间,比如说主从分离、定时任务拆为单独的服务等等。不过,在我们的性能分析中,重点仍然是我跟你一直灌输的分析思路,希望你记在心里了。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,我给你留两道题,请你思考一下:
|
||||
|
||||
1. 为什么要看全部的全局监控计数器?
|
||||
1. 单CPU高时,如何定位具体的问题点?你有什么思路?
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
407
极客时间专栏/高楼的性能工程实战课/基准场景/22 | 支付订单信息:如何高效解决for循环产生的内存溢出?.md
Normal file
407
极客时间专栏/高楼的性能工程实战课/基准场景/22 | 支付订单信息:如何高效解决for循环产生的内存溢出?.md
Normal file
@@ -0,0 +1,407 @@
|
||||
<audio id="audio" title="22 | 支付订单信息:如何高效解决for循环产生的内存溢出?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/99/25/99fac6baaba67668a5c5ac25353c4225.mp3"></audio>
|
||||
|
||||
你好,我是高楼。
|
||||
|
||||
今天,我们来优化支付订单接口。通过这个接口,我们来看看怎么高效解决for循环产生的内存溢出问题。
|
||||
|
||||
对于JVM内存溢出或泄露来说,通常性能人员都能定位到一个应用hang住了。但是,要想进一步判断出应用hang住的原因,并没有那么容易做到。因为内存大时做堆Dump比较费时,更重要的一点是,要想把堆里面的对象和栈关联起来是需要足够的经验和时间的。这也是其中的难点之一。
|
||||
|
||||
这节课我就带你来看看怎么解决这个难点。
|
||||
|
||||
不过,在此之前,我们会先处理一个熟悉的问题,就是数据库表加索引。很显然,我们在测试这个接口时又遇到它了。虽然我在[第16讲](https://time.geekbang.org/column/article/367285)中给你重点讲过这个问题,但是这一次的每秒全表扫描比之前要高得多。通过这次的讲解,我希望你能明白只要存在全表扫描,CPU消耗很快就会达到100%。同时,也希望你能借此看清楚全表扫描对CPU消耗的影响。
|
||||
|
||||
## 场景运行数据
|
||||
|
||||
首先,我们来运行一下场景:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/02/e1/02f5facd8d0196ae07c223b8dc1e4ae1.png" alt="">
|
||||
|
||||
这是一个典型的TPS太低、响应时间不断上升的性能瓶颈,对于这种瓶颈的分析逻辑,我在前面的课程里已经写过很多次了,相信你已经掌握。下面我们来看一下具体的问题是什么。
|
||||
|
||||
## 架构图
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/72/8f/72b7b04b1e2323d4fae9fcc30cb3be8f.png" alt="">
|
||||
|
||||
这个接口的链路比较简单:User - Gateway - Order - MySQL,我们大概记在脑子里就好。
|
||||
|
||||
## 第一阶段
|
||||
|
||||
在这里我就不拆分时间了,我们直接来看全局监控。因为第一阶段的问题相对来说比较简单,只是性能瓶颈的表现形式和之前不太一样。
|
||||
|
||||
### 全局监控分析
|
||||
|
||||
全局监控的数据如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0a/3a/0a2471dca622df4a813e8c06d31e5b3a.png" alt="">
|
||||
|
||||
看到这张图,你是不是有一种终于见到典型性能瓶颈的感觉?CPU使用率这么高!那还不简单?打栈看代码呀!
|
||||
|
||||
不过,我们得先查一下是什么东西导致k8s-worker-1的CPU使用率这么高的。这个worker上运行的服务如下:
|
||||
|
||||
```
|
||||
[root@k8s-master-2 ~]# kubectl get pods -o wide | grep worker-1
|
||||
mysql-min-6685c9ff76-4m5xr 1/1 Running 0 4d23h 10.100.230.14 k8s-worker-1 <none> <none>
|
||||
skywalking-es-init-ls7j5 0/1 Completed 0 4d11h 10.100.230.18 k8s-worker-1 <none> <none>
|
||||
[root@k8s-master-2 ~]#
|
||||
|
||||
```
|
||||
|
||||
可以看到,有两个服务在这个worker上跑着,一个是初始化容器,另一个是MySQL。初始化容器已经是完成的状态,那CPU使用率高肯定是因为MySQL了。因此,我们就进到容器中,执行下top,看看资源消耗到什么程度。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a4/5f/a420ef3454ee13f4f37834eyy0e5045f.png" alt="">
|
||||
|
||||
什么情况?CPU使用率这么高吗?既然这样,我们就得来查一下MySQL的全局监控了。
|
||||
|
||||
在这里,**查看全局监控其实是分析MySQL的必要步骤**。如果直接查看trx表,中间其实是有些跳跃的,因为查看MySQL的全局监控数据,才是承上启下的一步。我现在把这个过程写全一些,以免你产生困惑。
|
||||
|
||||
于是,我们执行一个mysqlreport命令,看看mysql的全局监控数据是怎样的。这里我截取了其中的一些重要信息:
|
||||
|
||||
```
|
||||
__ Questions ___________________________________________________________
|
||||
Total 307.93M 912.0/s
|
||||
+Unknown 201.91M 598.0/s %Total: 65.57
|
||||
DMS 43.20M 128.0/s 14.03
|
||||
Com_ 32.90M 97.5/s 10.69
|
||||
QC Hits 29.91M 88.6/s 9.71
|
||||
COM_QUIT 389 0.0/s 0.00
|
||||
Slow 20 ms 273.66k 0.8/s 0.09 %DMS: 0.63 Log:
|
||||
DMS 43.20M 128.0/s 14.03
|
||||
SELECT 32.39M 95.9/s 10.52 74.98
|
||||
INSERT 10.64M 31.5/s 3.46 24.63
|
||||
UPDATE 170.15k 0.5/s 0.06 0.39
|
||||
REPLACE 0 0/s 0.00 0.00
|
||||
DELETE 0 0/s 0.00 0.00
|
||||
Com_ 32.90M 97.5/s 10.69
|
||||
set_option 21.98M 65.1/s 7.14
|
||||
commit 10.70M 31.7/s 3.48
|
||||
admin_comma 137.68k 0.4/s 0.04
|
||||
|
||||
|
||||
__ SELECT and Sort _____________________________________________________
|
||||
Scan 20.04M 59.4/s %SELECT: 61.88
|
||||
Range 0 0/s 0.00
|
||||
Full join 32 0.0/s 0.00
|
||||
Range check 0 0/s 0.00
|
||||
Full rng join 0 0/s 0.00
|
||||
Sort scan 120 0.0/s
|
||||
Sort range 2.41k 0.0/s
|
||||
Sort mrg pass 0 0/s
|
||||
|
||||
```
|
||||
|
||||
你看,DMS中的select占比比较大。其实,如果只是select的占比比较大的话,倒不是什么大事,关键是在下面的数据中还有一个Scan(全表扫描),而全表扫描是典型的性能问题点。
|
||||
|
||||
看到这里,我想你应该非常清楚我接下来的套路了吧,就是找SQL,看执行计划,然后确定优化方案。如果你不太清楚,可以再看一下[第](https://time.geekbang.org/column/article/367285)[16](https://time.geekbang.org/column/article/367285)[讲](https://time.geekbang.org/column/article/367285)或[第](https://time.geekbang.org/column/article/370723)[20](https://time.geekbang.org/column/article/370723)[讲](https://time.geekbang.org/column/article/370723),其中都有描述。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
于是,我们现在进入到了定向监控分析阶段。通过查看innodb_trx表,我们看到了SQL中执行慢的语句,它的执行计划如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/82/4c/82c5c3ba11f32184972ef2ce32127f4c.png" alt="">
|
||||
|
||||
这是很典型的全表扫描,虽然数据量并不大,但是我们也要添加索引。添加索引的语句如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/c7/e510e1a94a6d59c11204d062da9ed4c7.png" alt="">
|
||||
|
||||
这里你要注意一下,在创建索引的时候,如果数据量太大,创建索引可能会卡住很长时间,这要取决于机器单CPU的能力。
|
||||
|
||||
### 优化效果
|
||||
|
||||
添加索引之后,我们直接来看一下优化效果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/1a/5d245afc0a324280d9d93c0102a0f81a.png" alt="">
|
||||
|
||||
你看,TPS要上千了!
|
||||
|
||||
其实,对于SQL的优化,如果我们只是加一个索引,那就算是非常简单的步骤了,并且效果也会非常好,TPS增加上千倍、甚至上万倍都有可能。可是,如果优化涉及到了业务逻辑,那就麻烦一些了。
|
||||
|
||||
如果你觉得这节课只是为了给你讲一个加索引的案例,那你就有些单纯了。下面,我们来看一个复杂的问题。
|
||||
|
||||
## 第二阶段
|
||||
|
||||
在我接着执行压力的时候,看到了一个非常有意思的情况,我们来一起折腾折腾!
|
||||
|
||||
### 场景运行数据
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/a4/291ee16344ffbdd94cd0ae1yy62a11a4.png" alt="">
|
||||
|
||||
你看这张图,在压力持续大概6分钟之后,TPS不稳定也就算了,居然还掉下来了!你掉下来也就算了,居然还断开了!你断开了也就算了,响应时间居然也不增加!
|
||||
|
||||
这可怎么分析呢?想要拆分时间都没有一个适合的理由呀!
|
||||
|
||||
这时候,就得用上哥的性能分析决策树了。我们把相关的全局监控计数器都看一看,一层层查下去,还好这个接口的逻辑链路也不怎么长。
|
||||
|
||||
### 全局监控分析
|
||||
|
||||
按照高老师的习惯,我们首先来看全局监控:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/78/62412b8c7159df58cbdcce7b196yya78.png" alt="">
|
||||
|
||||
从这张图上,我们什么也没看出来。所以,我们接着查性能分析决策树,一层层地看下去。当看到Order的GC健康状态时,我们看到了下面这些数据:
|
||||
|
||||
```
|
||||
[root@svc-mall-order-568bd9b79-twhcw /]# jstat -gcutil 1 1s
|
||||
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
|
||||
100.00 0.00 100.00 100.00 95.06 92.82 1182 34.966 495 3279.704 3314.670
|
||||
100.00 0.00 100.00 100.00 95.06 92.82 1182 34.966 495 3279.704 3314.670
|
||||
100.00 0.00 100.00 100.00 95.06 92.82 1182 34.966 495 3279.704 3314.670
|
||||
100.00 0.00 100.00 100.00 95.06 92.82 1182 34.966 495 3279.704 3314.670
|
||||
90.88 0.00 100.00 100.00 95.08 92.82 1182 34.966 495 3286.621 3321.58
|
||||
100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586
|
||||
100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586
|
||||
100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586
|
||||
100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586
|
||||
100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586
|
||||
100.00 0.00 100.00 100.00 95.08 92.82 1182 34.966 496 3286.621 3321.586
|
||||
...........................
|
||||
|
||||
```
|
||||
|
||||
有没有感到绝望?内存泄漏了!年轻代和年老代的内存都用到100%了!即便是FullGC之后,内存也没回收下来,可怜呀。
|
||||
|
||||
### 定向监控分析
|
||||
|
||||
既然是内存被用完了,那我们自然要查一下是什么样的对象把内存用完了。所以,我们进到容器里面,执行jmap -histo 1|more看一眼:
|
||||
|
||||
```
|
||||
num #instances #bytes class name
|
||||
----------------------------------------------
|
||||
1: 49727866 1397691896 [
|
||||
2: 12426103 795269200 [[
|
||||
3: 12426038 397633216 com.mysql.cj.protocol.a.result.ByteArrayRo
|
||||
4: 2002596 384498432 com.dunshan.mall.order.domain.OmsOrderDetai
|
||||
5: 12426082 198817312 com.mysql.cj.protocol.a.MysqlTextValueDecode
|
||||
6: 2070085 182840264 [Ljava.lang.Object
|
||||
7: 6008660 144207840 java.lang.Lon
|
||||
8: 2207452 132116320 [
|
||||
9: 4072895 97749480 java.util.ArrayLis
|
||||
10: 2002690 80107600 org.apache.ibatis.cache.CacheKe
|
||||
11: 2039613 65267616 java.util.HashMap$Nod
|
||||
12: 2197616 52742784 java.lang.Strin
|
||||
13: 14736 23246672 [Ljava.util.HashMap$Node
|
||||
14: 36862 3243856 java.lang.reflect.Metho
|
||||
15: 97195 3110240 java.util.concurrent.ConcurrentHashMap$Nod
|
||||
16: 62224 2986752 java.util.HashMa
|
||||
17: 19238 2452264 [
|
||||
18: 21482 2360328 java.lang.Clas
|
||||
19: 26958 1078320 java.util.LinkedHashMap$Entr
|
||||
...........................
|
||||
|
||||
```
|
||||
|
||||
从中我们似乎找到了问题点。你看,这里面有一个MySQL的result占的内存还挺大,同时,在它的下面我们也看到了OmsOrderDetail类,这个类是用来在数据库中查询订单的详细信息的。
|
||||
|
||||
从逻辑上来讲,我们看订单的详细信息,实际上是想查询数据库中的信息,进而把查询出来的数据放到应用的内存中。所以,MySQL的result查的数据越多,就会导致应用的JVM内存消耗越大。
|
||||
|
||||
你也许会想,接下来是不是直接去看OmsOrderDetail的代码就可以了?你可以去看,但是,我们这个案例并没有那么直接。因为我们已经知道代码了,逻辑也梳理清楚了,所以,再去查看代码其实也看不出什么问题来。
|
||||
|
||||
那为什么JVM内存消耗会高呢?这里我们就要查一下线程在做什么动作了:
|
||||
|
||||
```
|
||||
-- top
|
||||
[root@k8s-worker-3 ~]# docker exec -it 66d3639cf4a8 /bin/bash
|
||||
[root@svc-mall-order-568bd9b79-twhcw /]# top
|
||||
top - 16:10:50 up 11 days, 2:37, 0 users, load average: 3.92, 4.77, 3.35
|
||||
Tasks: 4 total, 1 running, 3 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 46.7 us, 8.6 sy, 0.0 ni, 43.6 id, 0.0 wa, 0.0 hi, 0.7 si, 0.3 st
|
||||
%Cpu1 : 23.3 us, 9.2 sy, 0.0 ni, 66.1 id, 0.0 wa, 0.0 hi, 1.0 si, 0.3 st
|
||||
%Cpu2 : 50.0 us, 7.2 sy, 0.3 ni, 41.4 id, 0.0 wa, 0.0 hi, 0.7 si, 0.3 st
|
||||
%Cpu3 : 46.4 us, 8.5 sy, 0.0 ni, 43.7 id, 0.0 wa, 0.0 hi, 1.0 si, 0.3 st
|
||||
%Cpu4 : 50.5 us, 8.0 sy, 0.0 ni, 41.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu5 : 50.2 us, 3.1 sy, 0.0 ni, 46.1 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st
|
||||
KiB Mem : 16265992 total, 171760 free, 9077080 used, 7017152 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 6676508 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
1 root 20 0 8788300 4.2g 13860 S 127.9 27.1 115:17.15 java
|
||||
575 root 20 0 11828 1776 1328 S 0.0 0.0 0:00.01 sh
|
||||
789 root 20 0 11964 1980 1484 S 0.0 0.0 0:00.02 bash
|
||||
802 root 20 0 56232 2012 1432 R 0.0 0.0 0:00.05 to
|
||||
|
||||
|
||||
-- top -Hp 1
|
||||
top - 16:11:39 up 11 days, 2:38, 0 users, load average: 8.87, 6.09, 3.87
|
||||
Threads: 85 total, 1 running, 84 sleeping, 0 stopped, 0 zombie
|
||||
%Cpu0 : 55.6 us, 7.1 sy, 0.0 ni, 36.6 id, 0.0 wa, 0.0 hi, 0.3 si, 0.3 st
|
||||
%Cpu1 : 41.3 us, 3.8 sy, 0.0 ni, 54.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu2 : 30.4 us, 9.9 sy, 0.0 ni, 59.4 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
|
||||
%Cpu3 : 60.3 us, 6.7 sy, 0.0 ni, 32.7 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
|
||||
%Cpu4 : 21.2 us, 9.2 sy, 0.0 ni, 69.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
|
||||
%Cpu5 : 45.6 us, 10.1 sy, 0.3 ni, 43.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.3 st
|
||||
KiB Mem : 16265992 total, 197656 free, 9071444 used, 6996892 buff/cache
|
||||
KiB Swap: 0 total, 0 free, 0 used. 6681848 avail Mem
|
||||
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
7 root 20 0 8788300 4.2g 13836 R 96.0 27.1 70:13.42 VM Thread
|
||||
26 root 20 0 8788300 4.2g 13836 S 0.7 27.1 0:05.70 VM Periodic Tas
|
||||
|
||||
```
|
||||
|
||||
执行了上面两个命令之后,你有没有注意到只有一个线程在消耗CPU?根据我们前面查看的GC状态,这个线程应该在忙着做FullGC。我们打印栈信息来确认一下,果然是它!
|
||||
|
||||
```
|
||||
"VM Thread" os_prio=0 tid=0x00007fb18c0f4800 nid=0x7 runnable
|
||||
|
||||
```
|
||||
|
||||
到这里,我们下一步该怎么做就非常清晰了,那就是打印一个堆Dump,来看看对象在内存中的消耗比例。
|
||||
|
||||
所以,我们现在执行下面这个命令,来生成堆Dump。
|
||||
|
||||
```
|
||||
jmap -dump:format=b,file=文件名 pid
|
||||
|
||||
```
|
||||
|
||||
然后,我们再把生成的堆Dump下载下来,用MAT打开。
|
||||
|
||||
在我打开堆文件的过程中出现了一个小插曲,这也是你需要留意的地方,那就是如果堆Dump的内存太大的话,我们打开堆Dump就会报这样的错:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/be/85/be3dc22bf50e7cdab4c3ea474a050d85.png" alt="">
|
||||
|
||||
这个时候我们就要到MemoryAnalyzer.ini文件中,把JVM的最大值参数-Xmx给调大。-Xmx的默认值是1G,至于要调到多大就要看你想打开多大的文件了。如果-Xmx调整得不够大,还会出现下面这样的错误:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/0e/dac4692b9da8b5680131aa4ea16b470e.png" alt="">
|
||||
|
||||
在我们费了九牛二虎之力,终于打开堆文件后,看到这样的信息:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/7f/1d6964cyy56e5934b8657234ea2cf47f.png" alt="">
|
||||
|
||||
有一段内存居然消耗了2.6G!这个可疑的内存点在上图中也出现了(用MAT或jmap来看对象的内存消耗,是两种不同的手段,你可以自己选择。只是在MAT上,我们可以看到可疑的内存消耗点的提醒,而在jmap中是不会给出这样的提醒的,需要我们自己判断),我们点进去看一眼,看看可疑的内存点里具体的对象是什么:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/02/bc/02cb9099ec17332ab42e2564c2a106bc.png" alt="">
|
||||
|
||||
你看,确实是SQL返回的数据量比较大,在上图的列表中居然有1千多万条记录。我们再把相应的栈展开看看:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4e/94/4e2dea48f6969a4a6f3919bddd4c3594.png" alt="">
|
||||
|
||||
看到OmsOrderDetail这个类了没?你可能会想,是支付订单信息这个接口有问题!但是证明还不足,我们要确定OmsOrderDetail是不是在这个接口中调用或生成的,才能判断出是不是这个接口的问题。
|
||||
|
||||
由于当前使用的接口是paySuccess,我们看一下paySuccess的调用逻辑,发现paySuccess有一个getDetail函数。看到这个“getDetail”,还有其中这个“Detail”字符,你是不是感觉和OmsOrderDetail可以对应上?那我们就来查看一下getDetail对应的代码,看看它和OmsOrderDetail之间是什么关系:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/51/4a/5133a446f6e72549a40daace912f794a.png" alt="">
|
||||
|
||||
不难发现,getDetail是一个OmsOrderDetail类。这么看来,我们的接口确实用到了OmsOrderDetail类,你是不是有一种抓住元凶的兴奋感?别着急,让人无奈的事情总是会出现的,下面我们看一下这段代码对应的SQL语句是一个什么样的逻辑。
|
||||
|
||||
通过查看代码,可以看到这个接口中有两个update和一个select,这三个语句分别是:
|
||||
|
||||
```
|
||||
<update id="updateByPrimaryKeySelective" parameterType="com.dunshan.mall.model.OmsOrder">
|
||||
update oms_orde
|
||||
........................
|
||||
where id = #{id,jdbcType=BIGINT
|
||||
</update
|
||||
|
||||
|
||||
<update id="updateByPrimaryKeySelective" parameterType="com.dunshan.mall.model.PmsSkuStock">
|
||||
update pms_sku_stoc
|
||||
........................
|
||||
where id = #{id,jdbcType=BIGINT
|
||||
</update
|
||||
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap"
|
||||
select
|
||||
<include refid="Base_Column_List" /
|
||||
from pms_sku_stoc
|
||||
where id = #{id,jdbcType=BIGINT
|
||||
</select
|
||||
|
||||
```
|
||||
|
||||
在我查了这三个SQL语句对应的SQL、表和索引之后,发现它们都是精准查找,并且索引也在第一阶段的分析中创建完了,按理说不会出现大的数据量。可是,我们在前面的确看到OmsOrderDetail产生了巨大的数据量,这是怎么一回事?
|
||||
|
||||
为了搞清楚这个问题,我们查查还有谁调用了OmsOrderDetail:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/74/44/745a6dcedd341c57eededccfb5804344.png" alt="">
|
||||
|
||||
我们找啊找,终于看到了两个for循环(其实在前面的代码段中,也可以看出来)。我点进去一看,发现它们是定时任务。这个时候,问题产生的逻辑就变得清晰了:由于存在for循环,接口调用时就会循环执行某段代码,这就成了JVM内存不断增加的一种可能性。
|
||||
|
||||
你还记得吗?在[上](https://time.geekbang.org/column/article/370976)[节课](https://time.geekbang.org/column/article/370976)中,我们其实也定位到了“for循环对应的SQL执行慢”这个问题,但是由于压力持续的时间不够长,内存没有被耗尽,所以内存被消耗光的问题并没有体现出来。而当场景执行的时间变长时,就出现了TPS断断续续的奇怪现象。
|
||||
|
||||
### 优化效果
|
||||
|
||||
到这里,我们的优化方案其实非常清楚了,就是做定时任务时不要一下子查那么多数据。因此我在这里加了一个limit,限定一次就查500行。如果处理不过来的话,我们可以直接写一个单独的服务,进行多线程处理。
|
||||
|
||||
修改之后,我们再次看看Order服务的JVM内存消耗:
|
||||
|
||||
```
|
||||
[root@svc-mall-order-f7b6d6f7-xl2kp /]# jstat -gcutil 1 1s
|
||||
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
|
||||
0.00 5.44 100.00 96.03 95.03 93.17 43710 1044.757 161 83.635 1128.39
|
||||
0.00 3.18 82.83 96.21 95.03 93.17 43713 1044.797 161 83.635 1128.43
|
||||
2.09 0.00 4.54 96.21 95.03 93.17 43718 1044.850 161 83.635 1128.48
|
||||
1.99 0.00 44.92 96.21 95.03 93.17 43722 1044.891 161 83.635 1128.52
|
||||
0.00 2.24 1.51 96.22 95.03 93.17 43727 1044.936 161 83.635 1128.57
|
||||
2.23 0.00 0.00 96.22 95.03 93.17 43732 1044.987 161 83.635 1128.62
|
||||
40.97 0.00 76.46 96.22 95.03 93.17 43736 1045.051 161 83.635 1128.68
|
||||
0.00 41.76 47.74 98.81 95.03 93.17 43741 1045.136 161 83.635 1128.77
|
||||
45.59 0.00 77.61 98.81 95.03 93.17 43746 1045.210 161 83.635 1128.84
|
||||
0.00 0.00 51.01 52.55 95.03 93.17 43749 1045.270 162 84.021 1129.29
|
||||
52.34 0.00 60.57 53.23 95.03 93.17 43754 1045.353 162 84.021 1129.37
|
||||
0.00 51.85 0.00 56.32 95.03 93.17 43759 1045.450 162 84.021 1129.47
|
||||
0.00 57.97 98.59 58.79 95.03 93.17 43764 1045.526 162 84.021 1129.54
|
||||
42.02 0.00 83.01 60.83 95.03 93.17 43768 1045.602 162 84.021 1129.62
|
||||
0.00 42.51 72.69 60.83 95.03 93.17 43773 1045.668 162 84.021 1129.68
|
||||
28.95 0.00 67.94 61.52 95.03 93.17 43778 1045.735 162 84.021 1129.75
|
||||
0.00 4.50 32.29 62.74 95.03 93.17 43783 1045.788 162 84.021 1129.80
|
||||
62.60 0.00 27.04 62.80 95.03 93.17 43788 1045.866 162 84.021 1129.88
|
||||
0.00 45.52 0.00 65.14 95.03 93.17 43793 1045.950 162 84.021 1129.97
|
||||
0.00 47.10 71.13 65.14 95.03 93.17 43797 1046.015 162 84.021 1130.03
|
||||
49.36 0.00 33.30 65.14 95.03 93.17 43802 1046.080 162 84.021 1130.10
|
||||
5.92 0.00 35.47 67.33 95.03 93.17 43806 1046.132 162 84.021 1130.15
|
||||
0.00 50.15 65.90 67.37 95.03 93.17 43811 1046.209 162 84.021 1130.23
|
||||
46.75 0.00 10.39 69.71 95.03 93.17 43816 1046.305 162 84.021 1130.32
|
||||
47.27 0.00 54.91 69.71 95.03 93.17 43820 1046.364 162 84.021 1130.38
|
||||
0.00 45.69 46.99 69.71 95.03 93.17 43825 1046.430 162 84.021 1130.45
|
||||
3.25 0.00 13.65 71.93 95.03 93.17 43830 1046.488 162 84.021 1130.50
|
||||
0.00 38.00 46.98 71.94 95.03 93.17 43835 1046.551 162 84.021 1130.57
|
||||
43.74 0.00 37.69 74.44 95.03 93.17 43840 1046.634 162 84.021 1130.65
|
||||
0.00 42.88 15.64 74.44 95.03 93.17 43845 1046.702 162 84.021 1130.72
|
||||
0.00 44.13 12.90 74.44 95.03 93.17 43849 1046.756 162 84.021 1130.77
|
||||
0.00 16.42 33.96 75.79 95.03 93.17 43853 1046.813 162 84.021 1130.83
|
||||
4.25 0.00 20.10 76.45 95.03 93.17 43858 1046.863 162 84.021 1130.88
|
||||
0.00 3.22 0.00 76.46 95.03 93.17 43863 1046.914 162 84.021 1130.93
|
||||
|
||||
```
|
||||
|
||||
从GC的状态来看,内存现在可以回收得正常一些了。这里我要说明一下,上面的JVM数据不是在场景一开始的时候抓取的,而是在场景执行了很久之后才抓取的。因为我们从上面的数据看到JVM内存已经可以正常回收了,所以,上面的数据是在确定没有内存溢出的前提下得到的有效数据。
|
||||
|
||||
不过,这里还是有问题的,不知道你有没有发现,那就是YGC过快。你通过YGC这一列就能看到,我一秒打印一次,一秒就有四五次的YGC,平均每次YGC时间大概在10~20毫秒。虽然这个问题还没有对TPS造成明显的影响,但是也在危险的边缘了。不过,也正因为它现在对TPS还没有造成明显的影响,所以,我在这里先不处理YGC快的问题了。
|
||||
|
||||
我们再来重新执行一遍场景,得到的场景执行结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/73/36/730851a50364bde422aef5f69c29ce36.png" alt="">
|
||||
|
||||
你看,TPS是不是有改善?而且不会再出现第二阶段刚开始的那种情况了,因为当时是压到了6分钟之后就开始出现问题了,而在这里,我们压了30多分钟,仍然没有出现掉下来的情况。
|
||||
|
||||
可以欢呼了,对不对?对的!我们不用再等翻车了。
|
||||
|
||||
## 总结
|
||||
|
||||
在这节课中,我们在分两个阶段描述了两个问题。
|
||||
|
||||
第一个问题比较简单,是全表扫描没加索引的问题,我们在前面也有过描述。之所以在这节课中又说一下,是因为SQL把CPU全都吃光了,而这种状态才是全表扫描在性能中比较常见的问题。
|
||||
|
||||
第二个问题和内存溢出有关,对于Java的应用来说,我们要把内存的溢出找出来,就必须理清楚代码的逻辑,知道哪个变量在哪里定义,谁在取值,谁在使用,还有层级关系也要划分清楚。现在的开发工具已经非常友好了,可以告诉我们代码的前后调用关系。
|
||||
|
||||
在这个例子中,难点其实不在于怎么找到这个内存溢出点,而是在于找到溢出的原因。我们只要看下HeapDump就可以知道溢出点在哪里,而引用这个类的是谁,可能会有很多地方,我们需要一个个去查看。
|
||||
|
||||
这个例子的特殊之处在于,我们的接口本身就使用了pms_sku_stock表和OmsOrderDetail类,但是从语句上来看,这个接口不会导致内存溢出。所以,我们才需要找出是谁使用了这个pms_sku_stock表和OmsOrderDetail类,并且会产生溢出。这个转折才是关键。
|
||||
|
||||
希望你能在这节课中受益。
|
||||
|
||||
## 课后作业
|
||||
|
||||
最后,我给你留两道题,请你思考一下:
|
||||
|
||||
<li>
|
||||
你能否快速找到需要创建索引的SQL?
|
||||
</li>
|
||||
<li>
|
||||
在内存不断增长时,如何快速定位出哪个对象导致的?
|
||||
</li>
|
||||
|
||||
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
|
||||
|
||||
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!
|
||||
Reference in New Issue
Block a user