1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        記一次Apache HTTP Client問題排查

        共 10999字,需瀏覽 22分鐘

         ·

        2023-08-25 05:35

        你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

        你來,我們一起精進(jìn)!你不來,我和你的競爭對手一起精進(jìn)!

        編輯:業(yè)余草

        來源:juejin.cn/post/7260458223236775992

        推薦:https://t.zsxq.com/11sCBGccJ

        自律才能自由

        現(xiàn)象

        通過日志查看,存在兩種異常情況。

        第一種:開始的時(shí)候HTTP請求會報(bào)超時(shí)異常。

        ?

        762663363 [2023-07-21 06:04:25] [executor-64] ERROR - com.xxl.CucmTool - CucmTool|sendRisPortSoap error,url:https://10.65.0.173:8443/realtimeservice/services/RisPortorg.apache.http.conn.HttpHostConnectException: Connect to xxx [/xxx] failed: 連接超時(shí)

        ?

        第二種:突然沒有新的HTTP請求日志了,現(xiàn)象就是HTTP請求后,一直卡主,等待響應(yīng)。

        HTTP Client代碼

        先查看一下HTTP的請求代碼。

        HTTP Client設(shè)置。

        private static CloseableHttpClient getHttpClient() {
            SSLContextBuilder builder = new SSLContextBuilder();
            CloseableHttpClient httpClient = null;
            try {
                builder.loadTrustMaterial(nullnew TrustStrategy() {
                    @Override
                    public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                        return true;
                    }
                });
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build(),
                    SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
                httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
            } catch (Exception e) {
                log.error("getHttpClient error:{}", e.getMessage(), e);
            }
            return httpClient;
        }

        「請求方式(未設(shè)置http connection timeout 和 socket timeout)」

        HttpPost httpPost = new HttpPost(url);
        try(CloseableHttpResponse response = getHttpClient().execute(httpPost)) {
            HttpEntity httpEntity = response.getEntity();
            if (httpEntity != null) {
                System.out.println(EntityUtils.toString(httpEntity, "UTF-8"));
            }
        catch (Exception e) {
            log.error("test,url:" + url, e);
        }

        進(jìn)一步分析。

        連接超時(shí)

        通過本地debug先找到了socket連接處的代碼,如下所示。

        socket連接請求在java.net.Socket#connect(java.net.SocketAddress, int)這里。

        連接超時(shí)

        可以看到如果不設(shè)置connect timeout,在java層面默認(rèn)是無限超時(shí),那實(shí)際是要受系統(tǒng)層面影響的。我們都知道TCP建立連接的第一步是發(fā)送syn,實(shí)際這一步系統(tǒng)層面會有一些控制。

        Linux環(huán)境

        linux下通過net.ipv4.tcp_syn_retries控制sync的超時(shí)情況

        ?

        Number of times initial SYNs for an active TCP connection attempt will be retransmitted. Should not be higher than 127. Default value is 6, which corresponds to 63seconds till the last retransmission with the current initial RTO of 1second. With this the final timeout for an active TCP connection attempt will happen after 127seconds.

        ?

        默認(rèn)重試次數(shù)為6次,重試的間隔時(shí)間從1s開始每次都翻倍,6次的重試時(shí)間間隔為1s, 2s, 4s, 8s, 16s,32s, 總共63s,第6次發(fā)出后還要等64s都知道第6次也超時(shí)了,所以,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s + 64 = 127 s,TCP才會把斷開這個連接。
        第 1 次發(fā)送 SYN 報(bào)文后等待 1s(2 的 0 次冪),如果超時(shí),則重試
        第 2 次發(fā)送后等待 2s(2 的 1 次冪),如果超時(shí),則重試
        第 3 次發(fā)送后等待 4s(2 的 2 次冪),如果超時(shí),則重試
        第 4 次發(fā)送后等待 8s(2 的 3 次冪),如果超時(shí),則重試
        第 5 次發(fā)送后等待 16s(2 的 4 次冪),如果超時(shí),則重試
        第 6 次發(fā)送后等待 32s(2 的 5 次冪),如果超時(shí),則重試
        第 7 次發(fā)送后等待 64s(2 的 6 次冪),如果超時(shí),則斷開這個連接。

        mac環(huán)境

        mac場景下是通過net.inet.tcp.keepinit參數(shù)控制syn超時(shí)(默認(rèn)是75s)。
        可以通過下面的命令查看

        sysctl -A |grep net.inet.tcp.keepinit

        net.inet.tcp.keepinit: 75000。

        通過telnet驗(yàn)證,確實(shí)是75s超時(shí)。

        telnet驗(yàn)證

        tcpdump抓包也可以看到一直進(jìn)行syn重試。

        tcpdump抓包

        讀取超時(shí)

        Apache Http Client 默認(rèn)的socket read timeout 是0。

        Apache Http Client
        socket read timeout

        通過代碼注釋可以看到,如果soTimeout是0的話,就意味著讀取超時(shí)不受限制,但是實(shí)際上這里也會有系統(tǒng)層面的控制,下面從HTTP層面和TCP層面做一下分析。

        HTTP Keep-alive

        首先,Apache httpClient 4.3版本中,如果請求中未做制定,那么默認(rèn)會使用HTTP 1.1,代碼如下。

        public static ProtocolVersion getVersion(HttpParams params) {
            Args.notNull(params, "HTTP parameters");
            Object param = params.getParameter("http.protocol.version");
            return (ProtocolVersion)(param == null ? HttpVersion.HTTP_1_1 : (ProtocolVersion)param);
        }

        對于HTTP 1.1版本來說,默認(rèn)會開啟Keep-alive,并使用默認(rèn)的keep-alive策略。

        public class DefaultConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {

            public static final DefaultConnectionKeepAliveStrategy INSTANCE = new DefaultConnectionKeepAliveStrategy();

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

        }

        其基本原理就是HTTP場景下,當(dāng)客戶端與服務(wù)端建立TCP連接以后,Httpd守護(hù)進(jìn)程會通過keep-alive timeout時(shí)間設(shè)置參數(shù),一個http產(chǎn)生的tcp連接在傳送完最后一個響應(yīng)后,如果守護(hù)進(jìn)程在這個keepalive_timeout里,一直沒有收到瀏覽器發(fā)過來http請求,則關(guān)閉這個http連接。
        這里有兩點(diǎn)要注意:

        • 可以看到keep-alive的超時(shí)時(shí)間是服務(wù)端返回時(shí),http client在響應(yīng)頭中解析到的。如果一直未收到服務(wù)端響應(yīng),那么客戶端會認(rèn)為keep-alive一直有效;-1的返回值也是如此。
        • 如果服務(wù)端有響應(yīng),如果服務(wù)端有響應(yīng),那么就會按照服務(wù)端的返回設(shè)置keep-alive的timeout,當(dāng)timeout到期后,就會從http client pool中移除,服務(wù)端關(guān)閉該TCP連接。

        下面是一個成功的HTTP client響應(yīng)信息,可以看到服務(wù)端給出的keep-alive時(shí)間是60s。

        HTTP client響應(yīng)信息
        keep-alive

        后續(xù)對于這個連接不做任何處理,可以看到60s以后斷開了連接。

        斷開連接

        TCP下的keep-alive機(jī)制

        TCP連接建立之后,如果某一方一直不發(fā)送數(shù)據(jù),或者隔很長時(shí)間才發(fā)送一次數(shù)據(jù),當(dāng)連接很久沒有數(shù)據(jù)報(bào)文傳輸時(shí)如何去確定對方還在線,到底是掉線了還是確實(shí)沒有數(shù)據(jù)傳輸,連接還需不需要保持,這種情況在TCP協(xié)議設(shè)計(jì)中keep-alive的目的。
        TCP協(xié)議中,當(dāng)超過一段時(shí)間之后,TCP自動發(fā)送一個數(shù)據(jù)為空的報(bào)文(偵測包)給對方,如果對方回應(yīng)了這個報(bào)文,說明對方還在線,連接可以繼續(xù)保持,如果對方?jīng)]有報(bào)文返回,并且重試了多次之后則認(rèn)為連接丟失,沒有必要保持連接。
        在Linux系統(tǒng)中有以下配置用于TCP的keep-alive。

        ?

        tcp_keepalive_time=7200:表示?;顣r(shí)間是 7200 秒(2小時(shí)),也就 2 小時(shí)內(nèi)如果沒有任何連接相關(guān)的活動,則會啟動保活機(jī)制
        tcp_keepalive_intvl=75:表示每次檢測間隔 75 秒;
        tcp_keepalive_probes=9:表示檢測 9 次無響應(yīng),認(rèn)為對方是不可達(dá)的,從而中斷本次的連接。
        也就是說在 Linux 系統(tǒng)中,最少需要經(jīng)過 2 小時(shí) 11 分 15 秒才可以發(fā)現(xiàn)一個「死亡」連接。

        ?

        在MAC下對應(yīng)的配置如下(單位為ms)

        ?

        net.inet.tcp.keepidle: 7200000
        net.inet.tcp.keepintvl: 75000
        net.inet.tcp.keepcnt: 3

        ?

        也就是說在Mac系統(tǒng)中,最少經(jīng)過2小時(shí)3分鐘45秒才可以發(fā)現(xiàn)一個「死亡」連接。

        對于TCP的keep-alive是默認(rèn)關(guān)閉的,可以通過應(yīng)用層面打開。
        對于Java應(yīng)用程序,默認(rèn)是關(guān)閉的,后面我們模擬在客戶端開啟該配置。

        ?

        public static final SocketOption SO_KEEPALIVE Keep connection alive. The value of this socket option is a Boolean that represents whether the option is enabled or disabled. When the SO_KEEPALIVE option is enabled the operating system may use a keep-alive mechanism to periodically probe the other end of a connection when the connection is otherwise idle. The exact semantics of the keep alive mechanism is system dependent and therefore unspecified. The initial value of this socket option is FALSE. The socket option may be enabled or disabled at any time.

        ?

        首先,修改mac的keep-alive設(shè)置,將時(shí)間調(diào)短一些。

        sysctl -w net.inet.tcp.keepidle=60000 net.inet.tcp.keepcnt=3 net.inet.tcp.keepintvl=10000 
        ?

        net.inet.tcp.keepidle: 60000
        net.inet.tcp.keepintvl: 10000
        net.inet.tcp.keepcnt: 3

        ?

        依然通過HTTP Client開啟keep alive配置

        SocketConfig socketConfig = SocketConfig.DEFAULT;
        SocketConfig keepAliveConfig = SocketConfig.copy(socketConfig).setSoKeepAlive(true).build();
        httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).setDefaultSocketConfig(keepAliveConfig).build();

        通過HTTP Client請求服務(wù)端一個耗時(shí)很長的接口,并通過TCP抓包可以看到以下內(nèi)容
        每隔60s,客戶端會向服務(wù)端發(fā)送?;畹倪B接。

        ?;钸B接

        再來驗(yàn)證一下,如果服務(wù)端此時(shí)不可用的情況。

        使用pfctl工具,模擬服務(wù)端不可達(dá)。

        可以看到客戶端每隔10s,累計(jì)嘗試3次,然后就會關(guān)閉該連接。

        服務(wù)端不可達(dá)
        累計(jì)嘗試3次
        關(guān)閉連接

        回歸問題

        連接超時(shí)問題

        此時(shí)服務(wù)器因?yàn)閭€別原因,無法正常連接。
        由于HTTP Client未設(shè)置對應(yīng)的超時(shí)時(shí)間,所以會依據(jù)系統(tǒng)的net.ipv4.tcp_syn_retries進(jìn)行重試。
        該異??蛻舳丝梢愿兄健?/p>

        請求卡主問題

        當(dāng)某個時(shí)間HTTP Client與服務(wù)器建立的正常的TCP連接后,服務(wù)器發(fā)生了異常,此時(shí)由于以下原因疊加

        • HTTP Client未設(shè)置socket讀取超時(shí)時(shí)間
        • HTTP keep-alive也由于服務(wù)端未響應(yīng)默認(rèn)不受限制
        • 另外TCP層面的keep alive也沒有手動開啟

        所以此時(shí)客戶端會一直持有該TCP連接等待服務(wù)器響應(yīng)。對應(yīng)到下圖的話,也就是橙色部分。

        持有TCP連接

        當(dāng)然最直接的解決方案就是設(shè)置socket read timeout時(shí)間即可。

        RequestConfig requestConfig = RequestConfig.custom()
            .setConnectionRequestTimeout(1000)
            .setSocketTimeout(1000)
            .setConnectTimeout(1000).build();
        httpPost.setConfig(requestConfig);

        當(dāng)時(shí)間到了會報(bào)read timeout 異常。

        read timeout 異常

        總結(jié)

        • 當(dāng)我們使用HTTP Client的時(shí)候,需要結(jié)合業(yè)務(wù)需要合理設(shè)置connect timeout和 socket timeout參數(shù)。
        • 當(dāng)進(jìn)行問題追蹤時(shí),需要利用HTTP和TCP的一些知識,以及tcpdump等抓包工具進(jìn)行問題驗(yàn)證。

        瀏覽 770
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            成人毛片免费视频 | 国产综合视频在线观看 | 特黄一区| 欧美一区二区三区大屁股撅起来 | 神马午夜久久 | 亚洲伦理一区二区三区 | 波多野结衣国产区42部 | 热精品视频 | 亚洲色图欧美日韩 | 亚洲视频免费在线观看 |