0%

深入理解feign、ribbon和hystrix三者的关系及超时配置

spring cloud中有几个重要的组件,深入理解它们之间的关系才能更好的使用它们:

  • ribbon:实现服务定位和客户端负载均衡;
  • hystrix:实现服务熔断、服务降级、资源隔离等;
  • feign:声明式的http客户端,用于服务之间的http调用。相比于resttemplate,feign与ribbon和hystrix集成更友好,是spring cloud的顶层组件。

上述ribbon和hystrix都是netflix贡献组件,目前它们都处于维护模式,不再增加新特性,将逐渐被spring cloud官方组件取代,例如从Hoxton.M2开始整合spring-cloud-loadbalancer用于替换ribbon,但目前还不成熟,还是老老实实用ribbon,而断路器方面spring cloud抽象了Spring Cloud Circuit Breaker,hystrix只是其中一个实现,还有其他实现可选,例如阿里贡献的sentinel

三者关系

hystrix和ribbon并没有直接关系。

feign底层默认是通过ribbon进行服务定位和负载均衡,使用feign时你感知不到ribbon的存在,也可以不使用ribbon。如果你使用resttemplate,则需要通过@LoadBalanced使用ribbon。

hystrix有两种使用情况,一种是在controller的handler方法上增加@HystrixCommand注解,作用的是整个handler;另一种情况是调用其他app服务时,也就是@feignclient注解的http客户端,此时调用这个http客户端的handler可不需要hystrix。

总结一下:服务之间的http调用可通过feign实现,feign底层是通过ribbon实现服务发现和负载均衡,不管是否使用feign,ribbon都是必不可少的;feign客户端可选的可启用hystrix支持,hystrix也可以用在服务端整个handler上。

image-20200323210614496

如上所示,app1.controll1.handler1调用app2.controller2.handler2的过程是:

  1. 如果feign.hystrix.enabled=true(默认为false),则feign通过jdk动态代理,将调用封装为HystrixCommand,在hystrix thread pool中执行,否则进入3和4;
  2. hystrix thread pool中执行http调用,还是回调feign接口;
  3. feign扩展了ribbon客户端,使用ribbon的服务定位和负载均衡获得可用服务;
  4. feign扩展的ribbon客户端发起对app2.controller2.handler2的http请求,ribbon可以开启重试,如果请求超时则自动重试。

了解上述关系对于如何设置超时时间至关重要。如果hystrix的超时时间到达,则1就返回fallback了,不会等到4执行完。一般hystrix的超时时间要大于feign的超时时间。

另外上述服务端设置了断路器,实际上客户端可以不用设置。

超时相关配置

feign

官方参考

有两种配置方式:

  1. 属性配置,包括全局和实例;
  2. 代码配置,也包括全局和实例。

优先级:实例>全局,属性>代码

属性总是优先的,可以设置feign.client.default-to-properties为false,使得代码配置优先。

全局属性配置名默认是default,可以设置feign.client.default-config为其他名字。

属性配置

1
2
3
4
5
6
7
8
9
10
11
12
13
feign:
hystrix:
enabled: true
client:
config:
# 全局配置
default:
connectTimeout: 5000
readTimeout: 5000
# 实例配置,feignName即@feignclient中的value,也就是服务名
feignName:
connectTimeout: 5000
readTimeout: 5000

代码配置

全局代码配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.microservice2;

import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;

import feign.Request;
import feign.Retryer;

public class FeignClientConfiguration {
@Bean
public Request.Options feignRequestOptions() {
// 默认连接超时10秒,读取超时60秒
return new Request.Options();
}

// 默认重试是关闭的
@Bean
public Retryer feignRetry() {
// 默认重试5次,首次间隔100毫秒,最大间隔1秒
return new Retryer.Default();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.microservice2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient //开启注册服务
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class) //开启feign消费服务
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}
实例代码配置
1
2
3
4
5
6
7
8
9
10
11
12
package com.example.microservice2;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "microservice1", fallback = HelloServiceHystric.class, configuration = FeignClientConfiguration.class) // 访问微服务1,指定断路器类
public interface HelloService {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
String hello(@RequestParam(value = "name") String name);
}

hystrix

官方参考

hystrix有四种配置方式:

  1. 全局代码默认属性;
  2. 全局属性配置;
  3. 实例代码配置;
  4. 实例属性配置。

优先级:1<2<3<4

和feign一样,也是属性优先代码,实例优先全局。

属性配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
hystrix:
command:
#全局默认配置
default:
#线程隔离相关
execution:
timeout:
#是否给方法执行设置超时时间,默认为true。一般我们不要改。
enabled: true
isolation:
#配置请求隔离的方式,这里是默认的线程池方式。还有一种信号量的方式semaphore。
strategy: THREAD
thread:
#方式执行的超时时间,默认为1000毫秒,在实际场景中需要根据情况设置
timeoutInMilliseconds: 10000
# 实例配置
HystrixCommandKey:
execution:
timeout:
enabled: true
isolation:
strategy: THREAD
thread:
timeoutInMilliseconds: 10000

代码配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

@RefreshScope // 开启配置更新
@RestController
@Slf4j
public class HelloController {

@Value("${serviceName}") // 读配置文件的这个属性
private String serviceName;

@RequestMapping("/hello")
@HystrixCommand(fallbackMethod = "helloError", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "4000") }) // 指定断路器方法,断路器监控用
public String hello(HttpServletRequest request, @RequestParam(value = "name", defaultValue = "Hugo") String name)
throws InterruptedException {
log.info("ServerName:{} time:{}", request.getServerName(),
DateFormatUtils.format(new Date(), "yyyyMMdd HH:mm:ss"));
TimeUnit.SECONDS.sleep(4);
return "hello " + name + ", my name is " + serviceName;
}

public String helloError(HttpServletRequest request, String name) {
return "microservice1 hystrix," + name + "!";
}
}

feign中的hystrix怎么配置

只能通过属性设置,那么commandkey是什么呢?

1
2
3
4
5
6
7
package com.example.microservice2;

@FeignClient(value = "microservice1", fallback = HelloServiceHystric.class, configuration = FeignClientConfiguration.class) // 访问微服务1,指定断路器类
public interface HelloService {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
String hello(@RequestParam(value = "name") String name);
}

上例中默认行为如下,groupkey为"microservice1",commandkey为"HelloService#hello(String)",threadpoolkey为null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface SetterFactory {

/**
* Returns a hystrix setter appropriate for the given target and method
*/
HystrixCommand.Setter create(Target<?> target, Method method);

/**
* Default behavior is to derive the group key from {@link Target#name()} and the command key from
* {@link Feign#configKey(Class, Method)}.
*/
final class Default implements SetterFactory {

@Override
public HystrixCommand.Setter create(Target<?> target, Method method) {
String groupKey = target.name();
String commandKey = Feign.configKey(target.type(), method);
return HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class Feign {

public static String configKey(Class targetType, Method method) {
StringBuilder builder = new StringBuilder();
builder.append(targetType.getSimpleName());
builder.append('#').append(method.getName()).append('(');
for (Type param : method.getGenericParameterTypes()) {
param = Types.resolve(targetType, targetType, param);
builder.append(Types.getRawType(param).getSimpleName()).append(',');
}
if (method.getParameterTypes().length > 0) {
builder.deleteCharAt(builder.length() - 1);
}
return builder.append(')').toString();
}

}

所以这样设置:

1
2
3
4
5
6
7
8
9
10
hystrix:
command:
HelloService#hello(String):
execution:
timeout:
enabled: true
isolation:
strategy: THREAD
thread:
timeoutInMilliseconds: 10000

也可以改变上述commandkey的默认行为:

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
package com.example.microservice2;

import java.lang.reflect.Method;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;

import org.springframework.context.annotation.Bean;

import feign.Target;
import feign.hystrix.SetterFactory;

public class FeignClientConfiguration {

@Bean
public SetterFactory feignHystrixSetter() {
return new MySetterFactory();
}
}

class MySetterFactory implements SetterFactory {
@Override
public HystrixCommand.Setter create(Target<?> target, Method method) {
String groupKey = target.name();
String commandKey = target.type().getName();
return HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
}
}

这样commandkey就会变成"com.example.microservice2.HelloService"。

ribbon

官方参考

ribbon只有属性配置,同样存在全局和实例配置,格式如下:

1
<clientName>.<nameSpace>.<propertyName>=<value>

nameSpace是可配置的,默认为ribbon。clientName可为远端服务名,即@feignclient的value,空表示全局配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 全局配置
ribbon:
# 服务最大重试次数,不包含第一次请求,默认0
MaxAutoRetries: 5
# 负载均衡切换次数,如果服务注册列表小于 nextServer count 那么会循环请求 A > B > A,默认1
MaxAutoRetriesNextServer: 3
#是否所有操作都进行重试
OkToRetryOnAllOperations: false
#连接超时时间,单位为毫秒,默认2秒
ConnectTimeout: 3000
#读取的超时时间,单位为毫秒,默认5秒
ReadTimeout: 3000
# 实例配置
clientName:
ribbon:
MaxAutoRetries: 5
MaxAutoRetriesNextServer: 3
OkToRetryOnAllOperations: false
ConnectTimeout: 3000
ReadTimeout: 3000

超时时间关系

feign超时

feign可以设置自身超时,也可以设置ribbon超时,那么它们的关系是怎么样的?看feign代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LoadBalancerFeignClient implements Client {

static final Request.Options DEFAULT_OPTIONS = new Request.Options();


IClientConfig getClientConfig(Request.Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName);
}
else {
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}
}

如果没有设置过feign超时,也就是等于默认值的时候,就会读取ribbon的配置,使用ribbon的超时时间和重试设置。否则使用feign自身的设置。两者是二选一的,且feign优先。

  • 如果设置的feign的超时,则超时时间大概是Retryer.Default.maxAttempts*(ConnectTimeout+ReadTimeout)
  • 如果仅设置了ribbon,则超时时间大概是(ConnectTimeout+ReadTimeout)*(MaxAutoRetries+1)*(MaxAutoRetriesNextServer+1);

建议使用ribbon超时设置。

feign重试和ribbon重试

feign自身重试目前只有一个简单的实现Retryer.Default,包含三个属性:

  • maxAttempts:重试次数,包含第一次
  • period:重试初始间隔时间,单位毫秒
  • maxPeriod:重试最大间隔时间,单位毫秒

重试间隔算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface Retryer extends Cloneable {

class Default implements Retryer {

/**
* Calculates the time interval to a retry attempt. <br>
* The interval increases exponentially with each attempt, at a rate of nextInterval *= 1.5
* (where 1.5 is the backoff factor), to the maximum interval.
*
* @return time in nanoseconds from now until the next attempt.
*/
long nextMaxInterval() {
long interval = (long) (period * Math.pow(1.5, attempt - 1));
return interval > maxPeriod ? maxPeriod : interval;
}

}
}

第一次重试间隔period,第二次period*1.5,第三次period*1.5*1.5,…,最大值不超过maxPeriod。

和ribbon的重试相比:

  • 重试次数包含了首次;
  • 不能设置多实例服务切换;
  • 重试有一个延迟时间。

feign超时和hystrix超时

hystrix的超时时间要大于feign的,否则没有等到feign超时,hystrix就fallback了,特别是重试机制会无法起作用。

-------------本文结束感谢您的阅读-------------