问:为什么需要服务容错保护?
答:在微服务架构中,系统往往被拆分成很多个服务单元,每个服务单元之间通过服务注册与订阅的方式相互依赖。而每个服务单元部署在不同的机器或者不同的进程中,依赖一般通过远程调用的方式进行。这样一来,就有可能因为网络故障等原因导致服务调用异常或延迟。如果此时调用方的请求不断增加(如网购的秒杀、抢票等场景),最后因为服务的请求形成积压、阻塞,结果导致服务瘫痪,宕机,俗称“服务雪崩”。为解决这一问题,断路器等技术应运而生,用于服务保护机制。
问:什么是断路器?
答:当某个服务单元发生故障时,通过断路器的故障监控,向调用方返回一个错误响应,而不是长时间等待。这就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。生活中,最常见的就是“电路保险丝”,当线路中出现短路时,保险丝立即断掉以切断电路,防止发生火灾等严重后果。
问:为什么用SpringCloud Hystrix?
答:SpringCloud Hystrix 实现了断路器、线程隔离等一系列服务保护功能,它基于 Netflix 的开源框架 Hystrix 实现的,能对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并和服务监控等强大功能。
这篇博客接着上一篇。
我们在 ribbon-consumer 模块的 pom.xml 配置文件中,增加 Hystrix 的依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
完整的 pom.xml 配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>MyProject</artifactId>
<groupId>com.study</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ribbon-consumer</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>${netflix.eureka.client.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
</project>
在 ribbon-consumer 模块的启动类 RibbonConsumerApplication 中加入注解 @EnableCircuitBreaker 开启断路器功能,完整的代码如下:
package com.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-19 上午 11:50
*/
@EnableEurekaClient
@SpringBootApplication
@EnableDiscoveryClient //让该应用注册为 Eureka 客户端应用,以获得服务发现的能力
@EnableCircuitBreaker //开启断路器功能
//也可以使用 @SpringCloudApplication 注解,这样就不需要
//@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker 这3个注解了
//@SpringCloudApplication
public class RibbonConsumerApplication {
@Bean
@LoadBalanced //开启客户端负载均衡功能
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(RibbonConsumerApplication.class,args);
}
}
说明:
我们跟进 @SpringCloudApplication 这个注解,发现它已经增加了如下注解,这说明一个标准的 SpringCloud 应用已经包含服务发现以及断路器功能。
接下来,我们在 study 上鼠标右键,创建一个 package 包:service,与 controller 同一级。
在 service 包下创建 HelloHystrixService 类,代码如下:
package com.study.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-20 下午 9:22
*/
@Service
public class HelloHystrixService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "errorFallBack")
public String helloHystrix(){
return restTemplate.getForEntity("http://EUREKA-CLIENT-BIANDAN/say",String.class).getBody();
}
//定义 Hystrix 容错之后的返回方法
public String errorFallBack(){
return "做开发有 error 不可耻!";
}
}
说明:
1、在方法 helloHystrix() 上增加 @HystrixCommand(fallbackMethod = "errorFallBack") 注解,并且制定了回调方法 errorFallBack(),然后我们新建 errorFallBack() 回调方法。
2、在类中加入 @Service 注解,主要是让这个类让 Spring 容器来管理,同时 @Service 用于标注业务层组件。当组件不好归类的时候,我们可以使用 @Component 这个注解进行标注。
接着,我们在 ribbon-consumer 这个模块的 RibbonController 类里加入调用 HelloHystrixService 类的 helloHystrix() 方法。增加如下代码:
//注入 helloHystrixService 对象
@Autowired
private HelloHystrixService helloHystrixService;
@RequestMapping(value = "/hystrix",method = RequestMethod.GET)
public String testHystrix(){
return helloHystrixService.helloHystrix();
}
完整代码如下:
package com.study.controller;
import com.study.service.HelloHystrixService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-19 下午 11:04
*/
@RestController //@RestController 注解相当于 @ResponseBody + @Controller 合在一起的作用。
//@Controller //如果使用 @Controller 注解,则需要在方法上增加 @ResponseBody 注解
public class RibbonController {
@Autowired
private RestTemplate restTemplate;
//@ResponseBody
@RequestMapping(value = "/ribbon",method = RequestMethod.GET)
public String ribbonConsumer(){
return restTemplate.getForEntity("http://EUREKA-CLIENT-BIANDAN/say",String.class).getBody();
}
//注入 helloHystrixService 对象
@Autowired
private HelloHystrixService helloHystrixService;
@RequestMapping(value = "/hystrix",method = RequestMethod.GET)
public String testHystrix(){
return helloHystrixService.helloHystrix();
}
}
接下来,我们依次启动服务:注册中心(EurekaServerApplication)、注册中心集群(EurekaServerClusterApplication 可不启动)、服务提供者(EurekaClientApplication 9000端口、然后修改端口号=9001,再启动,一共2个服务。具体方法见上一篇博客)、服务消费者(RibbonConsumerApplication)。
在浏览器中输入:http://main.study.com:10000/hystrix 多次刷新访问,浏览器显示如下内容:
eureka-client-biandan 说:让天下没有难写的代码!from port =9000
eureka-client-biandan 说:让天下没有难写的代码!from port =9001
服务正常访问。
1、测试服务故障或服务宕机情况
我们把 服务提供者(EurekaClientApplication 9000端口)这个服务关闭,只留9001端口服务,再次访问上述地址。
这时候,浏览器的显示结果如下(多次刷新,一般会交替出现):
做开发有 error 不可耻!
eureka-client-biandan 说:让天下没有难写的代码!from port =9001
说明 Hystrix 断路器功能生效,并没有让客户端长时间等待服务返回结果。
2、测试网络延迟或者服务处理延迟等故障
接下来,我们模拟服务阻塞的情况,也就是服务长时间未响应的情况。在这之前,我们先了解 Hystrix 默认的超时时间
打开依赖包,找到 com.netflix.hystrix:hystrix-core:1.5.18(根据实际依赖的 jar 版本来,本系列教程是这个版本)
在 core 包下找到 com.netflix.hystrix.HystrixCommandProperties 类,如图:
在类 HystrixCommandProperties 中搜索:default_executionTimeoutInMilliseconds
所以得知,Hystrix 默认超时时间是1秒(1000毫秒=1秒)。
我们可以修改 Hystrix 的超时时间,在 ribbon-consumer 模块的 application.yml 配置文件加入如下代码:
# 修改 Hystrix 超时时间
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 1500
接下来,我们改造一下服务提供者 eureka-client 模块的 EurekaClientApplication 类。改造 say() 方法:
@RequestMapping("/say")
public String sayHello() throws InterruptedException {
//生成 0—200 之间的随机数,模拟服务正常和超时的情况
int sleepTime = new Random().nextInt(2000);
System.out.println("线程休眠了:"+sleepTime+" 毫秒");
Thread.sleep(sleepTime);
return clientName + " 说:让天下没有难写的代码!from port =" + port;
}
完整代码如下:
package com.study;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-15 下午 11:30
*/
@SpringBootApplication
@EnableEurekaClient //表明此类是一个客户端
@RestController
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
//从 yml 配置文件里获取客户端的名字
@Value("${spring.application.name}")
private String clientName;
@Value("${server.port}")
private String port;//获取配置文件的端口号
//测试方法:向调用者返回一句签名
@RequestMapping("/say")
public String sayHello() throws InterruptedException {
//生成 0—200 之间的随机数,模拟服务正常和超时的情况
int sleepTime = new Random().nextInt(2000);
System.out.println("线程休眠了:"+sleepTime+" 毫秒");
Thread.sleep(sleepTime);
return clientName + " 说:让天下没有难写的代码!from port =" + port;
}
}
接下来,重启 服务提供者(EurekaClientApplication 9000端口、然后修改端口号=9001,再启动,一共2个服务。具体方法见上一篇博客),浏览器地址输入:http://main.study.com:10000/hystrix 多次刷新访问,情况如下:
当浏览器输出以下错误信息,我们去 eureka-client 服务(需要对应端口号的服务)的控制台查看信息:
做开发有 error 不可耻!
线程休眠了:1997 毫秒
当浏览器输出信息正常时,我们去 eureka-client 服务(需要对应端口号的服务)的控制台查看信息:
eureka-client-biandan 说:让天下没有难写的代码!from port =9001
线程休眠了:966 毫秒
OK,测试通过。断路器的好处就体现出来了:不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
fallback处理:
当命令执行失败的时候,Hystrix 会进入 fallback 尝试回退处理,我们称该操作为“服务降级”。能够引起服务降级的情况有几种:
1、当前命令处于“熔断/短路”状态,断路器是打开的时候;
2、当前命令的线程池、请求队列或者信号量被占满的时候;
3、HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候。