Okhttp3、http2多路复用POST请求高峰负载时响应时间长

Posted

技术标签:

【中文标题】Okhttp3、http2多路复用POST请求高峰负载时响应时间长【英文标题】:Okhttp3, http2 multiplexing POST requests high response time at peak load time 【发布时间】:2019-07-17 21:31:47 【问题描述】:

我的应用程序将向我的 tomcat 服务器(启用 http/2)发送大约 1000 个 POST 请求/分钟,这将轮询给定的 url 并返回 html 和响应时间,我想实现真正的 http/2 多路复用在我的应用程序和 tomcat 服务器之间重用 tcp 连接。我的客户端使用 okhttp,我可以成功建立连接并长时间重复使用它,但是当请求计数增加时(例如,排队:50,运行:50),响应时间也会增加(它飙升到 2000 毫秒- 15000ms 甚至更糟,通常需要 300ms-500ms)。我可以理解这是因为 tcp 连接过载过多的请求,所以我决定打开多个 tcp 连接并允许它在 tcp 连接之间分配请求负载。我强制客户端使用

打开多个连接
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(100);
dispatcher.setMaxRequestsPerHost(5);
ConnectionPool cp = new ConnectionPool(5, 5, TimeUnit.MINUTES);

在wireshark的帮助下,我可以看到打开了5个连接,也看到了第一次握手成功后立即关闭了4个请求,我不知道这是否是http/2多路复用的预期行为。

如果这是预期的行为,我该如何优化它以减少每个请求在峰值负载时间的响应时间?

或者是否可以在负载高峰期使用多个连接来分配负载?

我的示例程序如下,

Security.insertProviderAt(Conscrypt.newProvider(), 1);
sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(null, new TrustManager[] 
    new X509TrustManager() 
        @Override
        public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException 
        

        @Override
        public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException 
        

        @Override
        public java.security.cert.X509Certificate[] getAcceptedIssuers() 
            return new java.security.cert.X509Certificate[];
        
           
, new java.security.SecureRandom());
sslSocketFactory = sslContext.getSocketFactory();
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(100);
dispatcher.setMaxRequestsPerHost(5);
ConnectionPool cp = new ConnectionPool(5, 1, TimeUnit.DAYS);

okHttpClient = new OkHttpClient().newBuilder()
.sslSocketFactory(sslSocketFactory, (X509TrustManager)trustManager[0])
.dispatcher(dispatcher)
.connectionPool(cp)
.hostnameVerifier(new HostnameVerifier() 
    @Override
    public boolean verify(String hostname, SSLSession session) 
        return true;
    
)
.build();

我们使用 conscryt 在 jdk8 本身中启用 ALPN,

try 
    String url = "https://localhost:8081/myAent/getOutput?1000000004286_1520339351002_"+System.currentTimeMillis();
    String json = "<?xml version=\"1.0\" standalone=\"no\"?><UC mid=\"1000000005011\" pollNow=\"true\" use_ipv6=\"false\" rca=\"0\" rcaFN=\"P|TA|D|J|M\" pFtct=\"\" sFtct=\"\" issecondarydc=\"false\" ssDc=\"false\" nocache=\"1\" storeHtmlResp=\"true\" storeTroubleScrnSht=\"false\" moConfig=\"false\" xconlen=\"8265\"  noScreenshotRecheckForSSLErrors=\"true\" avgDnsTime=\"null\" isProxyRequired=\"false\" userroles=\"EVAL_USER\" uid=\"102030230293029021\" retryDelay=\"2\" retry=\"true\" idcLocUrl=\"http://localhost:8080/app/receivemultipartdata\" isRemoteAgent=\"true\" sendHeaders=\"false\" api=\"ab_345nnn4l4lj4lk23nl4k23342lk4l23j4\" ut=\""+(System.currentTimeMillis()+"")+"\" mt=\"URL\" dctimeout=\"70\" pollinterval=\"1440\" locid=\"48\" log=\"1\" currentstatus=\"1\" postUrl=\"https://example.com\"><Url acc=\"\" forced_ips=\"\" use_ipv6=\"false\" client_cert=\"\" mid=\"1000000005011\" sotimeout=\"60\" ua=\"\" ds=\"117.20.43.94\" ucc=\"\" md=\"false\" client_cert_pass=\"\" context=\"default\" unavail_alert=\"\" ssl_protocol=\"\" avail_alert=\"\" enabledns=\"false\" enableBouncyCastle=\"false\" cc=\"false\" a=\"https://www.example.com/tools.html\" upStatusCodes=\"\" regex_alert=\"\" probeproxy=\"false\" m=\"G\" keyword_case=\"0\" regex=\"\" rbc=\"\" t=\"30\" lc=\"English\"><PD></PD><CH hn=\"\" hd=\"_sep_\" hv=\"\"/><AI ps=\"\" un=\"\"/></Url></UC>";
    RequestBody body = RequestBody.create(MediaType.get("application/json; charset=utf-8"), json);
    Request request = new Request.Builder()
        .url(url)
        .post(body)
        .build();
    long nanoStartTime = System.nanoTime();
    okHttpClient.newCall(request).enqueue(new Callback() 
        @Override 
        public void onFailure(Call call, IOException e) 
            System.out.println("okhttp3:: Request failed"+ e);
        

        @Override
        public void onResponse(Call call, okhttp3.Response responseObj) throws IOException 
            try (ResponseBody body = responseObj.body()) 
                long nanoEndTime = System.nanoTime();
                long nanoDiffTime = TimeUnit.NANOSECONDS.toMillis(nanoEndTime - nanoStartTime);
                System.out.println("okhttp3:: Succeded response ***"+body+"$$$");
                System.out.println("okhttp3:: Request Succeded protocol ***"+responseObj.protocol()+"$$$, time is "+nanoDiffTime);
            
        
    );

 catch (Exception e) 
    // TODO Auto-generated catch block
    e.printStackTrace();

如何优化 okhttpclient 以使用多个 tcp 套接字/连接来实现 http/2 多路复用以分配请求负载。

客户: Tomcat Apache - 9.0.x, JDK-8, Http 库 - Okhttp3, 操作系统 - Ubuntu/Centos, 安全提供程序 - Conscrypt(用于在 jdk 8 中支持 ALPN)。

服务器: Tomcat阿帕奇 - 9.0.16, JDK -10.0.1, 操作系统 - Ubuntu/Centos, OpenSSL - 1.1.1a 支持 TLSv1.3

【问题讨论】:

【参考方案1】:

您看到的是 OkHttp 中连接合并的结果。 OkHttp 不提前知道是否会建立 HTTP/2 连接(与 HTTP/1.1 相比),因此允许连接的 setMaxRequestsPerHost 值继续进行。目前*没有客户端负载平衡,因此它们会迅速合并为一个连接,这就是您所看到的。

目前,您可以通过多个客户端或仔细管理完全不同的主机和连接来实现此目的。 n.b OkHttp 将在这里对您不利,并针对单个连接优于多个连接的典型情况进行优化,例如SSL 证书指定重叠的主题备用名称‎


*关注此问题以获得支持https://github.com/square/okhttp/issues/4530

【讨论】:

还有其他java客户端支持这个功能吗?喜欢 Apache httpcomponents5 还是 jetty? 你也可以配置 Tomcat 的 maxConcurrentStreams 并且 OkHttp 会支持它。 tomcat.apache.org/tomcat-8.5-doc/config/http2.html 文档解释说,一旦达到限制,它会抛出 STREAM_REFUSED 异常,无论是允许客户端在超过配置的限制后创建新连接,还是只创建一个具有配置限制的连接并拒绝接受任何更多的连接或并发请求(这会在发送请求时造成延迟,使响应时间变差)??

以上是关于Okhttp3、http2多路复用POST请求高峰负载时响应时间长的主要内容,如果未能解决你的问题,请参考以下文章

什么是http2的多路复用

简单讲解一下 http2 的多路复用

简单讲解一下 http2 的多路复用

HTTP2多路复用指什么

白话http2的多路复用

HTTP2特性优点解析