现象

经过日志检查,存在两种反常状况。
第一种:开端的时分HTTP恳求会报超时反常。

762663363 [2023-07-21 06:04:25] [executor-64] ERROR – com.xxl.CucmTool – CucmTool|sendRisPortSoap error,url:https://xxxxxx/realtimeservice/services/RisPort org.apache.http.conn.HttpHostConnectException: Connect to xxx [/xxx] failed: 衔接超时

第二种:突然没有新的HTTP恳求日志了,现象便是HTTP恳求后,一向卡主,等候呼应。

HTTP Client代码

先检查一下HTTP的恳求代码
HTTP Client设置

private static CloseableHttpClient getHttpClient() {
    SSLContextBuilder builder = new SSLContextBuilder();
    CloseableHttpClient httpClient = null;
    try {
        builder.loadTrustMaterial(null, new 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;
}

恳求方法(未设置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);
}

进一步剖析

衔接超时

经过本地debug先找到了socket衔接处的代码,如下所示。
socket衔接恳求在java.net.Socket#connect(java.net.SocketAddress, int)这儿

记一次Apache HTTP Client问题排查

能够看到假如不设置connect timeout,在java层面默许是无限超时,那实践是要受体系层面影响的。我们都知道TCP树立衔接的第一步是发送syn,实践这一步体系层面会有一些操控。

Linux环境

linux下经过net.ipv4.tcp_syn_retries操控sync的超时状况

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.

默许重试次数为6次,重试的距离时刻从1s开端每次都翻倍,6次的重试时刻距离为1s, 2s, 4s, 8s, 16s,32s, 一共63s,第6次发出后还要等64s都知道第6次也超时了,所以,一共需求 1s + 2s + 4s+ 8s+ 16s + 32s + 64 = 127 s,TCP才会把断开这个衔接。
第 1 次发送 SYN 报文后等候 1s(2 的 0 次幂),假如超时,则重试
第 2 次发送后等候 2s(2 的 1 次幂),假如超时,则重试
第 3 次发送后等候 4s(2 的 2 次幂),假如超时,则重试
第 4 次发送后等候 8s(2 的 3 次幂),假如超时,则重试
第 5 次发送后等候 16s(2 的 4 次幂),假如超时,则重试
第 6 次发送后等候 32s(2 的 5 次幂),假如超时,则重试
第 7 次发送后等候 64s(2 的 6 次幂),假如超时,则断开这个衔接。

mac环境

mac场景下是经过net.inet.tcp.keepinit参数操控syn超时(默许是75s)。
能够经过下面的命令检查

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

net.inet.tcp.keepinit: 75000
经过telnet验证,确实是75s超时

记一次Apache HTTP Client问题排查
tcpdump抓包也能够看到一向进行syn重试。
记一次Apache HTTP Client问题排查

读取超时

Apache Http Client 默许的socket read timeout 是0。

记一次Apache HTTP Client问题排查
记一次Apache HTTP Client问题排查
经过代码注释能够看到,假如soTimeout是0的话,就意味着读取超时不受约束,可是实践上这儿也会有体系层面的操控,下面从HTTP层面和TCP层面做一下剖析。

HTTP Keep-alive

首先,Apache httpClient 4.3版别中,假如恳求中未做制定,那么默许会运用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版别来说,默许会敞开Keep-alive,并运用默许的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场景下,当客户端与服务端树立TCP衔接今后,Httpd看护进程会经过keep-alive timeout时刻设置参数,一个http发生的tcp衔接在传送完最后一个呼应后,假如看护进程在这个keepalive_timeout里,一向没有收到浏览器发过来http恳求,则封闭这个http衔接。
这儿有两点要注意:

  • 能够看到keep-alive的超时时刻是服务端回来时,http client在呼应头中解析到的。假如一向未收到服务端呼应,那么客户端会以为keep-alive一向有用;-1的回来值也是如此。
  • 假如服务端有呼应,假如服务端有呼应,那么就会依照服务端的回来设置keep-alive的timeout,当timeout到期后,就会从http client pool中移除,服务端封闭该TCP衔接。

下面是一个成功的HTTP client呼应信息,能够看到服务端给出的keep-alive时刻是60s。

记一次Apache HTTP Client问题排查
记一次Apache HTTP Client问题排查

后续关于这个衔接不做任何处理,能够看到60s今后断开了衔接。

记一次Apache HTTP Client问题排查

TCP下的keep-alive机制

TCP衔接树立之后,假如某一方一向不发送数据,或许隔很长时刻才发送一次数据,当衔接很久没有数据报文传输时如何去确定对方还在线,到底是掉线了还是确实没有数据传输,衔接还需不需求坚持,这种状况在TCP协议设计中keep-alive的意图。
TCP协议中,当超过一段时刻之后,TCP主动发送一个数据为空的报文(侦测包)给对方,假如对方回应了这个报文,阐明对方还在线,衔接能够持续坚持,假如对方没有报文回来,而且重试了多次之后则以为衔接丢掉,没有必要坚持衔接。
在Linux体系中有以下装备用于TCP的keep-alive。


tcp_keepalive_time=7200:表明保活时刻是 7200 秒(2小时),也就 2 小时内假如没有任何衔接相关的活动,则会发动保活机制
tcp_keepalive_intvl=75:表明每次检测距离 75 秒;
tcp_keepalive_probes=9:表明检测 9 次无呼应,以为对方是不可达的,然后中止本次的衔接。
也便是说在 Linux 体系中,最少需求经过 2 小时 11 分 15 秒才能够发现一个「逝世」衔接。

在MAC下对应的装备如下(单位为ms)


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

也便是说在Mac体系中,最少经过2小时3分钟45秒才能够发现一个「逝世」衔接。

关于TCP的keep-alive是默许封闭的,能够经过应用层面打开。
关于Java应用程序,默许是封闭的,后面我们模拟在客户端敞开该装备。

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设置,将时刻调短一些。

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恳求服务端一个耗时很长的接口,并经过TCP抓包能够看到以下内容
每隔60s,客户端会向服务端发送保活的衔接。

记一次Apache HTTP Client问题排查
再来验证一下,假如服务端此刻不可用的状况。
运用pfctl东西,模拟服务端不可达。
能够看到客户端每隔10s,累计尝试3次,然后就会封闭该衔接。

记一次Apache HTTP Client问题排查

记一次Apache HTTP Client问题排查

记一次Apache HTTP Client问题排查

回归问题

衔接超时问题

此刻服务器因为个别原因,无法正常衔接。
因为HTTP Client未设置对应的超时时刻,所以会依据体系的net.ipv4.tcp_syn_retries进行重试。
该反常客户端能够感知到。

恳求卡主问题

当某个时刻HTTP Client与服务器树立的正常的TCP衔接后,服务器发生了反常,此刻因为以下原因叠加

  • HTTP Client未设置socket读取超时时刻
  • HTTP keep-alive也因为服务端未呼应默许不受约束
  • 另外TCP层面的keep alive也没有手动敞开

所以此刻客户端会一向持有该TCP衔接等候服务器呼应。对应到下图的话,也便是橙色部分。

记一次Apache HTTP Client问题排查
当然最直接的解决方案便是设置socket read timeout时刻即可。

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

当时刻到了会报read timeout 反常。

记一次Apache HTTP Client问题排查

总结

  • 当我们运用HTTP Client的时分,需求结合业务需求合理设置connect timeout和 socket timeout参数。
  • 当进行问题追踪时,需求利用HTTP和TCP的一些常识,以及tcpdump等抓包东西进行问题验证。

参阅文档

【1】 一文说清楚 Linux TCP 内核参数_linux tcp参数_DBA大董的博客-CSDN博客
【2】 服务端挂了,客户端的 TCP 衔接还在吗?
【3】JAVA socket keep alive 阐明docs.oracle.com/javase/8/do…