记一次调用时长优化之路,暨httpclient优化之路

背景

由于公司部分业务是php开发,而我所在的部门是java开发,这之间就需要相互调用,于是就有了代理项目,负责java通过restful方式调用php接口服务, 随着业务量的增长rt时间越来越慢,于是开始了排查。

定位问题    

通过pingpoint如下图

 

分析得知接口耗时主要花费在建立连接上。查看项目代码得知调用http使用的是http-client4.5.5,发现没用连接池,这个坑也是绝了。

解决方案

    增加资源池管理HttpClientConnectionManager,HttpClientConnectionManager需要配置这几项

代码如下

mport lombok.extern.slf4j.Slf4j;
import org.apache.http.*;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
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.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.junit.Test;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author zdd
 * @date 2019/3/4 下午3:58
 */
@Slf4j
public class HttpClientTests {
    /** 全局连接池对象 */
    private static final PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
    private static CloseableHttpClient httpClient;
    private static ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {

        public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
            // Honor 'keep-alive' header
            HeaderElementIterator it = new BasicHeaderElementIterator(
                    response.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (it.hasNext()) {
                HeaderElement he = it.nextElement();
                String param = he.getName();
                String value = he.getValue();
                if (value != null && param.equalsIgnoreCase("timeout")) {
                    try {
                        return Long.parseLong(value) * 1000;
                    } catch (NumberFormatException ignore) {
                    }
                }
            }
            return 30 * 1000;
        }

    };
    static final int timeOut = 30 * 1000;

    static {
        try {
            SSLContextBuilder builder = new SSLContextBuilder();
            builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build());
            // 配置同时支持 HTTP 和 HTPPS
            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().register(
                    "http", PlainConnectionSocketFactory.getSocketFactory()).register(
                    "https", sslsf).build();
            // 设置最大连接数
            connManager.setMaxTotal(200);
            // 设置每个连接的路由数
            connManager.setDefaultMaxPerRoute(200);
            // 初始化httpClient
            httpClient = getHttpClient();
        }catch (Exception e) {
            log.error("connManager Error",e);
        }
    }



    public static CloseableHttpClient getHttpClient() {
        // 创建Http请求配置参数
        RequestConfig requestConfig = RequestConfig.custom()
                // 获取连接超时时间
                .setConnectionRequestTimeout(timeOut)
                // 请求超时时间
                .setConnectTimeout(timeOut)
                // 响应超时时间
                .setSocketTimeout(timeOut)
                .build();
        CloseableHttpClient httpClient = HttpClients.custom()
                // 把请求相关的超时信息设置到连接客户端
                .setDefaultRequestConfig(requestConfig)
                // 把请求重试设置到连接客户端
                .setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))
                // 配置连接池管理对象
                .setConnectionManager(connManager)
                //设置Keep-Alive
                .setKeepAliveStrategy(myStrategy)
                .build();
        return httpClient;
    }

    /**
     * get 方式请求
     * @param url  请求URL
     * @return
     */
    public static String httpGet(String url ) {
        String msg =  "";
        HttpGet httpGet = new HttpGet(url);
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpGet);
            HttpEntity entity = response.getEntity();
            msg = EntityUtils.toString(entity, "UTF-8");
        } catch (Exception e) {
            log.error("http请求异常,url:{}",url,e);
        }  finally {
            if (null != response) {
                try {
                    EntityUtils.consume(response.getEntity());
                    response.close();
                } catch (IOException e) {
                    log.error("释放链接错误",e);
                }
            }
        }
        return msg;
    }


    /**
     * 设置post请求参数
     * @param httpost
     * @param params
     */
    private static void setPostParams(HttpPost httpost, Map<String, Object> params) {
        List<NameValuePair> nvps = new ArrayList<NameValuePair>();
        Set<String> keySet = params.keySet();
        for (String key : keySet) {
            nvps.add(new BasicNameValuePair(key, params.get(key).toString()));
        }
        try {
            httpost.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8"));
        } catch (Exception e) {
            log.info("设置post异常,", e);
        }
    }

    /**
     * 发送post请求
     * @param url
     * @param params
     * @return
     * @throws IOException
     */
    public static String httpPost(String url, Map<String, Object> params) throws IOException {
        //创建post对象
        HttpPost httppost = new HttpPost(url);
        setPostParams(httppost, params);
        CloseableHttpResponse response = null;
        try {
            response = getHttpClient().execute(httppost,
                    HttpClientContext.create());
            HttpEntity entity = response.getEntity();
            String result = EntityUtils.toString(entity, "utf-8");
            EntityUtils.consume(entity);
            return result;
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                if (response != null){
                    response.close();
                }
            } catch (IOException e) {
                log.error("关闭链接失败",e);
            }
        }
    }

    static class GetThread extends Thread {
        private static CloseableHttpClient httpClient;
        private String url;
        private String param;

        public GetThread(CloseableHttpClient client, String url) {
            httpClient = client;
            this.url = url;
        }

        public GetThread(CloseableHttpClient client, String url, String param) {
            httpClient = client;
            this.url = url;
            this.param = param;
        }

        public void run() {
            while (true) {
                HttpPost httpPost = new HttpPost(url);// 创建httpPost
                httpPost.setHeader("Accept", "application/json");
                httpPost.setHeader("Content-Type", "application/json");
                String charSet = "UTF-8";
                StringEntity entity = new StringEntity(param, charSet);
                httpPost.setEntity(entity);
                CloseableHttpResponse response = null;
                try {
                    response = httpClient.execute(httpPost);
                    HttpEntity responseEntity = response.getEntity();
                    String jsonString = EntityUtils.toString(responseEntity);
                    EntityUtils.consume(responseEntity);
                    System.out.println(jsonString);
                    Thread.sleep(1 * 1000);   // 线程休眠时间
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (response != null) {
                            response.close();
                        }
                        if (httpPost != null) {
                            httpPost.releaseConnection();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            }
        }


        public static void main(String[] args) {
            GetThread[] threads = new GetThread[2];
            for (int i = 0; i < 2; i++) {
                threads[i] = new GetThread(HttpClientTests.httpClient, "http://127.0.0.1:7801/test/hello", "{\"name\":\"张三\"}");
            }
            for (Thread tmp : threads) {
                tmp.start();
            }
        }
    }


}

 

求参数 RequestConfig 含义是:

  • connectTimeout 请求超时时间
  • socketTimeout 等待数据超时时间
  • connectionRequestTimeout 连接不够用时等待超时时间,一定要设置,如果不设置的话,如果连接池连接不够用,就会线程阻塞。

PoolingHttpClientConnectionManager,参数的意思

  • maxTotal 指的是连接池内连接最大数
  • defaultMaxPerRoute 一定要设置,路由指的是请求的系统,如访问 localhost:8080 和 localhost:8081 就是两个路由。perRote 指的是每个路由的连接数,因为我目前只连接一个系统的进行数据同步,所以 perRote 设置和最大连接数一样

因为我这都是通过一个域名访问php服务,所以maxTotal和defaultMaxPerRoute都设置一样,不设置defaultMaxPerRoute默认是2

创建 HttpClient 有两种方式,两种方式都统一管理连接

  • 一种是单例创建 httpClient,通过 setConnectionManager() 注入连接池(PoolingHttpClientConnectionManager), httpClient 是线程安全取决于 connectionManager,而该 pool 是线程安全(源码注释),另外官方文档上也有如下一段话

    While HttpClient instances are thread safe and can be shared between multiple threads of execution, it is highly recommended that each thread maintains its own dedicated instance of HttpContext .

  • 另外是可以创建多个 httpClient 实例,但是必须得注入同一个连接池

为什么使用连接池?

使用连接池的好处主要有

  • 在 keep-alive 时间内,可以使用同一个 tcp 连接发起多次 http 请求。
  • 如果不使用连接池,在大并发的情况下,每次连接都会打开一个端口,使系统资源很快耗尽,无法建立新的连接,可以限定最多打开的端口数。

我的理解是连接池内的连接数其实就是可以同时创建多少个 tcp 连接,httpClient 维护着两个 Set,leased(被占用的连接集合) 和 avaliabled(可用的连接集合) 两个集合,释放连接就是将被占用连接放到可用连接里面。

什么是 Keep-Alive

HTTP1.1 默认启用 Keep-Alive,我们的 client(如浏览器)访问 http-server(比如 tomcat/nginx/apache)连接,其实就是发起一次 tcp 连接,要经历连接三次握手,关闭四次握手,在一次连接里面,可以发起多个 http 请求。如果没有 Keep-Alive,每次 http 请求都要发起一次 tcp 连接。
下图是 apache-server 一次 http 请求返回的响应头,可以看到连接方式就是 Keep-Alive,另外还有 Keep-Alive 头,其中

  • timeout=5 5s 之内如果没有发起新的 http 请求,服务器将断开这次 tcp 连接,如果发起新的请求,断开连接时间将继续刷新为 5s
  • max=100 的意思在这次 tcp 连接之内,最多允许发送 100 次 http 请求,100 次之后,即使在 timeout 时间之内发起新的请求,服务器依然会断开这次 tcp 连接

可以通过 tcpdump -i lo0 -n  port 7801查看tcp链接过程

看端口变化,新建tcp会开启新端口

参考

 

使用 httpclient 连接池及注意事项

HttpClient连接池的使用

高并发场景下的httpClient优化使用

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值