Feign、Ribbon、Hystrix 接口超时时间配置分析
2024-10-28 08:52:07 # Technical # Notes

最近收到一个线上项目反馈,客户在调用我司的 openAPI 接口时频繁报请求超时的错误。去线上查看相关日志时确实发现很多 ERROR 日志:

1
2
3
4
BusinessClient#create(JSONObject) failed and no fallback available.

com.netflix.hystrix.exception.HystrixRuntimeException: BusinessClient#create(JSONObject) failed and no fallback available.
...

这些异常出现的原因是因为 BusinessClient#create 接口的响应太久导致 Ribbon 超时,从而触发 Hystrix 的熔断,所以第一时间便是去修改了相关的超时配置:

1
2
3
4
feign.hystrix.enabled = true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 600000
ribbon.ConnectTimeout = 1000
ribbon.ReadTimeout = 300000

本以为可以轻松解决,但事与愿违,修改配置后依然出现了超时异常,并且追踪日志发现超时的时间是 1 分钟,并不是配置中的时间。于是乎便有了以下对 Feign、Ribbon 和 Hystrix 超时时间的分析。

首先需要明确的是在 Spring Cloud 中,通常是将 Feign、Ribbon 和 Hystrix 组合使用。Feign 负责服务调用,Ribbon 提供负载均衡,Hystrix 负责熔断和容错

Feign 是一种声明式的 HTTP 客户端,它简化了服务间的调用过程。通过 Feign,开发者可以使用接口来定义服务,而不必直接编写 HTTP 请求的细节

Ribbon 是一个客户端负载均衡器,用于在微服务架构中实现服务的负载均衡。当多个服务实例存在时,Ribbon 会根据配置的负载均衡策略(如轮询、随机、权重等)自动选择一个可用的服务实例去执行请求

Hystrix 是 Netflix 开源的一个容错和延迟处理的库,主要用于防止单个服务故障影响整个系统。Hystrix 提供了熔断、降级、隔离等机制,能有效地提高服务的稳定性和容错性

Feign 的超时时间

先说明一个关键点,在使用 @FeignClient 声明一个 Feign 客户端时,如果未指明 url 属性值时,Feign 会通过注册中心查找指定服务进行请求,如果指明了 url,那么 Feign 仅作为一个 Http 客户端向指明的 url 发起请求

简而言之,言而总之,如果在 @FeignClient 中配置了 url 属性,Feign 就不会使用 Ribbon 来进行负载均衡,也就是 Ribbon 的超时时间不会生效

通过 @FeignClient 进入到 Feign 的源码,可以找到一个用来解析和配置 Feign 客户端属性的关键类

org.springframework.cloud.netflix.feign.FeignClientFactoryBean

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// 定义了一个 FeignClientFactoryBean 类,用于通过 Spring 的 FactoryBean 机制创建 Feign 客户端代理
@Data
@EqualsAndHashCode(callSuper = false)
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
ApplicationContextAware {
/***********************************
* WARNING! Nothing in this class should be @Autowired. It causes NPEs because of some lifecycle race condition.
***********************************/
// 类中注入的属性不能使用 @Autowired 注解,以避免 Spring 生命周期冲突导致的空指针异常

private Class<?> type; // 需要创建代理的接口类
private String name; // 服务名或客户端 ID,用于在注册中心查找服务
private String url; // Feign 客户端请求的 URL,如果设置则不依赖服务注册中心
private String path; // URL 路径的前缀
private boolean decode404; // 指示是否解码 404 响应(将 404 视为有效响应)
private ApplicationContext applicationContext; // Spring 应用上下文
private Class<?> fallback = void.class; // 降级处理类
private Class<?> fallbackFactory = void.class; // 降级工厂类

@Override
public void afterPropertiesSet() throws Exception {
// 初始化完成时校验 name 属性不为空
Assert.hasText(this.name, "Name must be set");
}


@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
// 设置 Spring 应用上下文
this.applicationContext = context;
}

protected Feign.Builder feign(FeignContext context) {
// 创建 Feign 客户端构建器,并配置日志、编码器、解码器、契约等基本组件
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.type);

// 获取 Feign.Builder 实例,配置必要的参数
Feign.Builder builder = get(context, Feign.Builder.class)
// required values
.logger(logger)
.encoder(get(context, Encoder.class))
.decoder(get(context, Decoder.class))
.contract(get(context, Contract.class));

// 配置可选的参数,例如日志级别、重试策略、错误解码器、请求选项等
Logger.Level level = getOptional(context, Logger.Level.class);
if (level != null) {
builder.logLevel(level);
}
Retryer retryer = getOptional(context, Retryer.class);
if (retryer != null) {
builder.retryer(retryer);
}
ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
if (errorDecoder != null) {
builder.errorDecoder(errorDecoder);
}
Request.Options options = getOptional(context, Request.Options.class);
if (options != null) {
builder.options(options);
}

// 获取当前服务的所有请求拦截器并添加到构建器
Map<String, RequestInterceptor> requestInterceptors = context.getInstances(
this.name, RequestInterceptor.class);
if (requestInterceptors != null) {
builder.requestInterceptors(requestInterceptors.values());
}

if (decode404) {
builder.decode404();
}

return builder;
}

// 获取指定类型的 Bean,如果不存在则抛出异常
protected <T> T get(FeignContext context, Class<T> type) {
T instance = context.getInstance(this.name, type);
if (instance == null) {
throw new IllegalStateException("No bean found of type " + type + " for "
+ this.name);
}
return instance;
}

// 获取指定类型的可选 Bean,如果不存在则返回 null
protected <T> T getOptional(FeignContext context, Class<T> type) {
return context.getInstance(this.name, type);
}

// 配置负载均衡并加载代理类,使用 Ribbon 的负载均衡机制请求目标服务实例
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}

// 如果未找到 Ribbon 负载均衡客户端,抛出异常
throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-ribbon?");
}

@Override
public Object getObject() throws Exception {
// 获取 Feign 上下文和构建器,并为客户端代理实例进行配置
FeignContext context = applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);

// 如果 url 未设置,通过服务名构建 URL,并通过负载均衡方式发起请求
if (!StringUtils.hasText(this.url)) {
String url;
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
}
else {
url = this.name;
}
url += cleanPath();
return loadBalance(builder, context, new HardCodedTarget<>(this.type,
this.name, url));
}
// 如果 url 有值,直接使用指定的 URL 发起请求
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// 不使用负载均衡客户端,因为已设置 URL,获取原始的非负载均衡客户端
client = ((LoadBalancerFeignClient)client).getDelegate();
}
builder.client(client);
}
// 获取 Targeter 代理客户端实例
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, new HardCodedTarget<>(
this.type, this.name, url));
}

// 清理 path 前后的斜杠,确保 path 格式规范
private String cleanPath() {
String path = this.path.trim();
if (StringUtils.hasLength(path)) {
if (!path.startsWith("/")) {
path = "/" + path;
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
}
return path;
}

@Override
public Class<?> getObjectType() {
// 返回代理对象的类型
return this.type;
}

@Override
public boolean isSingleton() {
// 标识该工厂 Bean 是否为单例
return true;
}

}

当指定 url 时,创建默认的 Client 用来发起 Http 请求

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
public interface Client {

/**
* 执行指定的请求(Request)并返回响应(Response)。
*
* @param request 需要发送的请求对象,支持重试。
* @param options 请求的配置选项,如连接超时等。
* @return 响应对象,包含状态码、响应体等。
* @throws IOException 网络连接错误。
*/
Response execute(Request request, Options options) throws IOException;

// 静态内部类 Default 实现了 Client 接口,用于提供 Feign 的默认客户端功能
public static class Default implements Client {

// SSL 套接字工厂,用于处理 HTTPS 连接
private final SSLSocketFactory sslContextFactory;
// 主机名验证器,用于验证 HTTPS 的主机名
private final HostnameVerifier hostnameVerifier;

/**
* 构造函数,允许传入自定义的 SSL 套接字工厂和主机名验证器。
* 如果为 null 则使用系统默认的 SSL 和主机名验证设置。
*/
public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
private final SSLSocketFactory sslContextFactory;
private final HostnameVerifier hostnameVerifier;
}

@Override
public Response execute(Request request, Options options) throws IOException
// 执行请求并返回响应对象
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection).toBuilder().request(request).build();
}

// 将 Feign 的 Request 对象转换为 HttpURLConnection 并发送请求
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
final HttpURLConnection
connection =
(HttpURLConnection) new URL(request.url()).openConnection();
// 如果是 HTTPS 请求,则应用 SSL 配置
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
if (sslContextFactory != null) {
sslCon.setSSLSocketFactory(sslContextFactory);
}
if (hostnameVerifier != null) {
sslCon.setHostnameVerifier(hostnameVerifier);
}
}

// 设置连接和读取的超时时间
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
connection.setAllowUserInteraction(false);
// 设置自动跟随重定向
connection.setInstanceFollowRedirects(true);
// 设置请求方法(GET、POST 等)
connection.setRequestMethod(request.method());

// 获取请求头中的内容编码,用于判断请求体是否需要进行 gzip 或 deflate 压缩
Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
boolean gzipEncodedRequest = contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
boolean deflateEncodedRequest = contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);

// 用于跟踪是否有 Accept 头
boolean hasAcceptHeader = false;
// 请求体的内容长度
Integer contentLength = null;

// 遍历请求头,添加到连接对象中
for (String field : request.headers().keySet()) {
if (field.equalsIgnoreCase("Accept")) {
hasAcceptHeader = true;
}
for (String value : request.headers().get(field)) {
if (field.equals(CONTENT_LENGTH)) {
if (!gzipEncodedRequest && !deflateEncodedRequest) {
contentLength = Integer.valueOf(value);
connection.addRequestProperty(field, value);
}
} else {
connection.addRequestProperty(field, value);
}
}
}

// 如果请求头中没有 Accept,设置默认值
if (!hasAcceptHeader) {
connection.addRequestProperty("Accept", "*/*");
}

// 处理请求体
if (request.body() != null) {
if (contentLength != null) {
// 如果已知内容长度,设置固定的流模式
connection.setFixedLengthStreamingMode(contentLength);
} else {
// 未知内容长度时,使用分块流模式
connection.setChunkedStreamingMode(8196);
}
// 允许输出(写入请求体)
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();

// 根据请求头对请求体进行 gzip 或 deflate 编码
if (gzipEncodedRequest) {
out = new GZIPOutputStream(out);
} else if (deflateEncodedRequest) {
out = new DeflaterOutputStream(out);
}
try {
// 写入请求体内容
out.write(request.body());
} finally {
try {
// 关闭流,确保资源释放
out.close();
} catch (IOException suppressed) { // NOPMD
}
}
}
return connection;
}

// 将 HttpURLConnection 响应转换为 Feign 的 Response 对象
Response convertResponse(HttpURLConnection connection) throws IOException {
// 获取 HTTP 状态码
int status = connection.getResponseCode();
// 获取响应消息
String reason = connection.getResponseMessage();

// 如果状态码小于 0,抛出异常
if (status < 0) {
throw new IOException(format("Invalid status(%s) executing %s %s", status,
connection.getRequestMethod(), connection.getURL()));
}

// 将连接的响应头存入 Map
Map<String, Collection<String>> headers = new LinkedHashMap<String, Collection<String>>();
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
// 仅存储非空的键值对
if (field.getKey() != null) {
headers.put(field.getKey(), field.getValue());
}
}

// 获取内容长度
Integer length = connection.getContentLength();
if (length == -1) {
// 若未知长度,则设置为 null
length = null;
}
InputStream stream;
// 如果状态码 >= 400,获取错误流;否则获取输入流
if (status >= 400) {
stream = connection.getErrorStream();
} else {
stream = connection.getInputStream();
}
// 构建并返回 Feign 的响应对象
return Response.builder()
.status(status)
.reason(reason)
.headers(headers)
.body(stream, length)
.build();
}
}
}

在 Feign 中,Options 类用于配置 HTTP 请求的超时时间,包括连接超时和读取超时。通过自定义 Options 实例,可以灵活地为 Feign 客户端配置这些超时时间

全局配置 Options

1
2
3
4
@Bean
public Options feignOptions() {
return new Options(5000, 10000); // 5秒连接超时,10秒读取超时
}

针对某个 Feign 客户端的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@FeignClient(name = "example-client", configuration = ExampleClientConfiguration.class)
public interface ExampleClient {
// 定义接口方法
}

@Configuration
public class ExampleClientConfiguration {

@Bean
public Options feignOptions() {
return new Options(2000, 5000); // 2秒连接超时,5秒读取超时
}
}

通过 Feign.Builder 自定义 Options

1
2
3
Feign.Builder builder = Feign.builder().options(new Request.Options(3000, 8000)); // 3秒连接超时,8秒读取超时

MyClient client = builder.target(MyClient.class, "http://example.com");

这种方式适用于不依赖 Spring 的纯 Feign 中,可以直接通过 Feign.Builder 来设置自定义 Options

Ribbon 的超时时间

Ribbon 通过一个实现 Client 接口的类来完成请求的负载均衡

org.springframework.cloud.netflix.feign.ribbon.LoadBalancerFeignClient

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 定义了一个负载均衡的 Feign 客户端类,实现了 Feign 的 Client 接口
public class LoadBalancerFeignClient implements Client {

// 定义了一个默认的请求选项配置,作为缺省配置用于请求参数的初始化
static final Request.Options DEFAULT_OPTIONS = new Request.Options();

// 定义了三个属性:
// 用于实际发送请求的底层 Feign 客户端
private final Client delegate;
// 用于创建负载均衡器客户端的工厂类,结合 Ribbon 实现负载均衡功能
private CachingSpringLoadBalancerFactory lbClientFactory;
// 用于获取每个客户端的负载均衡配置的工厂类
private SpringClientFactory clientFactory;

public LoadBalancerFeignClient(Client delegate,
CachingSpringLoadBalancerFactory lbClientFactory,
SpringClientFactory clientFactory) {
this.delegate = delegate;
this.lbClientFactory = lbClientFactory;
this.clientFactory = clientFactory;
}

// 重写 execute 方法
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
// 解析请求URL为URI对象
URI asUri = URI.create(request.url());

// 获取服务的主机名作为clientName,用于负载均衡选择
String clientName = asUri.getHost();

// 去掉URL中的主机部分,以便后续拼接负载均衡的URL
URI uriWithoutHost = cleanUrl(request.url(), clientName);

// 创建Ribbon请求对象,将请求包装在负载均衡器中
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);

// 获取客户端配置,基于传入的 options 和 clientName
IClientConfig requestConfig = getClientConfig(options, clientName);

// 使用负载均衡器执行请求并转换为 Feign 的 Response 对象返回
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
requestConfig).toResponse();
} catch (ClientException e) {
// 检查异常是否为 IOException 类型
IOException io = findIOException(e);
if (io != null) {
// 如果是 IOException,抛出该异常
throw io;
}

// 否则将 ClientException 包装为 RuntimeException 抛出
throw new RuntimeException(e);
}
}

// 获取客户端配置的方法
IClientConfig getClientConfig(Request.Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) {
// 如果使用的是默认选项,则从clientFactory中获取客户端配置
requestConfig = this.clientFactory.getClientConfig(clientName);
} else {
// 否则根据传入的 options 创建一个新的 FeignOptionsClientConfig 对象
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}

// 递归查找Throwable中的IOException
protected IOException findIOException(Throwable t) {
if (t == null) {
return null;
}
if (t instanceof IOException) {
return (IOException) t;
}
// 递归调用以获取根异常中的IOException
return findIOException(t.getCause());
}

public Client getDelegate() {
return this.delegate;
}

static URI cleanUrl(String originalUrl, String host) {
// 去除URL中的主机部分,用于构造负载均衡的URL
return URI.create(originalUrl.replaceFirst(host, ""));
}

private FeignLoadBalancer lbClient(String clientName) {
// 使用负载均衡工厂类创建或获取指定名称的负载均衡器实例
return this.lbClientFactory.create(clientName);
}

// 定义内部类,用于从 Request.Options 对象中创建 Ribbon 的客户端配置
static class FeignOptionsClientConfig extends DefaultClientConfigImpl {
public FeignOptionsClientConfig(Request.Options options) {
// 设置连接超时时间,使用传入 options 中的连接超时时间
setProperty(CommonClientConfigKey.ConnectTimeout,
options.connectTimeoutMillis());
// 设置读取超时时间,使用传入 options 中的读取超时时间
setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
}

@Override
public void loadProperties(String clientName) {}

@Override
public void loadDefaultValues() {}
}
}

Hystrix 超时时间

com.netflix.hystrix.HystrixCommandProperties 中设置了 Hystrix 相关配置参数

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
protected HystrixCommandProperties(HystrixCommandKey key, HystrixCommandProperties.Setter builder, String propertyPrefix) {
this.key = key;
this.circuitBreakerEnabled = getProperty(propertyPrefix, key, "circuitBreaker.enabled", builder.getCircuitBreakerEnabled(), default_circuitBreakerEnabled);
this.circuitBreakerRequestVolumeThreshold = getProperty(propertyPrefix, key, "circuitBreaker.requestVolumeThreshold", builder.getCircuitBreakerRequestVolumeThreshold(), default_circuitBreakerRequestVolumeThreshold);
this.circuitBreakerSleepWindowInMilliseconds = getProperty(propertyPrefix, key, "circuitBreaker.sleepWindowInMilliseconds", builder.getCircuitBreakerSleepWindowInMilliseconds(), default_circuitBreakerSleepWindowInMilliseconds);
this.circuitBreakerErrorThresholdPercentage = getProperty(propertyPrefix, key, "circuitBreaker.errorThresholdPercentage", builder.getCircuitBreakerErrorThresholdPercentage(), default_circuitBreakerErrorThresholdPercentage);
this.circuitBreakerForceOpen = getProperty(propertyPrefix, key, "circuitBreaker.forceOpen", builder.getCircuitBreakerForceOpen(), default_circuitBreakerForceOpen);
this.circuitBreakerForceClosed = getProperty(propertyPrefix, key, "circuitBreaker.forceClosed", builder.getCircuitBreakerForceClosed(), default_circuitBreakerForceClosed);
this.executionIsolationStrategy = getProperty(propertyPrefix, key, "execution.isolation.strategy", builder.getExecutionIsolationStrategy(), default_executionIsolationStrategy);
//this property name is now misleading. //TODO figure out a good way to deprecate this property name
this.executionTimeoutInMilliseconds = getProperty(propertyPrefix, key, "execution.isolation.thread.timeoutInMilliseconds", builder.getExecutionIsolationThreadTimeoutInMilliseconds(), default_executionTimeoutInMilliseconds);
this.executionTimeoutEnabled = getProperty(propertyPrefix, key, "execution.timeout.enabled", builder.getExecutionTimeoutEnabled(), default_executionTimeoutEnabled);
this.executionIsolationThreadInterruptOnTimeout = getProperty(propertyPrefix, key, "execution.isolation.thread.interruptOnTimeout", builder.getExecutionIsolationThreadInterruptOnTimeout(), default_executionIsolationThreadInterruptOnTimeout);
this.executionIsolationThreadInterruptOnFutureCancel = getProperty(propertyPrefix, key, "execution.isolation.thread.interruptOnFutureCancel", builder.getExecutionIsolationThreadInterruptOnFutureCancel(), default_executionIsolationThreadInterruptOnFutureCancel);
this.executionIsolationSemaphoreMaxConcurrentRequests = getProperty(propertyPrefix, key, "execution.isolation.semaphore.maxConcurrentRequests", builder.getExecutionIsolationSemaphoreMaxConcurrentRequests(), default_executionIsolationSemaphoreMaxConcurrentRequests);
this.fallbackIsolationSemaphoreMaxConcurrentRequests = getProperty(propertyPrefix, key, "fallback.isolation.semaphore.maxConcurrentRequests", builder.getFallbackIsolationSemaphoreMaxConcurrentRequests(), default_fallbackIsolationSemaphoreMaxConcurrentRequests);
this.fallbackEnabled = getProperty(propertyPrefix, key, "fallback.enabled", builder.getFallbackEnabled(), default_fallbackEnabled);
this.metricsRollingStatisticalWindowInMilliseconds = getProperty(propertyPrefix, key, "metrics.rollingStats.timeInMilliseconds", builder.getMetricsRollingStatisticalWindowInMilliseconds(), default_metricsRollingStatisticalWindow);
this.metricsRollingStatisticalWindowBuckets = getProperty(propertyPrefix, key, "metrics.rollingStats.numBuckets", builder.getMetricsRollingStatisticalWindowBuckets(), default_metricsRollingStatisticalWindowBuckets);
this.metricsRollingPercentileEnabled = getProperty(propertyPrefix, key, "metrics.rollingPercentile.enabled", builder.getMetricsRollingPercentileEnabled(), default_metricsRollingPercentileEnabled);
this.metricsRollingPercentileWindowInMilliseconds = getProperty(propertyPrefix, key, "metrics.rollingPercentile.timeInMilliseconds", builder.getMetricsRollingPercentileWindowInMilliseconds(), default_metricsRollingPercentileWindow);
this.metricsRollingPercentileWindowBuckets = getProperty(propertyPrefix, key, "metrics.rollingPercentile.numBuckets", builder.getMetricsRollingPercentileWindowBuckets(), default_metricsRollingPercentileWindowBuckets);
this.metricsRollingPercentileBucketSize = getProperty(propertyPrefix, key, "metrics.rollingPercentile.bucketSize", builder.getMetricsRollingPercentileBucketSize(), default_metricsRollingPercentileBucketSize);
this.metricsHealthSnapshotIntervalInMilliseconds = getProperty(propertyPrefix, key, "metrics.healthSnapshot.intervalInMilliseconds", builder.getMetricsHealthSnapshotIntervalInMilliseconds(), default_metricsHealthSnapshotIntervalInMilliseconds);
this.requestCacheEnabled = getProperty(propertyPrefix, key, "requestCache.enabled", builder.getRequestCacheEnabled(), default_requestCacheEnabled);
this.requestLogEnabled = getProperty(propertyPrefix, key, "requestLog.enabled", builder.getRequestLogEnabled(), default_requestLogEnabled);

// threadpool doesn't have a global override, only instance level makes sense
this.executionIsolationThreadPoolKeyOverride = forString().add(propertyPrefix + ".command." + key.name() + ".threadPoolKeyOverride", null).build();
}

需要注意的是,要想 Hystrix 在配置的时间下熔断,需要在 @FeignClient 中配置 fallback 属性

Feign 的客户端

顺带看到 Feign 在整合 Ribbon 的过程中,有提供两种默认的请求客户端 Apache HttpClientOkHttp

org.springframework.cloud.netflix.feign.ribbon.HttpClientFeignLoadBalancedConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
class HttpClientFeignLoadBalancedConfiguration {

@Autowired(required = false)
private HttpClient httpClient;

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
ApacheHttpClient delegate;
if (this.httpClient != null) {
delegate = new ApacheHttpClient(this.httpClient);
} else {
delegate = new ApacheHttpClient();
}
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}

org.springframework.cloud.netflix.feign.ribbon.OkHttpFeignLoadBalancedConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true)
class OkHttpFeignLoadBalancedConfiguration {

@Autowired(required = false)
private okhttp3.OkHttpClient okHttpClient;

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
OkHttpClient delegate;
if (this.okHttpClient != null) {
delegate = new OkHttpClient(this.okHttpClient);
} else {
delegate = new OkHttpClient();
}
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}

无独有偶,之前使用 Feign 作为 Http 客户端的时候遇到无法处理 PATCH 请求的情况,所以可以对 Feign 自定义客户端发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class CustomClientConfig {

@Bean
public Client customFeignClient() {
return new Client() {
@Override
public Response execute(Request request, Request.Options options) throws IOException {
// 实现自定义逻辑
return null;
}
};
}
}

总结

Feign 自身就是一个 Http 客户端,它有自己的超时时间,以及重试等机制。在 Feign 的基础之上,整合 Ribbon 和 Hystrix 组件形成一套完整的分布式系统 RPC 组件。在使用 @FeignClient 注解声明 Feign 客户端时,主要就是通过 url 是否有值来判断是走单纯的 http 请求还是走注册中心的服务发现来结合 Ribbon 做负载均衡。这里是十分容易「踩坑」的地方。