restTemplate未設(shè)置連接數(shù)導(dǎo)致服務(wù)雪崩問(wèn)題以及解決
背景
昨天發(fā)版遇到個(gè)線上問(wèn)題,由于運(yùn)維操作放量時(shí)隔離機(jī)器過(guò)多,導(dǎo)致只有大概三分之一的機(jī)器承載全部流量,等于單臺(tái)機(jī)器的流量突增至正常時(shí)候的三倍。
前置對(duì)外的api服務(wù)開(kāi)始瘋狂報(bào)錯(cuò):
ConnectionPoolTimeoutException:Timeout warning for connection from pool
問(wèn)題分析
連接池滿了。
查看下相關(guān)代碼,用了restTemplate去調(diào)用另外一個(gè)子系統(tǒng),繼續(xù)查看關(guān)于連接池的信息:
org.apache.http.conn.ConnectionPoolTimeoutException 是 Apache HttpClient 拋出的一種 山異常,表示從連接池獲取連接超時(shí)。
這種異常通常是由以下原因?qū)е拢?/p>
1.連接池中沒(méi)有可用的連接。當(dāng)請(qǐng)求到達(dá)時(shí),如果連接池中沒(méi)有可用的連接,就會(huì)嘗試創(chuàng)建新的連接。如果創(chuàng)建連接的速度很慢,或者連接池中的連接已經(jīng)用完了,就會(huì)出現(xiàn)ConnectionPoolTimeoutException 異常。
2.連接池中的連接都被占用。如果連接池中的所有連接都正在被占用,而且沒(méi)有連接釋放回池中,就會(huì)導(dǎo)致連接池超時(shí)異常。3.請(qǐng)求超時(shí)時(shí)間設(shè)置過(guò)短。如果設(shè)置的請(qǐng)求超時(shí)時(shí)間過(guò)短,就可能在等待連接的過(guò)程中超時(shí)。
迅速去查看連接池大小配置了多少,發(fā)現(xiàn)并沒(méi)有進(jìn)行相關(guān)的配置,那默認(rèn)的就是2,這樣遠(yuǎn)遠(yuǎn)不夠應(yīng)對(duì)當(dāng)前瞬時(shí)的大并發(fā)流量的。
問(wèn)題解決
立刻進(jìn)行了相關(guān)配置:
@Bean public RestTemplate buildRestTemplate(){ final ConnectionKeepAliveStrategy myStrategy = (response, context) -> { return 5 * 1000;//設(shè)置一個(gè)鏈接的最大存活時(shí)間 }; MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager(); PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS); // 總連接數(shù) pollingConnectionManager.setMaxTotal(1000); // 同路由的并發(fā)數(shù) 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); }
直接設(shè)置成了1000,交給測(cè)試壓測(cè),瞬時(shí)200的并發(fā)量壓測(cè)也沒(méi)有出現(xiàn)ConnectionPoolTimeoutException的問(wèn)題,看來(lái)成功解決了。
restTemplate優(yōu)化點(diǎn)補(bǔ)充
另外,restTemplate還有個(gè)點(diǎn)能夠優(yōu)化。
1. RestTemplate 介紹
RestTemplate和Spring提供的JdbcTemplate類似,對(duì)象一旦構(gòu)建(使用過(guò)程中不對(duì)其屬性進(jìn)行修改)就是線程安全的,多線程環(huán)境下可以安全使用。
2. 場(chǎng)景描述
A系統(tǒng)接口需要訪問(wèn)B系統(tǒng)接口,正常請(qǐng)求時(shí),這部分代碼耗時(shí)不是很明顯(約10ms)。后來(lái),進(jìn)行接口壓力測(cè)試,發(fā)現(xiàn)請(qǐng)求耗時(shí)長(zhǎng)達(dá)500ms,導(dǎo)致整個(gè)接口的tps很難上去,調(diào)整線程池參數(shù)效果努力無(wú)果,后來(lái)對(duì)請(qǐng)求B系統(tǒng)接口做了內(nèi)存級(jí)別的緩存(guava),tps增長(zhǎng)了3倍左右(約500)。
3.問(wèn)題分析
B系統(tǒng)給出的壓測(cè)數(shù)據(jù)顯示,單實(shí)例接口的tps在1000+,因此初步排除了B系統(tǒng)接口性能差的可能。于是,開(kāi)始深究系統(tǒng)本身代碼可能存在的問(wèn)題。
最開(kāi)始的代碼編寫(xiě)方式如下:
String url = "xxx.com/api"'; RestTemplate restTemplate = new RestTemplate(); MemeberCardCodeRspDTO result = restTemplate.getForObject(url, MemeberCardCodeRspDTO.class); System.out.println(result != null ? result.getMsg() : "null");
看上去簡(jiǎn)單明了,兩行代碼搞定,非常優(yōu)雅。
后來(lái)在組長(zhǎng)大大的幫助下,大致定位了問(wèn)題點(diǎn),覺(jué)得該部分代碼可能存在部分性能問(wèn)題,應(yīng)該抽取成單實(shí)例,即不能每次使用時(shí)重新new一個(gè)新的對(duì)象。
改造后的代碼為:
@Bean public RestTemplate buildRestTemplate(){ final ConnectionKeepAliveStrategy myStrategy = (response, context) -> { return 5 * 1000;//設(shè)置一個(gè)鏈接的最大存活時(shí)間 }; MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager(); PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS); // 總連接數(shù) pollingConnectionManager.setMaxTotal(1000); // 同路由的并發(fā)數(shù) 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注解將其定義成單實(shí)例對(duì)象,其他地方直接屬性注入后使用。
4. 性能分析
問(wèn)題得到解決后,課余時(shí)間又對(duì)該部分代碼做了一個(gè)粗略的定量分析,本機(jī)跑的數(shù)據(jù),還是能比較清晰地得出結(jié)論。
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;//設(shè)置一個(gè)鏈接的最大存活時(shí)間 }; PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS); // 總連接數(shù) pollingConnectionManager.setMaxTotal(1000); // 同路由的并發(fā)數(shù) 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); }
調(diào)整threads數(shù)量,跑了6組數(shù)據(jù),結(jié)果對(duì)比如下:
圖中比較清晰的可以看出,優(yōu)化過(guò)后的代碼性能提升比較明顯,且隨著并發(fā)任務(wù)數(shù)增加,耗時(shí)波動(dòng)不會(huì)太大。
問(wèn)題總結(jié)
第三方庫(kù)提供的各種方便的類,簡(jiǎn)化了編碼復(fù)雜度,方便了開(kāi)發(fā)者。使用不恰當(dāng)時(shí),細(xì)微的編碼可能埋藏著大的隱患。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- restTemplate實(shí)現(xiàn)跨服務(wù)API調(diào)用方式
- Spring?Cloud?Alibaba?Nacos服務(wù)治理平臺(tái)服務(wù)注冊(cè)、RestTemplate實(shí)現(xiàn)微服務(wù)之間訪問(wèn)負(fù)載均衡訪問(wèn)的問(wèn)題
- Java服務(wù)調(diào)用RestTemplate與HttpClient的使用詳解
- SpringCloud基于RestTemplate微服務(wù)項(xiàng)目案例解析
- springcloud中Ribbon和RestTemplate實(shí)現(xiàn)服務(wù)調(diào)用與負(fù)載均衡
- 關(guān)于springboot 中使用httpclient或RestTemplate做MultipartFile文件跨服務(wù)傳輸?shù)膯?wèn)題
相關(guān)文章
Java結(jié)構(gòu)型設(shè)計(jì)模式之橋接模式詳細(xì)講解
橋接,顧名思義,就是用來(lái)連接兩個(gè)部分,使得兩個(gè)部分可以互相通訊。橋接模式將系統(tǒng)的抽象部分與實(shí)現(xiàn)部分分離解耦,使他們可以獨(dú)立的變化。本文通過(guò)示例詳細(xì)介紹了橋接模式的原理與使用,需要的可以參考一下2022-09-09SpringSecurity自定義登錄接口的實(shí)現(xiàn)
本文介紹了使用Spring Security實(shí)現(xiàn)自定義登錄接口,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-01-01java8快速實(shí)現(xiàn)List轉(zhuǎn)map 、分組、過(guò)濾等操作
這篇文章主要介紹了java8快速實(shí)現(xiàn)List轉(zhuǎn)map 、分組、過(guò)濾等操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09詳解Java的Proxy動(dòng)態(tài)代理機(jī)制
Java有兩種代理方式,一種是靜態(tài)代理,另一種是動(dòng)態(tài)代理。對(duì)于靜態(tài)代理,其實(shí)就是通過(guò)依賴注入,對(duì)對(duì)象進(jìn)行封裝,不讓外部知道實(shí)現(xiàn)的細(xì)節(jié)。很多 API 就是通過(guò)這種形式來(lái)封裝的2021-06-06spring-boot項(xiàng)目啟動(dòng)遲緩異常排查解決記錄
這篇文章主要為大家介紹了spring-boot項(xiàng)目啟動(dòng)遲緩異常排查解決記錄,突然在本地啟動(dòng)不起來(lái)了,表象特征就是在本地IDEA上運(yùn)行時(shí),進(jìn)程卡住也不退出,應(yīng)用啟動(dòng)時(shí)加載相關(guān)組件的日志也不輸出2022-02-02如何在maven本地倉(cāng)庫(kù)中添加oracle的jdbc驅(qū)動(dòng)
文章介紹了在Maven項(xiàng)目中添加Oracle數(shù)據(jù)庫(kù)驅(qū)動(dòng)ojdbc5時(shí)遇到的問(wèn)題以及解決問(wèn)題的兩種方法,方法一為簡(jiǎn)單粗暴,但沒(méi)有體現(xiàn)Maven倉(cāng)庫(kù)的作用,需要手動(dòng)管理jar包,方法二為在Maven本地倉(cāng)庫(kù)中添加Oracle的JDBC驅(qū)動(dòng),過(guò)程較為繁瑣,但配置一次后可以多次使用2024-11-11