问题初始
线上环境运行一段时间便出现cpu爆满,导致服务不可用,无法提供正常服务。
排查过程
- 通过top命令查看进程使用状况
发现,进程号为5525的进程cpu利用率很高。
- 随即查看进程号为5525的进程中的线程情况,使用命令:
top -H -p 5525
复制代码
发现前四个线程cpu占用率很高,而且持有cpu时间很长,所以查看该线程是谁在使用,因为该线程号显示为十进制,所以首先将其转化为十六进制
printf "%x\n" 5530
复制代码
接着使用 jstack 命令查看该线程是谁持有
jstack 5525 | grep 159a
复制代码
发现竟然是GC占用,cpu利用率很高,而且持有cpu很长时间。继续-发现是GC的问题,接下来查看一下GC的状态信息
- 通过 jstat查看 gc 状态(没10秒gc状态)
jstat -gcutil 5525 2000 10
复制代码
可以看出内存的年轻代和年老带的利用率都达到了惊人的100%。FGC的次数也特别多,并且在不断飙升。可以推断出 程序肯定是在哪里的实现有问题,需要重点查看大对象或者异常多的对象信息。此时可以生成headdump文件拿到本地来分析
- 通过 jmap 命令生成dump文件,down下来进行分析
jmap -dump:format=b,file=dump.bin 5525
复制代码
- 通过 jstack 命令生成堆栈信息
jstack -l 5525 >> jstack.out
复制代码
使用eclipse的mat工具分析dump文件,结果如下
点开查看 发现问题出现在阿里云的oss中 sdk 的 com.aliyun.oss.common.comm.IdleConnectionReaper 类中 根据官方说明:使用方法及mat概念:www.lightskystreet.com/2015/09/01/…
www.alibabacloud.com/help/zh/doc… 然后分析代码,哪里创建的client,创建client的动作在哪里使用。
private OSSClient getOSSClient(){
OSSClient client = null;
client = new OSSClient(endpoint, accessKeyId, accessKeySecret);
return client;
}
复制代码
结果发现,每次都会创建一个新的对象,创建一个新的oss的链接 然后发现使用的地方
public boolean objectExist(String path) {
OSSClient ossClient = getOSSClient();
return ossClient.doesObjectExist(bucketName, path);
}
复制代码
业务中涉及到文件存储的一律都是oss存储,而且由于历史原因,有些路径不是oss路径,而是本地服务器路径,所以在返回文件链接时,会进行判断,
public static String getResourceUrl(String url) {
if (url != null && !url.trim().isEmpty()) {
if (url.trim().toLowerCase().startsWith("http")) {
return url.trim();
}
// 如果oss路径下能查到,则从oss查
if (url.startsWith("/")) {
url = url.substring(1);
}
// 注释掉去oss验证的步骤,不会有性能损耗
boolean objectExist = ossHelper.objectExist(url);
if (objectExist) {
return XTools.getUrl(getOssServer(), url);
}
// 否则从本地查
return XTools.getUrl(getResServer(), url);
}
return "";
}
复制代码
分析一下,在判断oss路径是否存在的时候,也就是调用boolean objectExist = ossHelper.objectExist(url);方法时,每次都会创建一个oss的链接,而我们的文件是很多的,所以每个文件返回访问链接的时候都会调用这个方法,所以创建的oss的链接是巨多的!!! 查询资料发现,如下网友也中招:
所以大概原因找到了,就是ossClient的链接太多了,扛不住了,所以一直在进行FGC,导致服务不可用了,最后找到相关的代码,发现有个小方法里面在每次上传或者下载的时候,都会去创建一个ossClient。修改了代码将ossClient调用的地方改成了单例。修改完线上跑了一段日子,后来也没有出现过这样的问题。
segmentfault.com/a/119000001…
代码优化
由此得出结论:优化ossHelper类中创建client的方法。并且在初始化 ossHelper 的 bean的时候,初始化创建client的连接,交给Spring管理。
private static OSSClient ossClient;
private OSSClient getOSSClient(){
if(ossClient == null){
synchronized (this){
ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
}
}
return ossClient;
}
复制代码
xml:
"ossHelper" class="com.ybl.crm.common.oss.OssHelper" init-method="getOSSClient">
"0" value="${oss.endpoint}"/>
"1" value="${oss.access.key.id}"/>
"2" value="${oss.access.key.secret}"/>
"3" value="${oss.bucket.name}"/>
"4" value="${oss.server}"/>
复制代码
注解方式:@Component声明组件,@PostConstruct声明需要随组件初始化的方法.
jmeter压测验证
经jmeter压测验证
samples:请求数
average:平均耗时,单位:毫秒
error:错误率
复制代码
// 此处是每秒启动20个线程,每个线程发送10个请求
Number of threads(users) : 用户数,即线程数
Ramp-up period(in seconds) : 线程多长时间全部启动起来
Loop Count : 每个线程发送的请求数
复制代码
两千个请求,平均每个请求2.3秒,请求很快,错误率很低。 原先写法的话,当请求到达一定次数,服务器服务已经无法响应,导致错误率上升!而且cpu一直爆满,其内部维护的client满了的话,会不断的触发GC,所以就导致频繁GC。