项目场景:
本项目涉及十几个微服务,其中核心服务四个。近期项目进入开发尾声,遂开始准备性能压测。由于是ToB项目,产品经理计算下来并发数定为880,基础业务数据中10w条为活跃数据,需维持websocket长连接生成任务包推送给指定设备,并执行回调业务,链路较长。测试环境申请了四台云服务器资源,均为4核16g。
压测前准备:
那当然是先review代码啦,还不是为了指标能好看点…
先记录几个简单的优化项
- 先将接口涉及的sql全部跑一遍,非必要查询字段去除,explain分析一下执行计划,缺失的索引加上,查看命中索引的类型,优化sql语句。 本项目创建配置类实现了mybatisPlus的扩展接口MetaObjectHandler,在插入和更新时会自动填充创建时间等字段
@ConditionalOnClass({MetaObjectHandler.class})
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
public MybatisPlusMetaObjectHandler() {
}
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "tenantId", () -> {
return HttpHeaderUtil.getHeaderByName("tenantId");
}, String.class);
this.strictInsertFill(metaObject, "createTime", () -> {
return new Date();
}, Date.class);
this.strictInsertFill(metaObject, "createUname", () -> {
return HttpHeaderUtil.getHeaderByName("UserName");
}, String.class);
}
}
同时查询的时候添加拦截器加上租户字段
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
private String tenantId= null;
.......
因此索引大部分为联合索引,需要注意联合索引的最左匹配原则,其余sql语句优化不再赘述。
- 代码检查是否有遍历循环数据库操作:提出循环外
- 是否循环内有重复定义对象、字符串拼接等
- list遍历循环写法比较:1万以内的数据,for循环的性能要高于foreach和stream;10万以内的数据明显可以看出stream效率最高,其次foreach,最后是for。
- hashMap三种遍历方式在Map容量10万、100万、1000万的情况下的耗时:
size | 10w | 100w | 1000w |
---|---|---|---|
entrySet | 18ms | 55ms | 511ms |
keySet | 24ms | 116ms | 1483ms |
Iterator entrySet | 12ms | 75ms | 684ms |
- 合理使用缓存,由于业务涉及短暂操作(30分钟内),且数据库联表查询较大(千万级),缓存使用较多,减少与数据库的交互
- 一些不重要的同步回调业务丢kafka里异步执行去
以上基本在编码阶段就已达成,自认为小的优化点已经差不多了,让测试同事开始十几个接口和kafka性能压测
问题描述
结果想必都知道了,大跌眼镜。
遂开始排查之路:
-
首当其冲想到的是代码是否仍有未优化的地方,由于该接口涉及四个微服务之间调用。当时未集成skyWalking,遂采取最土的方法,在每次微服务调用之前打印时间戳间隔毫秒数查看执行时间,结果发现,嗯,慢的就很平均,继续大略走读代码,未发现可优化的地方。
不过后来压测接近尾声时运维集成了skyWalking,还有待研究。
-
ok,那咱来排查服务器的问题,让测试同事再压一次,实时监控服务器,cpu瞬间飙升到97%
jps
查看正在运行中的Java虚拟机进程,找到对应服务进程pid
jmap -heap pid
查看堆内存,好家伙,初始堆大小仅分配1024M,眼看着新生代分配到的可怜的273M内存刷刷刷一眨眼就没了
jstat -gc pid 1000
跟踪了一分钟,FGC维持在4次。
ps -ef|grep java
查看启动命令,好家伙,该服务的初始堆内存大小好歹还有1024M,其他服务初始堆内存大小就512M,遂开始调整初始堆内存
-Xms4096m -Xmx4096m
同时添加gc打印、内存溢出dump生成及导出
-XX:+PrintGCDateStamps
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp
- 重新压测一遍,结果还是不尽如人意,平均响应时间仍高达8s,cpu消耗仍居高不下。
top -Hp pid
开始查看各个线程耗费cpu占比,发现较为平均,并无cpu消耗异常突出的线程,挑选一个线程打印堆栈信息,亦无发现异常。
printf "%x\n" tid
jstack pid| grep -A 10 十六进制tid
好吧,导出堆栈信息用jprofiler或者java visualVM分析看看
ok,解决了部分大字符串的问题,但仍然杯水车薪。
-
既然代码没有太大问题,那继续看服务器咯,四台云服务器挨个看,好家伙,突然发现四个核心微服务全部署在同一台服务器上,另有一台服务器处于空闲状态,难怪cpu消耗这么高,确实是有心无力,无可奈何啊。遂将四个核心微服务分开部署,堆内存初始大小拉齐,再次压测,效果立竿见影,一台cpu消耗80%,另两台cpu消耗30%
降了,终于降了! -
但是80%的cpu消耗还是很高,遂继续排查该进程的堆栈情况,发现仍然存在大字符串的情况,跟进代码果然发现漏网之鱼,遍历循环在不少地方打印了任务包相关序列化后对象。
那就调整日志打印级别呗,debug→info ,再次压测,守得云开见月明,cpu消耗降至60%多:
可以美滋滋睡个觉了。
至此,本次性能调优结束。在下抛砖引玉,期待各位大佬提供更多性能调优的思路和法子。
解决方案:
1.sql调优→①语句调优②索引合理使用③分库分表
2.代码性能调优→①着重注意遍历逻辑②缓存合理使用③消息中间件合理使用
3.jvm参数调优→分析堆栈信息及gc
4.服务器资源调优→合理分配资源
5.配置调优→配置中心部署前区分环境差异