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

news2024/10/6 2:22:09

背景

使用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占用内存过多的问题,可以把这个数值配置大一些,比如说100
      max-pool-prepared-statement-per-connection-size: 20
      # 单位毫秒。有两个含义:一个是Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接;另一个是testWhileIdle的判断依据
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      # 用来检测连接是否有效的sql
      validation-query: SELECT 1
      # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效
      test-while-idle: true
      # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
      test-on-borrow: false
      # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
      test-on-return: false
      filter:
        stat:
          log-slow-sql: false
          slow-sql-millis: 1000
          merge-sql: false
          enabled: true
        wall:
          config:
            multi-statement-allow: true
      stat-view-servlet:
        enabled: true
        url-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.coloradmin.cn/o/495571.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java中池化技术探讨

背景&#xff1a;在日常开发中&#xff0c;除了考虑IO操作、线程上下文切换、GC的影响性能外。还通过池化技术提高性能通过循环复用资源&#xff0c;降低资源创建和销毁带来的开销和损失&#xff0c;从而提高性能&#xff0c;例如对象池、内存池、线程池、连接池 一、对象池&a…

软件测试 - 测试用例设计方法之等价类划分和边界值分析

1. 等价类划分法 1.1 基本理论 等价类划分法是通过科学的方法找到具有共同特性的测试输入的集合&#xff0c;避免进行穷举测试&#xff0c;大大减少了测试用例的数量&#xff0c;从而提高测试效率。等价类划分法的典型应用场景就是输入框&#xff0c;适用于较少数量输入框的场…

晶振概述及工作原理

晶振在电路板中随处可见&#xff0c;只要用到处理器的地方就必定有晶振的存在&#xff0c;即使没有外部晶振&#xff0c;芯片内部也有晶振。 晶振概述 晶振一般指晶体振荡器。晶体振荡器是指从一块石英晶体上按一定方位角切下薄片&#xff08;简称为晶片&#xff09;&#xf…

虚拟服务器基础架构解决方案:用最小的工作量实现最大的价值

虚拟服务器基础架构解决方案&#xff1a;用最小的工作量实现最大的价值 一切皆可虚拟化&#xff01;包括服务器在内。NetApp 虚拟服务器基础架构解决方案有助于加快数据访问速度、构建创新服务并简化部署&#xff0c;从而实现最大价值。 为什么选择 NetApp 的虚拟服务器基础架…

pytorch矩阵乘法总结

1. element-wise&#xff08;*&#xff09; 按元素相乘&#xff0c;支持广播&#xff0c;等价于torch.mul() a torch.tensor([[1, 2], [3, 4]]) b torch.tensor([[2, 3], [4, 5]]) c a*b # 等价于torch.mul(a,b) # tensor([[ 2, 6], # [12, 20]]) a * torch.tenso…

详解C++类对象(上篇)——超详细

目录 一&#xff0c;面向对象&面向过程的认识(简单了解即可&#xff0c;逐步认识&#xff09; 二&#xff0c; 类 2.1 类的引入 2.2 类的定义 1. struct 2. class 类的两种定义方式&#xff1a; 2.3 封装&类的访问限定符 1. 封装概念 2. 类的访问限定符 …

低代码如何不写代码创建表单和维护表单

工作表新建与修改 新建工作表的流程包含 新建工作表/编辑公祖表为工作表添加字段&#xff0c;例如“员工档案”表中有姓名、性别、年龄等字段为字段设置属性工作表布局工作表预览、保存、关闭 1、新建工作表/修改工作表 新建工作表 修改工作表 2、为工作表添加字段 添加字段 左…

关于C语言的一些杂记2

文章目录 sizeof运算符内容关于基本概念的问题关于一些语句的理解和分号的注意字符的理解关于输出格式的扩展 本文内容摘自C技能树一些优秀的博主 sizeof运算符内容 关于基本概念的问题 sizeof是C语言的关键字&#xff0c;它用来计算变量&#xff08;或数据类型&#xff09;在…

2.Hive创建数据库

1.数据库操作 1.1 创建数据库 create database test comment Just for test location /abcd with dbproperties(aaabbb); comment后面指的是注释&#xff1b;location后面是数据库存放路径&#xff1b;dbproperties代表了数据库的属性 ps.避免要创建的数据库已经存在错误&…

Vue最新状态管理工具Pinia——Pinia的安装与使用

Pinia从了解到实际运用——pinia的安装与使用 知识回调&#xff08;不懂就看这儿&#xff01;&#xff09;场景复现一、环境搭建1.创建项目2.安装pinia 二、基本使用1.创建pinia示例并挂载2.基本使用访问state使用getters使用actions 3.详细示例&#xff08;详细注解&#xff0…

【23】核心易中期刊推荐——视觉/图像感知与识别人工智能算法及应用​​​​​​​

🚀🚀🚀NEW!!!核心易中期刊推荐栏目来啦 ~ 📚🍀 核心期刊在国内的应用范围非常广,核心期刊发表论文是国内很多作者晋升的硬性要求,并且在国内属于顶尖论文发表,具有很高的学术价值。在中文核心目录体系中,权威代表有CSSCI、CSCD和北大核心。其中,中文期刊的数…

2023年盐城工学院五年一贯制专转本旅游学概论考试大纲

2023年盐城工学院五年一贯制专转本旅游学概论考试大纲 一、考核对象 本课程的考核对象是五年一贯制高职专升本酒店管理专业考生。 二、考核方式 本课程考核采用闭卷考试的方式。 三、考核要求 掌握旅游学的基本原理&#xff0c;掌握旅游学的核心概念&#xff0c;具备旅游…

Android性能监控:主循环性能统计LooperStatsService详解

作者&#xff1a;飞起来_飞过来 简介 在Android性能监控和优化领域&#xff0c;一个会影响App性能表现的因素与Handler Message Looper机制有关。当Looper里面的Message处理不及时、或数量太多占用过多处理时间时&#xff0c;可能会出现卡顿感&#xff0c;并且不容易定位到卡顿…

WoShop多商户进口出口跨境电商uniapp商城源码

源码介绍&#xff1a;WoShop多商户跨境电商商城系统将传统的分销、积分、拼团等传统销售模式和直播带货、短视频带货等新型电商营销完美融为一体&#xff0c;专注技术&#xff0c;支持二次开发&#xff0c;专为用户、技术商提供跨境电商技术解决方案。 WoShop跨境电商源码产品…

网络弹性基础知识和实践

什么是网络弹性 弹性是网络处理中断并继续以可接受的标准向用户提供服务的能力。网络运营可能会受到配置错误、断电或操作员错误等问题的威胁。当这种可能性发生时&#xff0c;最终用户无法访问网络&#xff0c;从而对组织产生负面影响。高度弹性的网络可以通过在网络运行中断…

chatgpt官网拒绝访问怎么处理-chatGPT入口正确打开方式

chatgpt官网拒绝访问的原因有哪些 OpenAI是一家人工智能技术公司&#xff0c;其官网是OpenAI最重要的宣传与交流平台之一。但是&#xff0c;有时访问OpenAI官网可能会受到限制或拒绝访问。以下是可能导致OpenAI官网拒绝访问的几个常见原因&#xff1a; IP地址被封锁: OpenAI网…

【Python】只需2行代码,轻松将PDF转换成Word(含示范案例)

文章目录 一、前期准备二、pdf2docx功能三、限制四、案例 一、前期准备 可将 PDF 转换成 docx 文件的 Python 库。该项目通过 PyMuPDF 库提取 PDF 文件中的数据&#xff0c;然后采用 python-docx 库解析内容的布局、段落、图片、表格等&#xff0c;最后自动生成 docx 文件。 …

LFU缓存结构算法

设计LFU缓存结构 LFU&#xff1a;最近最少频率使用 基本思想&#xff1a; 当缓存满时&#xff0c;加入新数据&#xff0c;淘汰缓存中使用次数最少的key&#xff0c;当使用次数最少的key有多个&#xff0c;删除最早调用的key。 定义节点的数据结构 class Node{//使用频率int …

从零开始学习Linux运维,成为IT领域翘楚(八)

文章目录 &#x1f525;Linux进程管理&#x1f525;ps&#x1f525;top&#x1f525;htop &#x1f525;Linux进程管理 &#x1f525;ps 查看系统中所有进程 语法&#xff1a; ps [options] [--help]参数&#xff1a; &#x1f41f; -a 显示所有进程&#xff08;包括其他用…

Windows Server 安装docker

在windows 10 或windows 11 上使用docker&#xff0c;可以直接在docker 官网下载docker desktop安装即可。 但在windows server上则无法支持docker desktop&#xff0c;此时可通过如下方式安装&#xff1a; 以 管理员权限运行Power Shell&#xff0c;然后执行&#xff1a; 安装…