找回密码
立即注册
搜索
发回帖 发新帖

6676

积分

0

好友

843

主题
发表于 昨天 22:10 | 查看: 3| 回复: 0

来源:blog.csdn.net/u010285974/article/details/85696239

HttpClient 优化思路:

  1. 池化
  2. 长连接
  3. HttpClient 和 HttpGet 复用
  4. 合理的配置参数(最大并发请求数、各种超时时间、重试次数)
  5. 异步
  6. 多读源码

1. 背景

我们有个业务,会调用其他部门提供的一个基于 HTTP 的服务,日调用量在千万级别。之前使用 HttpClient 完成业务,但 QPS 一直上不去。于是翻了一遍业务代码,做了一些优化,记录在这里。

先对比优化前后:

优化之前,平均执行时间是 250ms;

优化之后,平均执行时间是 80ms,降低了三分之二的耗时,容器也不再动不动就告警线程耗尽了,清爽多了。

2. 分析

项目原来的实现比较粗略:每次请求时初始化一个 HttpClient,生成一个 HttpPost 对象,执行,然后从返回结果里取出 entity,保存成字符串,最后显式关闭 response 和 client。

我们一点点分析和优化。

2.1 HttpClient 反复创建开销

HttpClient 本身是一个线程安全的类,没有必要让每个线程每次使用时都创建,全局保留一个即可。

2.2 反复创建 TCP 连接的开销

tcp的三次握手与四次挥手 这两大裹脚布过程,对于高频次请求来说,开销实在太大。可以算一笔账:如果每次请求需要花 5ms 做握手和挥手,对于 QPS 为 100 的单系统来说,1 秒就要花 500ms 在连接协商上。总不能让每次请求都举行一次“握手 + 挥手”的全套礼仪吧?改成 keep-alive 方式复用连接,就能省下这笔消耗。

2.3 重复缓存 entity 的开销

原来的逻辑里使用了如下代码:

HttpEntity entity = httpResponse.getEntity();

String response = EntityUtils.toString(entity);

这里相当于额外把 content 复制了一份到字符串里,而原来的 httpResponse 还保留了一份 content 等待被消费。在高并发且 content 很大时,会消耗大量内存;并且原来的写法还需要显式关闭连接,比较麻烦。

3. 实现

按照上面的分析,需要做三件事:

  1. 单例的 client;
  2. 可缓存的保活连接;
  3. 更好地处理返回结果。

单例 client 比较简单,不再展开,重点看连接缓存。

提到连接缓存,很容易联想到数据库连接池。HttpClient 4 提供了 PoolingHttpClientConnectionManager 作为连接池。下面按步骤完成优化。

3.1 定义一个 keep-alive strategy

关于 keep-alive 这里不展开,只提一点:是否使用 keep-alive 要结合业务判断,它不是银弹。另外 keep-alive 和 time_wait/close_wait 之间也有不少故事。

在当前业务场景里,相当于少数固定客户端长时间、极高频次地访问服务端,启用 keep-alive 非常合适。

再多说一句,HTTP 的 keep-alive 和 TCP 的 KEEPALIVE 不是同一个东西。回到正文,定义一个 strategy 如下:

ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
    @Override
    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        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")) {
                return Long.parseLong(value) * 1000;
            }
        }
        return 60 * 1000; // 如果没有约定,则默认定义时长为 60s
    }
};

3.2 配置一个 PoolingHttpClientConnectionManager

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(500);
connectionManager.setDefaultMaxPerRoute(50); // 例如默认每路由最高 50 并发,具体依据业务来定

也可以针对每个路由单独设置并发数。

3.3 生成 HttpClient

httpClient = HttpClients.custom()
        .setConnectionManager(connectionManager)
        .setKeepAliveStrategy(kaStrategy)
        .setDefaultRequestConfig(RequestConfig.custom().setStaleConnectionCheckEnabled(true).build())
        .build();

注意:使用 setStaleConnectionCheckEnabled 来逐出已被关闭的连接并不推荐。更好的方式是手动启用一个线程,定时运行 closeExpiredConnections 和 closeIdleConnections 方法,如下所示。

public static class IdleConnectionMonitorThread extends Thread {

    private final HttpClientConnectionManager connMgr;
    private volatile boolean shutdown;

    public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
        super();
        this.connMgr = connMgr;
    }

    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000);
                    // Close expired connections
                    connMgr.closeExpiredConnections();
                    // Optionally, close connections that have been idle longer than 30 sec
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
            // terminate
        }
    }

    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }
}

3.4 执行 method 时降低开销

这里要注意:不要手动关闭 connection。

一种可行的获取内容方式是把 entity 复制一份:

res = EntityUtils.toString(response.getEntity(), "UTF-8");
EntityUtils.consume(response1.getEntity());

但更推荐定义一个 ResponseHandler,这样就不需要自己 catch 异常和关闭流。可以看一下相关源码:

public <T> T execute(final HttpHost target, final HttpRequest request,
        final ResponseHandler<? extends T> responseHandler, final HttpContext context)
        throws IOException, ClientProtocolException {
    Args.notNull(responseHandler, "Response handler");

    final HttpResponse response = execute(target, request, context);

    final T result;
    try {
        result = responseHandler.handleResponse(response);
    } catch (final Exception t) {
        final HttpEntity entity = response.getEntity();
        try {
            EntityUtils.consume(entity);
        } catch (final Exception t2) {
            // Log this exception. The original exception is more
            // important and will be thrown to the caller.
            this.log.warn("Error consuming content after an exception.", t2);
        }
        if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        }
        if (t instanceof IOException) {
            throw (IOException) t;
        }
        throw new UndeclaredThrowableException(t);
    }

    // Handling the response was successful. Ensure that the content has
    // been fully consumed.
    final HttpEntity entity = response.getEntity();
    EntityUtils.consume(entity); // 看这里看这里
    return result;
}

可以看到,如果通过 ResponseHandler 执行 execute 方法,最终会自动调用 consume。这个 consume 方法如下:

public static void consume(final HttpEntity entity) throws IOException {
    if (entity == null) {
        return;
    }
    if (entity.isStreaming()) {
        final InputStream instream = entity.getContent();
        if (instream != null) {
            instream.close();
        }
    }
}

最终它关闭了输入流。

4. 其他

通过以上步骤,基本完成了一个支持高并发的 HttpClient 写法。下面再补充一些配置和提醒。

4.1 HttpClient 的一些超时配置

CONNECTION_TIMEOUT 是连接超时时间,SO_TIMEOUT 是 socket 超时时间,两者不同:连接超时是发起请求前的等待时间,socket 超时是等待数据的超时时间。

HttpParams params = new BasicHttpParams();
// 设置连接超时时间
Integer CONNECTION_TIMEOUT = 2 * 1000; // 设置请求超时 2 秒,根据业务调整
Integer SO_TIMEOUT = 2 * 1000; // 设置等待数据超时时间 2 秒,根据业务调整

// 定义了当从 ClientConnectionManager 中检索 ManagedClientConnection 实例时使用的毫秒级超时时间
// 这个参数期望得到一个 java.lang.Long 类型的值。如果没设置,默认等于 CONNECTION_TIMEOUT,因此一定要设置
Long CONN_MANAGER_TIMEOUT = 500L; // 在 httpclient 4.2.3 中它曾被改成一个对象,直接传 long 会报错,后来又改回来了

params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT);
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, SO_TIMEOUT);
params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, CONN_MANAGER_TIMEOUT);
// 在提交请求之前 测试连接是否可用
params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, true);

// 另外设置 http client 的重试次数,默认是 3 次;当前是禁用掉(如果项目量不到,这个默认即可)
httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));

4.2 如果配置了 Nginx,Nginx 也要设置面向两端的 keep-alive

现在的业务几乎没有不用 Nginx 的情况。Nginx 默认和 client 端打开长连接,而和 server 端使用短连接。

注意 client 端的 keepalive_timeout 和 keepalive_requests 参数,以及 upstream 端的 keepalive 参数设置,这三个参数的意义这里不再赘述。

以上就是我的全部设置。通过这些调整,成功把单次请求耗时从 250ms 降到了 80ms 左右,效果显著。

JAR 包如下:

<!-- httpclient -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.6</version>
</dependency>

完整代码如下:

// Basic认证
private static final CredentialsProvider credsProvider = new BasicCredentialsProvider();
// httpClient
private static final CloseableHttpClient httpclient;
// httpGet方法
private static final HttpGet httpget;
//
private static final RequestConfig reqestConfig;
// 响应处理器
private static final ResponseHandler<String> responseHandler;
// jackson解析工具
private static final ObjectMapper mapper = new ObjectMapper();

static {
    System.setProperty("http.maxConnections", "50");
    System.setProperty("http.keepAlive", "true");
    // 设置basic校验
    credsProvider.setCredentials(
            new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM),
            new UsernamePasswordCredentials("", ""));
    // 创建http客户端
    httpclient = HttpClients.custom()
            .useSystemProperties()
            .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
            .setDefaultCredentialsProvider(credsProvider)
            .build();
    // 初始化httpGet
    httpget = new HttpGet();
    // 初始化HTTP请求配置
    reqestConfig = RequestConfig.custom()
            .setContentCompressionEnabled(true)
            .setSocketTimeout(100)
            .setAuthenticationEnabled(true)
            .setConnectionRequestTimeout(100)
            .setConnectTimeout(100).build();
    httpget.setConfig(reqestConfig);
    // 初始化response解析器
    responseHandler = new BasicResponseHandler();
}

/*
 * 功能:返回响应
 * @param [url]
 * @return org.apache.http.client.methods.CloseableHttpResponse
 */
public static String getResponse(String url) throws IOException {
    HttpGet get = new HttpGet(url);
    String response = httpclient.execute(get, responseHandler);
    return response;
}

/*
 * 功能:发送http请求,并用net.sf.json工具解析
 * @param [url]
 * @return org.json.JSONObject
 */
public static JSONObject getUrl(String url) throws Exception {
    try {
        httpget.setURI(URI.create(url));
        String response = httpclient.execute(httpget, responseHandler);
        JSONObject json = JSONObject.fromObject(response);
        return json;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

/*
 * 功能:发送http请求,并用jackson工具解析
 * @param [url]
 * @return com.fasterxml.jackson.databind.JsonNode
 */
public static JsonNode getUrl2(String url) {
    try {
        httpget.setURI(URI.create(url));
        String response = httpclient.execute(httpget, responseHandler);
        JsonNode node = mapper.readTree(response);
        return node;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

/*
 * 功能:发送http请求,并用fastjson工具解析
 * @param [url]
 * @return com.fasterxml.jackson.databind.JsonNode
 */
public static com.alibaba.fastjson.JSONObject getUrl3(String url) {
    try {
        httpget.setURI(URI.create(url));
        String response = httpclient.execute(httpget, responseHandler);
        com.alibaba.fastjson.JSONObject jsonObject = com.alibaba.fastjson.JSONObject.parseObject(response);
        return jsonObject;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}



上一篇:NV UEFI Unlocker解锁RTX 40/30/20及GTX 16隐藏AI算力,性能最高提升85%
下一篇:数据库连接池为何不用IO多路复用?JDBC的BIO设计之困
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-11-6 17:04 , Processed in 0.075970 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表