服务容错保护Hystrix
在微服务架构中,我们将服务拆分为多个服务,通过远程调用的方式执行,很容易出现因为网络或服务本身问题而导致的调用故障和延迟,并且导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最终就会出现因为等待故障方响应而形成任务积压,最终导致自身服务的瘫痪。
在分布式架构中,但某个服务单元发生故障之后,通过断路器的故障监测,向调用方返回一个响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
Hystrix实现了断路器,线程隔离等一系列服务保护功能。
Hystrix入门
- 导入相关依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> <version>RELEASE</version> </dependency>
|
- 在主类上添加
@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); }
}
|
- 改造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"; }
}
|
- 改造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(); } }
|
- 测试
首先启动eureka注册中心,然后分别启动两个EUREKA-CLIENT-HELLO服务,最后启动我们的服务消费者ribbon-consumer。
首先在服务都完好的情况下,我们请求ribbon-consumer,结果一切正常。随后我们关闭8084端口的EUREKA-CLIENT-HELLO服务。ribboon-consumer并没有出现长时间的等待和报错,而是调用了服务中的helloFallback方法。
原理分析

hystrix的整个工作流:
- 构造一个HystrixCommand或HystrixObservableCommand对象,用于封装请求,并在构造方法中配置请求被执行需要的参数。
- 执行命令。Hystrix提供了四种执行命令的方式。
- 判断是否使用缓存响应请求,若启用了缓存且缓存可用,直接使用缓存响应请求。
- 判断熔断器是否打开,如果打开,跳到第8步。
- 判断线程池/队列/信号是否已满,已满则跳到第8步
- 执行HystrixObservableCommand.construct()或HystrixCommand.run()如果执行失败或超时,跳到第8步,否则跳到第9步
- 统计熔断器监控指标
- 轴FallBack备用逻辑(服务降级)
- 返回请求响应
执行命令的四种方法:
execute()和queue()适用于HystrixCommand对象,而observe()和toObservable()适用于HystrixObservableCommand对象。
- execute()
以同步阻塞的方式执行run,只支持接收一个值对象。hystrix会从线程池中去一个线程来执行run,并等待返回值。
- queue()
以异步非阻塞方式执行run(),只支持接收一个值对象。调用queue()就返回一个用于接收执行结果的Future对象。若执行成功Future.get()返回单个返回值,但执行失败,如果没有重写fallback则抛出异常。
- 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使用“舱壁模式”来实现线程池的隔离,它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只会对该依赖服务的调用产生影响,而不会拖慢其它的依赖服务。
通过实现对依赖服务的线程池隔离,可以带来如下优势:
- 应用自身得到完全保护,不会受不可控的依赖服务影响。
- 可以有效的降低接入新服务的风险。如果新服务接入后运行不稳定或存在问题,不会隐形其它服务
- 当依赖的服务从失败恢复正常后,它的线程池能够很快的被清理并且能够马上恢复服务。
- 当依赖的服务出现配置错误时,线程池能够快速的反应此问题。
- 当依赖的服务出现很大的性能变动时,线程池监控指标信息可以反应出这样的变化。
- 每个专有的线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步访问。
使用注解实现请求缓存
@CacheResult该注解标记请求命令返回的结果应该被缓存,它必须与@hystrixCommand注解结合使用
@CacheRemove该注解用来让请求命令的缓存失效,失效的缓存根据定义的key决定。
@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){
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){
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){
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 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 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); }
}
|
- 定义HelloService接口
1 2 3 4 5 6 7
| @FeignClient("EUREKA-CLIENT-HELLO") public interface HelloService { @RequestMapping("/hello") String hello(); }
|
- 创建一个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 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
|
参数绑定
- 修改服务消费者Service类
1 2 3 4 5 6
| @FeignClient("EUREKA-CLIENT-HELLO") public interface HelloService {
@RequestMapping("/hello") String hello(@RequestHeader("args") String args); }
|
- 修改服务消费者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 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客户端服务降级逻辑
- 继承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; } }
|
- 在服务绑定接口中,通过fallback属性来指定对应的服务降级实现类
1 2 3 4 5 6 7 8
| @FeignClient(value = "EUREKA-CLIENT-HELLO",fallback = HelloServiceFallBack.class) public interface HelloService {
@RequestMapping("/hello") String hello(@RequestHeader("args") String args); }
|
- 在配置文件中开启服务hystrix
1 2 3 4
| feign: hystrix: enabled: true
|
API网关服务 Spring Cloud Zuul
API网关是一个更智能的应用服务器,它的存在就像整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过他来进行调度和过滤。它除了要实现请求的路由、负载均衡、校验过滤等功能之外,还想需要完成对服务治理框架的结合,、请求转发时的熔断机制、服务的聚合等一系列高级功能。
Zuul构建API网关
- 新建srpingboot工程,添加相关依赖
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>
|
- 编辑应用主类,开启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 2 3 4 5
| spring: application: name: api-gateway server: port: 6666
|
- 配置路由
传统路由的配置方式
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 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"); if(!"pass".equals(args)){ System.out.println("nopass"); context.setSendZuulResponse(false); context.setResponseStatusCode(401); }else{ System.out.println("pass"); } return null; } }
|
- 在主类中配置
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();
|
- filterType:该函数需要返回一个字符串来表示过滤器的类习惯。Zuul中定义了4种不同生命周期的过滤器类型
pre:可以在请求被录入之前调用
routing:在路由请求时被调用
post:在routing和error过滤器之后被调用
error: 在处理请求时发生错误时被调用
- filterOrder: 通过int值来表示过滤器的执行顺序,数值越小优先级越高
- shouldFilter:返回一个boolean值来判断该过滤器是否需要执行,我们可以通过此方法来指定过滤器的有效范围。
- run: 过滤器的具体逻辑。

