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

View File

@@ -0,0 +1,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 &gt; 1000' -n 3
trace org.springframework.security.oauth2.provider.endpoint.TokenEndpoint postAccessToken '#cost &gt; 1000' -n 3
trace org.springframework.security.oauth2.provider.token.AbstractTokenGranter getOAuth2Authentication '#cost &gt; 1000' -n 3
trace org.springframework.security.authentication.AuthenticationManager getOAuth2Authentication '#cost &gt; 500' -n 3
trace org.springframework.security.authentication.ProviderManager authenticate '#cost &gt; 500' -n 3
trace org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider authenticate '#cost &gt; 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的执行耗时有多少种手段
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果这节课让你有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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(&quot;HomeContent&quot;) == 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(&quot;HomeContent&quot;, result);
}
Object homeContent = redisService.get(&quot;HomeContent&quot;);
// 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中设置为如下值
```
&lt;stringProp name=&quot;ThreadGroup.num_threads&quot;&gt;20&lt;/stringProp&gt;
&lt;stringProp name=&quot;ThreadGroup.ramp_time&quot;&gt;600&lt;/stringProp&gt;
&lt;boolProp name=&quot;ThreadGroup.scheduler&quot;&gt;true&lt;/boolProp&gt;
&lt;stringProp name=&quot;ThreadGroup.duration&quot;&gt;700&lt;/stringProp&gt;
```
设置好试运行参数后,我们就可以在这样的场景下进一步设置足够的线程来运行,以达到资源使用率的最大化。
你可能会疑惑:难道不用更高的线程了吗?如果你想做一个正常的场景,那确实不需要用更高的线程了;如果你就是想知道压力线程加多了是什么样子,那你可以试试。我在性能场景执行时,也经常用各种方式压着玩。
不过话说回来确实有一种情况需要我们正儿八经地增加更多的压力那就是你的响应时间已经增加了可是增加得又不多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 &quot;conservative&quot; 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 &quot;powersave&quot; 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 &quot;powersave&quot; 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 &quot;conservative&quot; 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的运行逻辑是什么
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果这节课让你有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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&gt; 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 =&gt; 172.16.106.130:multiling-http 1.64Mb 2.04Mb 3.06Mb
&lt;= 26.2Mb 14.5Mb 19.8Mb
172.16.106.119:nfs =&gt; 172.16.106.100:apex-mesh 1.43Mb 2.18Mb 3.79Mb
&lt;= 25.5Mb 14.2Mb 14.4Mb
172.16.106.119:nfs =&gt; 172.16.106.195:vatp 356Kb 1.27Mb 1.35Mb
&lt;= 9.71Mb 7.04Mb 7.41Mb
172.16.106.119:nfs =&gt; 172.16.106.56:815 7.83Kb 4.97Kb 4.81Kb
&lt;= 302Kb 314Kb 186Kb
172.16.106.119:nfs =&gt; 172.16.106.79:device 11.0Kb 7.45Kb 7.57Kb
&lt;= 12.4Kb 22.0Kb 28.5Kb
172.16.106.119:ssh =&gt; 172.16.100.201:cnrprotocol 2.86Kb 2.87Kb 5.81Kb
&lt;= 184b 184b 525b
169.254.3.2:60010 =&gt; 225.4.0.2:59004 2.25Kb 2.40Kb 2.34Kb
&lt;= 0b 0b 0b
169.254.6.2:60172 =&gt; 225.4.0.2:59004 2.25Kb 2.40Kb 2.34Kb
&lt;= 0b 0b 0b
172.16.106.119:nfs =&gt; 172.16.106.149:986 0b 1.03Kb 976b
&lt;= 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-36C16G上:
<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-26C16G上去做这一步是为了减少网络软中断的争用。我们再看一下集群的整体性能
<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>
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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。
我们抓两段栈出来看一下,找一下锁之间的关系:
```
-- 第一处:
&quot;http-nio-8401-exec-884&quot; #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 &lt;0x000000071ab1a5d8&gt; (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)
.....................
-- 第二处:
&quot;http-nio-8401-exec-862&quot; #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 &lt;0x000000071ddad410&gt; (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 &lt;0x000000071ab1a5d8&gt;。其实,如果你有经验的话,一下子就能知道这里面是什么问题。不过,我们做性能分析的人要讲逻辑。
我在这里啰嗦几句,**当你碰到这种锁问题又不知道具体原因的时候要下意识地去打印一个完整的栈来看而不是再到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线程。
```
-- 第一处
&quot;http-nio-8401-exec-890&quot; #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 &lt;0x000000071ab1a5d8&gt; (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)
-- 第二处
&quot;http-nio-8401-exec-871&quot; #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 &lt;0x000000071ddad410&gt; (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 &amp;&amp; this.e.equals(e)) ||
(this.d != null &amp;&amp; this.d.equals(d))) {
BlindingRandomPair brp = null;
synchronized (this) {
if (!u.equals(BigInteger.ZERO) &amp;&amp;
!v.equals(BigInteger.ZERO))
brp = new BlindingRandomPair(u, v);
if (u.compareTo(BigInteger.ONE) &lt;= 0 ||
v.compareTo(BigInteger.ONE) &lt;= 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. 低效的代码有什么优化思路?
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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. 你能总结一下,这节课案例的证据链吗?
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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%的CPUES本来就是吃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 &lt;none&gt; &lt;none&gt;
elasticsearch-client-1 1/1 Running 0 6h45m 10.100.140.8 k8s-worker-2 &lt;none&gt; &lt;none&gt;
elasticsearch-data-0 1/1 Running 0 7h8m 10.100.18.197 k8s-worker-5 &lt;none&gt; &lt;none&gt;
elasticsearch-data-1 1/1 Running 0 7h8m 10.100.5.5 k8s-worker-7 &lt;none&gt; &lt;none&gt;
elasticsearch-data-2 1/1 Running 0 7h8m 10.100.251.67 k8s-worker-9 &lt;none&gt; &lt;none&gt;
elasticsearch-master-0 1/1 Running 0 7h8m 10.100.230.0 k8s-worker-1 &lt;none&gt; &lt;none&gt;
elasticsearch-master-1 1/1 Running 0 7h8m 10.100.227.131 k8s-worker-6 &lt;none&gt; &lt;none&gt;
elasticsearch-master-2 1/1 Running 0 7h8m 10.100.69.206 k8s-worker-3 &lt;none&gt; &lt;none&gt;
[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 &lt;none&gt; 9200:30200/TCP,9300:31614/TCP 34d app=elasticsearch-client,chart=elasticsearch,heritage=Helm,release=elasticsearch-client
elasticsearch-client-headless ClusterIP None &lt;none&gt; 9200/TCP,9300/TCP 34d app=elasticsearch-client
elasticsearch-data ClusterIP 10.96.16.151 &lt;none&gt; 9200/TCP,9300/TCP 7h41m app=elasticsearch-data,chart=elasticsearch,heritage=Helm,release=elasticsearch-data
elasticsearch-data-headless ClusterIP None &lt;none&gt; 9200/TCP,9300/TCP 7h41m app=elasticsearch-data
elasticsearch-master ClusterIP 10.96.207.238 &lt;none&gt; 9200/TCP,9300/TCP 7h41m app=elasticsearch-master,chart=elasticsearch,heritage=Helm,release=elasticsearch-master
elasticsearch-master-headless ClusterIP None &lt;none&gt; 9200/TCP,9300/TCP 7h41m app=elasticsearch-master
svc-mall-search ClusterIP 10.96.27.150 &lt;none&gt; 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 &quot;default/elasticsearch-client:http cluster IP&quot; -m tcp --dport 9200 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.96.140.52/32 -p tcp -m comment --comment &quot;default/elasticsearch-client:http cluster IP&quot; -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 &quot;default/elasticsearch-client:http&quot; -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 &quot;default/elasticsearch-client:http&quot; -j KUBE-MARK-MASQ
-A KUBE-SEP-LO263M5QW4XA6E3Q -p tcp -m comment --comment &quot;default/elasticsearch-client:http&quot; -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 &quot;default/elasticsearch-client:http&quot; -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-IFM4L7YNSTSJP4YT
-A KUBE-SVC-XCX4XZ2WPAE7BUZ4 -m comment --comment &quot;default/elasticsearch-client:http&quot; -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 &lt;none&gt; &lt;none&gt;
elasticsearch-data-1 1/1 Running 0 10h 10.100.5.5 k8s-worker-7 &lt;none&gt; &lt;none&gt;
elasticsearch-data-2 1/1 Running 0 10h 10.100.251.67 k8s-worker-9 &lt;none&gt; &lt;none&gt;
-- 查看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 &quot;default/elasticsearch-data:http cluster IP&quot; -m tcp --dport 9200 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.96.16.151/32 -p tcp -m comment --comment &quot;default/elasticsearch-data:http cluster IP&quot; -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 &quot;default/elasticsearch-data:transport cluster IP&quot; -m tcp --dport 9300 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.96.16.151/32 -p tcp -m comment --comment &quot;default/elasticsearch-data:transport cluster IP&quot; -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 &quot;default/elasticsearch-data:http&quot; -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-ZHLKOYKJY5GV3ZVN
-A KUBE-SVC-4LU6GV7CN63XJXEQ -m comment --comment &quot;default/elasticsearch-data:http&quot; -m statistic --mode random --probability 1 -j KUBE-SEP-6ILKZEZS3TMCB4VJ
-A KUBE-SVC-4LU6GV7CN63XJXEQ -m comment --comment &quot;default/elasticsearch-data:http&quot; -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 &quot;default/elasticsearch-data:http&quot; -j KUBE-MARK-MASQ
-A KUBE-SEP-ZHLKOYKJY5GV3ZVN -p tcp -m comment --comment &quot;default/elasticsearch-data:http&quot; -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 &quot;default/elasticsearch-data:http&quot; -j KUBE-MARK-MASQ
-A KUBE-SEP-6ILKZEZS3TMCB4VJ -p tcp -m comment --comment &quot;default/elasticsearch-data:http&quot; -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 &quot;default/elasticsearch-data:http&quot; -j KUBE-MARK-MASQ
-A KUBE-SEP-JOYLBDPA3LNXKWUK -p tcp -m comment --comment &quot;default/elasticsearch-data:http&quot; -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
{
&quot;number_of_replicas&quot;: 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 &lt;none&gt; &lt;none&gt;
elasticsearch-data-1 1/1 Running 0 17m 10.100.251.68 k8s-worker-9 &lt;none&gt; &lt;none&gt;
elasticsearch-data-2 1/1 Running 0 18m 10.100.140.9 k8s-worker-2 &lt;none&gt; &lt;none&gt;
```
现在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. 什么时候才需要去看组件内部的实现逻辑?
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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 &lt;none&gt; &lt;none&gt;
elasticsearch-master-0 1/1 Running 0 3d11h 10.100.230.60 k8s-worker-1 &lt;none&gt; &lt;none&gt;
mysql-min-d564fc4df-vs7d6 1/1 Running 0 22h 10.100.230.1 k8s-worker-1 &lt;none&gt; &lt;none&gt;
[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的部分中DMSData 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 &lt;5 ITEMS&gt;
# 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. 你能画出在第二阶段分析中的逻辑吗?
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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&lt;OmsCartItem&gt; 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内容如下
&lt;select id=&quot;selectByExample&quot; parameterType=&quot;com.dunshan.mall.model.OmsCartItemExample&quot; resultMap=&quot;BaseResultMap&quot;&gt;
select
&lt;if test=&quot;distinct&quot;&gt;
distinct
&lt;/if&gt;
&lt;include refid=&quot;Base_Column_List&quot; /&gt;
from oms_cart_item
&lt;if test=&quot;_parameter != null&quot;&gt;
&lt;include refid=&quot;Example_Where_Clause&quot; /&gt;
&lt;/if&gt;
&lt;if test=&quot;orderByClause != null&quot;&gt;
order by ${orderByClause}
&lt;/if&gt;
&lt;/select&gt;
```
这个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 &lt;none&gt; &lt;none&gt;
- 再查询对应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 &lt;none&gt; &lt;none&gt;
default elasticsearch-data-2 1/1 Running 0 4d2h 10.100.140.35 k8s-worker-2 &lt;none&gt; &lt;none&gt;
default elasticsearch-master-2 1/1 Running 4 20d 10.100.140.30 k8s-worker-2 &lt;none&gt; &lt;none&gt;
default gateway-mall-gateway-6567c8b49c-pc7rf 1/1 Running 0 15h 10.100.140.2 k8s-worker-2 &lt;none&gt; &lt;none&gt;
kube-system calico-node-rlhcc 1/1 Running 0 2d5h 172.16.106.149 k8s-worker-2 &lt;none&gt; &lt;none&gt;
kube-system coredns-59c898cd69-sfd9w 1/1 Running 4 36d 10.100.140.31 k8s-worker-2 &lt;none&gt; &lt;none&gt;
kube-system kube-proxy-l8xf9 1/1 Running 6 36d 172.16.106.149 k8s-worker-2 &lt;none&gt; &lt;none&gt;
monitoring node-exporter-mjsmp 2/2 Running 0 4d17h 172.16.106.149 k8s-worker-2 &lt;none&gt; &lt;none&gt;
nginx-ingress nginx-ingress-nbhqc 1/1 Running 0 5d19h 10.100.140.34 k8s-worker-2 &lt;none&gt; &lt;none&gt;
[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="">
可见走GatewayTPS只能有400多不走GatewayTPS能达到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 &quot;blacklist acpi_power_meter&quot; &gt;&gt; /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 -&gt; /proc/1/fd/1
lrwxrwxrwx 1 root root 12 Sep 10 2019 error.log -&gt; /proc/1/fd/2
lrwxrwxrwx 1 root root 12 Sep 10 2019 access.log -&gt; /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 -&gt; 'socket:[211552647]'
lrwx------ 1 root root 64 Jan 7 18:00 4 -&gt; 'anon_inode:[eventpoll]'
lrwx------ 1 root root 64 Jan 7 18:00 3 -&gt; 'socket:[211552615]'
l-wx------ 1 root root 64 Jan 7 18:00 2 -&gt; 'pipe:[211548854]'
l-wx------ 1 root root 64 Jan 7 18:00 1 -&gt; 'pipe:[211548853]'
lrwx------ 1 root root 64 Jan 7 18:00 0 -&gt; /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效率引发的性能问题
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下这节课再见!

View 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 &lt;none&gt; &lt;none&gt;
cloud-redis-7f7db7f45c-t5g46 2/2 Running 0 2d8h 10.100.69.196 k8s-worker-3 &lt;none&gt; &lt;none&gt;
elasticsearch-master-2 1/1 Running 0 3h28m 10.100.69.209 k8s-worker-3 &lt;none&gt; &lt;none&gt;
svc-mall-cart-558d787dc7-g6qgh 1/1 Running 0 2d11h 10.100.69.201 k8s-worker-3 &lt;none&gt; &lt;none&gt;
svc-mall-order-fbfd8b57c-kbczh 1/1 Running 0 2d11h 10.100.69.202 k8s-worker-3 &lt;none&gt; &lt;none&gt;
svc-mall-portal-846d9994f8-m7jbq 1/1 Running 0 38h 10.100.69.207 k8s-worker-3 &lt;none&gt; &lt;none&gt;
svc-mall-search-c9c8bc847-h7sgv 1/1 Running 0 161m 10.100.69.210 k8s-worker-3 &lt;none&gt; &lt;none&gt;
[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:&lt;init&gt;() #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:&lt;init&gt;() #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 &lt;none&gt; &lt;none&gt;
cloud-redis-7f7db7f45c-t5g46 2/2 Running 1 3d4h 10.100.69.196 k8s-worker-3 &lt;none&gt; &lt;none&gt;
elasticsearch-master-2 1/1 Running 0 23h 10.100.69.209 k8s-worker-3 &lt;none&gt; &lt;none&gt;
svc-mall-cart-79c667bf56-j76h6 1/1 Running 0 20h 10.100.69.213 k8s-worker-3 &lt;none&gt; &lt;none&gt;
svc-mall-order-fbfd8b57c-kbczh 1/1 Running 0 3d7h 10.100.69.202 k8s-worker-3 &lt;none&gt; &lt;none&gt;
svc-mall-portal-846d9994f8-m7jbq 1/1 Running 0 2d10h 10.100.69.207 k8s-worker-3 &lt;none&gt; &lt;none&gt;
svc-mall-search-c9c8bc847-h7sgv 1/1 Running 0 23h 10.100.69.210 k8s-worker-3 &lt;none&gt; &lt;none&gt;
[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会掉下来的情况大概描述一下你的思路。
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下这节课再见!

View 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的栈中有大量这样的内容
```
&quot;http-nio-8086-exec-421&quot; 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(&lt;generated&gt;)
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&lt;OmsOrder&gt; cancelOrderList = orderMapper.selectByExample(example);
```
这段代码对应的select语句是
```
&lt;select id=&quot;selectByExample&quot; parameterType=&quot;com.dunshan.mall.model.OmsOrderExample&quot; resultMap=&quot;BaseResultMap&quot;&gt;
select
&lt;if test=&quot;distinct&quot;&gt;
distinct
&lt;/if&gt;
&lt;include refid=&quot;Base_Column_List&quot; /&gt;
from oms_order
&lt;if test=&quot;_parameter != null&quot;&gt;
&lt;include refid=&quot;Example_Where_Clause&quot; /&gt;
&lt;/if&gt;
&lt;if test=&quot;orderByClause != null&quot;&gt;
order by ${orderByClause}
&lt;/if&gt;
&lt;/select&gt;
```
这是一个典型的语句没过滤的问题。像这样的开发项目也最多就是做个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 &lt;9 ITEMS&gt;
```
通过上面的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表
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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] &quot;POST /mall-order/order/generateOrder HTTP/1.1&quot; 200 726 8201 151 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] &quot;POST /mall-order/order/generateOrder HTTP/1.1&quot; 200 726 8201 147 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] &quot;POST /mall-order/order/generateOrder HTTP/1.1&quot; 200 726 8201 141 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] &quot;POST /mall-order/order/generateOrder HTTP/1.1&quot; 200 726 8201 122 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] &quot;POST /mall-order/order/generateOrder HTTP/1.1&quot; 200 726 8201 125 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] &quot;POST /mall-order/order/generateOrder HTTP/1.1&quot; 200 726 8201 150 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] &quot;POST /mall-order/order/generateOrder HTTP/1.1&quot; 200 726 8201 177 ms
```
- Order
```
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] &quot;POST /order/generateOrder HTTP/1.1&quot; 200 738 &quot;-&quot; &quot;Apache-HttpClient/4.5.12 (Java/1.8.0_261)&quot; 72 ms 72 ms
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] &quot;POST /order/generateOrder HTTP/1.1&quot; 200 738 &quot;-&quot; &quot;Apache-HttpClient/4.5.12 (Java/1.8.0_261)&quot; 94 ms 93 ms
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] &quot;POST /order/generateOrder HTTP/1.1&quot; 200 738 &quot;-&quot; &quot;Apache-HttpClient/4.5.12 (Java/1.8.0_261)&quot; 76 ms 76 ms
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] &quot;POST /order/generateOrder HTTP/1.1&quot; 200 738 &quot;-&quot; &quot;Apache-HttpClient/4.5.12 (Java/1.8.0_261)&quot; 95 ms 95 ms
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] &quot;POST /order/generateOrder HTTP/1.1&quot; 200 738 &quot;-&quot; &quot;Apache-HttpClient/4.5.12 (Java/1.8.0_261)&quot; 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] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 814 &quot;-&quot; &quot;okhttp/3.14.8&quot; 2348 ms 2348 ms
10.100.69.248 - - [04/Feb/2021:00:37:17 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 816 &quot;-&quot; &quot;okhttp/3.14.8&quot; 4155 ms 4155 ms
10.100.69.248 - - [04/Feb/2021:00:37:17 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 817 &quot;-&quot; &quot;okhttp/3.14.8&quot; 4968 ms 1813 ms
10.100.69.248 - - [04/Feb/2021:00:37:15 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 810 &quot;-&quot; &quot;okhttp/3.14.8&quot; 2333 ms 2333 ms
10.100.69.248 - - [04/Feb/2021:00:37:17 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 815 &quot;-&quot; &quot;okhttp/3.14.8&quot; 5206 ms 4970 ms
10.100.69.248 - - [04/Feb/2021:00:37:20 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 818 &quot;-&quot; &quot;okhttp/3.14.8&quot; 6362 ms 6362 ms
10.100.69.248 - - [04/Feb/2021:00:37:20 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 818 &quot;-&quot; &quot;okhttp/3.14.8&quot; 6710 ms 6710 ms
10.100.69.248 - - [04/Feb/2021:00:37:20 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 817 &quot;-&quot; &quot;okhttp/3.14.8&quot; 6696 ms 6587 ms
10.100.69.248 - - [04/Feb/2021:00:37:21 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 813 &quot;-&quot; &quot;okhttp/3.14.8&quot; 7987 ms 7976 ms
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 814 &quot;-&quot; &quot;okhttp/3.14.8&quot; 8784 ms 8784 ms
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 817 &quot;-&quot; &quot;okhttp/3.14.8&quot; 9100 ms 8764 ms
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 834 &quot;-&quot; &quot;okhttp/3.14.8&quot; 9126 ms 9013 ms
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 817 &quot;-&quot; &quot;okhttp/3.14.8&quot; 9058 ms 9058 ms
10.100.69.248 - - [04/Feb/2021:00:37:23 +0800] &quot;GET /sso/feign/info HTTP/1.1&quot; 200 820 &quot;-&quot; &quot;okhttp/3.14.8&quot; 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 &lt;none&gt; &lt;none&gt;
monitor-mall-monitor-d8bb58fcb-kfbcj 1/1 Running 0 23d 10.100.231.242 k8s-worker-8 &lt;none&gt; &lt;none&gt;
skywalking-oap-855f96b777-5nxll 1/1 Running 6 37h 10.100.231.235 k8s-worker-8 &lt;none&gt; &lt;none&gt;
skywalking-oap-855f96b777-6b7jd 1/1 Running 5 37h 10.100.231.234 k8s-worker-8 &lt;none&gt; &lt;none&gt;
svc-mall-admin-75ff7dcc9b-8gtr5 1/1 Running 0 17d 10.100.231.208 k8s-worker-8 &lt;none&gt; &lt;none&gt;
svc-mall-demo-5584dbdc96-fskg9 1/1 Running 0 17d 10.100.231.207 k8s-worker-8 &lt;none&gt; &lt;none&gt;
svc-mall-member-5fc984b57c-bk2fd 1/1 Running 0 12d 10.100.231.231 k8s-worker-8 &lt;none&gt; &lt;none&gt;
[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 diagramidea的一个插件打开就看到了这样一张很长的业务逻辑图
<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(&quot;ids&quot;) List&lt;Long&gt; ids,@Param(&quot;status&quot;) 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上不去、资源也用不上的情况
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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 &lt;none&gt; &lt;none&gt;
[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的栈看看具体的函数。我们把那两个PID311、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 &lt; 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 &lt; 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 = &quot;0 0/20 * ? * ?&quot;)
private void cancelTimeOutOrder(){
Integer count = portalOrderService.cancelTimeOutOrder();
LOGGER.info(&quot;取消订单释放锁定库存:{}&quot;,count);
}
```
很显然这是一个定时计划每20分钟执行一次。到这里问题就很清楚了原来是定时任务调用了这两个批量的查询语句导致了两个CPU使用率达到100%,并且也持续了一段时间。
像这样的定时任务我们要格外关注一下注意把它和实时业务分开部署和处理减少批量业务对实时业务的资源争用。如果放在一起处理那就要控制好要批量查询的数据量级让SQL的查询变得合理。
由于数据库可用的CPU比较多这个定时任务对我们的TPS并没有产生什么明显的影响在这里我们不用做什么处理以后注意分开就好了。
## 总结
在这节课中虽然我们的优化并没有让TPS明显增加但是因为分析的技术细节不一样我也非常完整地记录了整个分析过程。
在第一阶段的分析中,我们运用的还是之前提到的分析思路。不同点在于,对于一个非常成熟的固定组件,我们要想优化它,就要去了解它的架构,找到它的相关性能参数。因为在实际的性能项目中,面对这样的组件,我们往往没有时间去纠结内部的实现,需要非常快速地作出判断。如果时间允许,你倒是可以慢慢折腾。
其实理解一个技术组件的原理,并没有想像中的那么高不可攀、深不可测,只要耐心看下去,你总会成长。
在第二阶段的分析中我们由某几个CPU高的现象分析到了具体的SQL问题。这个过程虽然简单但是从这个问题上我们可以看出这个系统还有很多的优化空间比如说主从分离、定时任务拆为单独的服务等等。不过在我们的性能分析中重点仍然是我跟你一直灌输的分析思路希望你记在心里了。
## 课后作业
最后,我给你留两道题,请你思考一下:
1. 为什么要看全部的全局监控计数器?
1. 单CPU高时如何定位具体的问题点你有什么思路
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!

View 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 &lt;none&gt; &lt;none&gt;
skywalking-es-init-ls7j5 0/1 Completed 0 4d11h 10.100.230.18 k8s-worker-1 &lt;none&gt; &lt;none&gt;
[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。我们打印栈信息来确认一下果然是它
```
&quot;VM Thread&quot; 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这三个语句分别是
```
&lt;update id=&quot;updateByPrimaryKeySelective&quot; parameterType=&quot;com.dunshan.mall.model.OmsOrder&quot;&gt;
update oms_orde
........................
where id = #{id,jdbcType=BIGINT
&lt;/update
&lt;update id=&quot;updateByPrimaryKeySelective&quot; parameterType=&quot;com.dunshan.mall.model.PmsSkuStock&quot;&gt;
update pms_sku_stoc
........................
where id = #{id,jdbcType=BIGINT
&lt;/update
&lt;select id=&quot;selectByPrimaryKey&quot; parameterType=&quot;java.lang.Long&quot; resultMap=&quot;BaseResultMap&quot;
select
&lt;include refid=&quot;Base_Column_List&quot; /
from pms_sku_stoc
where id = #{id,jdbcType=BIGINT
&lt;/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时间大概在1020毫秒。虽然这个问题还没有对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>
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!