Http调用篇

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
image-20231015160832329

0%