0 前言
上节针对生成订单信息这个接口做了三个阶段的分析定位和优化动作,让TPS变得正常。不过,系统资源并没有完全用起来,这个接口显然还有优化空间。性能优化的过程中,要把资源都用起来。
在性能环境中做优化,把资源用起来是为了看系统的最大容量在哪里。这并不意味着,你可以在生产环境中让硬件使用到这种程度。
对于一个不可控的系统容量来说,资源使用率高,极有可能导致各种问题出现。所以,安全稳妥起见,很多生产环境的资源利用率都是非常低的,倘若用得超过20%,运维都得半夜惊出一身冷汗。
而我们在性能环境中的测试结果,要想给生产环境配置个明确且可借鉴的结论,就必须先去分析生产的业务容量,然后再来确定当生产业务容量达到峰值的时候,相应的硬件资源用到多少比较合理。
不过,在我们的优化环境中,可通过把一个系统用起来,来判断软件的容量能力。所以,接着上节内容,来第四阶段。你将看到在业务逻辑复杂的情况下,我们该怎么做优化。
1 第四阶段
现在正常分析时间消耗到哪去了,解决快慢问题,才能进而解决资源没用起来的问题。
1.1 拆分响应时间
之前都用APM拆分响应时间,这次用日志拆分时间。
Gateway:
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 151 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 147 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 141 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 122 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 125 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 150 ms
10.100.79.93 - - [04/Feb/2021:00:13:17 +0800] "POST /mall-order/order/generateOrder HTTP/1.1" 200 726 8201 177 ms
1
2
3
4
5
6
7
Order:
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 72 ms 72 ms
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 94 ms 93 ms
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 76 ms 76 ms
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 95 ms 95 ms
10.100.79.106 - - [04/Feb/2021:00:13:31 +0800] "POST /order/generateOrder HTTP/1.1" 200 738 "-" "Apache-HttpClient/4.5.12 (Java/1.8.0_261)" 90 ms 90 ms
先不用看后面的服务,因为从这个接口往后就直接到数据库,先看应用本身有没有问题。
这里只截取部分数据,但并不是说我们只看这些就够。在项目中的话,可通过写脚本或其他的方式自己做响应时间的统计。
从上面的信息可看,这接口的整个响应时间150ms,而在order服务上就消耗90毫秒。所以,下面我们要分析:为什么在order上会消耗这么久的时间。
1.2 定向监控分析
想知道Order服务的时间消耗,得知道Order应用中的线程都在做什么动作,先直接分析Order的栈。
Spring Boot Admin查看线程的整体状态:
线程确实繁忙。至于这些线程在做什么,通过栈的内容可知,再确定优化方向。
但由于系统资源还没用到上限,先调整Tomcat线程数,加大些,争取让Order应用把硬件资源用起来。
原值:
max: 20
修改为:
max: 100
调整后结果:
性能更差……事实证明,偷懒绕不过坑,只有再次查看响应时间消耗到哪。
通过用各个服务的日志拆分响应时间,发现在Member服务有这样日志(为让你看清楚,截取一些时间消耗较大的日志,这是改Order的Tomcat线程池后的结果):
10.100.69.248 - - [04/Feb/2021:00:37:15 +0800] "GET /sso/feign/info HTTP/1.1" 200 814 "-" "okhttp/3.14.8" 2348 ms 2348 ms
10.100.69.248 - - [04/Feb/2021:00:37:17 +0800] "GET /sso/feign/info HTTP/1.1" 200 816 "-" "okhttp/3.14.8" 4155 ms 4155 ms
10.100.69.248 - - [04/Feb/2021:00:37:17 +0800] "GET /sso/feign/info HTTP/1.1" 200 817 "-" "okhttp/3.14.8" 4968 ms 1813 ms
10.100.69.248 - - [04/Feb/2021:00:37:15 +0800] "GET /sso/feign/info HTTP/1.1" 200 810 "-" "okhttp/3.14.8" 2333 ms 2333 ms
10.100.69.248 - - [04/Feb/2021:00:37:17 +0800] "GET /sso/feign/info HTTP/1.1" 200 815 "-" "okhttp/3.14.8" 5206 ms 4970 ms
10.100.69.248 - - [04/Feb/2021:00:37:20 +0800] "GET /sso/feign/info HTTP/1.1" 200 818 "-" "okhttp/3.14.8" 6362 ms 6362 ms
10.100.69.248 - - [04/Feb/2021:00:37:20 +0800] "GET /sso/feign/info HTTP/1.1" 200 818 "-" "okhttp/3.14.8" 6710 ms 6710 ms
10.100.69.248 - - [04/Feb/2021:00:37:20 +0800] "GET /sso/feign/info HTTP/1.1" 200 817 "-" "okhttp/3.14.8" 6696 ms 6587 ms
10.100.69.248 - - [04/Feb/2021:00:37:21 +0800] "GET /sso/feign/info HTTP/1.1" 200 813 "-" "okhttp/3.14.8" 7987 ms 7976 ms
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] "GET /sso/feign/info HTTP/1.1" 200 814 "-" "okhttp/3.14.8" 8784 ms 8784 ms
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] "GET /sso/feign/info HTTP/1.1" 200 817 "-" "okhttp/3.14.8" 9100 ms 8764 ms
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] "GET /sso/feign/info HTTP/1.1" 200 834 "-" "okhttp/3.14.8" 9126 ms 9013 ms
10.100.69.248 - - [04/Feb/2021:00:37:22 +0800] "GET /sso/feign/info HTTP/1.1" 200 817 "-" "okhttp/3.14.8" 9058 ms 9058 ms
10.100.69.248 - - [04/Feb/2021:00:37:23 +0800] "GET /sso/feign/info HTTP/1.1" 200 820 "-" "okhttp/3.14.8" 9056 ms 9056 ms
显然Member服务响应时间太长。而在生成订单信息这个接口中,也确实调用Member服务,因为要使用Token。既然是Order的Tomcat线程池加大了,导致Member服务响应如此慢,有理由判断:Order消耗时间长是因为Member服务不能提供Order请求时的快速响应,即Member的性能差。
要分析Member性能为何差,可直接到Member打印栈信息。
完整的分析逻辑先看全局监控数据,再看定向监控数据。通过全局监控数据看整体资源消耗:
worker-8 CPU资源居然用这么高!说明在前面增加Order的Tomcat线程数有价值。现在,瓶颈点到另一个地方,Member服务。
既然worker-8的资源使用率高,看它上面的Pod,Member就在worker8:
[root@k8s-master-2 ~]# kubectl get pods -o wide | grep k8s-worker-8
elasticsearch-client-0 1/1 Running 0 38h 10.100.231.233 k8s-worker-8 <none> <none>
monitor-mall-monitor-d8bb58fcb-kfbcj 1/1 Running 0 23d 10.100.231.242 k8s-worker-8 <none> <none>
skywalking-oap-855f96b777-5nxll 1/1 Running 6 37h 10.100.231.235 k8s-worker-8 <none> <none>
skywalking-oap-855f96b777-6b7jd 1/1 Running 5 37h 10.100.231.234 k8s-worker-8 <none> <none>
svc-mall-admin-75ff7dcc9b-8gtr5 1/1 Running 0 17d 10.100.231.208 k8s-worker-8 <none> <none>
svc-mall-demo-5584dbdc96-fskg9 1/1 Running 0 17d 10.100.231.207 k8s-worker-8 <none> <none>
svc-mall-member-5fc984b57c-bk2fd 1/1 Running 0 12d 10.100.231.231 k8s-worker-8 <none> <none>
[root@k8s-master-2 ~]#
同时,我们还能发现,这个节点上有不少服务,而这些服务都是比较吃CPU的,并且在压力过程中,还出现了sy cpu消耗很高的情况,我截两个瞬间的数据给你看看,一个是sy cpu高的情况,一个是us cpu高的情况,具体如下所示:
- sys cpu高的情况
[root@k8s-worker-8 ~]# top
top - 00:38:51 up 28 days, 4:27, 3 users, load average: 78.07, 62.23, 39.14
Tasks: 275 total, 17 running, 257 sleeping, 1 stopped, 0 zombie
%Cpu0 : 4.2 us, 95.4 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.4 st
%Cpu1 : 1.8 us, 98.2 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 2.1 us, 97.9 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 1.0 us, 99.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 16266296 total, 1819300 free, 7642004 used, 6804992 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 8086580 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12902 root 20 0 1410452 32280 17744 S 48.1 0.2 751:39.59 calico-node -felix
9 root 20 0 0 0 0 R 34.8 0.0 131:14.01 [rcu_sched]
3668 techstar 20 0 4816688 1.3g 23056 S 33.9 8.5 111:17.12 /usr/share/elasticsearch/jdk/bin/java -Xshare:auto -Des.networkaddress.cache.ttl=60 -Des.networkaddress+
26105 root 20 0 119604 6344 2704 R 25.8 0.0 0:02.36 runc --root /var/run/docker/runtime-runc/moby --log /run/containerd/io.containerd.runtime.v1.linux/moby+
26163 root 20 0 19368 880 636 R 25.2 0.0 0:00.95 iptables-legacy-save -t nat
26150 root 20 0 18740 3136 1684 R 21.6 0.0 0:01.18 runc init
26086 root 20 0 18744 5756 2376 R 20.3 0.0 0:03.10 runc --root /var/run/docker/runtime-runc/moby --log /run/containerd/io.containerd.runtime.v1.linux/moby+
410 root 20 0 0 0 0 S 19.4 0.0 42:42.56 [xfsaild/dm-1]
14 root 20 0 0 0 0 S 14.8 0.0 54:28.76 [ksoftirqd/1]
6 root 20 0 0 0 0 S 14.2 0.0 50:58.94 [ksoftirqd/0]
26158 root 20 0 18740 1548 936 R 14.2 0.0 0:00.90 runc --version
31715 nfsnobo+ 20 0 129972 19856 9564 S 11.3 0.1 12:41.98 ./kube-rbac-proxy --logtostderr --secure-listen-address=[172.16.106.56]:9100 --tls-cipher-suites=TLS_EC+
10296 root 20 0 3402116 113200 39320 S 10.3 0.7 2936:50 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubern+
22 root rt 0 0 0 0 S 8.7 0.0 3:18.08 [watchdog/3]
26162 root 20 0 139592 2792 2508 R 8.4 0.0 0:00.39 /opt/cni/bin/calico
6843 root 20 0 965824 110244 30364 S 7.7 0.7 1544:20 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
24 root 20 0 0 0 0 S 7.4 0.0 49:03.89 [ksoftirqd/3]
3636 techstar 20 0 4368 364 280 S 6.8 0.0 0:12.19 /tini -- /usr/local/bin/docker-entrypoint.sh eswrapper
26159 root 20 0 18740 760 552 R 6.5 0.0 0:00.28 runc --version
1755 root 20 0 411108 5836 4416 S 4.8 0.0 35:39.97 /usr/libexec/packagekitd
- us cpu高的情况
[root@k8s-worker-8 ~]# top
top - 00:43:01 up 28 days, 4:31, 3 users, load average: 72.51, 68.20, 47.01
Tasks: 263 total, 2 running, 260 sleeping, 1 stopped, 0 zombie
%Cpu0 : 77.2 us, 15.7 sy, 0.0 ni, 2.2 id, 0.0 wa, 0.0 hi, 4.8 si, 0.0 st
%Cpu1 : 77.0 us, 15.7 sy, 0.0 ni, 2.3 id, 0.0 wa, 0.0 hi, 5.0 si, 0.0 st
%Cpu2 : 70.3 us, 20.9 sy, 0.0 ni, 2.9 id, 0.0 wa, 0.0 hi, 5.9 si, 0.0 st
%Cpu3 : 76.6 us, 12.2 sy, 0.0 ni, 5.1 id, 0.0 wa, 0.0 hi, 6.1 si, 0.0 st
KiB Mem : 16266296 total, 1996620 free, 7426512 used, 6843164 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 8302092 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
20072 root 20 0 7944892 689352 15924 S 137.1 4.2 3127:04 java -Dapp.id=svc-mall-member -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.+
29493 root 20 0 3532496 248960 17408 S 98.3 1.5 0:06.70 java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Dmode=no-init -Xmx2g -Xms2g -cl+
28697 root 20 0 3711520 1.0g 18760 S 61.6 6.7 124:41.08 java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Dmode=no-init -Xmx2g -Xms2g -cl+
25885 root 20 0 3716560 1.2g 18908 S 59.3 7.6 183:12.97 java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Dmode=no-init -Xmx2g -Xms2g -cl+
6843 root 20 0 965824 109568 30364 S 7.6 0.7 1544:49 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
3668 techstar 20 0 4816688 1.3g 23056 S 6.6 8.5 111:58.56 /usr/share/elasticsearch/jdk/bin/java -Xshare:auto -Des.networkaddress.cache.ttl=60 -Des.networkaddress+
10296 root 20 0 3402372 111692 39320 S 6.6 0.7 2937:43 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubern+
18 root rt 0 0 0 0 S 5.0 0.0 5:57.54 [migration/2]
6 root 20 0 0 0 0 S 2.6 0.0 51:21.52 [ksoftirqd/0]
410 root 20 0 0 0 0 D 2.6 0.0 43:08.23 [xfsaild/dm-1]
28310 root 20 0 7807048 565740 15924 S 2.6 3.5 1036:53 java -Dapp.id=svc-mall-admin -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent.s+
29741 root 20 0 7749608 540376 15848 S 2.3 3.3 304:41.47 java -Dapp.id=svc-mall-monitor -javaagent:/opt/skywalking/agent/skywalking-agent.jar -Dskywalking.agent+
12902 root 20 0 1410452 30368 17744 S 2.0 0.2 752:30.32 calico-node -felix
16712 root 0 -20 0 0 0 S 2.0 0.0 1:56.16 [kworker/2:0H]
6381 root 20 0 7782400 491476 15928 S 1.7 3.0 441:08.96 java -Dapp.id=svc-mall-demo -
从sy cpu高的top数据看,这节点显然在不断调度系统资源,通过top中的rcu_sched/softirq等进程可知,这显然是因为k8s在这节点过多安排任务。所以,先把Member服务移到另一个worker,然后看到TPS:
TPS增加到400多,方向对的。
那之前修改Order服务的Tomcat线程数没看到效果?因为压力已到Member服务,这让Member服务所在worker节点资源使用率增加,导致Member服务无法正常响应请求。因此,整个TPS看起来没有什么优化效果。现在,移走Member服务,看到效果明显增加,这说明方向还在正确道路。
再回看整体的资源监控:
现在,没有一个worker的资源用满或接近用满,完全不符“把资源都用起来”目标。
长时间压力中,发现资源怎么也用不上去。而且在上节第三阶段的最后一个图也能清楚看到这点。
至此,查看了一次次性能分析决策树,也调整一些参数,如Spring Boot中的Tomcat连接池、JDBC池、Jedis池、MQ池等,TPS似有增加趋势,但很不明显。
只能开始新一轮定位。
2 第五阶段
2.1 定位时间消耗
跟踪方法的执行过程来判断时间消耗。优化过程中,唯有思路不变,手段任选。
这方法和我们用日志拆分时间的逻辑一样。可直接用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
为何说这几个方法重要?对跟踪的内容,我们主要判断:消耗时间的方法是不是固定的。若时间不是消耗在固定的方法,就麻烦了,因为这说明不是方法本身问题,而是其他的资源影响了方法的执行时间;若时间一直消耗在固定的方法较容易,只要接着去跟踪这个方法。
而我反复跟踪了多次之后,总是发现上面几个方法都比较消耗时间。既然已经知道了方法的时间消耗,那全局监控已救不了。
2.2 定向监控分析
根据分析思路,定向监控分析前,反复分析了全局监控计数器,没觉得有什么资源使用问题。并从压力工具到数据库,也没发现阻塞点。
但上面我们也确实看到响应时间消耗在了几个具体方法,并且这几个方法并不是一直都消耗这么长的时间,而是有快有慢。
反复确认后,有必要看业务逻辑。因为对于一个复杂的业务来说,若业务代码逻辑太长,不管咋优化,都不会有啥效果,最后只能按扩容。
不过即便加机器,也要给出加机器逻辑。若业务可优化,更要尽力一试。从成本说,优化代码是更优选。
既然我们想优化业务,得知业务的调用逻辑。打开IDEA,找到generateOrder方法,把sequence diagram打开看到业务逻辑图:
获取用户名
获取购物车列表
获取促销活动信息
判断库存
判断优惠卷
判断积分
计算金额
转订单并插库
获取地址信息
计算赠送积分和成长值
插入订单表
更新优惠卷状态
扣积分
删除购物车商品
发送取消订单消息
返回结果
下订单动作真长。对这样复杂接口,若业务逻辑要求必须这样,那在接口上就没啥优化空间。在前面,已将TPS优化到400多,在这样硬件机器,基本就这样了。
不是要去设计一个下订单的业务逻辑,因为每个企业下订单逻辑有不同业务限制,修改业务逻辑需要所有的相关人员一起商讨确定。不过,可通过分析结果给出优化建议。
优惠卷、积分、发送延时取消订单信息的步骤都从下订单步骤删掉。不是我要这样改业务逻辑,而是想看这样改后,TPS是否增加。若增加,说明方向对,即这个业务逻辑需要再和各方商量,重新设计。
修改后的TPS:
确实有效果。若你在项目中遇到这样的接口,优化建议就可提:
分库分表
利用缓存
异步处理非关键步骤
大接口拆成小接口
但建议终归是建议,通常在一个企业中,对于这样的接口,技术团队会根据具体的业务逻辑做长时间的技术分析,判断如何实现。如果确实没办法在技术上做优化,那就只能上最后一招:扩容!这扩容就不再是扩某段,而是一整条链路上涉及到的服务。
在一个业务链路中,每个企业通常都是根据发展的速度做相应的技术沉淀:
如果技术团队太追潮流了,学习成本大,不见得是好事
太陈旧,维护的成本大,也不见得是好事
只有根据实际的业务发展不断地演进业务流程和技术实现,才是正道。
3 第六阶段
3.1 定位TPS会降下来的问题
接着压,又出现:
TPS掉的心碎!仍按性能分析决策树,一个个计数器看过去,最后在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(锁等待):
有锁等待,就查锁关系,看什么在等待锁。看具体的锁关系前,查看一下应用日志。
为什么要看应用日志呢?因为对DB,锁是为保护数据的一致性,而产生锁的事务自然从应用中来。在MySQL中和在应用中看到的事务SQL应该对应。而现在我们只看到MySQL中的锁,还不知道在应用中会是什么样子,所以,要看应用日志。
不要用重压力工具中的某些具有唯一性的参数化数据。因为当参数化数据用重了,在数据库中执行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语句被阻塞。过一会儿之后,你就会看到:
所有insert都在LOCK WAIT状态,这就是表级锁对insert产生的影响。再查一下锁和锁等待的话,就会看到:
lock_mode这一列的值全是X,即X锁。排他锁(X锁),又叫写锁。图中的锁类型(lock_type)全是RECORD,锁住的是索引,且索引是GEN_CLUST_INDEX,说明这锁等待是因为innodb创建的隐藏的聚集索引。
当一个SQL没走任何索引时,就会在每条聚集索引后面加X锁,这和表级锁现象一样,只是在原理上有所不同。为了方便描述,仍用“表级锁”描述。
要查锁,就得看看是谁持有锁。经过仔细查看上面的INNODB_LOCK_WAIT后,确定这锁对应的事务ID是157710723,对应SQL:
update oms_order set status=4 where id in ( 0 );
去代码查这段update代码:
/**
* 批量修改订单状态
*/
int updateOrderStatus(@Param("ids") List<Long> ids,@Param("status") Integer status);
原来这是批量任务调用:
批量任务问题在于,在一个订单表中做批量更新操作,并且这个批量查询的内容还挺多。因为上面的ID是0,表示订单未支付,而未支付订单在这表很多,所以,在更新时做大范围查找,会进而导致表级锁。
这个批量任务的设计明显有问题。要在订单表中做更新这么大的动作,那也应该精准更新,而非范围更新。对订单的更新逻辑,可选择其他实现方式。
锁的原因找到,现在要把范围更新改为非常精准的更新,让它不产生表级锁。修改后,重新执行场景:
从优化效果来看,TPS已经达到700。对这样一个复杂的接口来说,不错。
其实,这个订单业务还有很多优化空间:
异步生成订单序列号,然后存放到Redis里,随用随取
批量业务需要重新设计
读写分离之后,对业务代码也做相应更新
……
由于订单逻辑是电商复杂一步,再说就超出性能范畴。
4 总结
性能优化过程中,问题是像洋葱一样一个个剥开的。虽然有可能一个优化动作就可以产生很好的效果,但是我们一定不要着急,要慢慢分析问题。
一阶段,修改线程池产生了效果,但也出现了新问题
二阶段,解决查询大量数据导致内存被耗光的问题
三阶段,解决了索引问题
四阶段,重新调配资源,让系统的调度更合理
五阶段,定位方法的时间消耗问题,要在分析了业务逻辑之后再做相应的优化,不要因一味追求性能的优化效果而纠结
六阶段中,定位了批量任务设计不合理的问题。在正经的批量任务中,批量产生的表级锁和insert的功能点,一定要分开。
总之,在分析的过程中,我们不要界定问题的边界,遇到什么问题就解决什么问题,不急不燥,不卑不亢。
如何快速定位内存被消耗光的情况?
使用jmap可以查看消耗内存占比,对消耗内存占比较大的方法进行分析。
比如数据库查询结果太大,存入内存会消耗大量内存;或者循环调用;
如何快速定位业务逻辑导致的TPS上不去、资源也用不上的情况?
分析响应时间。找出响应时间长的节点,时间消耗在哪里,在干什么从而根据情况继续分析
参考
日志响应时间配置可以参考本文:https://mp.weixin.qq.com/s/JeKdirFrM5LGAqZrrdKoXA