Http调用篇
1. 超时
对于 HTTP 调用,虽然应用层走的是 HTTP 协议,但网络层面始终是 TCP/IP 协议。TCP/IP 是面向连接的协议,在传输数据之前需要建立连接。几乎所有的网络框架都会提供这么两个超时参数。
1.1 连接超时
连接超时参数( ConnectTimeout): 让用户配置建连阶段的最长等待时间
误区 :
连接超时配置得特别长,比如 60 秒。
一般而言,TCP三次握手的时间非常的短,如果很久没有建立连接,很可能是网络或防火墙的问题。如果几秒连接不上,可能永远也连接不上。所以连接超时时间设置特别长意义不大,1~5s即可。
排查连接超时问题,却没理清连的是哪里。
一般而言,服务端会有很多个节点,如果通过客户端负载均衡,那么是直接与服务端建立连接,如果服务端是通过Nginx的反向代理来负载均衡,那么是与Nginx建立连接。
直接连接服务端(排查服务端的问题)
连接Nginx(排查Nginx的问题)
1.2 读取超时
读取超时参数(ReadTimeout): 用来控制从 Socket 上读取数据的最长等待时间。
误区 :
出现了读取超时,服务端的执行就会中断。
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 服务器都是把服务端请求提交到线程池处理的,只要服务端收到了请求,网络层面的超时和断开便不会影响服务端的执行。因此,出现读取超时不能随意假设服务端的处理情况,需要根据业务状态考虑如何进行后续处理。
认为读取超时只是 Socket 网络层面的概念,是数据传输的最长耗时,故将其配置得非常短,比如 100 毫秒。
确切地说,读取超时指的是,向 Socket 写入数据后,我们等到 Socket 返回数据的超时时间,其中包含的时间或者说绝大部分的时间,是服务端处理业务逻辑的时间。
认为超时时间越长任务接口成功率就越高,将读取超时参数配置得太长。
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