什么是Hystrix

前面已经讲完了 Feign 和 Ribbon,今天我们来研究 Netflix 团队开发的另一个类库--Hystrix。

从抽象层面看,Hystrix 是一个保护器。它可以保护我们的应用不会因为某个依赖的故障而 down 掉。

目前,官方已不再迭代 Hystrix,一方面是认为 Hystrix 已经足够稳定了,另一方面是转向了更具弹性的保护器(而不是根据预先配置来启用保护),例如 resilience4j。当然,停止迭代并不是说 Hystrix 已经没有价值,它的很多思想仍值得学习和借鉴。

和之前一样,本文研究的 Hystrix 是原生的,而不是被 Spring 层层封装的。

Hystrix解决了什么问题

关于这个问题,官方已经给了详细的答案(见文末链接的官方 wiki)。这里我结合着给出自己的一些理解(下面的图也是借用官方的)。

我们的应用经常需要去调用某些依赖。这里说的依赖,一般是远程服务,那为什么不直接说远程服务呢?因为 Hystrix 适用的场景要更宽泛一些,当我们学完 Hystrix 就会发现,即使是应用里调用的普通方法也可以算是依赖。

调用这些依赖,有可能会遇到异常:调用失败或调用超时

先说说调用失败。当某个依赖 down 掉时,我们的应用调用它都会失败。针对这种情况,我们会考虑快速失败,从而减少大量调用失败的开销。

再说说调用超时。不同于调用失败,这个时候依赖还是可用的,只是需要花费更多的时间来获取我们想要的东西。当流量比较大时,线程池将很快被耗尽。在大型的项目中,一个依赖的超时带来的影响会被放大,甚至会导致整个系统瘫痪。所以,调用失败也需要快速失败。

针对上面说的的异常,Hystrix 可以及时将故障的依赖隔离开,后续的调用都会快速失败,直到依赖恢复正常。

如何实现

调用失败或超时到达一定的阈值后,Hystrix 的保护器将被触发开启。

调用依赖之前,Hystrix 会检查保护器是否开启,如果开启会直接走 fall back,如果没有开启,才会执行调用操作。

另外,Hystrix 会定时地去检查依赖是否已经恢复,当依赖恢复时,将关闭保护器,整个调用链路又恢复正常。

当然,实际流程要更复杂一些,还涉及到了缓存、线程池等。官方提供了一张图,并给出了较为详细的描述。

如何使用

这里我用具体例子来说明各个节点的逻辑,项目代码见文末链接。

包装为command

首先,要使用 Hystrix,我们需要将对某个依赖的调用请求包装成一个 command,具体通过继承HystrixCommandHystrixObservableCommand进行包装。继承后我们需要做三件事:

  1. 在构造中指定 commandKey 和 commandGroupKey。需要注意的是,相同 commandGroupKey 的 command 会共用一个线程池,相同 commandKey 的会共用一个保护器和缓存。例如,我们需要根据用户 id 从 UC 服务获取用户对象,可以让所有 UC 接口共用一个 commandGroupKey,而不同的接口采用不同的 commandKey。
  2. 重写 run 或 construct 方法。这个方法里放的是我们调用某个依赖的代码。我可以放调用远程服务的代码,也可以随便打印一句话,因此,我前面说过,依赖的定义可以更宽泛一些,而不仅限于远程服务。
  3. 重写 getFallback 方法。当快速失败时,就会走这个方法。
public class CommandGetUserByIdFromUserService extends HystrixCommand<DataResponse<User>> {

    private final String userId;

    public CommandGetUserByIdFromUserService(String userId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // 相同command group共用一个ThreadPool
.andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// 相同command key共用一个CircuitBreaker、requestCache
);
this.userId = userId;
} /**
* 执行最终任务,如果继承的是HystrixObservableCommand则重写construct()
*/
@Override
protected DataResponse<User> run() {
return userService.getUserById(userId);
} /**
* 该方法在以下场景被调用
* 1. 最终任务执行时抛出异常;
* 2. 最终任务执行超时;
* 3. 断路器开启时,请求短路;
* 4. 连接池、队列或信号量耗尽
*/
@Override
protected DataResponse<User> getFallback() {
return DataResponse.buildFailure("fail or timeout");
}
}

执行command

然后,只有执行 command,上面的图就“动起来”了。有四种方法执行 command,调用 execute() 或 observe() 会马上执行,而调用 queue() 或 toObservable() 不会马上执行,要等 future.get() 或 observable.subscribe() 时才会被执行。

    @Test
public void testExecuteWays() throws Exception { DataResponse<User> response = new CommandGetUserByIdFromUserService("1").execute();// execute()=queue().get() 同步
LOG.info("command.execute():{}", response); Future<DataResponse<User>> future = new CommandGetUserByIdFromUserService("1").queue();//queue()=toObservable().toBlocking().toFuture() 同步
LOG.info("command.queue().get():{}", future.get()); Observable<DataResponse<User>> observable = new CommandGetUserByIdFromUserService("1").observe();//hot observable 异步 observable.subscribe(x -> LOG.info("command.observe():{}", x)); Observable<DataResponse<User>> observable2 = new CommandGetUserByIdFromUserService("1").toObservable();//cold observable 异步 observable2.subscribe(x -> LOG.info("command.toObservable():{}", x));
}

是否使用缓存

接着,进入 command 的逻辑后,Hystrix 会先判断是否使用缓存。

默认情况下,缓存是禁用的,我们可以通过重写 command 的 getCacheKey() 来开启(只要返回非空,都会开启)。

    @Override
protected String getCacheKey() {
return userId;
}

需要注意一点,用到缓存(HystrixRequestCache)、请求日志(HystrixRequestLog)、批处理(HystrixCollapser)时需要初始化HystrixRequestContext,并按以下 try...finally 格式调用:

    @Test
public void testCache() {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
try {
CommandGetUserByIdFromUserService command1 = new CommandGetUserByIdFromUserService("1");
command1.execute();
// 第一次调用时缓存里没有
assertFalse(command1.isResponseFromCache()); CommandGetUserByIdFromUserService command2 = new CommandGetUserByIdFromUserService("1");
command2.execute();
// 第二次调用直接从缓存拿结果
assertTrue(command2.isResponseFromCache());
} finally {
context.shutdown();
}
// zzs001
}

保护器是否开启

接着,Hystrix 会判断保护器是否开启。

这里我在 command 的 run 方法中手动制造 fail 或 time out。另外,我们可以通过 HystrixCommandProperties 调整保护器开启的阈值。

    public CommandGetUserByIdFromUserService(String userId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // 相同command group共用一个ThreadPool
.andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// 相同command key共用一个CircuitBreaker、requestCache
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(10)
.withCircuitBreakerErrorThresholdPercentage(50)
.withMetricsHealthSnapshotIntervalInMilliseconds(1000)
.withExecutionTimeoutInMilliseconds(1000)
));
this.userId = userId;
}
@Override
protected DataResponse<User> run() {
LOG.info("执行最终任务,线程为:{}", Thread.currentThread());
// 手动制造超时
/*try {
Thread.sleep(1200);
} catch(InterruptedException e) {
e.printStackTrace();
}*/
// 手动制造异常
throw new RuntimeException("");
//return UserService.instance().getUserById(userId);
}

这个时候,当调用失败达到一定阈值后,保护器被触发开启,后续的请求都会直接走 fall back。

    @Test
public void testCircuitBreaker() {
CommandGetUserByIdFromUserService command;
int count = 1;
do {
command = new CommandGetUserByIdFromUserService("1");
command.execute();
count++;
} while(!command.isCircuitBreakerOpen());
LOG.info("调用{}次之后,断路器开启", count); // 这个时候再去调用,会直接走fall back
command = new CommandGetUserByIdFromUserService("1");
command.execute();
assertTrue(command.isCircuitBreakerOpen());
}

连接池、队列或信号量是否耗尽

即使保护器是关闭状态,我们也不能马上调用依赖,需要先检查连接池或信号量是否耗尽(通过 HystrixCommandProperties 可以配置使用线程池还是信号量)。

因为默认的线程池比较大,所以,这里我通过 HystrixThreadPoolProperties 调小了线程池。

    public CommandGetUserByIdFromUserService(String userId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService")) // 相同command group共用一个ThreadPool
.andCommandKey(HystrixCommandKey.Factory.asKey("UserService_GetUserById"))// 相同command key共用一个CircuitBreaker、requestCache
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(2)
.withMaxQueueSize(5)
.withQueueSizeRejectionThreshold(5)
));
this.userId = userId;
}

这个时候,当线程池耗尽后,后续的请求都会直接走 fall back,而保护器并没有开启。

    @Test
public void testThreadPoolFull() throws InterruptedException { int maxRequest = 100; int i = 0;
do {
CommandGetUserByIdFromUserService command = new CommandGetUserByIdFromUserService("1");
command.toObservable().subscribe(v -> LOG.info("non-blocking command.toObservable():{}", v));
LOG.info("是否线程池、队列或信号量耗尽:{}", command.isResponseRejected()); } while(i++ < maxRequest - 1); // 这个时候再去调用,会直接走fall back
CommandGetUserByIdFromUserService command = new CommandGetUserByIdFromUserService("1");
command.execute();
// 线程池、队列或信号量耗尽
assertTrue(command.isResponseRejected());
assertFalse(command.isCircuitBreakerOpen()); Thread.sleep(10000);
// zzs001
}

结语

以上简单地讲完了 Hystrix。阅读官方的 wiki,再结合上面的几个例子,相信大家可以对 Hystrix 有较深的了解。

最后,感谢阅读,欢迎私信交流。

参考资料

Home · Netflix/Hystrix Wiki · GitHub

相关源码请移步:https://github.com/ZhangZiSheng001/hystrix-demo

本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/15567420.html

如何使用原生的Hystrix的更多相关文章

  1. SpringCloud实战-Feign声明式服务调用

    在前面的文章中可以发现当我们通过RestTemplate调用其它服务的API时,所需要的参数须在请求的URL中进行拼接,如果参数少的话或许我们还可以忍受,一旦有多个参数的话,这时拼接请求字符串就会效率 ...

  2. SpringCloud-Feign声明式服务调用

    在前面的文章中可以发现当我们通过RestTemplate调用其它服务的API时,所需要的参数须在请求的URL中进行拼接,如果参数少的话或许我们还可以忍受,一旦有多个参数的话,这时拼接请求字符串就会效率 ...

  3. 微服务(二)hystrix

    特性 1.延迟和失败容忍 防止级联错误,错误回退,优雅降级.快速失败和恢复 线程和信号量隔离 2.实时监控和配置更改 3.并发 并行执行,请求缓存,自动批处理失败请求 总运行流程 当你发出请求后,hy ...

  4. spring cloud: Hystrix(五):如禁止单个FeignClient使用hystrix

    spring cloud: Hystrix(五):如禁止单个FeignClient使用hystrix 首先application.yml / applicatoin.propreties的配置项:fe ...

  5. 服务容错保护断路器Hystrix之六:缓存功能的使用

    高并发环境下如果能处理好缓存就可以有效的减小服务器的压力,Java中有许多非常好用的缓存工具,比如Redis.EHCache等,当然在Spring Cloud的Hystrix中也提供了请求缓存的功能, ...

  6. 服务容错保护断路器Hystrix之二:Hystrix工作流程解析

    一.总运行流程 当你发出请求后,hystrix是这么运行的 红圈 :Hystrix 命令执行失败,执行回退逻辑.也就是大家经常在文章中看到的“服务降级”. 绿圈 :四种情况会触发失败回退逻辑( fal ...

  7. 传统项目利用Hystrix实现热点接口的服务隔离

    这段时间接了个需求,需要在我目前负责的数据系统上加个接口,主要是实现用户行为的记录.前端对接的项目主要有公司的PC,WAP,WEIXIN,APP等,每个端大概有两台左右的负载.因为目前我的这个项目主要 ...

  8. Hystrix使用

    Hystrix是Netflix开源的一款容错系统,能帮助使用者码出具备强大的容错能力和鲁棒性的程序.如果某程序或class要使用Hystrix,只需简单继承HystrixCommand/Hystrix ...

  9. zuul源码分析-探究原生zuul的工作原理

    前提 最近在项目中使用了SpringCloud,基于zuul搭建了一个提供加解密.鉴权等功能的网关服务.鉴于之前没怎么使用过Zuul,于是顺便仔细阅读了它的源码.实际上,zuul原来提供的功能是很单一 ...

随机推荐

  1. 使用Golang搭建gRPC服务提供给.NetCore客户端调用

    目录 gRPC概述 RPC gRPC又是什么呢 安装 Golang IDE(Goland) Protocol Buffer 下载Protocal Buffer 配置Protocal Buffer编译器 ...

  2. Java程序的执行过程

    Java程序的执行过程 编译器将 Java 源代码编译成字节码class文件 类加载到 JVM 里面后,执行引擎把字节码转为可执行代码 执行的过程,再把可执行代码转为机器码,由底层的操作系统完成执行

  3. Java-多态(下)

    多态 一种类型的多种状态 还有一个小汽车的例子再理解下 汽车接口(相当于父类) package com.oop.demo10; public interface Car { String getNam ...

  4. NOIP&CSP 考前 Dev-cpp 的选项问题和考试心态

    (进入考场后您将获得一个崭新的 \(Dev-cpp\),没有中文,没有编译选项,没有缺省源:我还将获得一个崭新的脑子,没有心态,没有智商,没有调试能力--) 中文 \[Step1 \] \[Step2 ...

  5. Java初步学习——2021.09.23每日报告,第三周周四

    (1)今天做了什么: (2)明天准备做什么? (3)遇到的问题,如何解决? 学习数组,编写了一个随机选牌的代码.自己最开始一直想只设置一个字符串数组,利用随机数来输出,但那样对字符串赋值会比较麻烦.可 ...

  6. Flask的环境配置

      Flask django是大而全,提供所有常用的功能 flask是小而精,只提供核心功能 环境配置 为了防止 django和 flask环境相互冲突,可以使用 虚拟环境分割开 pip instal ...

  7. python反序列化1(__reduce__)

    part1:不求甚解的复现 对于服务端源码:    编写恶意序列化对象生成程序: 将生成的恶意序列化对象输入服务端user,使其执行系统命令.(上面那俩其实都行) part2:原理解释 b'xxx'是 ...

  8. 什么,你还使用 webpack?别人都在用 vite 搭建项目了

    一.vite 到底是干嘛的? vite 实际上就是一个面向现代浏览器,基于 ES module 实现了一个更轻快的项目构建打包工具. vite 是法语中轻快的意思. vite 的特点: 1.轻快的冷服 ...

  9. 【数据结构与算法Python版学习笔记】树——相关术语、定义、实现方法

    概念 一种基本的"非线性"数据结构--树 根 枝 叶 广泛应用于计算机科学的多个领域 操作系统 图形学 数据库 计算机网络 特征 第一个属性是层次性,即树是按层级构建的,越笼统就越 ...

  10. Elasticsearch 中为什么选择倒排索引而不选择 B 树索引

    目录 前言 为什么全文索引不使用 B+ 树进行存储 全文检索 正排索引 倒排索引 倒排索引如何存储数据 FOR 压缩 RBM 压缩 倒排索引如何存储 字典树(Tria Tree) FST FSM 构建 ...