httpclient的三个超时参数分析

71 阅读2分钟

背景

关于httpclient的三个超时参数,之前都有接触过,但还是停留在概念层面。最近恰好遇到个相关的问题,顺便做个验证,加深对这三个参数的理解。 简单描述下遇到的问题:项目某个调用依赖,测试环境未出现异常,但是上线偶现,实质上就是httpclient的超时设置线下、线上存在差别。

说明

示例使用java开发,引入的httpclient是apache版本。具体如下。

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.4</version>
        </dependency>

准备工作

先准备一个用来测试的client,后续实验中会修改对应的超时参数。还需要一个测试例子。而测试的地址,使用了苹果开发获取公钥的https://appleid.apple.com/auth/keys。具体原因是这个地址访问稳定,而且耗时较长,有正常的返回。下面是本地ping的结果,可以看到基本稳定在210ms左右,所以非常适合用来做超时验证。

$ ping appleid.apple.com
PING appleid.apple.com.akadns.net (17.141.5.102) 56(84) bytes of data.
64 bytes from appleid-prn-s.apple.com (17.141.5.102): icmp_seq=1 ttl=41 time=210 ms
64 bytes from appleid-prn-s.apple.com (17.141.5.102): icmp_seq=2 ttl=41 time=211 ms
64 bytes from appleid-prn-s.apple.com (17.141.5.102): icmp_seq=3 ttl=41 time=210 ms
64 bytes from appleid-prn-s.apple.com (17.141.5.102): icmp_seq=4 ttl=41 time=210 ms
64 bytes from appleid-prn-s.apple.com (17.141.5.102): icmp_seq=5 ttl=41 time=210 ms
64 bytes from appleid-prn-s.apple.com (17.141.5.102): icmp_seq=6 ttl=41 time=210 ms
64 bytes from appleid-prn-s.apple.com (17.141.5.102): icmp_seq=7 ttl=41 time=210 ms
  1. 测试代码
import org.junit.Test;

import java.util.concurrent.TimeUnit;

public class HttpClientTest {
    String url = "https://appleid.apple.com/auth/keys";

    @Test
    public void runTest() {
        for (int i = 0; i < 20; i++) {
            String msg = "";
            try {
                HttpClientUtil.get(url);
                TimeUnit.SECONDS.sleep(1); //避免被限流,隔一秒请求!!
            } catch (Exception e) {
                msg = e.getMessage();
            }
            System.out.println(String.format("id: %s\texp:%s", i, msg));
        }
    }
}

  1. client
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class HttpClientUtil {
    private static PoolingHttpClientConnectionManager cm;
    private static RequestConfig requestConfig;

    static {
        cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(1);
        cm.setDefaultMaxPerRoute(2);
        requestConfig = RequestConfig.custom()
                //修改这几个参数来验证
                .setConnectTimeout(10000)
                .setConnectionRequestTimeout(1)
                .setSocketTimeout(10000)
                .build();
    }

    private static CloseableHttpClient getClient() {
        return HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(requestConfig).build();
    }

    public static String get(String url) throws IOException {
        CloseableHttpClient httpClient = getClient();
        HttpGet httpGet = new HttpGet(url);
        CloseableHttpResponse response = httpClient.execute(httpGet);
        if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
            if (response.getEntity() != null) {
                return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            }
        }
        return String.format("code:%s", response.getStatusLine().getStatusCode());
    }
}

验证

思路是分别设置不同的参数值,保证每次只有一种情况出现超时,再通过具体的报错信息和堆栈,理清实际的超时判断逻辑。 以下分别是三个参数的验证过程

connectTimeout

设置该值为远小于ping时长的值,运行试例后得到验证结果,全部是 org.apache.http.conn.ConnectTimeoutException: Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out。通过堆栈和异常结果,可知这个参数用来控制建立连接的超时。

 requestConfig = RequestConfig.custom()
                .setConnectTimeout(100)  //100ms < 210ms
                .setConnectionRequestTimeout(10000)
                .setSocketTimeout(10000)
                .build();

结果为全部connect timed out

id: 0	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 1	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 2	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 3	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 4	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 5	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 6	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 7	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 8	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 9	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 10	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 11	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 12	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 13	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 14	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 15	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 16	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 17	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 18	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out
id: 19	exp:Connect to appleid.apple.com:443 [appleid.apple.com/17.141.5.102] failed: connect timed out

socketTimeout

将该值设置为100,其他较高。 报错全部为, java.net.SocketTimeoutException: Read timed out,debug从堆栈看出超时在读取阶段。

        requestConfig = RequestConfig.custom()
                .setConnectTimeout(10000)
                .setConnectionRequestTimeout(10000)
                .setSocketTimeout(100)
                .build();

再将socketTimeout 设置为220,可以看到是部分失败。

        requestConfig = RequestConfig.custom()
                .setConnectTimeout(10000)
                .setConnectionRequestTimeout(10000)
                .setSocketTimeout(220)
                .build();

结果:

id: 0	exp:
id: 1	exp:
id: 2	exp:Read timed out
id: 3	exp:Read timed out
id: 4	exp:
id: 5	exp:Read timed out
id: 6	exp:Read timed out
id: 7	exp:Read timed out
id: 8	exp:Read timed out
id: 9	exp:Read timed out
id: 10	exp:
id: 11	exp:
id: 12	exp:Read timed out
id: 13	exp:
id: 14	exp:
id: 15	exp:
id: 16	exp:
id: 17	exp:
id: 18	exp:
id: 19	exp:

connectionRequestTimeout

将该参数设置为如下的值:

  requestConfig = RequestConfig.custom()
                .setConnectTimeout(10000)
                .setConnectionRequestTimeout(1)
                .setSocketTimeout(10000)
                .build();

如果还是上面的测试代码,是没有问题可以正常运行的。原因是虽然连接池中只有一个链接,但是依次运行,连接用完就释放了,不会抛出connectionRequestTimeout的异常。所以这次需要改下测试代码,如下:

    String url = "https://appleid.apple.com/auth/keys";

    @Test
    public void runTest() throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            String msg = "";
            try {

//                TimeUnit.SECONDS.sleep(1); //避免被限流,隔一秒请求!!
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            HttpClientUtil.get(url);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }).start();

            } catch (Exception e) {
                msg = e.getMessage();
            }
            System.out.println(String.format("id: %s\texp:%s", i, msg));
        }

        Thread.currentThread().join();
    }

放在多线程里,让产生竞争态,一个连接没有使用完毕,再次尝试从连接池获取的时候,就会超过设置的ConnectionRequestTimeout时间,所以这个参数控制从连接池获取连接。再查看堆栈,确认了结论是正确的。

         final int timeout = config.getConnectionRequestTimeout();
            managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);

错误堆栈:

org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.leaseConnection(PoolingHttpClientConnectionManager.java:286)
	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager$1.get(PoolingHttpClientConnectionManager.java:263)
	at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:190)
	at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184)
	at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88)
	at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
	at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:107)

总结

以上就是所有的验证过程,最后说下结论:

  1. ConnectTimeout 控制建立连接,超时抛出 org.apache.http.conn.ConnectTimeoutException 异常。
  2. ConnectionRequestTimeout 控制从连接池获取连接,超时抛出 org.apache.http.conn.ConnectionPoolTimeoutException 异常。
  3. SocketTimeout 控制从连接读取数据,超时抛出 java.net.SocketTimeoutException异常。

具体的其实可以在对应代码的注释中得到,试验会更直观和方便理解。

    /**
     * Returns the timeout in milliseconds used when requesting a connection
     * from the connection manager. A timeout value of zero is interpreted
     * as an infinite timeout.
     * <p>
     * A timeout value of zero is interpreted as an infinite timeout.
     * A negative value is interpreted as undefined (system default).
     * </p>
     * <p>
     * Default: {@code -1}
     * </p>
     */
    public int getConnectionRequestTimeout() {
        return connectionRequestTimeout;
    }
    
     /**
         * Determines the timeout in milliseconds until a connection is established.
         * A timeout value of zero is interpreted as an infinite timeout.
         * <p>
         * A timeout value of zero is interpreted as an infinite timeout.
         * A negative value is interpreted as undefined (system default).
         * </p>
         * <p>
         * Default: {@code -1}
         * </p>
         */
        public int getConnectTimeout() {
            return connectTimeout;
        }
    
        /**
         * Defines the socket timeout ({@code SO_TIMEOUT}) in milliseconds,
         * which is the timeout for waiting for data  or, put differently,
         * a maximum period inactivity between two consecutive data packets).
         * <p>
         * A timeout value of zero is interpreted as an infinite timeout.
         * A negative value is interpreted as undefined (system default).
         * </p>
         * <p>
         * Default: {@code -1}
         * </p>
         */
        public int getSocketTimeout() {
            return socketTimeout;
        }