从0开始用SpringCloud搭建微服务系统【三】

服务间通信

微服务间可以使用 HTTP 协议,RESTful 规范进行通信。Spring Cloud 提供了 2 种 RESTful 调用方式:Ribbon 和 Feign 。

Ribbon

客户端软负载组件,支持Eureka对接,支持多种可插拔LB策略。依赖 spring-cloud-starter-netflix-eureka-client 中已经默认加载了 Ribbon 的依赖。

Ribbon作为Spring Cloud的负载均衡机制的实现:

  1. Ribbon可以单独使用,作为一个独立的负载均衡组件。只是需要我们手动配置 服务地址列表。
  2. Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表(DiscoveryClient),并基于负载均衡算法,请求其中一个服务提供者实例。
  3. Ribbon与OpenFeign和RestTemplate进行无缝对接,让二者具有负载均衡的能力。OpenFeign默认集成了ribbon。

Ribbon 的自定义配置以及一些高级使用可以参考官方文档:Client Side Load Balancer: Ribbon

Ribbon组成

官网首页:https://github.com/Netflix/ribbon

ribbon-core: 核心的通用性代码。api一些配置。

ribbon-eureka:基于eureka封装的模块,能快速集成eureka。

ribbon-examples:学习示例。

ribbon-httpclient:基于apache httpClient封装的rest客户端,集成了负载均衡模块,可以直接在项目中使用。

ribbon-loadbalancer:负载均衡模块。

ribbon-transport:基于netty实现多协议的支持。比如http,tcp,udp等。

调用方式

使用RestTemplate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class LoadBalancerClientConfig {
/**
* 负载均衡的RestTemplate。
*/
@LoadBalanced
@Bean(name = "loadBalanced")
RestTemplate loadBalanced() {
return new RestTemplate();
}
/**
* 常规的RestTemplate。
*/
@Primary
@Bean(name = "restTemplate")
RestTemplate restTemplate() {
return new RestTemplate();
}
}

以上代码中使用 @LoadBalanced 注解,这样就可以让 RestTemplate 在请求时拥有客户端负载均衡的能力。

1
2
3
4
5
6
7
8
@Autowired
@Qualifier("loadBalanced")
private RestTemplate loadBalanced;
public String getTime() {
return loadBalanced.getForEntity("http://service-producer/get_time", String.class).getBody();
}
使用DiscoveryClient
1
2
3
4
5
6
7
8
9
10
11
12
@Autowired
private DiscoveryClient discoveryClient;
public List<String> getAllInstance(@RequestParam("service_id") String serviceId) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
return instances.stream()
.map(instance -> String.format("http://%s:%s", instance.getHost(), instance.getPort()))
.collect(Collectors.toList());
}

负载均衡算法

Ribbon 提供了很多负载均衡的策略。详情可见:

策略名称 策略描述
BestAvailableRule(最低并发策略) 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略。
AvailabilityFilteringRule(可用过滤策略) 会先过滤掉多次访问故障而处于断路器跳闸状态的服务和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问。
WeightedResponseTimeRule(响应时间加权策略) 据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule。响应时间长,权重低,被选择的概率低。反之,同样道理。此策略综合了各种因素(网络,磁盘,IO等),这些因素直接影响响应时间。
RetryRule(重试策略) 先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败则在指定的时间会进行重试,进行获取可用的服务。如多次获取某个服务失败,就不会再次获取该服务。主要是在一个时间段内,如果选择一个服务不成功,就继续找可用的服务,直到超时。
RoundRobinRule(轮询策略) 以简单轮询选择一个服务器。按顺序循环选择一个server。
RandomRule(随机策略) 随机选择一个服务器。
ZoneAvoidanceRule(区域权衡策略)【默认实现】 复合判断Server所在区域的性能和Server的可用性,轮询选择服务器。

切换负载均衡策略

注解方式
1
2
3
4
5
6
7
@Configuration
public class DefaultRibbonConfig {
@Bean
public IRule ribbonRule() {
return new BestAvailableRule();
}
}

如上,配置IRule的新值,直接可以切换负载均衡策略

配置文件方式

给所有的服务指定负载均衡策略:

1
2
3
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

给特定的服务指定负载均衡策略:

1
<服务名>.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

Ribbon拦截

在服务调用的时候,我们可能不仅仅是简单地进行调用,会涉及到一些接口的校验、权限的校验等。要实现这些,可以实现ClientHttpRequestInterceptor接口。

1
2
3
4
5
6
7
8
9
10
11
12
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
System.out.println("拦截啦!!!");
System.out.println(httpRequest.getURI());
ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes);
System.out.println(response.getHeaders());
return response;
}
}

添加到resttemplate中

1
2
3
4
5
6
7
8
@LoadBalanced
@Bean(name = "loadBalanced")
@Primary
RestTemplate loadBalanced() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor());
return restTemplate;
}

超时

Ribbon的配置如下:

1
2
3
4
#连接超时时间(ms)
ribbon.ConnectTimeout=1000
#业务逻辑超时时间(ms)
ribbon.ReadTimeout=6000

重试

1
2
3
4
5
6
#同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=1
#重试负载均衡其他的实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetriesNextServer=1
#是否所有操作都重试
ribbon.OkToRetryOnAllOperations=false

使用ribbon重试机制,请求失败后,每个6秒会重新尝试

Feign

Feign 是 Netflix 开发的声明式、模板化的 HTTP 客户端,Feign 的使用非常简单,创建一个接口,在接口上加入一些注解,这样就完成了代码开发。

Feign 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程

Spring Cloud OpenFeign

Spring Cloud OpenFeign 通过自动配置并绑定到Spring Environment和其他Spring编程模型习惯用法,为Spring Boot应用程序提供OpenFeign集成。

官方文档:Spring Cloud OpenFeign

服务调用

使用 Feign 必须引入 spring-cloud-starter-openfeign。在启动类 Application@EnableFeignClients 注解。

服务提供者
1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping
public interface HelloApi {
@GetMapping(value = "/v0.1/greeting")
String greeting();
}
@RestController
public class HelloController implements HelloApi {
@Override
public String greeting(){
return "hello world";
}
}
服务消费者:简单使用

简单使用Feign不需要代码耦合,但是需要硬编码接口的信息。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@FeignClient(value = "producer-service")
public interface HelloClient{
@GetMapping(value = "/v0.1/greeting")
String greeting();
}
@Service
public class TestService {
@Autowired
HelloClient helloClient;
public String test(){
helloClient.greeting();
}
}

@FeignClient也可以脱离Eureka使用,如:@FeignClient(name = "xxx",url="") 这个url就是接口的地址。

服务消费者:接口继承方式

此方法需要引入服务提供者提供的接口jar包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FeignClient(value = "producer-service")
public interface HelloService extends HelloApi {
}
@Service
public class TestService {
@Autowired
HelloService helloService;
public String test(){
helloService.greeting();
}
}

以上代码中,@FeignClient(value = "producer-service") 指定了使用哪一个服务。

高级用法

自定义配置

feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。

允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。

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
@Configuration
@Slf4j
@EnableConfigurationProperties(FeignSecurityProperties.class)
@ConditionalOnClass(value = {RequestInterceptor.class, Decoder.class, Encoder.class})
public class FeignAutoConfig {
@Autowired
private FeignSecurityProperties feignSecurityProperties;
/**
* 内部微服务请求,加上timestamp,并且按字典序进行签名,附上sig
*
* @return RequestInterceptor 请求拦截器
*/
@Bean
@ConditionalOnMissingBean(RequestInterceptor.class)
public RequestInterceptor requestTokenBearerInterceptor() {
String apiSecretKey = feignSecurityProperties.getApiSignature().getInternalApiKey();
return requestTemplate -> ApiSigUtil.sig(apiSecretKey, requestTemplate);
}
@Bean
public Decoder feignDecoder() {
HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(customObjectMapper());
ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter);
return new ResponseEntityDecoder(new SpringDecoder(objectFactory));
}
@Bean
public Encoder feignEncoder() {
HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(customObjectMapper());
ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(jacksonConverter);
return new SpringEncoder(objectFactory);
}
public ObjectMapper customObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
return objectMapper;
}
@Bean
@ConditionalOnMissingBean(Retryer.class)
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
@Bean
@ConditionalOnMissingBean(Request.Options.class)
Request.Options feignOptions() {
return new Request.Options(30 * 1000, 30 * 1000);
}
}

如上面所示,在配置类上加上@Configuration注解,且该类在@ComponentScan所扫描的包中,那么该类中的配置信息就会被所有的@FeignClient共享。如果想要对某些的@FeignClient添加指定的配置,则:不指定@Configuration注解(或者指定configuration,用注解忽略),而是手动使用:@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class)

拦截器

上面代码中:

1
2
3
4
5
6
@Bean
@ConditionalOnMissingBean(RequestInterceptor.class)
public RequestInterceptor requestTokenBearerInterceptor() {
String apiSecretKey = feignSecurityProperties.getApiSignature().getInternalApiKey();
return requestTemplate -> ApiSigUtil.sig(apiSecretKey, requestTemplate);
}

RequestInterceptor是一个请求拦截器,我们可以继承它做很多事情,如:

1
2
3
4
5
6
7
8
9
10
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class MyBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("Authorization", "Basic cm9vdDpyb290");
}
}

如果是BasicAuth认证,可以重写BasicAuthRequestInterceptor。

然后,在配置文件中添加:

1
2
3
4
5
6
feign:
client:
config:
service-valuation:
request-interceptors:
- cn.webfuse.passenger.feign.interceptor.MyBasicAuthRequestInterceptor
配置文件扩展

指定服务名配置:

1
2
3
4
5
6
7
feign:
client:
config:
service-valuation:
connect-timeout: 5000
read-timeout: 5000
logger-level: full

通用配置:

1
2
3
4
5
6
7
feign:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
logger-level: full

属性配置比Java代码优先级高。也可通过配置设置java代码优先级高:

1
2
3
feign:
client:
default-to-properties: false
压缩
1
2
3
4
5
6
7
feign:
compression: # 开启请求与响应的GZIP压缩
request:
enabled: true
min-request-size: 10000 # 单位是B
response:
enabled: true
超时

Feign默认支持Ribbon;Ribbon的重试机制和Feign的重试机制有冲突,所以源码中默认关闭Feign的重试机制,使用Ribbon的重试机制。

保留原始异常信息

当调用服务时,如果服务返回的状态码不是200,就会进入到FeignErrorDecoder中,因此如果我们要解析异常信息,就要重写ErrorDecoder

原理

  1. 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。
  2. 当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。当定义的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。
  3. 然后由RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。

参考资源