记一次SpringBoot应用性能调优过程

news/2025/2/22 1:17:56/

背景

使用SpringBoot、MyBatis-Plus开发一个接口转发的能,将第三方接口注册到平台中,由平台对外提供统一的地址,平台转发时记录接口的转发日志信息。开发完成后使用Jmeter进行性能测试,使用100个线程、持续压测180秒,测试结果如下,每秒仅支持8个并发。
在这里插入图片描述

服务器参数

服务器作用CPU核数内存
Jmeter压测1632
MySQL压测1632
接口模拟第三方接口816
平台平台816

优化过程

XSS拦截器

首先通过 jstack 命令查看下进程堆栈信息,并在堆栈信息中查询项目的包名,很快找到了几个拦截器的信息,拦截器如下

public class XssEscapeFilter implements Filter {public ServletInputStream getInputStream() throws IOException {BufferedReader br = new BufferedReader(new InputStreamReader(orgRequest.getInputStream()));String line = br.readLine();String result = "";while (line != null) {result += clean(line);line = br.readLine();}return new WrappedServletInputStream(new ByteArrayInputStream(result.getBytes()));}
}
...

该拦截器用于对请求的内容先解析成字符串、并对内容进行标签替换,然后在重新放入到流中,先把这个过滤器去除了, 去除后性能测试结果如下,达到了每秒42并发

HTTP连接池

接口转发时需要用到apache httpclient工具,于是找到了http设置连接池的方法,代码如下:

@Bean("closeableHttpClient")
public CloseableHttpClient closeableHttpClient() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {try {//https 配置TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, null, null, NoopHostnameVerifier.INSTANCE);Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", csf).build();PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);// 最大连接数connectionManager.setMaxTotal(1000);// 路由链接数connectionManager.setDefaultMaxPerRoute(100);RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(60000).setConnectTimeout(60000).setConnectionRequestTimeout(10000).build();CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).setConnectionManager(connectionManager).evictExpiredConnections().evictIdleConnections(300, TimeUnit.SECONDS).build();log.info("初始化HttpClient成功,连接池配置:{}", httpConfig);Thread httpMonitorThread = new Thread(() -> {while (true) {final PoolStats poolStats = connectionManager.getTotalStats();log.info("等待个数: {} , 执行中个数: {} , 空闲个数: {} , 使用个数: {}/{}", poolStats.getPending(), poolStats.getLeased(), poolStats.getAvailable(), poolStats.getLeased() + poolStats.getAvailable(), poolStats.getMax());try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {log.error(ExceptionUtils.getStackTrace(e));}}});httpMonitorThread.setName("httpMonitor");httpMonitorThread.start();return httpClient;} catch (Exception e) {log.error("初始化HttpClient失败", e);throw e;}
}

通过监控发现HTTP没有出现等待连接情况

数据库连接池

平台使用的是Druid连接池,配置如下:

spring:datasource:druid:# 初始化时建立物理连接的个数,初始化发生在显示调用init方法,或者第一次getConnection时initial-size: 100# 最大连接池数量max-active: 1000# 最小连接池数量min-idle: 100# 获取连接时最大等待时间,单位毫秒;配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true来使用非公平锁。max-wait: 60000# 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭pool-prepared-statements: true# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100max-pool-prepared-statement-per-connection-size: 20# 单位毫秒。有两个含义:一个是Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接;另一个是testWhileIdle的判断依据time-between-eviction-runs-millis: 60000min-evictable-idle-time-millis: 300000# 用来检测连接是否有效的sqlvalidation-query: SELECT 1# 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效test-while-idle: true# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能test-on-borrow: false# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。test-on-return: falsefilter:stat:log-slow-sql: falseslow-sql-millis: 1000merge-sql: falseenabled: truewall:config:multi-statement-allow: truestat-view-servlet:enabled: trueurl-pattern: /druid/*# 访问SQL监控页面时的登录用户名login-username: admin# 访问SQL监控页面时的登录密码login-password: admin

这里的max-active 不要超过数据库的最大连接个数,通过show variables like '%max_connections%'; 命令可以查询数据库设置的最大连接个数

SQL查询改成缓存查询

每次转发前都需要到数据库进行信息查询,这里把数据库查询改为从JVM缓存查询,去除后性能测试结果如下,达到了每秒 315并发
在这里插入图片描述

Logback日志修改为异步打印

使用的logback日志框架,在进行接口转发时会进行日志的打印,在调整日志级别为ERROR时,发现TPS会增加很多,所以猜测和日志打印也有关系,经过查找资料发现,默认日志打印是同步的,可以使用异步打印来提升性能,修改如下

    <!-- 异步日志输出 --><appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"><appender-ref ref="FILE" /><!--    解决异步行号不输出问题    --><includeCallerData>true</includeCallerData><!--    日志队列大小,默认 256    --><queueSize>1000</queueSize></appender>

还有一个参数比上面的效果更好,就是在 Appender标签中添加 <immediateFlush>false</immediateFlush> ,当日志的大小达到8k后就会自动写入到日志文件中,带来的问题是没有办法实时查看打印的日志且默认8k的缓冲没有找到修改的办法。

日志异步存储数据库

接口转发时会记录日志方便后续进行问题查找,这里是在响应给客户端前会进行日志存储,是同步存储的,当把这个日志存储代码注释时发现TPS很快就达到了2000,所以猜测是由于存储缓慢导致了系统的TPS上不去,然后就利用SpringBoot的@Aync注解并结合线程池ThreadPoolTaskExecutor进行异步处理,线程池代码如下:

@Slf4j
@Configuration
public class LogSyncThreadPoolConfiguration {/*** 把springboot中的默认的异步线程线程池给覆盖掉。用ThreadPoolTaskExecutor来进行处理**/@Bean(name = "logThreadPoolTaskExecutor")public ThreadPoolTaskExecutor getThreadPoolTaskExecutor(ApiPlatFormConfig apiPlatFormConfig) {ThreadPoolConfig threadPool = apiPlatFormConfig.getThreadPool();ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();threadPoolTaskExecutor.setCorePoolSize(threadPool.getCorePoolSize());threadPoolTaskExecutor.setMaxPoolSize(threadPool.getMaxPoolSize());threadPoolTaskExecutor.setQueueCapacity(threadPool.getQueueCapacity());threadPoolTaskExecutor.setKeepAliveSeconds(threadPool.getKeepAliveSeconds());threadPoolTaskExecutor.setThreadNamePrefix(threadPool.getThreadNamePrefix());threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());threadPoolTaskExecutor.initialize();threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);if (threadPool.getMonitor()) {log.info("开启线程池监控");Thread threadPoolMonitor = new Thread(() -> {while (true) {int poolSize = threadPoolTaskExecutor.getPoolSize();int activeCount = threadPoolTaskExecutor.getActiveCount();int queueSize = threadPoolTaskExecutor.getThreadPoolExecutor().getQueue().size();long completedTaskCount = threadPoolTaskExecutor.getThreadPoolExecutor().getCompletedTaskCount();log.info("【{}】线程池信息, 最大线程数: {}, 核心线程数: {}, 当前线程池大小: {}, 当前活动线程数: {}, 当前队列长度: {}/{}, 已完成任务个数: {}", threadPool.getThreadNamePrefix(), threadPool.getMaxPoolSize(), threadPool.getCorePoolSize(), poolSize, activeCount, queueSize, threadPool.getQueueCapacity(), completedTaskCount);try {TimeUnit.SECONDS.sleep(threadPool.getMonitorIntervalSeconds());} catch (InterruptedException e) {throw new RuntimeException(e);}}});threadPoolMonitor.setName("监控线程");threadPoolMonitor.start();}return threadPoolTaskExecutor;}
}

无论上述的线程池大小怎么调整,发现TPS的变化幅度不大,于是怀疑是不是数据库的性能不行,并对数据库进行了性能测试

数据库性能测试

将日志的插入语句拿出来当做测试案例,同样的使用 100个线程压测180秒,数据库插入性能如下,数据库支持每秒1300并发,所以程序还是有优化空间的
在这里插入图片描述

使用队列+批量提交

使用线程池虽然是异步了,但是始终是一条一条的往数据库中插入的,如果改为批量插入的话,应该会提高性能,所以将日志存储的地方修改为存储到队列中,这里使用LinkedBlockingQueue队列,并启动一个线程一直消费该队列,当消费的数量达到批量提交的个数时进行数据库插入,启动一个线程监控队列的消费情况,代码案例如下:

Thread logQueueMonitorThread = new Thread(() -> {long lastCount = 0;while (true) {long nowCount = count.get();log.info("队列当前积压数量:{} , 新增处理数: {} , SQL批量插入大小: {} ,  总处理数: {} , 总异常数: {}", QUEUE.size(), nowCount - lastCount, logQueueConfig.getSqlBatchSize(), nowCount, errorCounter.get());lastCount = nowCount;try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {log.error(ExceptionUtils.getStackTrace(e));}}});logQueueMonitorThread.setName("logQueueMonitor");logQueueMonitorThread.start();

经过测试发现TPS可以达到了1300左右,而且队列的积压个数没有明显的增长,一直小于批量提交的个数。
在这里插入图片描述

其他说明

在上面的机器中测试发现一个简单的SpringBoot应用,里面写一个测试接口直接返回固定字符串,性能接近20000TPS每秒,而平台也按照上述操作测试接口发现性能在3000TPS左右,性能损耗非常大,不知道是不是因为用了Shiro导致,后面会再进行单独的测试验证。


http://www.ppmy.cn/news/69448.html

相关文章

20230517提升cv1826的打印等级

20230517提升cv1826的打印等级 2023/5/17 17:43 https://www.xitongjiaocheng.com/linux/2017/53494.html Linux内核log等级与printk打印消息控制 时间&#xff1a;2017-03-13 出处&#xff1a;系统之家复制分享人气(206次) 【大中小】 printk打印消息控制 // linux/includ…

Web 自动化笔记-第一章 Selenium 环境搭建

1.自动化测试能解决什么问题&#xff1f; 1. 解决-回归测试 2. 解决-压力测试 3. 解决-兼容性测试 4. 提高测试效率,保证产品质量 回归测试&#xff1a; 项目在发新版本之后对项目之前的功能进行验证 压力测试&#xff1a; 可以理解多用户同时去操作软件&#xff0c; 统计软件服…

蓝桥杯第十四届青少年Python组省赛试题--第4题

提示信息&#xff1a; 杨辉三角就是一个用数排列起来的三角形&#xff08;如下图&#xff09;&#xff0c;杨辉三角规则如下&#xff1a; 1&#xff09;每行第一个数和最后一个数都为1&#xff0c;其它每个数等于它左上方和右上方的两数之和&#xff1b; 2&#xff09;第n行有n…

功能测试4年,5月份被辞退,2023年的功能真的没有出路了

在测试行业摸爬滚打5年&#xff0c;以前经常听到开发对我说&#xff0c;天天的点点点有意思没&#xff1f; 和IT圈外的同学、朋友聊起自己的工作&#xff0c;往往一说自己是测试&#xff0c;无形中也会被大家轻视&#xff0c;总有人会问你&#xff0c;为啥干测试啊&#xff0c…

Go 语言指针

Go 语言指针 Go 语言中指针是很容易学习的&#xff0c;Go 语言中使用指针可以更简单的执行一些任务。 接下来让我们来一步步学习 Go 语言指针。 我们都知道&#xff0c;变量是一种使用方便的占位符&#xff0c;用于引用计算机内存地址。 Go 语言的取地址符是 &&#xf…

广告变现数据分析,提高媒体广告变现效果的关键指标!

​广告变现是一种极为普遍的商业化运营方式&#xff0c;其实质在于通过在不同媒体平台上投放广告&#xff0c;从而实现盈利&#xff0c;然而&#xff0c;广告变现的成败往往关乎于各种媒体基本信息类指标及广告变现相关指标的表现。 因此&#xff0c;在本文中&#xff0c;我们…

实验9 分类问题

1. 实验目的 ①掌握逻辑回归的基本原理&#xff0c;实现分类器&#xff0c;完成多分类任务&#xff1b; ②掌握逻辑回归中的平方损失函数、交叉熵损失函数以及平均交叉熵损失函数。 2. 实验内容 ①能够使用TensorFlow计算Sigmoid函数、准确率、交叉熵损失函数等&#xff0c;…

智能安防系统-视频监控系统

一、智能安防系统 1、智能安防系统介绍 安全防范系统成为了智慧城市与物联网行业应用中的一个非常重要的子系统。 安防系统主要包括&#xff1a;视频监控系统、入侵报警系统、出入口控制系统、电子巡查系统以及智能停车场管理系统等5个子系统。 AI人工智能安防系统功能&#xf…