所以,我们要“收拾”的还是worker-7和worker-8。
既然Auth服务在worker-7上,member服务在worker-8上,就像前面说的,我们不如就从Auth服务开始。
## Auth服务定向分析
对于Auth服务,我们从哪里开始分析呢?其实,我们可以按部就班。既然是Auth服务导致worker-7的CPU使用率偏高,那我们就可以走下面这个证据链:
按照这个证据链,我们应该先看进程。不过,仗着傻小子火气壮(俗称:艺高人胆大),我直接就去看线程状态了,想看看能不能凭经验蒙对一把。于是,我打开了Spring Boot Admin的线程页面:
有没有满目疮痍的感觉?人生就是这样,到处都有惊吓。
在我颤抖着手点开一些红色的地方之后,看到了类似这样的信息:
可以看到,阻塞数非常大,达到了842。此外,锁拥有者ID是86676,锁拥有者名称是线程823。
我们抓两段栈出来看一下,找一下锁之间的关系:
```
-- 第一处:
"http-nio-8401-exec-884" #86813 daemon prio=5 os_prio=0 tid=0x00007f2868073000 nid=0x559e waiting for monitor entry [0x00007f2800c6d000]
java.lang.Thread.State: BLOCKED (on object monitor
at java.security.Provider.getService(Provider.java:1035)
- waiting to lock <0x000000071ab1a5d8> (a sun.security.provider.Sun)
at sun.security.jca.ProviderList.getService(ProviderList.java:332)
.....................
at com.dunshan.mall.auth.util.MD5Util.toMD5(MD5Util.java:11)
at com.dunshan.mall.auth.config.MyPasswordEncoder.matches(MyPasswordEncoder.java:23)
.....................
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
at com.dunshan.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
.....................
at com.dunshan.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
.....................
-- 第二处:
"http-nio-8401-exec-862" #86728 daemon prio=5 os_prio=0 tid=0x00007f28680d6000 nid=0x553a waiting for monitor entry [0x00007f2802b8c000]
java.lang.Thread.State: BLOCKED (on object monitor
at sun.security.rsa.RSACore$BlindingParameters.getBlindingRandomPair(RSACore.java:404)
- waiting to lock <0x000000071ddad410> (a sun.security.rsa.RSACore$BlindingParameters)
at sun.security.rsa.RSACore.getBlindingRandomPair(RSACore.java:443)
.....................
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46)
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
at com.dunshan.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
.....................
at com.dunshan.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
.....................
```
这两个栈的内容并不是同一时刻出现的,说明这个BLOCKED一直存在。但是不管怎么样,这个栈在做RSA加密,它和Token部分有关。
其中,线程http-nio-8401-exec-884是BLOCKED状态,那就说明有其他线程持有这个锁,所以我们自然要看一下线程栈中的waiting to lock <0x000000071ab1a5d8>。其实,如果你有经验的话,一下子就能知道这里面是什么问题。不过,我们做性能分析的人要讲逻辑。
我在这里啰嗦几句,**当你碰到这种锁问题,又不知道具体原因的时候,要下意识地去打印一个完整的栈来看,而不是再到Spring Boot Admin里胡乱点**。为什么不建议你这么做?原因有这么几个:
- 由于线程太多,点着看逻辑关系比较累;
- 不断在刷,眼晕;
- 我不喜欢。
所以,对于前面遇到的锁问题,我们首先要做的就是到容器中的jstack里打印一下栈,把它下载下来,然后祭出工具打开看一眼。
你可能会问,为什么不用Arthas之类的工具直接在容器里看?主要是因为Arthas的Dashboard在Thread比较多的时候,看起来真心累。
下面这张图就是jstack打印出来的栈,在下载之后用工具打开的效果。
是不是有种买彩票的感觉?看起来有那么多的BLOCKED状态的线程(多达842个),居然一个都没蒙到!我本来想抓BLOCKED状态的线程,并且线程描述是“Waiting on monitor”,但是,从上面的线程描述统计来看,一个也没见。哼,真生气。
这时候,身为一个做性能分析的人,我们一定要记得倒杯茶,静静心,默默地把jstack连续再执行几遍。我在这里就连续执行了10遍,然后再找每个栈的状态。
终于,Waiting on monitor来了:
看起来有得玩了!接下来让我们看看究竟是谁阻塞住了上面的线程。
我们先在相应的栈里,找到对应的持有锁的栈。下面是栈中的阻塞关系。
- **第一个栈**
- **第二个栈**
你要注意,这是两个栈文件。所以,我们要分别从这两个栈文件里找到各自的对应等待关系。下面这段代码就对应了上面的Waiting线程。
```
-- 第一处
"http-nio-8401-exec-890" #86930 daemon prio=5 os_prio=0 tid=0x00007f28680a5800 nid=0x561d waiting for monitor entry [0x00007f2800263000]
java.lang.Thread.State: BLOCKED (on object monitor
at java.security.Provider.getService(Provider.java:1035)
- locked <0x000000071ab1a5d8> (a sun.security.provider.Sun)
at sun.security.jca.ProviderList.getService(ProviderList.java:332)
.....................
at com.dunshan.mall.auth.util.MD5Util.toMD5(MD5Util.java:11)
at com.dunshan.mall.auth.config.MyPasswordEncoder.matches(MyPasswordEncoder.java:23)
.....................
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46)
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
at com.dunshan.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
at com.dunshan.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
-- 第二处
"http-nio-8401-exec-871" #86739 daemon prio=5 os_prio=0 tid=0x00007f28681d6800 nid=0x5545 waiting for monitor entry [0x00007f2801a7b000]
java.lang.Thread.State: BLOCKED (on object monitor
at sun.security.rsa.RSACore$BlindingParameters.getBlindingRandomPair(RSACore.java:404)
- locked <0x000000071ddad410> (a sun.security.rsa.RSACore$BlindingParameters)
at sun.security.rsa.RSACore.getBlindingRandomPair(RSACore.java:443)
.....................
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46)
at com.dunshan.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
at com.dunshan.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
at com.dunshan.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
```
你看上面locked这一行的锁ID,既然找到了这两处持有锁的栈,那我们就通过上面的栈,到源码中找到这两处栈的代码:
```
-- 第一处同步代码块
public synchronized Service getService(String type, String algorithm) {
checkInitialized()
// avoid allocating a new key object if possible
ServiceKey key = previousKey
if (key.matches(type, algorithm) == false) {
key = new ServiceKey(type, algorithm, false);
previousKey = key
if (serviceMap != null) {
Service service = serviceMap.get(key)
if (service != null) {
return service;
ensureLegacyParsed()
return (legacyMap != null) ? legacyMap.get(key) : null;
-- 第二处同步代码块
// return null if need to reset the parameters
BlindingRandomPair getBlindingRandomPair(
BigInteger e, BigInteger d, BigInteger n) {
if ((this.e != null && this.e.equals(e)) ||
(this.d != null && this.d.equals(d))) {
BlindingRandomPair brp = null;
synchronized (this) {
if (!u.equals(BigInteger.ZERO) &&
!v.equals(BigInteger.ZERO))
brp = new BlindingRandomPair(u, v);
if (u.compareTo(BigInteger.ONE) <= 0 ||
v.compareTo(BigInteger.ONE) <= 0) {
// need to reset the random pair next time
u = BigInteger.ZERO
v = BigInteger.ZERO
} else {
u = u.modPow(BIG_TWO, n)
v = v.modPow(BIG_TWO, n)
} // Otherwise, need to reset the random pair.
return brp;
return null;
```
你可以看到,第一处是JDK中提供的getService类采用了全局同步锁定,导致的分配key时产生争用,这个其实在JDK的Bug List中有过描述,详见[JDK-7092821](https://bugs.openjdk.java.net/browse/JDK-7092821)。准确来说,它不算是Bug,如果你想改的话,可以换一个库。
第二处是JDK中提供的RSA方法,是为了防范时序攻击特意设计成这样的。RSA中有大素数的计算,为了线程安全,RSA又加了锁。关于RSA的逻辑,你可以去看下源代码的/sun/security/rsa/RSACore.java中的逻辑。
不过,RSA是一种低效的加密方法,当压力发起来的时候,这样的synchronized类必然会导致BLOCKED出现。对此,在源码中有下面这样一段注释,其中建议先计算u/v,可以提高加密效率。
```
* Computing inverses mod n and random number generation is slow, s
* it is often not practical to generate a new random (u, v) pair for
* each new exponentiation. The calculation of parameters might even be
* subject to timing attacks. However, (u, v) pairs should not be
* reused since they themselves might be compromised by timing attacks,
* leaving the private exponent vulnerable. An efficient solution to
* this problem is update u and v before each modular exponentiation
* step by computing:
*
* u = u ^ 2
* v = v ^ 2
* The total performance cost is small
```
既然我们已经知道了这两个BLOCKED产生的原因,那下一步的操作就比较简单了。
- 针对第一处锁:实现自己的方法,比如说实现一个自己的分布式锁。
- 针对第二处锁:换一个高效的实现。
至此,我们就找到了应用中BLOCKED的逻辑。因为我们这是一个性能专栏,所以我就不再接着整下去了。如果你是在一个项目中,分析到这里就可以把问题扔给开发,然后去喝茶了,让他们伤脑筋去,哈哈。
不过,这只是一句玩笑而已,你可别当真。作为性能分析人员,我们要给出合情合理并且有证据链的分析过程,这样我们和其他团队成员沟通的时候,才会更加简单、高效。
## Member服务定向分析
分析完Auth服务后,我们再来看看Member服务的性能怎么样。因为全局监控数据前面我们已经展示了,所以这里不再重复说明,我们直接来拆分一下对Member服务调用时的响应时间。
- **Gateway上的响应时间**
- **Member上的响应时间**
- **Auth上的响应时间**
从上面的信息来看,这几段都有不同的时间消耗: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. 低效的代码有什么优化思路?
记得在留言区和我讨论、交流你的想法,每一次思考都会让你更进一步。
如果你读完这篇文章有所收获,也欢迎你分享给你的朋友,共同学习进步。我们下一讲再见!