暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【经验与坑】高并发场景下的http连接优化

一.研究背景

最近实验过程中遇到了一个小问题,个人认为是值得拿出来说一下的。整体系统是分为三部分的,分为客户端、中间数据转发以及HBase服务端。整体架构图如下所示:


那么这部分主要记录 http 通信的优化过程。一开始实现的比较仓促,使用的是传统的 HttpURLConnection 来做请求。但是在后续测试过程中,前期请求耗时是很优秀的,但是后期效率越来越低。所以想到了使用 HttpClient 连接池进行连接复用。


二.相关底层技术


(1)Http 长连接 短连接


应用层的Http协议是基于传输层的TCP协议的,所以Http长连接短连接本质上就是TCP长连接短连接。


我们从 Http1.0 1.1两个版本说起。


在Http1.0中,默认使用的是短连接,俗话说的就是 C和S每进行一次HTTP请求就要建立一次TCP连接,任务结束关闭,所以如果有大量重复地址请求的话就会反复在TCP三次握手和四次挥手当中花费巨大的时间。

在1.0版本中我们需要手动设置请求响应头的 Connection属性

    Connection:keep-alive

    这样就实现了长连接,即保活连接。


    Http1.1进行了优化,默认便使用了长连接模式,这种情况下,每次打开一个网页完成后,C和S之间用于传输HTTP数据的TCP连接并不会关闭,如果C想要再次访问S上的网页资源,会继续使用这条已经建立好的连接。但是这个保活机制并不是永久有效的,毕竟S空间资源有限,它是通过S上的服务器软件设置的。


    (2)TCP中的KEEPALIVE 和 Http 的 keep-alive 的区别

    TCP-keepalive:

    是由系统内核实现的。具体逻辑流程如下:

    如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。


    • 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

    • 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。


    Http的keepalive:

    用于设置http长连接,通过设置响应头中的Connection属性实现。


    (3)连接池技术

    池化技术充满了整个项目,无论是线程池还是请求连接池。它们作为创建和管理连接的缓冲池技术,目前已广泛用于诸如数据库连接等长连接的维护和管理中,能够有效减少系统的响应时间,节省服务器资源。


    三.实验部分

    基于PoolingHttpClientConnectionManager实现请求连接池

      package service;


      import org.apache.http.HttpHost;
      import org.apache.http.HttpRequest;
      import org.apache.http.NoHttpResponseException;
      import org.apache.http.client.HttpRequestRetryHandler;
      import org.apache.http.client.config.RequestConfig;
      import org.apache.http.client.methods.HttpRequestBase;
      import org.apache.http.client.protocol.HttpClientContext;
      import org.apache.http.config.Registry;
      import org.apache.http.config.RegistryBuilder;
      import org.apache.http.conn.routing.HttpRoute;
      import org.apache.http.conn.socket.ConnectionSocketFactory;
      import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
      import org.apache.http.conn.socket.PlainConnectionSocketFactory;
      import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
      import org.apache.http.impl.client.CloseableHttpClient;
      import org.apache.http.impl.client.HttpClients;
      import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;


      import javax.net.ssl.SSLException;
      import javax.net.ssl.SSLHandshakeException;
      import java.io.IOException;
      import java.io.InterruptedIOException;
      import java.net.UnknownHostException;


      /**
      * @author han56
      * @description 功能描述 HttpClient工具类
      * @create 2022/5/27 下午1:49
      */
      public class HttpClientUtil {


      static final int timeOut = 10*1000;


      private static CloseableHttpClient httpClient = null;


      private final static Object lock = new Object();


      //配置方法
      private static void config(HttpRequestBase httpRequestBase){
      //配置请求超时设置
      RequestConfig requestConfig = RequestConfig.custom()
      .setConnectionRequestTimeout(timeOut)
      .setConnectTimeout(timeOut)
      .setSocketTimeout(timeOut)
      .build();
      httpRequestBase.setConfig(requestConfig);
      }


      //获取HttpClient对象
      public static CloseableHttpClient getHttpClient(String url){
      String hostName = url.split("/")[2];
              int port = xxxxx;
      if (hostName.contains(":")) {
      String[] arr = hostName.split(":");
      hostName = arr[0];
      port = Integer.parseInt(arr[1]);
      }
      if (httpClient == null) {
      synchronized (lock) {
      if (httpClient == null) {
      httpClient = createHttpClient(20000, 4000, 10000, hostName, port);
      }
      }
      }
      return httpClient;
      }


      //创建HttpClient对象
      public static CloseableHttpClient createHttpClient(int maxTotal,
      int maxPerRoute, int maxRoute, String hostname, int port){
      ConnectionSocketFactory plainsf = PlainConnectionSocketFactory
      .getSocketFactory();
      LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory
      .getSocketFactory();
      Registry<ConnectionSocketFactory> registry = RegistryBuilder
      .<ConnectionSocketFactory> create().register("http", plainsf)
      .register("https", sslsf).build();
      PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
      registry);
      // 将最大连接数增加
      cm.setMaxTotal(maxTotal);
      // 将每个路由基础的连接增加
      cm.setDefaultMaxPerRoute(maxPerRoute);
      HttpHost httpHost = new HttpHost(hostname, port);
      // 将目标主机的最大连接数增加
      cm.setMaxPerRoute(new HttpRoute(httpHost), maxRoute);


      // 请求重试处理
      HttpRequestRetryHandler httpRequestRetryHandler = (exception, executionCount, context) -> {
      if (executionCount >= 5) {// 如果已经重试了5次,就放弃
      return false;
      }
      if (exception instanceof NoHttpResponseException) {// 如果服务器丢掉了连接,那么就重试
      return true;
      }
      if (exception instanceof SSLHandshakeException) {// 不要重试SSL握手异常
      return false;
      }
      if (exception instanceof InterruptedIOException) {// 超时
      return false;
      }
      if (exception instanceof UnknownHostException) {// 目标服务器不可达
      return false;
      }
      if (exception instanceof SSLException) {// SSL握手异常
      return false;
      }


      HttpClientContext clientContext = HttpClientContext
      .adapt(context);
      HttpRequest request = clientContext.getRequest();
      // 如果请求是幂等的,就再次尝试
      return request == null;
      };


      return HttpClients.custom()
      .setConnectionManager(cm)
      .setRetryHandler(httpRequestRetryHandler).build();
          }
      }



      通过控制变量法设置实验,根据博主 程序漫漫 的建议,对系统部分模块的效率的测试应保证另一个模块的速度恒定,所以将 Netty数据中台->HBase Cluster 这一部分的耗时设置了恒定不变的常量,防止影响前段 Http 请求的测试。

      程序漫漫

      你好我叫孙策,公众号:程序漫漫游戏服务器入门



      四.结论


      在24个文件并发上传状态下 每次请求携带5000条数据:

      HttpClient连接池效率

      HttpURLConnctiont效率


      实验结果可以大概表明,HttpClient连接池模式下连接更加稳定且效率高于HttpUrlConnct。




      文章转载自码农智涵的程序人生,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

      评论