前言
spring cloud 用的是 hystrix,是一个容错组件。
Hystrix实现了 超时机制和断路器模式。
Hystrix是Netflix开源的一个类库,用于隔离远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。主要有以下几点功能:
- 为系统提供保护机制。在依赖的服务出现高延迟或失败时,为系统提供保护和控制。
- 防止雪崩。
- 包裹请求:使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中运行。
- 跳闸机制:当某服务失败率达到一定的阈值时,Hystrix可以自动跳闸,停止请求该服务一段时间。
- 资源隔离:Hystrix为每个请求都的依赖都维护了一个小型线程池,如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。防止级联失败。
- 快速失败:Fail Fast。同时能快速恢复。侧重点是:(不去真正的请求服务,发生异常再返回),而是直接失败。
- 监控:Hystrix可以实时监控运行指标和配置的变化,提供近实时的监控、报警、运维控制。
- 回退机制:fallback,当请求失败、超时、被拒绝,或当断路器被打开时,执行回退逻辑。回退逻辑我们自定义,提供优雅的服务降级。
- 自我修复:断路器打开一段时间后,会自动进入“半开”状态,可以进行打开,关闭,半开状态的转换。
1、模拟hystrix
其实Hystrix完全可以用try catch实现,这里解释一下逻辑。
* try{
*
* 1. 发起向服务方的请求;
* 1.1 判断连接超时
* -> 这次请求 记录到服务里
* http请求 线程消耗
*
*
* map(URI,线程数)
* 线程池(线程数)
* 阈值 阀值
*
* 计数 连续失败次数 达到阈值
* count ++;
* if(count == 10){
*
* new romdom == 1 按时间
* 发请求
*
*
* throw exception;
* }
*
*
* 请求/不请求/半请求
* 开 关 半开
*
* if (当前线程满了){
* throw exception
* }
*
*
* 1.2 尝试向其他服务器发起请求
*
*
* 注解
*
*
* 2. 还是没成功
*
* }catch(Exception e){
*
* 1. 避免返回不友好的错误信息
* -> 好看点儿的页面 重试按钮 联系邮箱
*
*
* 2. return 另外一个东西 写到MQ里 admin 发个邮件
*
* return "客观稍后再来";
*
* }
*
*
* Hystrix 干的就是这件事儿
*/
这里的降级就是在请求一个服务器后,没请求到,请求其他服务器,还是不行,那么返回一种比较友好的界面,比如服务器正忙,一般用兜底数据处理。
而所谓熔断,则是在请求服务的阈值达到了一个上限后,不去请求了,直接到catch里,进行降级。但是一般会用半请求的方式,隔一段时间再请求下服务。
限流就是用线程隔离或者信号量隔离的方式,固定某个服务用多少个线程,防止consumer端调用provider时,线程不够用的情况。
降级一般是再某一段时间内,流量特别大,将资源都给另外的服务做的。熔断则是防止服务断了之后会影响到其他服务做的。
降级的方案一般有两种,一种返回一个友好的界面,重试按钮。另外一种是返回另外一个页面,但是写到MQ里,发个邮件通知。
2、Hystrix独立使用
在请求的那一端,
在里面的run就是try,getfallback就是catch(运行失败)
public class HystrixTest extends HystrixCommand {
protected HystrixTest(HystrixCommandGroupKey group) {
super(group);
// TODO Auto-generated constructor stub
}
public static void main(String[] args) {
// HystrixTest hystrixTest = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext"));
/**
* execute():以同步阻塞方式执行run()。以demo为例,调用execute()后,
* hystrix先创建一个新线程运行run(),
* 接着调用程序要在execute()调用处一直阻塞着,直到run()运行完成
*/
// System.out.println("result:" + hystrixTest.execute());
/**
* queue():以异步非阻塞方式执行run()。以demo为例,
* 一调用queue()就直接返回一个Future对象,
* 同时hystrix创建一个新线程运行run(),
* 调用程序通过Future.get()拿到run()的返回结果,
* 而Future.get()是阻塞执行的
*/
Future<String> futureResult = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")).queue();
String result = "";
try {
result = futureResult.get();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("程序结果:"+result);
}
@Override
protected Object run() throws Exception {
// TODO Auto-generated method stub
System.out.println("执行逻辑");
int i = 1/0;
return "ok";
}
@Override
protected Object getFallback() {
// TODO Auto-generated method stub
return "getFallbackgetFallback";
}
}
程序测试结果。(因为走了run,有个错误,然后返回到了getfallback中,与try,catch的逻辑一样)
3、Hystrix整合RestTemplate
先启动Eureka服务server。
在userconsumer方编写service:
@Service
public class RestService {
@Autowired
RestTemplate template;
// 请求不同进back方法。
@HystrixCommand(defaultFallback = "back")
public String alive() {
String url = "http://user-provider/User/alive";
String str= template.getForObject(url, String.class);
return str;
}
public String back(){
return "哈哈";
}
}
在consumer的controller层编写:
@GetMapping("/alive2")
public String alive2() {
return "Consumer" + port + "->>>>>>>" +rest.alive();
}
在provider方用一个错误测试:
@Override
public String alive() {
int i = 1/0;
return "port:" + port;
}
效果:
如果去除错误的情况:
能够正常访问port端口。
3、Hystrix整合RestTemplate
有两种方式可以整合feign。
直接指定一个类或者拿fallbackfactory去做。
第一种方式:
在consumerApi中的@FeignClient中再加个配置属性。
@FeignClient(name = "user-provider",fallback =userback.class)
需要在properties加:(默认feign调用失败不用hystrix处理)
feign.hystrix.enabled=true
注意在整合的时候,会出现user已经被映射了的问题,注意需要在发布的api中改一下:
feign和hystrix结合使用的时候,在发布的api里,会在方法中的@RequestMapping(“/alive”)的路径中加上“/user”,此时就变成了@RequestMapping(“/user/alive”),但是这个方法在User-Provider中已经被注册过了所以会报重复“There is already ‘XXXX’ bean method”的错误!feign与hystrix会注册两次!这是个bug。只有去了才能使用。
下面写userback的代码:(实现下consumerApi,然后每个方法对应失败的措施)
@Component
public class userback implements ConsumerApi {
@Override
public Map<Integer, String> getMap(Integer id) {
return null;
}
@Override
public Map<Integer, String> getMap2(Integer id, String name) {
return null;
}
@Override
public Map<Integer, String> getMap3(Map<String, Object> map) {
return null;
}
@Override
public Map<Integer, String> postMap(Map<String, Object> map) {
return null;
}
@Override
public String alive() {
return "降级了";
}
@Override
public String getById(Integer id) {
return null;
}
}
上面调用alive时,失败会显示降级了。成功则显示port
失败:
在这降级页面可以做兜底数据。(很关键)
返回有点数据,但是数据不是很新。
不同的错误而导致的异常,比如连接或者服务不可用。需要粒度更细。
这里是第二种方式:
comsumerapi用这样的注解方式:
@FeignClient(name = "user-provider",fallbackFactory = userproviderBackFactory.class)
public interface ConsumerApi extends UserApi {
这里的userproviderfactort并不是实现consumerapi,而是实现一个工厂类。
@Component
public class userproviderBackFactory implements FallbackFactory<ConsumerApi> {
//这里的异常包含了本地的异常也包含了远端的异常
@Override
public ConsumerApi create(Throwable cause) {
return new ConsumerApi() {
@Override
public Map<Integer, String> getMap(Integer id) {
return null;
}
@Override
public Map<Integer, String> getMap2(Integer id, String name) {
return null;
}
@Override
public Map<Integer, String> getMap3(Map<String, Object> map) {
return null;
}
@Override
public Map<Integer, String> postMap(Map<String, Object> map) {
return null;
}
@Override
public String alive() {
System.out.println(cause);
if(cause instanceof FeignException.InternalServerError){
return "远程服务器500";
}
cause.printStackTrace();
System.out.println(ToStringBuilder.reflectionToString(cause));
return "呵呵";
}
@Override
public String getById(Integer id) {
return null;
}
};
}
这里提前挂差到了provier关掉的时候,会出现InternalServerError错误,在alive里面捕捉到了,返回远程服务器500.
provier方和上面第一种方法类似,故不赘述。
4、信号量隔离与线程隔离
Hystrix支持两种隔离模式,
默认情况下hystrix使用线程池控制请求隔离
线程池隔离技术,是用 Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。
信号量隔离主要维护的是Tomcat的线程,不需要内部线程池,更加轻量级。
Hystrix针对具体的服务,线程池里给他分配线程数。(线程隔离)不至于给consumer拖死。一个服务的线程池挂了,不会影响其他服务的线程池。
下面是信号量隔离机制。
一个用户一个worker线程,这种时阻塞式web。
如果100个worker线程,那么有100用户可以执行。所以hystrix的线程池不能设置太低与太高。
信号量检查当前(一个)服务的信号量是否有余额。拿不到信号量直接fallback。信号量绑定的是tomcat的worker线程池里的线程。没有维护一个线程池,比较好。
那为什么要用线程池隔离?好处是有失败策略,可以异步请求,解放worker的线程阻塞,可以将异常线程池隔离。
算法速度快、代码逻辑健壮用信号量。一般web请求用线程池隔离。
使用,需要加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>
spring-cloud-starter-netflix-hystrix-dashboard
</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后需要在consumer启动项上加注解:
@EnableHystrixDashboard
注意,这个dashbord是actuator,所以也需要actuator的依赖。
这里还需要properties加:
management.endpoints.web.exposure.include=*
hystrix.dashboard.proxy-stream-allow-list=*
`这里默认情况下是线程隔离,所以如果想用信号量隔离,添加properties:
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
访问hystrix.stream:
最后有个threadpool,是用的线程隔离方式。
还有个图形化的。
url则是actuator/hystrix.stream.进入后可以看到pool size为1.
访问多次pool size则会不断加。
这里poolsize最大为10个。这是线程池做的隔离。
下面是信号量隔离的图。
没有用线程池。