java开发接口吞吐量提升10多倍技巧

目录
  • 背景
  • 分析过程
    • 定位“慢”原因
    • 继续定位“慢”的原因
    • 定位CPU使用率高的原因
  • 总结
    • TODO

背景

公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。

当时一想,500/s吞吐量还不简单。Tomcat按照100个线程,那就是单线程1S内处理5个请求,200ms处理一个请求即可。这个没有问题,平时接口响应时间大部分都100ms左右,还不是分分钟满足的事情。

然而压测一开,100 的并发,吞吐量居然只有 50 ...

而且再一查,100的并发,CPU使用率居然接近 80% ...

从上图可以看到几个重要的信息。

最小值: 表示我们非并发场景单次接口响应时长。还不足100ms。挺好!

最大值: 并发场景下,由于各种锁或者其他串行操作,导致部分请求等待时长增加,接口整体响应时间变长。5秒钟。有点过分了!!!

再一看百分位,大部分的请求响应时间都在4s。无语了!!!

所以 1s钟的 吞吐量 单节点只有 50 。距离 500 差了10倍。 难受!!!!

分析过程

定位“慢”原因

这里暂时先忽略 CPU 占用率高的问题

首先平均响应时间这么慢,肯定是有阻塞。先确定阻塞位置。重点检查几处:

  • 锁 (同步锁、分布式锁、数据库锁)
  • 耗时操作 (链接耗时、SQL耗时)

结合这些先配置耗时埋点。

  • 接口响应时长统计。超过500ms打印告警日志。
  • 接口内部远程调用耗时统计。200ms打印告警日志。
  • Redis访问耗时。超过10ms打印告警日志。
  • SQL执行耗时。超过100ms打印告警日志。

上述配置生效后,通过日志排查到接口存在慢SQL。具体SQL类似与这种:

<!-- 主要类似与库存扣减 每次-1 type 只有有限的几种且该表一共就几条数据(一种一条记录)-->
<!-- 压测时可以认为 type = 1 是写死的 -->
update table set field = field - 1 where type = 1 and filed > 1;

上述SQL相当于并发操作同一条数据,肯定存在锁等待。日志显示此处的等待耗时占接口总耗时 80% 以上。

二话不说先改为敬。因为是压测环境,直接改为异步执行,确认一下效果。

PS:当时心里是这么想的: 妥了,大功告成。就是这里的问题!绝壁是这个原因!优化一下就解决了。当然,如果这么简单就没有必要写这篇文章了...

优化后的效果:

嗯...

emm...

好! 这个优化还是很明显的,提升提升了近2倍。

此时已经感觉到有些不对了,慢SQL已经解决了(异步了~ 随便吧~ 你执行 10s我也不管了),虽然对吞吐量的提升没有预期的效果。但是数据是不会骗人的。

最大值: 已经从 5s -> 2s

百分位值: 4s -> 1s

这已经是很大的提升了。

继续定位“慢”的原因

通过第一阶段的“优化”,我们距离目标近了很多。废话不多说,继续下一步的排查。

我们继续看日志,此时日志出现类似下边这种情况:

2023-01-04 15:17:05:347 INFO **.**.**.***.50 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:348 INFO **.**.**.***.21 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:350 INFO **.**.**.***.47 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:465 INFO **.**.**.***.234 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:467 INFO **.**.**.***.123 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:581 INFO **.**.**.***.451 [TID: 1s22s72s8ws9w00] **********************
2023-01-04 15:17:05:702 INFO **.**.**.***.72 [TID: 1s22s72s8ws9w00] **********************

前三行info日志没有问题,间隔很小。第4 ~ 第5,第6 ~ 第7,第7 ~ 第8 很明显有百毫秒的耗时。检查代码发现,这部分没有任何耗时操作。那么这段时间干什么了呢?

  • 发生了线程切换,换其他线程执行其他任务了。(线程太多了)
  • 日志打印太多了,压测5分钟日志量500M。(记得日志打印太多是有很大影响的)
  • STW。(但是日志还在输出,所以前两种可能性很高,而且一般不会停顿百毫秒)

按照这三个思路做了以下操作:

首先,提升日志打印级别到DEBUG。emm... 提升不大,好像增加了10左右。

然后,拆线程 @Async 注解使用线程池,控制代码线程池数量(之前存在3个线程池,统一配置的核心线程数为100)结合业务,服务总核心线程数控制在50以内,同步增加阻塞最大大小。结果还可以,提升了50,接近200了。

最后,观察JVM的GC日志,发现YGC频次4/s,没有FGC。1分钟内GC时间不到1s,很明显不是GC问题,不过发现JVM内存太小只有512M,直接给了4G。吞吐量没啥提升,YGC频次降低为2秒1次。

唉,一顿操作猛如虎。

PS:其实中间还对数据库参数一通瞎搞,这里不多说了。

其实也不是没有收获,至少在减少服务线程数量后还是有一定收获的。另外,已经关注到了另外一个点:CPU使用率,减少了线程数量后,CPU的使用率并没有明显的下降,这里是很有问题的,当时认为CPU的使用率主要与开启的线程数量有关,之前线程多,CPU使用率较高可以理解。但是,在砍掉了一大半的线程后,依然居高不下这就很奇怪了。

此时关注的重点开始从代码“慢”方向转移到“CPU高”方向。

定位CPU使用率高的原因

CPU的使用率高,通常与线程数相关肯定是没有问题的。当时对居高不下的原因考虑可能有以下两点:

  • 有额外的线程存在。
  • 代码有部分CPU密集操作。

然后继续一顿操作:

  • 观察服务活跃线程数。
  • 观察有无CPU占用率较高线程。

在观察过程中发现,没有明显CPU占用较高线程。所有线程基本都在10%以内。类似于下图,不过有很多线程。

没有很高就证明大家都很正常,只是多而已...

此时没有下一步的排查思路了。当时想着,算了打印一下堆栈看看吧,看看到底干了啥~

在看的过程中发现这段日志:

"http-nio-6071-exec-9" #82 daemon prio=5 os_prio=0 tid=0x00007fea9aed1000 nid=0x62 runnable [0x00007fe934cf4000]
   java.lang.Thread.State: RUNNABLE
	at org.springframework.core.annotation.AnnotationUtils.getValue(AnnotationUtils.java:1058)
	at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.resolveExpression(AbstractAspectJAdvisorFactory.java:216)
	at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.<init>(AbstractAspectJAdvisorFactory.java:197)
	at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAnnotation(AbstractAspectJAdvisorFactory.java:147)
	at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(AbstractAspectJAdvisorFactory.java:135)
	at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvice(ReflectiveAspectJAdvisorFactory.java:244)
	at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.instantiateAdvice(InstantiationModelAwarePointcutAdvisorImpl.java:149)
	at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.<init>(InstantiationModelAwarePointcutAdvisorImpl.java:113)
	at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisor(ReflectiveAspectJAdvisorFactory.java:213)
	at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisors(ReflectiveAspectJAdvisorFactory.java:144)
	at org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors(BeanFactoryAspectJAdvisorsBuilder.java:149)
	at org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors(AnnotationAwareAspectJAutoProxyCreator.java:95)
	at org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator.shouldSkip(AspectJAwareAdvisorAutoProxyCreator.java:101)
	at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:333)
	at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:291)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:455)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1243)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
	at cn.hutool.extra.spring.SpringUtil.getBean(SpringUtil.java:117)
        ......
        ......

上边的堆栈发现了一个点: 在执行getBean的时候,执行了createBean方法。我们都知道Spring托管的Bean都是提前实例化好放在IOC容器中的。createBean要做的事情有很多,比如Bean的初始化,依赖注入其他类,而且中间还有一些前后置处理器执行、代理检查等等,总之是一个耗时方法,所以都是在程序启动时去扫描,加载,完成Bean的初始化。

而我们在运行程序线程堆栈中发现了这个操作。而且通过检索发现竟然有近200处。

通过堆栈信息很快定位到执行位置:

<!--BeanUtils 是 hutool 工具类。也是从IOC容器获取Bean 等价于 @Autowired 注解 -->
RedisTool redisTool = BeanUtils.getBean(RedisMaster.class);

而RedisMaster类

@Component
@Scope("prototype")
public class RedisMaster implements IRedisTool {
    // ......
}

没错就是用了多例。而且使用的地方是Redis(系统使用Jedis客户端,Jedis并非线程安全,每次使用都需要新的实例),接口对Redis的使用还是比较频繁的,一个接口得有10次左右获取Redis数据。也就是说执行10次左右的createBean逻辑 ...

叹气!!!

赶紧改代码,直接使用万能的 new 。

在看结果之前还有一点需要提一下,由于系统有大量统计耗时的操作。实现方式是通过:

long start = System.currentTimeMillis();
// ......
long end = System.currentTimeMillis();
long runTime = start - end;

或者Hutool提供的StopWatch:

这里感谢一下huoger 同学的评论,当时还误以为该方式能够降低性能的影响,但是实际上也只是一层封装。底层使用的是 System.nanoTime()。

StopWatch watch = new StopWatch();
watch.start();
// ......
watch.stop();
System.out.println(watch.getTotalTimeMillis());

而这种在并发量高的情况下,对性能影响还是比较大的,特别在服务器使用了一些特定时钟的情况下。这里就不多说,感兴趣的可以自行搜索一下。

最终结果:

排查涉及的命令如下:

查询服务进程CPU情况: top –Hp pid

查询JVM GC相关参数:jstat -gc pid 2000 (对 pid [进程号] 每隔 2s 输出一次日志)

打印当前堆栈信息: jstack -l pid >> stack.log

总结

结果是好的,过程是曲折的。总的来说还是知识的欠缺,文章看起来还算顺畅,但都是事后诸葛亮,不对,应该是时候臭皮匠。基本都是边查资料边分析边操作,前后花费了4天时间,尝试了很多。

  • Mysql : Buffer Pool 、Change Buffer 、Redo Log 大小、双一配置...
  • 代码 : 异步执行,线程池参数调整,tomcat 配置,Druid连接池配置...
  • JVM : 内存大小,分配,垃圾收集器都想换...

总归一通瞎搞,能想到的都试试。

后续还需要多了解一些性能优化知识,至少要做到排查思路清晰,不瞎搞。

最后5行代码有哪些:

  • new Redis实例:1
  • 耗时统计:3
  • SQL异步执行 @Async: 1(上图最终的结果是包含该部分的,时间原因未对SQL进行处理,后续会考虑Redis原子操作+定时同步数据库方式来进行,避免同时操数据库)

TODO

问题虽然解决了。但是原理还不清楚,需要继续深挖。

为什么createBean对性能影响这么大?

如果影响这么大,Spring为什么还要有多例?

System.currentTimeMillis并发度多少才会对性能产生影响,影响有多大?

很多公司(包括大厂)在业务代码中,还是会频繁的使用System.currentTimeMillis获取时间戳。比如:时间字段赋值场景。所以,性能影响肯定会有,但是影响的门槛是不是很高。

继续学习性能优化知识

  • 吞吐量与什么有关?

首先,接口响应时长。直接影响因素还是接口响应时长,响应时间越短,吞吐量越高。一个接口响应时间100ms,那么1s就能处理10次。

其次,线程数。现在都是多线程环境,如果同时10个线程处理请求,那么吞吐量又能增加10倍。当然由于CPU资源有限,所以线程数也会受限。理论上,在 CPU 资源利用率较低的场景,调大tomcat线程数,以及并发数,能够有效的提升吞吐量。

最后,高性能代码。无论接口响应时长,还是 CPU 资源利用率,都依赖于我们的代码,要做高性能的方案设计,以及高性能的代码实现,任重而道远。

  • CPU使用率的高低与哪些因素有关?

CPU使用率的高低,本质还是由线程数,以及CPU使用时间决定的。

假如一台10核的机器,运行一个单线程的应用程序。正常这个单线程的应用程序会交给一个CPU核心去运行,此时占用率就是10%。而现在应用程序都是多线程的,因此一个应用程序可能需要全部的CPU核心来执行,此时就会达到100%。

此外,以单线程应用程序为例,大部分情况下,我们还涉及到访问Redis/Mysql、RPC请求等一些阻塞等待操作,那么CPU就不是时刻在工作的。所以阻塞等待的时间越长,CPU利用率也会越低。也正是因为如此,为了充分的利用CPU资源,多线程也就应运而生(一个线程虽然阻塞了,但是CPU别闲着,赶紧去运行其他的线程)。

  • 一个服务线程数在多少比较合适(算上Tomcat,最终的线程数量是226),执行过程中发现即使tomcat线程数量是100,活跃线程数也很少超过50,整个压测过程基本维持在20左右。

以上就是java开发接口吞吐量提升10多倍技巧的详细内容,更多关于java提升接口吞吐量的资料请关注我们其它相关文章!

(0)

相关推荐

  • java接口性能优化技巧

    目录 背景 哪些问题会引起接口性能问题 问题解决 慢查询(基于 mysql) ①深度分页 ②未加索引 ③索引失效 ④join 过多 or 子查询过多 ⑤in 的元素过多 ⑥单纯的数据量过大 业务逻辑复杂 ①循环调用 ②顺序调用 线程池设计不合理 锁设计不合理 机器问题(fullGC,机器重启,线程打满) 万金油解决方式 ①缓存 ②回调 or 反查 背景 我负责的系统在去年初就完成了功能上的建设,然后开始进入到推广阶段.随着推广的逐步深入,收到了很多好评的同时也收到了很多对性能的吐槽. 刚刚收到吐

  • Java 数据结构算法Collection接口迭代器示例详解

    目录 Java合集框架 Collection接口 迭代器 Java合集框架 数据结构是以某种形式将数据组织在一起的合集(collection).数据结构不仅存储数据,还支持访问和处理数据的操作 在面向对象的思想里,一种数据结构也被认为是一个容器(container)或者容器对象(container object),它是一个能存储其他对象的对象,这里的其他对象常被称为数据或者元素 定义一种数据结构从实质上讲就是定义一个类.数据结构类应该使用数据域存储数据,并提供方法支持查找.插入和删除等操作 Ja

  • Java中垃圾回收器GC对吞吐量的影响测试

    在看内存管理术语表的时候偶然发现了"Pig in the Python(注:有点像中文里的贪心不足蛇吞象)"的定义,于是便有了这篇文章.表面上看,这个术语说的是GC不停地将大对象从一个分代提升到另一个分代的情景.这么做就好比巨蟒整个吞食掉它的猎物,以至于它在消化的时候都没办法移动了. 在接下来的这24个小时里我的头脑中充斥着这个令人窒息的巨蟒的画面,挥之不去.正如精神病医生所说的,消除恐惧最好的方法就是说出来.于是便有了这篇文章.不过接下的故事我们要讲的不是蟒蛇,而是GC的调优.我对天

  • java接口性能从20s优化到500ms示例详解

    目录 前言 1. 案发现场 2. 现状 3. 第一次优化 4. 第二次优化 5. 第三次优化 5.1 前端做分页 5.2 分批调用接口 前言 接口性能问题,对于从事后端开发的同学来说,是一个绕不开的话题.想要优化一个接口的性能,需要从多个方面着手. 其实,我之前也写过一篇接口性能优化相关的文章<java接口性能优化小技巧>,发表之后在全网广受好评,感兴趣的小伙们可以仔细看看. 本文将会接着接口性能优化这个话题,从实战的角度出发,聊聊我是如何优化一个慢查询接口的. 上周我优化了一下线上的批量评分

  • java开发接口吞吐量提升10多倍技巧

    目录 背景 分析过程 定位“慢”原因 继续定位“慢”的原因 定位CPU使用率高的原因 总结 TODO 背景 公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测.这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求. 当时一想,500/s吞吐量还不简单.Tomcat按照100个线程,那就是单线程1S内处理5个请求,200ms处理一个请求即可.这个没有问题,平时接口响应时间大部分都100ms左右,还不是分分钟满足的事

  • Java开发中最让人头疼的十个bug

    目录 前言 错误一:Array 转换成 ArrayList 错误二:检查数组是否包含某个值 错误三:在 List 中循环删除元素 错误四:Hashtable 和 HashMap 错误五:使用原始类型的集合 错误六:访问级别问题 错误七:ArrayList 和 LinkedList 错误八:可变和不可变 错误九:构造函数 错误十:到底是使用 "" 还是构造函数 后记 前言 作为 Java 开发,我们在写代码的过程中难免会产生各种奇思妙想的 bug ,有些 bug 就挺让人无奈的,比如说各

  • JAVA下单接口优化实战TPS性能提高10倍

    概述 最近公司的下单接口有些慢,老板担心无法支撑双11,想让我优化一把,但是前提是不允许大改,因为下单接口太复杂了,如果改动太大,怕有风险.另外开发成本和测试成本也非常大.对于这种有挑战性的任务,我向来是非常喜欢的,因为在解决问题的过程中,可以学习到很多东西. 当时我只是知道下单接口慢,但是没人告诉我慢在哪里,也即是说,哪些瓶颈导致下单接口慢了.其实没人知道也没关系的,因为我们可以通过压测来找到具体的瓶颈. 下面会详细介绍一下,在本次压测中遇到的问题以及如何解决,期间用了什么工具. 用到的工具和

  • Docker遇到Intellij IDEA,Java开发提升了十倍生产力

    Idea是Java开发利器,SpringBoot是Java生态中最流行的微服务框架,docker是时下最火的容器技术,那么它们结合在一起会产生什么化学反应呢? 一.开发前准备 1. Docker的安装可以参考https://docs.docker.com/install/ 2. 配置docker远程连接端口 vi /usr/lib/systemd/system/docker.service 找到 ExecStart,在最后面添加 -H tcp://0.0.0.0:2375,如下图所示 3. 重启

  • 分享5个Java接口性能提升的通用技巧

    目录 前言 1. 并发调用 2. 避免大事务 3. 添加合适的索引 4. 返回更少的数据 5. 使用缓存 前言 作为后端开发人员,我们总是在编写各种API,无论是为前端web提供数据支持的HTTP REST API ,还是提供内部使用的RPC API.这些API在服务初期可能表现不错,但随着用户数量的增长,一开始响应很快的API越来越慢,直到用户抱怨:“你的系统太糟糕了.” 我只是浏览网页.为什么这么慢?”.这时候你就需要考虑如何优化你的API性能了. 要想提高你的API的性能,我们首先要知道哪

  • Java开发人员最常犯的10个错误

    这个列表总结了10个Java开发人员最常犯的错误. Array转ArrayList 当需要把Array转成ArrayList的时候,开发人员经常这样做: List<String> list = Arrays.asList(arr); Arrays.asList()会返回一个ArrayList,但是要特别注意,这个ArrayList是Arrays类的静态内部类,并不是java.util.ArrayList类.java.util.Arrays.ArrayList类实现了set(), get(),c

  • java开发微信分享接口的步骤

    微信分享接口的java开发的一些小步骤,具体内容如下 1.配置接口信息进行验证 代码如下: /** * 访问没认证的地址跳转 * * @param request * @return 登录页面 * @throws Exception */ @RequestMapping(value = "/checkWxDomainUrl", method = RequestMethod.GET) public void checkWxDomainUrl(HttpServletRequest requ

  • Java后台接口开发初步实战教程

    上图是查询列表的接口,get方式 上图是用户注册的接口,同样是get,post方式也很简单 开发工具:IntelliJ IDEA 2016.3.5 ORM框架:MyBatis 数据库:MySql 服务器:tomcat7.0 公司使用的的orm框架是Hibernate,使用起来感觉比mybatis好用多了,毕竟经过了公司这么多项目的考验,总比自己用mybatis写的项目可靠,但以下分享的还是mybatis的代码 注册接口方法:http://192.168.1.116:8080/register?u

  • java开发之基于Validator接口的SpringMVC数据校验方式

    Spring MVC 提供了两种数据校验的方式: 1.基于 Validator 接口. 2.使用 Annotation JSR - 303 标准进行校验. 基于 Validator 接口的⽅式需要自定义 Validator 验证器,每⼀条数据的验证规则需要开发者⼿动完成, 使⽤ Annotation JSR - 303 标准则不需要⾃定义验证器,通过注解的方式可以直接在实体类中添加每个属性的验证规则,这种方式更加方便,实际开发中推荐使用. 1.定义实体类Account package entit

  • Java开发SpringBoot集成接口文档实现示例

    目录 swagger vs smart-doc Swagger的代码侵入性比较强 原生swagger不支持接口的参数分组 简单罗列一下smart-doc的优点 SpringBoot集成 smart-doc 引入依赖,版本选择最新版本 新建配置文件smart-doc.json 通过执行maven 命令生成对应的接口文档 访问接口文档 功能增强 1. 开启调试 2. 通用响应体 3. 自定义Header 4. 参数分组 5. idea配置doc 6. 完整配置 小结 之前我在SpringBoot老鸟

随机推荐