服务容错保护Hystrix

在微服务架构中,我们将服务拆分为多个服务,通过远程调用的方式执行,很容易出现因为网络或服务本身问题而导致的调用故障和延迟,并且导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最终就会出现因为等待故障方响应而形成任务积压,最终导致自身服务的瘫痪。

在分布式架构中,但某个服务单元发生故障之后,通过断路器的故障监测,向调用方返回一个响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。

Hystrix实现了断路器,线程隔离等一系列服务保护功能。

Hystrix入门

  1. 导入相关依赖
1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>RELEASE</version>
</dependency>
  1. 在主类上添加@EnableCircuitBreaker注册开启断路器功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class RibbonConsumerApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

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

}

  1. 改造service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class HelloService {

@Autowired
RestTemplate restTemplate;

@HystrixCommand(fallbackMethod = "helloFallback")
public String helloService(){
return restTemplate.getForEntity("http://EUREKA-CLIENT-HELLO/hello",String.class).getBody();
}
public String helloFallback(){
return "error";
}

}

  1. 改造controller
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class ConsumerController {

@Autowired
HelloService helloService;


@RequestMapping(value = "/ribbon-consumer")
public String helloConsumer(){
return helloService.helloService();
}
}

  1. 测试
    首先启动eureka注册中心,然后分别启动两个EUREKA-CLIENT-HELLO服务,最后启动我们的服务消费者ribbon-consumer。
    首先在服务都完好的情况下,我们请求ribbon-consumer,结果一切正常。随后我们关闭8084端口的EUREKA-CLIENT-HELLO服务。ribboon-consumer并没有出现长时间的等待和报错,而是调用了服务中的helloFallback方法。

原理分析

u5LaCt.png
hystrix的整个工作流:

  1. 构造一个HystrixCommand或HystrixObservableCommand对象,用于封装请求,并在构造方法中配置请求被执行需要的参数。
  2. 执行命令。Hystrix提供了四种执行命令的方式。
  3. 判断是否使用缓存响应请求,若启用了缓存且缓存可用,直接使用缓存响应请求。
  4. 判断熔断器是否打开,如果打开,跳到第8步。
  5. 判断线程池/队列/信号是否已满,已满则跳到第8步
  6. 执行HystrixObservableCommand.construct()或HystrixCommand.run()如果执行失败或超时,跳到第8步,否则跳到第9步
  7. 统计熔断器监控指标
  8. 轴FallBack备用逻辑(服务降级)
  9. 返回请求响应

执行命令的四种方法:
execute()和queue()适用于HystrixCommand对象,而observe()和toObservable()适用于HystrixObservableCommand对象。

  1. execute()
    以同步阻塞的方式执行run,只支持接收一个值对象。hystrix会从线程池中去一个线程来执行run,并等待返回值。
  2. queue()
    以异步非阻塞方式执行run(),只支持接收一个值对象。调用queue()就返回一个用于接收执行结果的Future对象。若执行成功Future.get()返回单个返回值,但执行失败,如果没有重写fallback则抛出异常。
  3. observe()
    事件注册前执行run()/construct(),支持接收多个值对象,取决于发射源。调用observe()会返回一个hot Observable,也就是说,调用observe()自动触发执行run()/construct(),无论是否存在订阅者。

如果继承的是HystrixCommand,hystrix会从线程池中取一个线程以非阻塞方式执行run();如果继承的是HystrixObservableCommand,将以调用线程阻塞执行construct()。
4. toObservable()
事件注册后执行run()/construct(),支持接收多个值对象,取决于发射源。调用toObservable()会返回一个cold Observable,也就是说,调用toObservable()不会立即触发执行run()/construct(),必须有订阅者订阅Observable时才会执行。

如果继承的是HystrixCommand,hystrix会从线程池中取一个线程以非阻塞方式执行run(),调用线程不必等待run();如果继承的是HystrixObservableCommand,将以调用线程堵塞执行construct(),调用线程需等待construct()执行完才能继续往下走。

依赖隔离

Hystrix使用“舱壁模式”来实现线程池的隔离,它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只会对该依赖服务的调用产生影响,而不会拖慢其它的依赖服务。
通过实现对依赖服务的线程池隔离,可以带来如下优势:

  1. 应用自身得到完全保护,不会受不可控的依赖服务影响。
  2. 可以有效的降低接入新服务的风险。如果新服务接入后运行不稳定或存在问题,不会隐形其它服务
  3. 当依赖的服务从失败恢复正常后,它的线程池能够很快的被清理并且能够马上恢复服务。
  4. 当依赖的服务出现配置错误时,线程池能够快速的反应此问题。
  5. 当依赖的服务出现很大的性能变动时,线程池监控指标信息可以反应出这样的变化。
  6. 每个专有的线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步访问。

使用注解实现请求缓存

  1. @CacheResult该注解标记请求命令返回的结果应该被缓存,它必须与@hystrixCommand注解结合使用
  2. @CacheRemove该注解用来让请求命令的缓存失效,失效的缓存根据定义的key决定。
  3. @CacheKey该注解用来在请求命令的参数上标记,使其作为缓存的key之,如果没有标注则会使用所有参数。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    @CacheResult
@HystrixCommand(fallbackMethod = "helloFallback")
public String helloService(){
return restTemplate.getForEntity("http://EUREKA-CLIENT-HELLO/hello",String.class).getBody();
}
````

```java
@CacheResult(cacheKeyMethod="getUserByIdCacheKey")
@HystrixCommand(fallbackMethod = "helloFallback")
public User getUserById(Long id){
return restTemplate.getForEntity("http://EUREKA-CLIENT-HELLO/users/{1}",User.class,id);
}

private Long getUserByIdCacheKey(Long id){
return id;
}
1
2
3
4
5
6
7
@CacheResult
@HystrixCommand(fallbackMethod = "helloFallback")
public User getUserById(@CacheKey("id") Long id){
//通过@CacheKey实现的方式更简单,但是器优先级比cacheKeyMethod的优先级低
return restTemplate.getForEntity("http://EUREKA-CLIENT-HELLO/users/{1}",User.class,id);
}

@CacheKey注解除了可以指定方法方法参数作为缓存Key之外,还允许访问对象的内部属性作为缓存key

1
2
3
4
5
6
7
@CacheResult
@HystrixCommand(fallbackMethod = "helloFallback")
public User getUserById(@CacheKey("id") User user){
//通过@CacheKey实现的方式更简单,但是器优先级比cacheKeyMethod的优先级低
return restTemplate.getForEntity("http://EUREKA-CLIENT-HELLO/users/{1}",User.class,user.getId());
}

缓存清理

1
2
3
4
5
6
7
8
9
10
11
12
13
@CacheResult
@HystrixCommand(fallbackMethod = "helloFallback")
public User getUserById(@CacheKey("id") Long id){
//通过@CacheKey实现的方式更简单,但是器优先级比cacheKeyMethod的优先级低
return restTemplate.getForEntity("http://EUREKA-CLIENT-HELLO/users/{1}",User.class,id);
}

@CacheRemove(commandKey="getUserById")
@HystrixCommand
public void update(@CacheKey("id") User user){
eturn restTemplate.getForEntity("http://EUREKA-CLIENT-HELLO/users",User.class);
}

使用注解实现请求合并器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class UserService {

@Autowired
private RestTemplate restTemplate;

@HystrixCollapser(batchMethod = "findAll",collapserProperties = {
@HystrixProperty(name = "timerDelayInMilliseconds",value = "100")
})
public User find(long id){
return null;
}

@HystrixCommand
public List<User> findAll(List<Long> ids){
return restTemplate.getForObject("http://EUREKA-CLIENT-HELLO/users?ids={1}",List.class, StringUtil.join(ids,","));
}
}

声明式服务调用 Spring Cloud Feign

使用feign

  1. 添加相关依赖
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>RELEASE</version>
</dependency>

  1. 修改主类
1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class FeignConsumerApplication {

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

}
  1. 定义HelloService接口
1
2
3
4
5
6
7
@FeignClient("EUREKA-CLIENT-HELLO")
public interface HelloService {

@RequestMapping("/hello")
String hello();
}

  1. 创建一个ConsumerController
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class ConsumerController {

@Autowired
HelloService helloService;


@RequestMapping("/feign-consumer")
public String helloConsumer(){
return helloService.hello();
}
}
  1. 添加相关配置
1
2
3
4
5
6
7
8
9
10
spring:
application:
name: feign-consumer
server:
port: 8088
eureka:
client:
service-url:
defaultZone: http://localhost:8083/eureka

参数绑定

  1. 修改服务消费者Service类
1
2
3
4
5
6
@FeignClient("EUREKA-CLIENT-HELLO")
public interface HelloService {

@RequestMapping("/hello")
String hello(@RequestHeader("args") String args);
}
  1. 修改服务消费者Controller
1
2
3
4
5
6
7
8
9
@RestController
public class ConsumerController {
@Autowired
HelloService helloService;
@RequestMapping("/feign-consumer")
public String helloConsumer(String args){
return helloService.hello(args);
}
}
  1. 服务提供者
1
2
3
4
@RequestMapping("hello")
public String hello(@RequestHeader String args) throws InterruptedException {
return "hello"+ args;
}

相关配置

在定义Feign客户端的时候,我们使用@FeignCline注解。在初始化过程中,SpringCloud Feign会根据该注解的name属性和value属性指定的服务名,自动的创建一个同名的Ribbon客户端。
可以根据Ribbon服务名,来进行配置.

1
2
3
EUREKA-CLIENT-HELLO.ribbon.ConnectTimeout=500
EUREKA-CLIENT-HELLO.ribbon.ReadTimeout=2000
EUREKA-CLIENT-HELLO.ribbon.OkToRetryOnAllOperations=true

Feign客户端服务降级逻辑

  1. 继承HelloService接口,实现接口,实现服务降级逻辑
1
2
3
4
5
6
7
8
@Component
public class HelloServiceFallBack implements HelloService {

@Override
public String hello(@RequestParam("args") String args) {
return "error "+args;
}
}
  1. 在服务绑定接口中,通过fallback属性来指定对应的服务降级实现类
1
2
3
4
5
6
7
8
//fallback服务降级实现类
@FeignClient(value = "EUREKA-CLIENT-HELLO",fallback = HelloServiceFallBack.class)
public interface HelloService {

@RequestMapping("/hello")
String hello(@RequestHeader("args") String args);
}

  1. 在配置文件中开启服务hystrix
1
2
3
4
feign:
hystrix:
enabled: true

API网关服务 Spring Cloud Zuul

API网关是一个更智能的应用服务器,它的存在就像整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过他来进行调度和过滤。它除了要实现请求的路由、负载均衡、校验过滤等功能之外,还想需要完成对服务治理框架的结合,、请求转发时的熔断机制、服务的聚合等一系列高级功能。

Zuul构建API网关

  1. 新建srpingboot工程,添加相关依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
  1. 编辑应用主类,开启Zuul API网关服务功能
1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {

public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
  1. 编写配置信息
1
2
3
4
5
spring:
application:
name: api-gateway
server:
port: 6666
  1. 配置路由
    传统路由的配置方式
1
2
3
4
5
zuul:
routes:
apiAUrl: # 可以任意指定路由的名字
path: /api-a-utl/**
url: http://localhost:8080

面向服务的路由
通过整合Eureka,我们可以让路由的path不是映射到具体的utl,而是映射到某个具体的服务。

添加eureka客户端的的相关依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改配置文件整合eureka

1
2
3
4
5
6
7
8
9
10
11
12
zuul:
routes:
apiA:
path: /apia/**
serviceId: feign-consumer # 这里是服务的id
apiB:
path: /apib/**
serviceId: eureka-client-hello
eureka:
client:
service-url:
defaultZone: http://localhost:8083/eureka

请求过滤

  1. 编写过滤逻辑
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
package com.example.apigateway;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.LoggerFactory;

class AccessFilter extends ZuulFilter {


@Override
public String filterType() {
//获取过滤器的类型
return "pre";
}

@Override
public int filterOrder() {
//获取过滤器的顺序
return 0;
}

@Override
public boolean shouldFilter() {
//判断该过滤器是否需要被执行
return true;
}

@Override
public Object run() throws ZuulException {
//执行过滤逻辑
RequestContext context = RequestContext.getCurrentContext();
String args = context.getRequest().getParameter("args");
//这里模拟的过滤逻辑时参数为pass的才不会被过滤掉
if(!"pass".equals(args)){
System.out.println("nopass");
context.setSendZuulResponse(false);
context.setResponseStatusCode(401);
}else{
System.out.println("pass");
}

return null;
}
}

  1. 在主类中配置
1
2
3
4
5

@Bean
public AccessFilter accessFilter(){
return new AccessFilter();
}

路径匹配

通配符

通配符 说明
匹配任意单个字符
* 匹配任意数量的字符
** 匹配任意数量的字符,支持多级目录

忽略表达式

1
2
3
zuul.ignored-patterns=/**/hello/**
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceid=hello-service

过滤器详解

在Zuul中实现的过滤器必须包含四个基本特征:过滤类型、执行顺序、执行条件、具体操作。它们分别对应着ZuulFilter接口中定义的4个抽象方法

1
2
3
4
String filterType();
int filterOrder();
boolean shouldFilter();
Object run();
  1. filterType:该函数需要返回一个字符串来表示过滤器的类习惯。Zuul中定义了4种不同生命周期的过滤器类型
    pre:可以在请求被录入之前调用
    routing:在路由请求时被调用
    post:在routing和error过滤器之后被调用
    error: 在处理请求时发生错误时被调用
  2. filterOrder: 通过int值来表示过滤器的执行顺序,数值越小优先级越高
  3. shouldFilter:返回一个boolean值来判断该过滤器是否需要执行,我们可以通过此方法来指定过滤器的有效范围。
  4. run: 过滤器的具体逻辑。

KpGdJS.png

KpYkH1.png