公司接了一个快递行业的单子,为了保证局方能在双十一期间系统能顺利运行,也为了证实我们应用能够抗下双十一的峰值,于是配合局方做了一次应对双十一流量高峰全链路压测。模拟局方接口发起sip协议,通过华为云的接入,并获取sip-calluuid转换为sip头,经防火墙、负载均衡器最终负载到每台机器,其中一台服务器的负载并发量为1200路并发,持续压测了十几分钟后,明显发现服务响应时间变慢,通过promotheus监控查看到调局方接口的响应延迟5+ minutes,但我们应用用的RestTemplate设置了读超时时间和写超时时间是5s,怎么算也不会到5m。通过lsof -I命令查看发现大量TCP请求出现了Time_Wait
为什么出现大量的Time_wait,这个还得从TCP的四次挥手说起,当tcp连接发起断开请求的时候并不是立马断开,而是主动关闭的一方先发起FIN请求,等被动方进入CLOSE_WAIT后,主动放将TCP状态改为TIME_WAIT,等待2MSL后才最终关闭TCP连接。那么TCP为什么设计TIME_WAIT,有两点好处:1,可靠地实现TCP全双工连接地终止;2,运行老地重发分节在网络中消逝。那么长连接出现大量地TIME_WAIT是为了保证双工通信时可靠的,我们java的http请求都是短链接,我们知道http请求是无状态的,这里TIME_WAIT是维持2MSL,这个可以查阅《TCP/IP详解》,
翻阅网络上的资料,如果linux系统中出现大量的TIME_WAIT,修改linux系统的内核参数可解决此问题。步骤如下:
vi /etc/sysctl.conf,减小keepalive的连接时间,加大SYNC队列的长度,容纳更多的网络连接数。
net.ipv4.tcp_keepalive_time = 1200
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟。
net.ipv4.ip_local_port_range = 1024 65000
#表示用于向外连接的端口范围。缺省情况下很小:32768到61000,改为1024到65000。
net.ipv4.tcp_max_syn_backlog = 8192
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_tw_buckets = 5000
#表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。
默认为180000,改为5000。对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量,但是对于 Squid,效果却不大。此项参数可以控制TIME_WAIT套接字的最大数量,避免Squid服务器被大量的TIME_WAIT套接字拖死。
修改linux参数后,再次开始压测,但情况并不像资料说的那么好,过了半个小时后请求又出现延迟,虽然情况好了很多,但已经超过了我们期望的每个请求响应时长在5s内,继续查阅资料资料,发现我们使用的RestTemplate内部还是HttpClient,如果没有配置连接数和并发数,默认的值少得可怜,修改内核中HttpClient的连接数,继续压测一切顺利,每次响应时间在2s内。
/**
* 发送http请求,响应超时时间设置相对较长
* 应用于推送数据等对结果无依赖
* @return
*/
@Bean
public RestTemplate restTemplate() {
// 长连接保持30秒
PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
// 总连接数
pollingConnectionManager.setMaxTotal(800);
// 同路由的并发数
pollingConnectionManager.setDefaultMaxPerRoute(800);
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.setConnectionManager(pollingConnectionManager);
// 重试次数,默认是3次,没有开启
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true));
// 保持长连接配置,需要在头添加Keep-Alive
httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
List
headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36"));
headers.add(new BasicHeader("Accept-Encoding", "gzip,deflate"));
headers.add(new BasicHeader("Accept-Language", "zh-CN"));
// headers.add(new BasicHeader("Connection", "Keep-Alive"));
httpClientBuilder.setDefaultHeaders(headers);
HttpClient httpClient = httpClientBuilder.build();
// httpClient连接配置,底层是配置RequestConfig
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
// 连接超时
clientHttpRequestFactory.setConnectTimeout(config.getConnectionTimeOut() );
// 数据读取超时时间,即SocketTimeout
clientHttpRequestFactory.setReadTimeout(config.getSoketTimeOut());
// 连接不够用的等待时间,不宜过长,必须设置,比如连接不够用时,时间过长将是灾难性的
clientHttpRequestFactory.setConnectionRequestTimeout(200);
// 缓冲请求数据,默认值是true。通过POST或者PUT大量发送数据时,建议将此属性更改为false,以免耗尽内存。
// clientHttpRequestFactory.setBufferRequestBody(false);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(clientHttpRequestFactory);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler());
return restTemplate;
}