在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在 Spring Cloud 可以用 RestTemplate+Ribbon 和 Feign 来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证 100% 可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet 容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩” 效应。
为了解决这个问题,业界提出了断路器模型。
在生活中,如果电路的负载过高,保险箱会自动跳闸,以保护家里的各种电器,这就是熔断器的一个活生生例子。在 Hystrix 中也存在这样一个熔断器,当所依赖的服务不稳定时,能够自动熔断,并提供有损服务,保护服务的稳定性。在运行过程中,Hystrix 会根据接口的执行状态(成功、失败、超时和拒绝),收集并统计这些数据,根据这些信息来实时决策是否进行熔断。
Netflix has created a library called Hystrix that implements the circuit breaker pattern. In a microservice architecture it is common to have multiple layers of service calls.
. —-摘自官网
Netflix 开源了 Hystrix 组件,实现了断路器模式,SpringCloud 对这一组件进行了整合。 在微服务架构中,一个请求需要调用多个服务是非常常见的,如下图:
较底层的服务如果出现故障,会导致连锁故障。当对特定的服务的调用的不可用达到一个阀值(Hystric 是 5 秒 20 次)断路器将会被打开。
断路打开后,可用避免连锁故障,fallback 方法可以直接返回一个固定值。
在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有的时候某些依赖服务出现故障也是很正常的。
Hystrix 可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制。
Hystrix 通过将依赖服务进行资源隔离,进而阻止某个依赖服务出现故障时在整个系统所有的依赖服务调用中进行蔓延;同时 Hystrix 还提供故障时的 fallback 降级机制。
总而言之,Hystrix 通过这些方法帮助我们提升分布式系统的可用性和稳定性。
Hystrix 是高可用性保障的一个框架。Netflix(可以认为是国外的优酷或者爱奇艺之类的视频网站)的 API 团队从 2011 年开始做一些提升系统可用性和稳定性的工作,Hystrix 就是从那时候开始发展出来的。
在 2012 年的时候,Hystrix 就变得比较成熟和稳定了,Netflix 中,除了 API 团队以外,很多其他的团队都开始使用 Hystrix。
时至今日,Netflix 中每天都有数十亿次的服务间调用,通过 Hystrix 框架在进行,而 Hystrix 也帮助 Netflix 网站提升了整体的可用性和稳定性。
2018 年 11 月,Hystrix 在其 Github 主页宣布,不再开放新功能,推荐开发者使用其他仍然活跃的开源项目。维护模式的转变绝不意味着 Hystrix 不再有价值。相反,Hystrix 激发了很多伟大的想法和项目,我们高可用的这一块知识还是会针对 Hystrix 进行讲解。
•对依赖服务调用时出现的调用延迟和调用失败进行控制和容错保护。
•在复杂的分布式系统中,阻止某一个依赖服务的故障在整个系统中蔓延。比如某一个服务故障了,导致其它服务也跟着故障。
•提供 fail-fast(快速失败)和快速恢复的支持。
•提供 fallback 优雅降级的支持。
•支持近实时的监控、报警以及运维操作。
•阻止任何一个依赖服务耗尽所有的资源,比如 tomcat 中的所有线程资源。
•避免请求排队和积压,采用限流和 fail fast 来控制故障。
•提供 fallback 降级机制来应对故障。
•使用资源隔离技术,比如 bulkhead(舱壁隔离技术)、circuit breaker(断路技术)来限制任何一个依赖服务的故障的影响。
•通过近实时的统计/监控/报警功能,来提高故障发现的速度。
•通过近实时的属性和配置热修改功能,来提高故障处理和恢复的速度。
•保护依赖服务调用的所有故障情况,而不仅仅只是网络故障情况。
改造 serice-ribbon 工程的代码,首先在 pox.xml 文件中加入 spring-cloud-starter-hystrix 的起步依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
在程序的启动类 SpringCloudServiceRibbonApplication 加@EnableHystrix注解开启 Hystrix:
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableHystrixDashboard
public class SpringCloudServiceRibbonApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudServiceRibbonApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
}
改造 UserService 类,在 query 方法上加上@HystrixCommand注解。该注解对该方法创建了熔断器的功能,并指定了 fallbackMethod 熔断方法,熔断方法直接返回了一个对象,代码如下:
@Service
public class UserService {
@Autowired
RestTemplate restTemplate;
@HystrixCommand(commandKey="queryCommandKey",groupKey = "queryGroup",threadPoolKey="queryThreadPoolKey",fallbackMethod = "queryFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100"),//指定多久超时,单位毫秒。超时进fallback
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "3"),//判断熔断的最少请求数,默认是10;只有在一个统计窗口内处理的请求数量达到这个阈值,才会进行熔断与否的判断
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),//判断熔断的阈值,默认值50,表示在一个统计窗口内有50%的请求处理失败,会触发熔断
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "100"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "100000")
})
public List<User> query(){
return restTemplate.getForObject("http://service-user/user/query",List.class);
}
public List<User> queryFallback(){
List<User> list = new ArrayList<>();
User user = new User();
user.setId("1211");
user.setName("queryFallback");
list.add(user);
return list;
}
}
启动:service-ribbon 工程,当我们访问http://127.0.0.1:9527/user/query,浏览器显示:
[{
"id": "id0",
"name": "testname0"
},
{
"id": "id1",
"name": "testname1"
},
{
"id": "id2",
"name": "testname2"
}
]
此时关闭 service-user 工程,当我们再访问http://127.0.0.1:9527/user/query,浏览器会显示:
[{
"id": "1211",
"name": "queryFallback"
}]
这就说明当 service-user 工程不可用的时候,service-ribbon 调用 service-user 的 API 接口时,会执行快速失败,直接返回一组字符串,而不是等待响应超时,这很好的控制了容器的线程阻塞。
Feign 是自带断路器的,在 D 版本的 Spring Cloud 中,它没有默认打开。需要在配置文件中配置打开它,在配置文件加以下代码:
feign:
hystrix:
enabled: true
基于 service-feign 工程进行改造,只需要在 FeignClient 的 UserService 接口的注解中加上 fallback 的指定类就行了:
@FeignClient(value="service-user",fallback = UserServiceFallback.class)
public interface UserService {
@RequestMapping(value="/user/query",method = RequestMethod.GET)
public List<User> query();
}
UserServiceFallback 需要实现 UserService 接口,并注入到 Ioc 容器中,代码如下:
@Component
public class UserServiceFallback implements UserService {
@Override
public List<User> query() {
List<User> list = new ArrayList<>();
User user = new User();
user.setId("1211");
user.setName("feignFallback");
list.add(user);
return list;
}
}
启动 servcie-feign 工程,浏览器打开http://127.0.0.1:9528/user/queryservice-user 工程没有启动,网页显示:注意此时
[{
"id": "1211",
"name": "feignFallback",
"date": null
}]
这证明断路器起到作用了。
基于 service-ribbon 改造,Feign 的改造和这一样。
首选在 pom.xml 引入 spring-cloud-starter-hystrix-dashboard 的起步依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>
在主程序启动类中加入@EnableHystrixDashboard 注解,开启 hystrixDashboard:
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableHystrixDashboard
public class ServiceRibbonApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceRibbonApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
打开浏览器:访问http://localhost:9527/hystrix,界面如下:
点击 monitor stream,进入下一个界面,访问:http://127.0.0.1:9527/user/query
此时会出现监控界面:
看单个的 Hystrix Dashboard 的数据并没有什么多大的价值,要想看这个系统的 Hystrix Dashboard 数据就需要用到 Hystrix Turbine。Hystrix Turbine 将每个服务 Hystrix Dashboard 数据进行了整合。Hystrix Turbine 的使用非常简单,只需要引入相应的依赖和加上注解和配置就可以了。
下面的流程图展示了当使用 Hystrix 的依赖请求,Hystrix 是如何工作的。
下面将更详细的解析每一个步骤都发生哪些动作:
第一步就是构建一个 HystrixCommand 或者 HystrixObservableCommand 对象,该对象将代表你的一个依赖请求,向构造函数中传入请求依赖所需要的参数。
如果构建 HystrixCommand 中的依赖返回单个响应,例如:
HystrixCommand command = new HystrixCommand(arg1, arg2);
如果依赖需要返回一个 Observable 来发射响应,就需要通过构建 HystrixObservableCommand 对象来完 成,例如:
•HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
•有 4 种方式可以执行一个 Hystrix 命令。
K value = command.execute();
Future fValue = command.queue();
Observable ohValue = command.observe(); //hot observable
•Observable ocValue = command.toObservable(); //cold observable
同步调用方法 execute() 实际上就是调用 queue().get() 方法,queue() 方法的调用的是 toObservable().toBlocking().toFuture().也就是说,最终每一个 HystrixCommand 都是通过 Observable 来实现的,即使这些命令仅仅是返回一个简单的单个值。
•如果这个命令的请求缓存已经开启,并且本次请求的响应已经存在于缓存中,那么就会立即返回一个包含缓存响应的 Observable(下面将 Request Cache 部分将对请求的 cache 做讲解)。
当命令执行时,Hystrix 会检查回路器是否被打开。
如果回路器被打开(或者 tripped),那么 Hystrix 就不会再执行命令,而是直接路由到第 8 步,获取 fallback 方法,并执行 fallback 逻辑。
•如果回路器关闭,那么将进入第 5 步,检查是否有足够的容量来执行任务。(其中容量包括线程池的容量,队列的容量等等)。
•如果与该命令相关的线程池或者队列已经满了,那么 Hystrix 就不会再执行命令,而是立即跳到第 8 步,执行 fallback 逻辑。
•在这里,Hystrix 通过你写的方法逻辑来调用对依赖的请求,通过下列之一的调用:
HystrixObservableCommand.construct() —返回一个发射响应的 Observable 或者发送一个 onError() 的通知。
如果 run()或 construct()方法超出命令的超时值,则线程将抛出 TimeoutException(如果命令本身未在其自己的线程中运行,则将抛出单独的计时器线程)。
在这种情况下,Hystrix 将响应路由到 8.获取回退,如果该方法不取消/中断,它将丢弃最终返回值 run()或 construct()方法。
请注意,没有办法强制潜在的线程停止工作 - 最好的 Hystrix 可以在 JVM 上执行的操作是将其抛出 InterruptedException。
如果由 Hystrix 包装的工作不遵守 InterruptedExceptions,则 Hystrix 线程池中的线程将继续其工作,尽管客户端已经收到 TimeoutException。
这种行为可以使 Hystrix 线程池饱和,尽管负载 “正确脱落”。
大多数 Java HTTP 客户端库不解释 InterruptedExceptions。
因此,请确保在 HTTP 客户端上正确配置连接和读/写超时。
如果该命令没有抛出任何异常并且它返回了响应,则 Hystrix 在执行一些日志记录和度量报告后返回此响应。
在 run()的情况下,Hystrix 返回一个 Observable,它发出单个响应,然后发出 onCompleted 通知;
在 construct()的情况下,Hystrix 返回由 construct()返回的相同 Observable。
Hystrix 会报告成功、失败、拒绝和超时的指标给回路器,回路器包含了一系列的滑动窗口数据,并通过该数据进行统计。
•它使用这些统计数据来决定回路器是否应该熔断,如果需要熔断,将在一定的时间内不在请求依赖 [短路请求],当再一次检查请求的健康的话会重新关闭回路器。
•当命令执行失败时,Hystrix 会尝试执行自定义的 Fallback 逻辑:
写一个 fallback 方法,提供一个不需要网络依赖的通用响应,从内存缓存或者其他的静态逻辑获取数据。如果再 fallback 内必须需要网络的调用,更好的做法是使用另一个 HystrixCommand 或者 HystrixObservableCommand。
如果你的命令是继承自 HystrixCommand,那么可以通过实现 HystrixCommand.getFallback() 方法返回一个单个的 fallback 值。
如果你的命令是继承自 HystrixObservableCommand,那么可以通过实现 HystrixObservableCommand.resumeWithFallback() 方法返回一个 Observable,并且该 Observable 能够发射出一个 fallback 值。
Hystrix 会把 fallback 方法返回的响应返回给调用者。
如果你没有为你的命令实现 fallback 方法,那么当命令抛出异常时,Hystrix 仍然会返回一个 Observable,但是该 Observable 并不会发射任何的数据,并且会立即终止并调用 onError() 通知。通过这个 onError 通知,可以将造成该命令抛出异常的原因返回给调用者。
失败或不存在回退的结果将根据您如何调用 Hystrix 命令而有所不同:
•execute():抛出一个异常。
•queue():成功返回一个 Future,但是如果调用 get() 方法,将会抛出一个异常。
•observe():返回一个 Observable,当你订阅它时,它将立即终止,并调用 onError() 方法。
•toObservable():返回一个 Observable,当你订阅它时,它将立即终止,并调用 onError() 方法。
•如果 Hystrix 命令执行成功,它将以 Observable 形式返回响应给调用者。根据你在第 2 步的调用方式不同,在返回 Observablez 之前可能会做一些转换。
•execute():通过调用 queue() 来得到一个 Future 对象,然后调用 get() 方法来获取 Future 中包含的值。
•queue():将 Observable 转换成 BlockingObservable,在将 BlockingObservable 转换成一个 Future。
•observe():订阅返回的 Observable,并且立即开始执行命令的逻辑,
•toObservable():返回一个没有改变的 Observable,你必须订阅它,它才能够开始执行命令的逻辑。
下图显示了 HystrixCommand 或 HystrixObservableCommand 如何与 HystrixCircuitBreaker 及其逻辑和决策流程进行交互,包括计数器在断路器中的行为方式。
回路器打开和关闭有如下几种情况:
•假设回路中的请求满足了一定的阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())
•假设错误发生的百分比超过了设定的错误发生的阈值 HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
•回路器状态由 CLOSE 变换成 OPEN
•如果回路器打开,所有的请求都会被回路器所熔断。
•一定时间之后 HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds(),下一个的请求会被通过(处于半打开状态),如果该请求执行失败,回路器会在睡眠窗口期间返回 OPEN,如果请求成功,回路器会被置为关闭状态,重新开启 1 步骤的逻辑。
Hystrix 的熔断器实现在 HystrixCircuitBreaker 类中,比较重要的几个参数如下:
1、circuitBreaker.enabled
熔断器是否启用,默认是 true
2、circuitBreaker.forceOpen
熔断器强制打开,始终保持打开状态,默认是 false
3、circuitBreaker.forceClosed
熔断器强制关闭,始终保持关闭状态,默认是 false
4、circuitBreaker.requestVolumeThreshold
滑动窗口内(10s)的请求数阈值,只有达到了这个阈值,才有可能熔断。默认是 20,如果这个时间段只有 19 个请求,就算全部失败了,也不会自动熔断。
5、circuitBreaker.errorThresholdPercentage
错误率阈值,默认 50%,比如(10s)内有 100 个请求,其中有 60 个发生异常,那么这段时间的错误率是 60,已经超过了错误率阈值,熔断器会自动打开。
6、circuitBreaker.sleepWindowInMilliseconds
熔断器打开之后,为了能够自动恢复,每隔默认 5000ms 放一个请求过去,试探所依赖的服务是否恢复。
•在最新代码中,已经弃用了 allowRequest(),取而代之的是 attemptExecution() 方法。
和 allowRequest() 方法相比,唯一改进的地方是通过 compareAndSet 修改状态值。通过 attemptExecution() 方法的返回值决定执行正常逻辑,还是降级逻辑。
1、如果 circuitBreaker.forceOpen=true,说明熔断器已经强制开启,所有请求都会被熔断。
2、如果 circuitBreaker.forceClosed =true,说明熔断器已经强制关闭,所有请求都会被放行。
3、circuitOpened 默认-1,用以保存最近一次发生熔断的时间戳。
4、如果 circuitOpened 不等于-1,说明已经发生熔断,通过 isAfterSleepWindow() 判断当前是否需要进行试探。
这里就是熔断器自动恢复的逻辑,如果当前时间已经超过上次熔断的时间戳 + 试探窗口 5000ms,则进入 if 分支,通过 compareAndSet 修改变量 status,竞争试探的能力。其中 status 代表当前熔断器的状态,包含 CLOSED, OPEN, HALF_OPEN,只有试探窗口之后的第一个请求可以执行正常逻辑,且修改当前状态为 HALF_OPEN,进入半熔断状态,其它请求执行 compareAndSet(Status.OPEN, Status.HALF_OPEN) 时都返回 false,执行降级逻辑。
5、如果试探请求发生异常,则执行 markNonSuccess()
通过 compareAndSet 修改 status 为熔断开启状态,并更新当前熔断开启的时间戳。
6、如果试探请求返回成功,则执行 markSuccess()
通过 compareAndSet 修改 status 为熔断关闭状态,并重置接口统计数据和 circuitOpened 标识为-1,后续请求开始执行正常逻辑。
说了这么多,如何实现自动熔断还没提到,在 Hystrix 内部有一个 Metric 模块,专门统计每个 Command 的执行状态,包括成功、失败、超时、线程池拒绝等,在熔断器的中 subscribeToStream() 方法中,通过订阅数据流变化,实现函数回调,当有新的请求时,数据流发生变化,触发回调函数 onNext
在 onNext 方法中,参数 hc 保存了当前接口在前 10s 之内的请求状态(请求总数、失败数和失败率),其主要逻辑是判断请求总数是否达到阈值 requestVolumeThreshold,失败率是否达到阈值 errorThresholdPercentage,如果都满足,说明接口的已经足够的不稳定,需要进行熔断,则设置 status 为熔断开启状态,并更新 circuitOpened 为当前时间戳,记录上次熔断开启的时间。
Hystrix 采用舱壁模式来隔离相互之间的依赖关系,并限制对其中任何一个的并发访问。
客户端(第三方包、网络调用等)会在单独的线程执行,会与调用的该任务的线程进行隔离,以此来防止调用者调用依赖所消耗的时间过长而阻塞调用者的线程。
•[Hystrix uses separate, per-dependency thread pools as a way of constraining any given dependency so latency on the underlying executions will saturate the available threads only in that pool]
您可以在不使用线程池的情况下防止出现故障,但是这要求客户端必须能够做到快速失败(网络连接/读取超时和重试配置),并始终保持良好的执行状态。
Netflix,设计 Hystrix,并且选择使用线程和线程池来实现隔离机制,有以下几个原因:
•很多应用会调用多个不同的后端服务作为依赖。
•每个服务会提供自己的客户端库包。
•每个客户端的库包都会不断的处于变更状态。
•[Client library logic can change to add new network calls]
•每个客户端库包都可能包含重试、数据解析、缓存等等其他逻辑。
•对用户来说,客户端库往往是 “黑盒” 的,对于实现细节、网络访问模式。默认配置等都是不透明的。
•[In several real-world production outages the determination was “oh, something changed and properties should be adjusted” or “the client library changed its behavior.]
•即使客户端本身没有改变,服务本身也可能发生变化,这些因素都会影响到服务的性能,从而导致客户端配置失效。
•传递依赖可以引入其他客户端库,这些客户端库不是预期的,也许没有正确配置。
•大部分的网络访问是同步执行的。
•客户端代码中也可能出现失败和延迟,而不仅仅是在网络调用中。
使用线程池的好处
•通过线程在自己的线程池中隔离的好处是:
简而言之,由线程池提供的隔离功能可以使客户端库和子系统性能特性的不断变化和动态组合得到优雅的处理,而不会造成中断。
注意:虽然单独的线程提供了隔离,但您的底层客户端代码也应该有超时和/或响应线程中断,而不能让 Hystrix 的线程池处于无休止的等待状态。
线程池的缺点
线程池最主要的缺点就是增加了 CPU 的计算开销,每个命令都会在单独的线程池上执行,这样的执行方式会涉及到命令的排队、调度和上下文切换。
•Netflix 在设计这个系统时,决定接受这个开销的代价,来换取它所提供的好处,并且认为这个开销是足够小的,不会有重大的成本或者是性能影响。
线程成本
Hystrix 在子线程执行 construct() 方法和 run() 方法时会计算延迟,以及计算父线程从端到端的执行总时间。所以,你可以看到 Hystrix 开销成本包括(线程、度量,日志,断路器等)。
Netflix API 每天使用线程隔离的方式处理 10 亿多的 Hystrix Command 任务,每个 API 实例都有 40 多个线程池,每个线程池都有 5-20 个线程(大多数设置为 10)
•下图显示了一个 HystrixCommand 在单个 API 实例上每秒执行 60 个请求(每个服务器每秒执行大约 350 个线程执行总数):
在中间位置(或者下线位置)不需要单独的线程池。
在第 90 线上,单独线程的成本为 3ms。
在第 99 线上,单独的线程花费 9ms。但是请注意,线程成本的开销增加远小于单独线程(网络请求)从 2 跳到 28 而执行时间从 0 跳到 9 的增加。
对于大多数 Netflix 用例来说,这样的请求在 90%以上的开销被认为是可以接受的,这是为了实现韧性的好处。
对于非常低延迟请求(例如那些主要触发内存缓存的请求),开销可能太高,在这种情况下,可以使用另一种方法,如信号量,虽然它们不允许超时,提供绝大部分的有点,而不会产生开销。然而,一般来说,开销是比较小的,以至于 Netflix 通常更偏向于通过单独的线程来作为隔离实现。
上面提到了线程池隔离的缺点,当依赖延迟极低的服务时,线程池隔离技术引入的开销超过了它所带来的好处。这时候可以使用信号量隔离技术来代替,通过设置信号量来限制对任何给定依赖的并发调用量。下图说明了线程池隔离和信号量隔离的主要区别:
使用线程池时,发送请求的线程和执行依赖服务的线程不是同一个,而使用信号量时,发送请求的线程和执行依赖服务的线程是同一个,都是发起请求的线程。
您可以使用信号量(或计数器)来限制对任何给定依赖项的并发调用数,而不是使用线程池/队列大小。这允许 Hystrix 在不使用线程池的情况下卸载负载,但它不允许超时和离开。如果您信任客户端而您只想减载,则可以使用此方法。
HystrixCommand 和 HystrixObservableCommand 支持 2 个地方的信号量:回退:当 Hystrix 检索回退时,它总是在调用 Tomcat 线程上执行此操作。执行:如果将属性 execution.isolation.strategy 设置为 SEMAPHORE,则 Hystrix 将使用信号量而不是线程来限制调用该命令的并发父线程数。您可以通过定义可以执行多少并发线程的动态属性来配置信号量的这两种用法。您应该使用在调整线程池大小时使用的类似计算来调整它们的大小(以毫秒为单位返回的内存中调用可以在 5000rps 下执行,信号量仅为 1 或 2 ......但默认值为 10)。注意:如果依赖项与信号量隔离然后变为潜在的,则父线程将保持阻塞状态,直到基础网络调用超时。信号量拒绝将在限制被触发后开始,但填充信号量的线程无法离开。
由于 Hystrix 默认使用线程池做线程隔离,使用信号量隔离需要显示地将属性 execution.isolation.strategy 设置为 ExecutionIsolationStrategy.SEMAPHORE,同时配置信号量个数,默认为 10。客户端需向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进入 fallback 流程。
信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。
线程池和信号量都可以做线程隔离,但各有各的优缺点和支持的场景,对比如下:
线程切换 | 支持异步 | 支持超时 | 支持熔断 | 限流 | 开销 | |
---|---|---|---|---|---|---|
信号量 | 否 | 否 | 否 | 是 | 是 | 小 |
线程池 | 是 | 是 | 是 | 是 | 是 | 大 |
线程池和信号量都支持熔断和限流。相比线程池,信号量不需要线程切换,因此避免了不必要的开销。但是信号量不支持异步,也不支持超时,也就是说当所请求的服务不可用时,信号量会控制超过限制的请求立即返回,但是已经持有信号量的线程只能等待服务响应或从超时中返回,即可能出现长时间等待。线程池模式下,当超过指定时间未响应的服务,Hystrix 会通过响应中断的方式通知线程立即结束并返回。
您可以使用请求合并器(HystrixCollapser 是抽象父代)来提前发送 HystrixCommand,通过该合并器您可以将多个请求合并为一个后端依赖项调用。
下面的图展示了两种情况下的线程数和网络连接数,第一张图是不使用请求合并,第二张图是使用请求合并(假定所有连接在短时间窗口内是 “并发的”,在这种情况下是 10ms)。
为什么使用请求合并
•事情请求合并来减少执行并发 HystrixCommand 请求所需要的线程数和网络连接数。请求合并以自动方式执行的,不需要代码层面上进行批处理请求的编码。
全局上下文(所有的 tomcat 线程)
理想的合并方式是在全局应用程序级别来完成的,以便来自任何用户的任何 Tomcat 线程的请求都可以一起合并。
例如,如果将 HystrixCommand 配置为支持任何用户请求获取影片评级的依赖项的批处理,那么当同一个 JVM 中的任何用户线程发出这样的请求时,Hystrix 会将该请求与其他请求一起合并添加到同一个 JVM 中的网络调用。
•请注意,合并器会将一个 HystrixRequestContext 对象传递给合并的网络调用,为了使其成为一个有效选项,下游系统必须处理这种情况。
用户请求上下文(单个 tomcat 线程)
如果将 HystrixCommand 配置为仅处理单个用户的批处理请求,则 Hystrix 仅仅会合并单个 Tomcat 线程的请求。
•例如,如果一个用户想要加载 300 个影片的标签,Hystrix 能够把这 300 次网络调用合并成一次调用。
对象建模和代码的复杂性
有时候,当你创建一个对象模型对消费的对象而言是具有逻辑意义的,这与对象的生产者的有效资源利用率不匹配。
例如,给你 300 个视频对象,遍历他们,并且调用他们的 getSomeAttribute() 方法,但是如果简单的调用,可能会导致 300 次网络调用(可能很快会占满资源)。
有一些手动的方法可以解决这个问题,比如在用户调用 getSomeAttribute() 方法之前,要求用户声明他们想要获取哪些视频对象的属性,以便他们都可以被预取。
或者,您可以分割对象模型,以便用户必须从一个位置获取视频列表,然后从其他位置请求该视频列表的属性。
这些方法可以会使你的 API 和对象模型显得笨拙,并且这种方式也不符合心理模式与使用模式。由于多个开发人员在代码库上工作,可能会导致低级的错误和低效率开发的问题。因为对一个用例的优化可以通过执行另一个用例和通过代码的新路径来打破。
通过将合并逻辑移到 Hystrix 层,不管你如何创建对象模型,调用顺序是怎样的,或者不同的开发人员是否知道是否完成了优化或者是否完成。
•getSomeAttribute()方法可以放在最适合的地方,并以任何适合使用模式的方式被调用,并且合并器会自动将批量调用放置到时间窗口。
HystrixCommand 和 HystrixObservableCommand 实现可以定义一个缓存键,然后用这个缓存键以并发感知的方式在请求上下文中取消调用(不需要调用依赖即可以得到结果,因为同样的请求结果已经按照缓存键缓存起来了)。
以下是一个涉及 HTTP 请求生命周期的示例流程,以及在该请求中执行工作的两个线程:
请求 cache 的好处有:
•不同的代码路径可以执行 Hystrix 命令,而不用担心重复的工作。
这在许多开发人员实现不同功能的大型代码库中尤其有用。
例如,多个请求路径都需要获取用户的 Account 对象,可以像这样请求:
Account account = new UserGetAccount(accountId).execute();
//or
Observable accountObservable = new UserGetAccount(accountId).observe();
Hystrix RequestCache 将只执行一次底层的 run()方法,执行 HystrixCommand 的两个线程都会收到相同的数据,尽管实例化了多个不同的实例。
•整个请求的数据检索是一致的。
每次执行该命令时,不再会返回一个不同的值(或回退),而是将第一个响应缓存起来,后续相同的请求将会返回缓存的响应。
•消除重复的线程执行。
由于请求缓存位于 construct()或 run()方法调用之前,Hystrix 可以在调用线程执行之前取消调用。
如果 Hystrix 没有实现请求缓存功能,那么每个命令都需要在构造或者运行方法中实现,这将在一个线程排队并执行之后进行。
Spring Boot 中有一种非常解耦的扩展机制:Spring Factories.这种机制实际上是仿照 java 中的 SPI 扩展机制实现的。
SPI 的全名为 Service Provider Interface,简单总结下 Java SPI 机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如 日志模块的方案,xml 解析模块、jdbc 模块的方案等。面向的对象设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及了具体的实现类,就违反了可插拔的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI 就是提供这样的一种机制:为某个接口寻找服务的实现的机制,有点类似 IOC 的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制很重要。
在 Spring 中也有一种类似与 Java SPI 的加载机制。它在 META-INF/spring.factories 文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。
这种自定义的 SPI 机制是 Spring Boot Starter 实现的基础。
spring-core 包里定义了 SpringFactoriesLoader 类,这个类实现了检索 META-INF/spring.factories 文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表。
loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表。
上面的两个方法的关键都是从指定的 ClassLoader 中获取 spring.factories 文件,并解析得到类名列表
https://github.com/Netflix/Hystrix/wiki
作者:京东物流 冯志文
来源:京东云开发者社区 自猿其说 Tech