restTemplate未设置连接数导致服务雪崩问题以及解决

 更新时间:2024年10月29日 14:24:16   作者:莱特昂  
面对线上问题,仔细分析原因,及时调整配置,能有效解决问题,本文详细描述了线上遇到流量突增引发的问题,通过查看代码和连接池信息,分析出问题的原因是连接池满了,连接池大小配置不足以应对大并发流量,通过调整连接池大小配置

背景

昨天发版遇到个线上问题,由于运维操作放量时隔离机器过多,导致只有大概三分之一的机器承载全部流量,等于单台机器的流量突增至正常时候的三倍。

前置对外的api服务开始疯狂报错:

ConnectionPoolTimeoutException:Timeout warning for connection from pool

问题分析

连接池满了。

查看下相关代码,用了restTemplate去调用另外一个子系统,继续查看关于连接池的信息:

org.apache.http.conn.ConnectionPoolTimeoutException 是 Apache HttpClient 抛出的一种 山异常,表示从连接池获取连接超时。

这种异常通常是由以下原因导致:

1.连接池中没有可用的连接。当请求到达时,如果连接池中没有可用的连接,就会尝试创建新的连接。如果创建连接的速度很慢,或者连接池中的连接已经用完了,就会出现ConnectionPoolTimeoutException 异常。

2.连接池中的连接都被占用。如果连接池中的所有连接都正在被占用,而且没有连接释放回池中,就会导致连接池超时异常。3.请求超时时间设置过短。如果设置的请求超时时间过短,就可能在等待连接的过程中超时。

迅速去查看连接池大小配置了多少,发现并没有进行相关的配置,那默认的就是2,这样远远不够应对当前瞬时的大并发流量的。

问题解决

立刻进行了相关配置:

@Bean
public RestTemplate buildRestTemplate(){
    final ConnectionKeepAliveStrategy myStrategy = (response, context) -> {
        return 5 * 1000;//设置一个链接的最大存活时间
    };
    MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
    PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
    // 总连接数
    pollingConnectionManager.setMaxTotal(1000);
    // 同路由的并发数
    pollingConnectionManager.setDefaultMaxPerRoute(1000);
    HttpClient httpClient = HttpClientBuilder.create().setConnectionManager(pollingConnectionManager)
    .setKeepAliveStrategy(myStrategy).build();
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(
    httpClient);
    factory.setConnectTimeout(3000);
    factory.setReadTimeout(5000);
    return new RestTemplate(factory);
}

直接设置成了1000,交给测试压测,瞬时200的并发量压测也没有出现ConnectionPoolTimeoutException的问题,看来成功解决了。

restTemplate优化点补充

另外,restTemplate还有个点能够优化。

1. RestTemplate 介绍

RestTemplate和Spring提供的JdbcTemplate类似,对象一旦构建(使用过程中不对其属性进行修改)就是线程安全的,多线程环境下可以安全使用。

2. 场景描述

A系统接口需要访问B系统接口,正常请求时,这部分代码耗时不是很明显(约10ms)。后来,进行接口压力测试,发现请求耗时长达500ms,导致整个接口的tps很难上去,调整线程池参数效果努力无果,后来对请求B系统接口做了内存级别的缓存(guava),tps增长了3倍左右(约500)。

3.问题分析

B系统给出的压测数据显示,单实例接口的tps在1000+,因此初步排除了B系统接口性能差的可能。于是,开始深究系统本身代码可能存在的问题。

最开始的代码编写方式如下:

String url = "xxx.com/api"';
RestTemplate restTemplate = new RestTemplate();
MemeberCardCodeRspDTO result = restTemplate.getForObject(url, MemeberCardCodeRspDTO.class);
System.out.println(result != null ? result.getMsg() : "null");

看上去简单明了,两行代码搞定,非常优雅。

后来在组长大大的帮助下,大致定位了问题点,觉得该部分代码可能存在部分性能问题,应该抽取成单实例,即不能每次使用时重新new一个新的对象。

改造后的代码为:

@Bean
public RestTemplate buildRestTemplate(){
    final ConnectionKeepAliveStrategy myStrategy = (response, context) -> {
        return 5 * 1000;//设置一个链接的最大存活时间
    };
    MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
    PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
    // 总连接数
    pollingConnectionManager.setMaxTotal(1000);
    // 同路由的并发数
    pollingConnectionManager.setDefaultMaxPerRoute(1000);
    HttpClient httpClient = HttpClientBuilder.create().setConnectionManager(pollingConnectionManager)
    .setKeepAliveStrategy(myStrategy).build();
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(
    httpClient);
    factory.setConnectTimeout(3000);
    factory.setReadTimeout(5000);
    return new RestTemplate(factory);
}

直接依赖spring bean注解将其定义成单实例对象,其他地方直接属性注入后使用。

4. 性能分析

问题得到解决后,课余时间又对该部分代码做了一个粗略的定量分析,本机跑的数据,还是能比较清晰地得出结论。

private final String url = "xxxx.com/api";
private int loopCount = 400;
private int concurrentThread = 400;
private RestTemplate restTemplate;

@BeforeTest
public void init() {
   final ConnectionKeepAliveStrategy myStrategy = (response, context) -> {
        return 5 * 1000;//设置一个链接的最大存活时间
   };
   PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
   // 总连接数
   pollingConnectionManager.setMaxTotal(1000);
   // 同路由的并发数
   pollingConnectionManager.setDefaultMaxPerRoute(1000);
   HttpClient httpClient = HttpClientBuilder.create().setConnectionManager(pollingConnectionManager)
   .setKeepAliveStrategy(myStrategy).build();
   HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(
   httpClient);
   factory.setConnectTimeout(3000);
   factory.setReadTimeout(5000);
   restTemplate = new RestTemplate(factory);
}
@Test
public void testHttp1() {
  long start = System.currentTimeMillis();
  ExecutorService executor = Executors.newFixedThreadPool(concurrentThread);
  for (int i =0 ; i< loopCount; i++){
      executor.submit(() -> {
          MemeberCardCodeRspDTO result = restTemplate.getForObject(url, MemeberCardCodeRspDTO.class);
          System.out.println(result != null ? result.getMsg() : "null");
      });
  }
  try {
      executor.shutdown();
      executor.awaitTermination(30, TimeUnit.MINUTES); // or longer.
   } catch (InterruptedException e) {
      e.printStackTrace();
   }
   long time = System.currentTimeMillis() - start;
   System.out.printf("Tasks1 took %d ms to run%n", time);
}

@Test
public void testHttp2() {
   long start = System.currentTimeMillis();
   ExecutorService executor = Executors.newFixedThreadPool(concurrentThread);
   for (int i =0 ; i< loopCount; i++){
       executor.submit(() -> {
           RestTemplate restTemplate = new RestTemplate();
           MemeberCardCodeRspDTO result = restTemplate.getForObject(url, MemeberCardCodeRspDTO.class);
           ystem.out.println(result != null ? result.getMsg() : "null");
       });
   }
   try {
        executor.shutdown();
        executor.awaitTermination(30, TimeUnit.MINUTES); // or longer.
   } catch (InterruptedException e) {
        e.printStackTrace();
   }
   long time = System.currentTimeMillis() - start;
   System.out.printf("Tasks2 took %d ms to run%n", time);
}

调整threads数量,跑了6组数据,结果对比如下:

图中比较清晰的可以看出,优化过后的代码性能提升比较明显,且随着并发任务数增加,耗时波动不会太大。

问题总结

第三方库提供的各种方便的类,简化了编码复杂度,方便了开发者。使用不恰当时,细微的编码可能埋藏着大的隐患。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java集合中的LinkedHashSet源码解读

    Java集合中的LinkedHashSet源码解读

    这篇文章主要介绍了Java集合中的LinkedHashSet源码解读,在LinkedHashMap中,双向链表的遍历顺序通过构造方法指定,如果没有指定,则使用默认顺序为插入顺序,即accessOrder=false,需要的朋友可以参考下
    2023-12-12
  • Java结构型设计模式之桥接模式详细讲解

    Java结构型设计模式之桥接模式详细讲解

    桥接,顾名思义,就是用来连接两个部分,使得两个部分可以互相通讯。桥接模式将系统的抽象部分与实现部分分离解耦,使他们可以独立的变化。本文通过示例详细介绍了桥接模式的原理与使用,需要的可以参考一下
    2022-09-09
  • IDEA如何实现右键翻译

    IDEA如何实现右键翻译

    这篇文章主要介绍了IDEA如何实现右键翻译问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • SpringSecurity自定义登录接口的实现

    SpringSecurity自定义登录接口的实现

    本文介绍了使用Spring Security实现自定义登录接口,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-01-01
  • java8快速实现List转map 、分组、过滤等操作

    java8快速实现List转map 、分组、过滤等操作

    这篇文章主要介绍了java8快速实现List转map 、分组、过滤等操作,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • Java四种遍历Map的方法

    Java四种遍历Map的方法

    今天小编就为大家分享一篇关于Java四种遍历Map的方法,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • Java C++题解eetcode940不同的子序列 II

    Java C++题解eetcode940不同的子序列 II

    这篇文章主要为大家介绍了Java C++题解eetcode940不同的子序列 II实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • 详解Java的Proxy动态代理机制

    详解Java的Proxy动态代理机制

    Java有两种代理方式,一种是静态代理,另一种是动态代理。对于静态代理,其实就是通过依赖注入,对对象进行封装,不让外部知道实现的细节。很多 API 就是通过这种形式来封装的
    2021-06-06
  • spring-boot项目启动迟缓异常排查解决记录

    spring-boot项目启动迟缓异常排查解决记录

    这篇文章主要为大家介绍了spring-boot项目启动迟缓异常排查解决记录,突然在本地启动不起来了,表象特征就是在本地IDEA上运行时,进程卡住也不退出,应用启动时加载相关组件的日志也不输出
    2022-02-02
  • 如何在maven本地仓库中添加oracle的jdbc驱动

    如何在maven本地仓库中添加oracle的jdbc驱动

    文章介绍了在Maven项目中添加Oracle数据库驱动ojdbc5时遇到的问题以及解决问题的两种方法,方法一为简单粗暴,但没有体现Maven仓库的作用,需要手动管理jar包,方法二为在Maven本地仓库中添加Oracle的JDBC驱动,过程较为繁琐,但配置一次后可以多次使用
    2024-11-11

最新评论