性能优化案例-提升单机QPS,CPU优化

系统优化案例-提升单机QPS,CPU优化

背景

最近公司接入ADX广告竞价相关业务。需要我们做ADX竞价系统。该业务有一下特性:

  1. 请求量巨大 6W+QPS
  2. 媒体方要求询价接口延时极低 65ms内返回。(很多大厂都要求30ms)
  3. 对于我们公司来说业务复杂性又很高。

我们在接入媒体方ADX询价业务时,我们申请了75台机器(8c16g)。但是只能支撑起7k的的QPS。
(不要鄙视我们,因为要快速响应业务。系统都是在老系统上修修补补上去的)。平均单台机器吞吐在100不到。主要的瓶颈是CPU。当单机QPS达到100时,CPU已经到达了75%,线上已经告警了。

所以我们问题点就在cpu上。OK我们开始吧。

分析

一般CPU问题,大概方向

  1. GC问题
  2. C2编译问题
  3. IO问题
  4. 代码复杂度过高

我们这个ADX引擎系统。没有IO。并且系统都有灰度预热。所以2、3直接可以排除。

GC问题:

查看CAT监控图:
在这里插入图片描述
上图可以看到GC 每分钟平均 发生miorGC 25次。每分钟 gc耗时0.6s
通过 jinfo -flags pid 查看启动参数。两个参数比较异常:
-XX:MaxGCPauseMillis=20ms
-XX:G1ReservePercent=25
解释一下:
MaxGCPauseMillis G1的预估GC暂停耗时。默认200ms。如果设置的很低会导致每次gc只能回收很少的空间。就会导致频繁GC且可能导致gc回收速度跟不上内存分配速度导致FULLGC。如果设置很大则会导致系统停顿时间很长。
G1ReservePercent G1 保留内存,为了防止GC回收时,内存分配不够。默认15。该参数如果设置很大,也会导致频繁GC。如果设置很小。可能会导致内存空闲不够。导致FUllGC.

所以很简单。只要把 MaxGCPauseMillis 调大到 150ms; G1ReservePercent 调整到 15%。
优化后:

在这里插入图片描述
通过上面简单操作同等QPS,cpu下降了10%左右。

代码复杂度过高

那这个问题就比较复杂了。
一般来讲肯定要通过有一下步骤
1、top 查看cpu损耗最大的进程
2、ps -mp 查看该进程下那个线程最耗用cpu
3、jstack pid 根据上面找到的 线程id 找到对应那个线程。分析堆栈

哎可惜了。上面并没有帮我们找到问题。因为我们cpu基本分散在各个线程上的。没有那个线程特别耗用CPU。

嘿嘿难不倒我,祭出线上问题神器。async-profiler 。阿里巴巴也把该工具集成到arthas里面去了。

简单介绍下该工具的原理:
该工具就是监控你的jvm进程。隔一段时间,采集一下所有线程的栈帧,那么此时栈顶的方法即表示在执行的方法。通过采样的次数基本能确定CPU耗用在那个方法上。并且该工具还支持导出火焰图。不要太爽

OK 我们实操:

## 下载 arthas jar包到服务器
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
## 启动监控程序
java -jar arthas-boot.jar

程序会帮你列出所有的java进程,输入对应的进程号即可进入arthas

在这里插入图片描述

## 开启监控cpu(默认)
$ profiler start
Started [cpu] profiling
## 关闭监控cpu,并默认输出.svg的火焰图报告
$ profiler stop
profiler output file: /tmp/demo/arthas-output/20191125-135546.svg
OK

在这里插入图片描述
我们找到对应的文件下载下来。浏览器打开就可以看到。
在这里插入图片描述
这里科普一下火焰图:
宽度:代表占用cpu的时间长度
高度:你的调用栈
颜色:cpu的负载情况
具体可以自己去学习一下。

根据上面的火焰图我们可以着重关注宽度比较长的、并且包名是我们自己公司的包名

最终让我逮到。
在这里插入图片描述
copyAndPutItem 这个方法,在火焰图中多次出现,简单加了一下该方法耗用CPU大概占了50%。ok问题基本就是他了。那么具体他哪里耗CPU呢。
说来这个问题真的很low。(手动鄙视之前写这段代码的老哥)

可以点击火焰图的栈顶。发现大部分都出现在hashMap.putVal()方法。
然后找到源程序。发现并没有hashMap啊。what?搞我?
其实不然,由于涉及到公司隐私我简单伪代码一下:

每次请求过来都要走一遍这个逻辑:

//list1-n 是从本地缓存里拿出来的
for(大概一两千次){
new HashSet(list1)
new HashSet(list2)
new HashSet(list3)
new HashSet(list4)
new HashSet(list5)
new HashSet(list6)
.....
}

看出问题没?对没错就是 new HashSet(list),我的神啊,怎么能干这种事。大家可以去看看HashSet的源码,其实这步操作就等于new 一个 HashMap 然后吧list里的元素一个个putVal进去。
那么问题找到了怎么解决呢?
其实很简单,你这个list不是从缓存中拿到的吗。我在缓存的时候直接缓存set不就完事了吗。为啥要在一个大的for循环里面做这种事呢。

效果

通过上面简简单单的优化。直接CPU降了40% 多。然后继续压测一波,单机已经能够承载250QPS了。后面有根据火焰图对日志打印,其他代码进行优化。现在单机可以承载350QPS了。

但是离目标还是很远。哎路且长。

总结

通过上面的过程,可以发现很多同学在写代码的时候根本不考虑性能问题。只要功能ok就没问题。其实不然,一个程序员如果没有这点敏感度,是做不好的。迟早是要被行业所淘汰。对代码有一颗敬畏之心。
记录一下这次的排查过程,也给自己提个醒,远离这样的代码。做一个快乐的码er。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七层汉堡王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值