全链路压测出现大量TIME_WAIT

公司接了一个快递行业的单子,为了保证局方能在双十一期间系统能顺利运行,也为了证实我们应用能够抗下双十一的峰值,于是配合局方做了一次应对双十一流量高峰全链路压测。模拟局方接口发起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详解》,全链路压测出现大量TIME_WAIT

 

 

翻阅网络上的资料,如果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<Header> headers = new ArrayList<>();

        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(ytoOrderConfig.getConnectionTimeOut() + 1000);

        // 数据读取超时时间,即SocketTimeout

        clientHttpRequestFactory.setReadTimeout(ytoOrderConfig.getSoketTimeOut() + 1000);

        // 连接不够用的等待时间,不宜过长,必须设置,比如连接不够用时,时间过长将是灾难性的

        clientHttpRequestFactory.setConnectionRequestTimeout(200);

        // 缓冲请求数据,默认值是true。通过POST或者PUT大量发送数据时,建议将此属性更改为false,以免耗尽内存。

        // clientHttpRequestFactory.setBufferRequestBody(false);

 

        RestTemplate restTemplate = new RestTemplate();

        restTemplate.setRequestFactory(clientHttpRequestFactory);

        restTemplate.setErrorHandler(new DefaultResponseErrorHandler());

        return restTemplate;

    }