Java高并发异步请求实战,Jmeter暴力压测下的解决方案

目录

一、模拟并发实战环境

二、高并发下异步请求解决方案一:异步请求

[2.1 @Async注解](#2.1 @Async注解)

主启动类加注解

定义异步任务类并使用@Component标记组件被容器扫描,异步方法加上@Async

注意:@Async失效的几种情况

[2.2 @Async注解的弊端](#2.2 @Async注解的弊端)

无限制的资源消耗

缺乏队列限制

难以监控和管理

共享同一线程池

[2.3 自定义线程池](#2.3 自定义线程池)

创建自定义线程池的配置类

@Async注解引用自定义线程池

[2.4 实践出真知-线程池多参数调整-现象报告对比分析](#2.4 实践出真知-线程池多参数调整-现象报告对比分析)

[异步发送 + resttemplate未池化(小参数)](#异步发送 + resttemplate未池化(小参数))

异步发送+resttemplate未池化(大参数)

自定义线程池的弊端

三、高并发下异步请求解决方案二:RestTemplate池化

[3.1 RestTemplate的弊端Broken pipe](#3.1 RestTemplate的弊端Broken pipe)

[3.2 重新认识RestTemplate](#3.2 重新认识RestTemplate)

[3.3 高性能RestTemplate封装配置实战](#3.3 高性能RestTemplate封装配置实战)

[3.4 【10倍+QPS提升】Jmeter5.x压测 优化后RestTemplate前后性能对比](#3.4 【10倍+QPS提升】Jmeter5.x压测 优化后RestTemplate前后性能对比)

[四、异步@Async自定义线程池 + RestTemplate池化顶级优化策略](#四、异步@Async自定义线程池 + RestTemplate池化顶级优化策略)


一、模拟并发实战环境

这是一段非常简单的代码,采用RestTemplate调用的我本地另一个服务的接口,这里我们选择Jmeter来压测此方法接口。

总共调用10万次接口,我们再来看看聚合结果。

我们发现调用接口失败率为2.27%,这个已经不低了,下面我们使用几种常见的优化方式来解决这个问题。

二、高并发下异步请求解决方案一:异步请求

2.1 @Async注解

主启动类加注解

定义异步任务类并使用@Component标记组件被容器扫描,异步方法加上@Async

注意:@Async失效的几种情况

  • 注解@Async的方法不是public方法

  • 注解@Async的返回值只能为void或者Future

  • 注解@Async方法使用static修饰也会失效

  • spring无法扫描到异步类,没加注解@Async 或 @EnableAsync注解

  • 调用方与被调方不能在同一个类

    • Spring 在扫描bean的时候会扫描方法上是否包含@Async注解,动态地生成一个子类(即proxy代理类),当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用时增加异步作用

    • 如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个 bean,所以就失效了

    • 所以调用方与被调方不能在同一个类,主要是使用了动态代理,同一个类的时候直接调用,不是通过生成的动态代理类调用

    • 一般将要异步执行的方法单独抽取成一个类

  • 类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象

  • 在Async 方法上标注@Transactional是没用的,但在Async 方法调用的方法上标注@Transactional 是有效的

2.2 @Async注解的弊端

记住,@Async源码中有三大核心参数,记住它你就能顺利干倒面试官,那么是哪三大呢?

(1)核心线程数:8

(2)队列容量:Integer.MAX_VALUE ( 21亿多)

(3)最大线程数:Integer.MAX_VALUE ( 21亿多)

好,记住这三个参数的值,我们再来说说这三个参数都是干啥的:

首先核心线程数8,我们比如同时有多个请求进来,那么先占用默认线程池的核心线程数的8来工作,其余的请求都放在队列中阻塞着,那么如果队列容量21亿个也满了,那么就会启动最大线程数,也就是说,会创建新的线程去执行任务。

好,那么使用默认线程池有没有什么弊端呢?当然有。

无限制的资源消耗

在高并发场景下会导致大量线程被创建,耗尽系统资源。你想想,默认的参数值都是21亿多,那么资源消耗多大啊。

缺乏队列限制

队列容量是21亿,极易出现OOM

难以监控和管理

  • 默认线程池难以进行监控和性能调优

  • 无法统一管理线程池参数

共享同一线程池

  • 所有@Async方法共享同一个默认线程池

  • 一个耗时任务可能阻塞其他异步任务

2.3 自定义线程池

创建自定义线程池的配置类

java 复制代码
package net.xdclass.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {

    @Bean("threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor(){

        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        //线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活
        //如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
        threadPoolTaskExecutor.setCorePoolSize(8);

        //缓存队列(阻塞队列)当核心线程数达到最大时,新任务会放在队列中排队等待执行
        threadPoolTaskExecutor.setQueueCapacity(124);

        //最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
        //当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
        threadPoolTaskExecutor.setMaxPoolSize(64);

        //当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
        //允许线程空闲时间60秒,当maxPoolSize的线程在空闲时间到达的时候销毁
        //如果allowCoreThreadTimeout=true,则会直到线程数量=0
        threadPoolTaskExecutor.setKeepAliveSeconds(30);

        //spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。
        //jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的
        threadPoolTaskExecutor.setThreadNamePrefix("CLAY的Async前缀:");
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);

        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行
        //AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
        //DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
        //DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }

}

@Async注解引用自定义线程池

java 复制代码
@Service
@Slf4j
public class NotifyServiceImpl implements NotifyService {

    @Autowired
    private RestTemplate restTemplate;

    @Override
    @Async("threadPoolTaskExecutor")
    public void testSend() {
        ResponseEntity<String> forEntity = restTemplate.getForEntity("http://localhost:8002/abc/test", String.class);
        log.info(forEntity.getBody());
    }
}

可以看到确实使用了我们自定义的线程池。

2.4 实践出真知-线程池多参数调整-现象报告对比分析

异步发送 + resttemplate未池化(小参数)

java 复制代码
threadPoolTaskExecutor.setCorePoolSize(4);
threadPoolTaskExecutor.setMaxPoolSize(16);
threadPoolTaskExecutor.setQueueCapacity(32);

QPS少,并且后台出现大量异常错误,因为你队列容量设置的太少,所以导致大量请求被拒绝。

异步发送+resttemplate未池化(大参数)

java 复制代码
threadPoolTaskExecutor.setCorePoolSize(32);
threadPoolTaskExecutor.setMaxPoolSize(64);
threadPoolTaskExecutor.setQueueCapacity(10000);
//如果等待队列长度为10万,则qps瞬间很高8k+,可能oom

QPS一开始瞬间很高,然后慢慢下降,因为瞬间都进入到了阻塞队列。

自定义线程池的弊端

  • 采用异步发送用户体验变好了,但是存在丢失的可能,阻塞队列存储内存中,如果队列长度过多则重启容易出现丢失数据情况

  • 采用了异步发送了+阻塞队列存缓冲,刚开始瞬间QPS高,但是后续也降低很多

  • 问题是在哪里?消费方角度,提高消费能力

三、高并发下异步请求解决方案二:RestTemplate池化

3.1 RestTemplate的弊端Broken pipe

我们单纯使用RestTemplate去调用接口,也就是同步调用,并发量上来,会出现错误

Caused by: java.io.IOException: Broken pipe。

这是什么意思呢?

  • 服务端向前端socket连接管道写返回数据时 链接(pipe)却断开了

    • 从应用角度分析,这是因为客户端等待返回超时了,主动断开了与服务端链接

    • 连接数设置太小,并发量增加后,造成大量请求排队等待

    • 网络延迟,是否有丢包

    • 内存是否足够多支持对应的并发量

白话说,就类似于客户端请求服务端,你服务端处理问题时间太长了,客户端等不了了,就断开了socket连接,等服务端处理完任务,发现连接通道没了,就报错Broken pipe异常了。

3.2 重新认识RestTemplate

  • RestTemplate是Spring提供的用于访问Rest服务的客户端

  • 底层通过使用java.net包下的实现创建HTTP 请求

  • 通过使用ClientHttpRequestFactory指定不同的HTTP请求方式,主要提供了两种实现方式

    • SimpleClientHttpRequestFactory(默认)

      • 底层使用J2SE提供的方式,既java.net包提供的方式,创建底层的Http请求连接

      • 主要createRequest 方法( 断点调试),每次都会创建一个新的连接,每次都创建连接会造成极大的资源浪费,而且若连接不能及时释放,会因为无法建立新的连接导致后面的请求阻塞

    • HttpComponentsClientHttpRequestFactory

      • 底层使用HttpClient访问远程的Http服务

问题解决:

  • RestTemplate是Spring提供的用于访问Rest服务的客户端

  • 底层通过使用java.net包下的实现创建HTTP 请求

  • 通过使用ClientHttpRequestFactory指定不同的HTTP请求方式,主要提供了两种实现方式

    • SimpleClientHttpRequestFactory(默认)

      • 底层使用J2SE提供的方式,既java.net包提供的方式,创建底层的Http请求连接

      • 主要createRequest 方法( 断点调试),每次都会创建一个新的连接,每次都创建连接会造成极大的资源浪费,而且若连接不能及时释放,会因为无法建立新的连接导致后面的请求阻塞

    • HttpComponentsClientHttpRequestFactory

      • 底层使用HttpClient访问远程的Http服务

3.3 高性能RestTemplate封装配置实战

java 复制代码
package net.xdclass.config;

import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
        return new RestTemplate(factory);
    }

    @Bean
    public ClientHttpRequestFactory httpRequestFactory() {
        return new HttpComponentsClientHttpRequestFactory(httpClient());
    }

    /**
     * @return
     */
    @Bean
    public HttpClient httpClient() {
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", SSLConnectionSocketFactory.getSocketFactory())
                .build();

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);

        //设置整个连接池最大连接数
        connectionManager.setMaxTotal(500);

        //MaxPerRoute路由是对maxTotal的细分,每个主机的并发,这里route指的是域名
        connectionManager.setDefaultMaxPerRoute(200);
        RequestConfig requestConfig = RequestConfig.custom()
                //返回数据的超时时间
                .setSocketTimeout(20000)
                //连接上服务器的超时时间
                .setConnectTimeout(10000)

                //从连接池中获取连接的超时时间
                .setConnectionRequestTimeout(1000)
                .build();

        return HttpClientBuilder.create()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(connectionManager)
                .build();
    }

}

3.4 【10倍+QPS提升】Jmeter5.x压测 优化后RestTemplate前后性能对比

同步发送+resttemplate未池化

  • 压测结果 几百 吞吐量

同步发送+resttemplate池化

  • 压测结果

同步请求的QPS居然也能达到近3000的吞吐量。

四、异步@Async自定义线程池 + RestTemplate池化顶级优化策略

最终我们可以将两个方案结合到一起,就非常完美了,比如我们发短信功能,就可以采用二者结合到一起的方案:

java 复制代码
package net.xdclass.component;

import lombok.extern.slf4j.Slf4j;
import net.xdclass.config.SmsConfig;
import net.xdclass.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
@Slf4j
public class SmsComponent {

    /**
     * 发送地址
     */
    private static final String URL_TEMPLATE = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private SmsConfig smsConfig;

    @Async("threadPoolTaskExecutor")
    public void send(String to, String templateId, String value) {

        long beginTime = CommonUtil.getCurrentTimestamp();

        String url = String.format(URL_TEMPLATE, to, templateId, value);

        HttpHeaders headers = new HttpHeaders();

        headers.set("Authorization", "APPCODE " + smsConfig.getAppCode());

        HttpEntity<String> entity = new HttpEntity<>(headers);
        ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);

        long endTime = CommonUtil.getCurrentTimestamp();
        log.info("耗时={}, url={}, body={}", endTime - beginTime, url, response.getBody());

        log.info("url={},body={}", url, response.getBody());
        if (response.getStatusCode() == HttpStatus.OK) {
            log.info("发送短信成功,响应信息:{}", response.getBody());
        } else {
            log.error("发送短信失败,响应信息:{}", response.getBody());
        }
    }

}
相关推荐
郝学胜-神的一滴2 天前
Effective Modern C++ 条款37:使std::thread在所有路径最后都不可结合
开发语言·c++·程序人生·多线程·并发·std
only-qi3 天前
SimpleAsyncTaskExecutor:@Async 的默认异步执行器
线程池·异步
only-qi3 天前
Spring Boot 异步任务深度解析:从入门到避坑指南
java·spring boot·线程池·async
C雨后彩虹5 天前
ThreadLocal全面总结,从理论到实践再到面试高频题
java·面试·多线程·同步·异步·threadlocal
在坚持一下我可没意见6 天前
ideaPool论坛系统测试报告
java·spring boot·功能测试·selenium·jmeter·mybatis·压力测试
C雨后彩虹6 天前
跨线程数据传递InheritableThreadLocal的原理
java·多线程·同步·异步·threadlocal
ps酷教程7 天前
CompletableFuture学习
java·并发
linweidong8 天前
别让老板等:千人并发下的实时大屏极致性能优化实录
jmeter·clickhouse·性能优化·sentinel·doris·物化视图·离线数仓
007张三丰8 天前
Python 多线程与异步爬虫实战:以今日头条为例
爬虫·python·多线程·异步·asyncio·aiohttp·今日头条