Prerequisites
In microservices architecture, if the downstream dependencies do not do request degradation processing, the downstream abnormal dependencies are not isolated, and it is likely that one or two services or as small as one or two interface abnormalities will lead to the unavailability of all upstream services and even affect the whole business line.
Request degradation processing is still relatively mainstream is Netfilx
produced Hystrix
.
Hystrix works on the following principles.
- Isolating requests based on thread pools or semaphores, once the downstream service fails to respond within the specified configured timeout will enter a pre-defined or default degraded implementation.
- The status of each request is recorded, and the rate of processing failure within a sliding window exceeds a set threshold to trigger the fuse (Circle Breaker) to open, and all requests go directly to the preset or default degradation logic after the fuse is opened.
- After the fuse is opened and the time since the fuse was opened or the last trial request release exceeds the set value, the fuse device enters the half-open state and allows the release of a trial request.
- After the request success rate increases, the fuse is determined to be closed based on statistical data and all requests are released normally.
Instead of going into the details of Hystrix, we will move on to how to use Hystrix in Spring Cloud Gateway, mainly including the built-in Hystrix filters and custom filters combined with Hystrix to achieve the functionality we want. In addition to introducing the spring-cloud-starter-gateway dependency, you also need to introduce spring-cloud-starter-netflix-hystrix.
Instead of going into the details of Hystrix
, we will move on to how to use Hystrix
in Spring Cloud Gateway, mainly including the built-in Hystrix filters and custom filters combined with Hystrix
to achieve the functionality we want. In addition to introducing the spring-cloud-starter-gateway
dependency, you also need to introduce spring-cloud-starter-netflix-hystrix
.
1
2
3
4
5
6
7
8
9
10
|
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
|
Use the built-in Hystrix filter
The built-in Hystrix
filter is the HystrixGatewayFilterFactory
, which supports the following configurations.
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
|
public static class Config {
// If the following Setter is configured as null, name will be used as the HystrixCommandKey of the Hystrix
private String name;
// Setter property of Hystrix, mainly used to configure the KEY and other properties of the command
private Setter setter;
// The target URI for degradation must start with forward, and the URI will match the controller method applied to the gateway
private URI fallbackUri;
public String getName() {
return name;
}
public Config setName(String name) {
this.name = name;
return this;
}
public Config setFallbackUri(String fallbackUri) {
if (fallbackUri != null) {
setFallbackUri(URI.create(fallbackUri));
}
return this;
}
public URI getFallbackUri() {
return fallbackUri;
}
// Note that for this method, the configured fallbackUri should start with forward as the schema, otherwise it will throw an exception
public void setFallbackUri(URI fallbackUri) {
if (fallbackUri != null && !"forward".equals(fallbackUri.getScheme())) {
throw new IllegalArgumentException("Hystrix Filter currently only supports 'forward' URIs, found " + fallbackUri);
}
this.fallbackUri = fallbackUri;
}
public Config setSetter(Setter setter) {
this.setter = setter;
return this;
}
}
|
In addition,
(1) the global Hystrix
configuration will also take effect for HystrixGatewayFilterFactory
;
(2) HystrixGatewayFilterFactory
can be used as default-filters for all routing configurations as under-the-hood filters and function as such.
For point (1), if we configure the following in application.yaml
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// The execution timeout is 1 second and will take effect for the HystrixGatewayFilterFactory bound to the following route order_route
hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 1000
spring:
cloud:
gateway:
routes:
- id: order_route
uri: http://localhost:9091
predicates:
- Path=/order/**
filters:
- name: Hystrix
args:
name: HystrixCommand
fallbackUri: forward:/fallback
|
The configured hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds
will take effect on the HystrixGatewayFilterFactory
bound to the route order_route
.
For point (2), we can configure HystrixGatewayFilterFactory
as the default filter, so that all routes will be associated with this filter, but it is recommended not to do so unless necessary: HystrixGatewayFilterFactory
is the default filter.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
spring:
cloud:
gateway:
routes:
- id: order_route
uri: http://localhost:9091
predicates:
- Path=/order/**
default-filters:
- name: Hystrix
args:
name: HystrixCommand
fallbackUri: forward:/fallback
|
When I was testing, I found that the Setter
mentioned above could not be configured, presumably because the Setter
object of Hystrix
is multi-packaged and there is no way to set the property for the time being. Next we have to add a controller method to the gateway service for handling redirected /fallback
requests.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@RestController
public class FallbackController {
@RequestMapping(value = "/fallback")
@ResponseStatus
public Mono<Map<String, Object>> fallback(ServerWebExchange exchange, Throwable throwable) {
Map<String, Object> result = new HashMap<>(8);
ServerHttpRequest request = exchange.getRequest();
result.put("path", request.getPath().pathWithinApplication().value());
result.put("method", request.getMethodValue());
if (null != throwable.getCause()) {
result.put("message", throwable.getCause().getMessage());
} else {
result.put("message", throwable.getMessage());
}
return Mono.just(result);
}
}
|
Controller method entries are handled by the internal components of Spring Cloud Gateway
and can call back some useful types such as ServerWebExchange
instances, specific exception instances and so on.
Custom Filters with Hystrix
HystrixGatewayFilterFactory
should meet business needs in most cases, but here also do a customization of a filter that integrates Hystrix
and implements the following functionality.
- Creates a new instance of the Hystrix command to be invoked based on each request URL.
- Each URL can specify a unique thread pool configuration, or use the default if not specified.
- A separate Hystrix timeout can be configured for each URL.
This means that each different external request URL is isolated by Hystrix using a thread pool. Of course, such a filter only makes sense if the number of different URLs for external requests is limited, otherwise there is a risk of creating too many thread pools causing system performance degradation, which is counterproductive. The modification is as follows.
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
173
174
|
@Component
public class CustomHystrixFilter extends AbstractGatewayFilterFactory<CustomHystrixFilter.Config> {
private static final String FORWARD_KEY = "forward";
private static final String NAME = "CustomHystrix";
private static final int TIMEOUT_MS = 1000;
private final ObjectProvider<DispatcherHandler> dispatcherHandlerProvider;
private volatile DispatcherHandler dispatcherHandler;
private boolean processConfig = false;
public CustomHystrixFilter(ObjectProvider<DispatcherHandler> dispatcherHandlerProvider) {
super(Config.class);
this.dispatcherHandlerProvider = dispatcherHandlerProvider;
}
private DispatcherHandler getDispatcherHandler() {
if (dispatcherHandler == null) {
dispatcherHandler = dispatcherHandlerProvider.getIfAvailable();
}
return dispatcherHandler;
}
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList(NAME_KEY);
}
@Override
public GatewayFilter apply(Config config) {
processConfig(config);
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().pathWithinApplication().value();
int timeout = config.getTimeout().getOrDefault(path, TIMEOUT_MS);
CustomHystrixCommand command = new CustomHystrixCommand(config.getFallbackUri(), exchange, chain, timeout, path);
return Mono.create(s -> {
Subscription sub = command.toObservable().subscribe(s::success, s::error, s::success);
s.onCancel(sub::unsubscribe);
}).onErrorResume((Function<Throwable, Mono<Void>>) throwable -> {
if (throwable instanceof HystrixRuntimeException) {
HystrixRuntimeException e = (HystrixRuntimeException) throwable;
HystrixRuntimeException.FailureType failureType = e.getFailureType();
switch (failureType) {
case TIMEOUT:
return Mono.error(new TimeoutException());
case COMMAND_EXCEPTION: {
Throwable cause = e.getCause();
if (cause instanceof ResponseStatusException || AnnotatedElementUtils
.findMergedAnnotation(cause.getClass(), ResponseStatus.class) != null) {
return Mono.error(cause);
}
}
default:
break;
}
}
return Mono.error(throwable);
}).then();
};
}
/**
* YAML parsing does not support '/' in MAP's KEY, so here you can only use '-' instead
*
* @param config config
*/
private void processConfig(Config config) {
if (!processConfig) {
processConfig = true;
if (null != config.getTimeout()) {
Map<String, Integer> timeout = new HashMap<>(8);
config.getTimeout().forEach((k, v) -> {
String key = k.replace("-", "/");
if (!key.startsWith("/")) {
key = "/" + key;
}
timeout.put(key, v);
});
config.setTimeout(timeout);
}
}
}
@Override
public String name() {
return NAME;
}
private class CustomHystrixCommand extends HystrixObservableCommand<Void> {
private final URI fallbackUri;
private final ServerWebExchange exchange;
private final GatewayFilterChain chain;
public CustomHystrixCommand(URI fallbackUri,
ServerWebExchange exchange,
GatewayFilterChain chain,
int timeout,
String key) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(key))
.andCommandKey(HystrixCommandKey.Factory.asKey(key))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(timeout)));
this.fallbackUri = fallbackUri;
this.exchange = exchange;
this.chain = chain;
}
@Override
protected Observable<Void> construct() {
return RxReactiveStreams.toObservable(this.chain.filter(exchange));
}
@Override
protected Observable<Void> resumeWithFallback() {
if (null == fallbackUri) {
return super.resumeWithFallback();
}
URI uri = exchange.getRequest().getURI();
boolean encoded = containsEncodedParts(uri);
URI requestUrl = UriComponentsBuilder.fromUri(uri)
.host(null)
.port(null)
.uri(this.fallbackUri)
.build(encoded)
.toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
ServerHttpRequest request = this.exchange.getRequest().mutate().uri(requestUrl).build();
ServerWebExchange mutated = exchange.mutate().request(request).build();
return RxReactiveStreams.toObservable(getDispatcherHandler().handle(mutated));
}
}
public static class Config {
private String id;
private URI fallbackUri;
/**
* url -> timeout ms
*/
private Map<String, Integer> timeout;
public String getId() {
return id;
}
public Config setId(String id) {
this.id = id;
return this;
}
public URI getFallbackUri() {
return fallbackUri;
}
public Config setFallbackUri(URI fallbackUri) {
if (fallbackUri != null && !FORWARD_KEY.equals(fallbackUri.getScheme())) {
throw new IllegalArgumentException("Hystrix Filter currently only supports 'forward' URIs, found " + fallbackUri);
}
this.fallbackUri = fallbackUri;
return this;
}
public Map<String, Integer> getTimeout() {
return timeout;
}
public Config setTimeout(Map<String, Integer> timeout) {
this.timeout = timeout;
return this;
}
}
}
|
In fact, most of the code is similar to the built-in Hystrix filter, only the command transformation function part and the configuration loading processing part have been changed. The configuration file is as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
spring:
cloud:
gateway:
routes:
- id: hystrix_route
uri: http://localhost:9091
predicates:
- Host=localhost:9090
filters:
- name: CustomHystrix
args:
id: CustomHystrix
fallbackUri: forward:/fallback
timeout:
# 这里暂时用-分隔URL,因为/不支持
order-remote: 2000
application:
name: route-server
server:
port: 9090
|
The gateway adds a /fallback
processing controller as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@RestController
public class FallbackController {
@RequestMapping(value = "/fallback")
@ResponseStatus
public Mono<Map<String, Object>> fallback(ServerWebExchange exchange, Throwable throwable) {
Map<String, Object> result = new HashMap<>(8);
ServerHttpRequest request = exchange.getRequest();
result.put("path", request.getPath().pathWithinApplication().value());
result.put("method", request.getMethodValue());
if (null != throwable.getCause()) {
result.put("message", throwable.getCause().getMessage());
} else {
result.put("message", throwable.getMessage());
}
return Mono.just(result);
}
}
|
Intentional downstream service interruption points.
1
2
3
4
5
6
7
8
|
curl http://localhost:9090/order/remote
response :
{
"path": "/fallback",
"method": "GET",
"message": null # <== Here the message is null because it is a timeout exception
}
|
Just in line with the expected results.
Summary
This article is just to Hystrix
and filter application to provide a usable example and problem solving ideas, specific how to use or need for real scenarios.
Reference https://www.throwx.cn/2019/05/25/spring-cloud-gateway-hystrix/