Http调用篇

1. 超时

对于 HTTP 调用,虽然应用层走的是 HTTP 协议,但网络层面始终是 TCP/IP 协议。TCP/IP 是面向连接的协议,在传输数据之前需要建立连接。几乎所有的网络框架都会提供这么两个超时参数。

1.1 连接超时

连接超时参数( ConnectTimeout): 让用户配置建连阶段的最长等待时间

误区

  1. 连接超时配置得特别长,比如 60 秒。

    一般而言,TCP三次握手的时间非常的短,如果很久没有建立连接,很可能是网络或防火墙的问题。如果几秒连接不上,可能永远也连接不上。所以连接超时时间设置特别长意义不大,1~5s即可。

  2. 排查连接超时问题,却没理清连的是哪里。

    一般而言,服务端会有很多个节点,如果通过客户端负载均衡,那么是直接与服务端建立连接,如果服务端是通过Nginx的反向代理来负载均衡,那么是与Nginx建立连接。

  • 直接连接服务端(排查服务端的问题)
  • 连接Nginx(排查Nginx的问题)

1.2 读取超时

读取超时参数(ReadTimeout): 用来控制从 Socket 上读取数据的最长等待时间。

误区

  1. 出现了读取超时,服务端的执行就会中断。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    @RestController
    @RequestMapping("clientreadtimeout")
    @Slf4j
    public class ClientReadTimeoutController {

    private String getResponse(String url, int connectTimeout, int readTimeout) throws IOException {
    return Request.Get("http://localhost:8080/clientreadtimeout" + url)
    .connectTimeout(connectTimeout)
    .socketTimeout(readTimeout)
    .execute()
    .returnContent()
    .asString();
    }

    @GetMapping("client")
    public String client() throws IOException {
    log.info("client1 called");
    //服务端5s超时,客户端读取超时2秒
    return getResponse("/server?timeout=5000", 1000, 2000);
    }

    @GetMapping("server")
    public void server(@RequestParam("timeout") int timeout) throws InterruptedException {
    log.info("server called");
    TimeUnit.MILLISECONDS.sleep(timeout);
    log.info("Done");
    }
    }

    调用 client 接口后,从日志中可以看到,客户端 2 秒后出现了 SocketTimeoutException,原因是读取超时,服务端却丝毫没受影响在 3 秒后执行完成。

    类似 Tomcat 的 Web 服务器都是把服务端请求提交到线程池处理的,只要服务端收到了请求,网络层面的超时和断开便不会影响服务端的执行。因此,出现读取超时不能随意假设服务端的处理情况,需要根据业务状态考虑如何进行后续处理。

  2. 认为读取超时只是 Socket 网络层面的概念,是数据传输的最长耗时,故将其配置得非常短,比如 100 毫秒。

    确切地说,读取超时指的是,向 Socket 写入数据后,我们等到 Socket 返回数据的超时时间,其中包含的时间或者说绝大部分的时间,是服务端处理业务逻辑的时间

  3. 认为超时时间越长任务接口成功率就越高,将读取超时参数配置得太长。

    HTTP请求一般是需要获得结果,属于同步调用。当服务端处理时间过长,客户端的线程(Tomcat线程)一直处于等待状态,当出现大量超时时,并发情况下可能会创建大量的线程,最终导致程序崩溃。我们应该设置一个较短的读取超时时间,以防止被下游服务拖慢,通常不会设置超过 30 秒的读取超时。

    对定时任务或异步任务来说,读取超时配置得长些问题不大

1.3 Feign 和 Ribbon 配合使用,你知道怎么配置超时吗?

为Feign 配置超时参数比较复杂,为 Feign 配置超时参数的复杂之处在于,Feign 自己有两个超时参数,它使用的负载均衡组件 Ribbon 本身还有相关配置。

结论:

结论一:Feign和Ribbon都不设置,默认情况下取Ribbon的读取超时 1 秒,如此短的读取超时算是坑点一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RibbonClientConfiguration {
// ...
/**
* Ribbon client default connect timeout.
*/
public static final int DEFAULT_CONNECT_TIMEOUT = 1000;

/**
* Ribbon client default read timeout.
*/
public static final int DEFAULT_READ_TIMEOUT = 1000;
@Bean
@ConditionalOnMissingBean
public IClientConfig ribbonClientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.loadProperties(this.name);
config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
return config;
}
}
结论二:如果要配置 Feign 的读取超时,就必须同时配置连接超时,才能生效。(==我看源码的时候已经修复了==)
1
2
3
4
if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
builder.options(new Request.Options(config.getConnectTimeout(),
config.getReadTimeout()));
}

==Feign创建Request时,如果获取连接超时时间或者读取超时时间未配置会取默认值。==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
// 其中创建Options时默认连接超时时间为10s,读取超时时间为60s
private int readTimeoutMillis = new Request.Options().readTimeoutMillis();
private int connectTimeoutMillis = new Request.Options().connectTimeoutMillis();

protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config,
Feign.Builder builder){
connectTimeoutMillis = config.getConnectTimeout() != null
? config.getConnectTimeout() : connectTimeoutMillis;
readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout()
: readTimeoutMillis;

builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS,
readTimeoutMillis, TimeUnit.MILLISECONDS, true));
}
}
结论三:单独的超时可以覆盖全局超时,这符合预期。

对单独的 Feign Client 设置超时时间,可以把 default 替换为 Client 的 name:

1
2
3
4
feign.client.config.default.readTimeout=3000
feign.client.config.default.connectTimeout=3000
feign.client.config.clientsdk.readTimeout=2000
feign.client.config.clientsdk.connectTimeout=2000
结论四:除了可以配置 Feign,也可以配置 Ribbon 组件的参数来修改两个超时时间。

==注意==:首字母需大写

1
2
ribbon.ReadTimeout=4000
ribbon.ConnectTimeout=4000
结论五:同时配置 Feign 和 Ribbon 的超时,以 Feign 为准。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
feign.client.config.default.readTimeout=3000
feign.client.config.default.connectTimeout=3000
ribbon.ReadTimeout=4000
ribbon.ConnectTimeout=4000
执行耗时:3006ms 错误:Read timed out executing POST http://clientsdk/feignandribbon/server
plaintext
public class LoadBalancerFeignClient implements Client {
IClientConfig getClientConfig(Request.Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) { // false
requestConfig = this.clientFactory.getClientConfig(clientName);
}
else {
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}
}

2. 重试

2.1 Ribbon 会自动重试请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// DefaultClientConfigImpl
// 同一个服务其他实例的最大重试次数,不包括第一次调用的实例。默认值为1
public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1;
// 同一实例最大重试次数,不包括首次调用。默认值为0
public static final int DEFAULT_MAX_AUTO_RETRIES = 0;
// 是否所有操作都允许重试。默认值为false
public static final Boolean DEFAULT_OK_TO_RETRY_ON_ALL_OPERATIONS = Boolean.FALSE;

// RibbonLoadBalancedRetryPolicy
public boolean canRetry(LoadBalancedRetryContext context) {

HttpMethod method = context.getRequest().getMethod();
return HttpMethod.GET == method || lbContext.isOkToRetryOnAllOperations();
}

@Override
public boolean canRetrySameServer(LoadBalancedRetryContext context) {

return sameServerCount < lbContext.getRetryHandler().getMaxRetriesOnSameServer()
&& canRetry(context);

}

@Override
public boolean canRetryNextServer(LoadBalancedRetryContext context) {

// this will be called after a failure occurs and we increment the counter
// so we check that the count is less than or equals to too make sure
// we try the next server the right number of times
return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer()
&& canRetry(context);
}

3. 并发

defaultMaxPerRoute=2,也就是同一个主机 / 域名的最大并发请求数为 2

4. 项目

image-20231015160832329