为什么需要 Hystrix?
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用(RPC)。为了保证其高可用,单个服务又必须集群部署。由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务累计,导致服务瘫痪,甚至导致服务“雪崩”。为了解决这个问题,就出现断路器模型。
Hystrix 是一个帮助解决分布式系统交互时超时处理和容错的类库, 它同样拥有保护系统的能力.
什么是服务雪崩
分布式系统中经常会出现某个基础服务不可用造成整个系统不可用的情况, 这种现象被称为服务雪崩效应. 为了应对服务雪崩, 一种常见的做法是手动服务降级. 而Hystrix的出现,给我们提供了另一种选择.
服务雪崩应对策略
针对造成服务雪崩的不同原因, 可以使用不同的应对策略:
- 流量控制
- 改进缓存模式
- 服务自动扩容
- 服务调用者降级服务
流量控制 的具体措施包括:
- 网关限流
- 用户交互限流
- 关闭重试
服务雪崩解决办法
1.服务雪崩的原因
(1)某几个机器故障:例如机器的硬驱动引起的错误,或者一些特定的机器上出现一些的bug(如,内存中断或者死锁)。
(2)服务器负载发生变化:某些时候服务会因为用户行为造成请求无法及时处理从而导致雪崩,例如阿里的双十一活动,若没有提前增加机器预估流量则会造服务器压力会骤然增大二挂掉。
(3)人为因素:比如代码中的路径在某个时候出现bug
2.解决或缓解服务雪崩的方案
一般情况对于服务依赖的保护主要有3中解决方案:
(1)熔断模式:这种模式主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
(2)隔离模式:这种模式就像对系统请求按类型划分成一个个小岛的一样,当某个小岛被火少光了,不会影响到其他的小岛。例如可以对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响,如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回,不再调用后续资源。这种模式使用场景非常多,例如将一个服务拆开,对于重要的服务使用单独服务器来部署,再或者公司最近推广的多中心。
(3)限流模式:上述的熔断模式和隔离模式都属于出错后的容错处理机制,而限流模式则可以称为预防模式。限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。
3.熔断设计
在熔断的设计主要参考了hystrix的做法。其中最重要的是三个模块:熔断请求判断算法、熔断恢复机制、熔断报警
(1)熔断请求判断机制算法:使用无锁循环队列计数,每个熔断器默认维护10个bucket,每1秒一个bucket,每个blucket记录请求的成功、失败、超时、拒绝的状态,默认错误超过50%且10秒内超过20个请求进行中断拦截。
(2)熔断恢复:对于被熔断的请求,每隔5s允许部分请求通过,若请求都是健康的(RT<250ms)则对请求健康恢复。
(3)熔断报警:对于熔断的请求打日志,异常请求超过某些设定则报警
4.隔离设计
隔离的方式一般使用两种
(1)线程池隔离模式:使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)
(2)信号量隔离模式:使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃该类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)
5 超时机制设计
超时分两种,一种是请求的等待超时,一种是请求运行超时。
等待超时:在任务入队列时设置任务入队列时间,并判断队头的任务入队列时间是否大于超时时间,超过则丢弃任务。
运行超时:直接可使用线程池提供的get方法
Hystrix的功能
1. 服务降级
所有的RPC技术里面服务降级是一个最为重要的话题,所谓的降级指的是当服务的提供方不可使用的时候,程序不会出现异常,而会出现本地的操作调。
2. 依赖隔离
在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的Command放入A线程池, 调用账户服务的Command放入B线程池. 这样做的主要优点是运行环境被隔离开了. 这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时, 不会对系统的其他服务造成影响. 但是带来的代价就是维护多个线程池会对系统带来额外的性能开销. 如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话, 可以使用Hystrix的信号模式(Semaphores)来隔离资源.
3. 服务熔断
什么是熔断机制
熔断机制,就是下游服务出现问题后,为保证整个系统正常运行下去,而提供一种降级服务的机制,通过返回缓存数据或者既定数据,避免出现系统整体雪崩效应。在springcloud中,该功能可通过配置的方式加入到项目中。
当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力。
Hystrix的应用
还只用之前的两个工程,order和product,order调用product的服务
1. 在order服务中加入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2.在order服务的启动类中加入@EnableCircuitBreaker或@EnableHystrix
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker// 同@EnableHystrix
public class SellOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SellOrderApplication.class, args);
}
}
2.1 @EnableHystrix中使用了@EnableCircuitBreaker,所以两个注解都可以
2.2 应用同时使用eureka client和hystrix,可以使用@SpringCloudApplication,简化注解的使用
3. 参数配置
3.1注解式配置
3.1.1 局部参数配置
在调用product应用接口的方法上使用@HystrixCommand注解
@HystrixCommand (fallbackMethod = "fallbackMethod",//回調方法
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000"),//超時時間
//熔断设置
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")
}
)
@HystrixCommand
@RequestMapping("getList")
public List<ProductInfoOutPut> getProducts(Integer i) {
return productClient.findListByIds(Arrays.asList("123456"));
}
public List<ProductInfoOutPut> fallbackMethod() {
return new ArrayList<>();
}
fallbackMethod:调用后端服务失败时调用的方法来返回结果
HystrixProperty:进行Hystrix的参数配置
详细的参数配置可在hystrix-core包下的com.netflix.hystrix.HystrixCommandProperties包中查看
- execution.isolation.thread.timeoutInMilliseconds:该属性用来配置 HystrixCommand 执行的超时时间,单位为毫秒,默认值 1000 ,超出此时间配置,Hystrix 会将该执行命令为 TIMEOUT 并进入服务降级处理逻辑
- circuitBreaker.enabled:该属性用来确定当服务请求命令失败时,是否使用断路器来跟踪其健康指标和熔断请求,默认值 true
- circuitBreaker.requestVolumeThreshold:该属性用来设置在滚动时间窗中,断路器的最小请求数。例如:默认值 20 的情况下,如果滚动时间窗(默认值 10秒)内仅收到19个请求,即使这19个请求都失败了,断路器也不会打开。
- circuitBreaker.sleepWindowInMilliseconds:该属性用来设置当断路器打开之后的休眠时间窗。默认值 5000 毫秒,休眠时间窗结束之后,会将断路器设置为"半开"状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为"打开"状态,如果成功就设置为"关闭"状态。
- circuitBreaker.errorThresholdPercentage:该属性用来设置断路器打开的错误百分比条件。例如,默认值为 50 的情况下,表示在滚动时间窗中,在请求数量超过 circuitBreaker.requestVolumeThreshold 阈值的请求下,如果错误请求数的百分比超过50,就把断路器设置为"打开"状态,否则就设置为"关闭"状态。
- circuitBreaker.forceOpen:该属性用来设置断路器强制进入"打开"状态,会拒绝所有请求,该属性优先于 circuitBreaker.forceClosed
- circuitBreaker.forceClosed:该属性用来设置断路器强制进入"关闭"状态,会接收所有请求。
3.1.2 全局参数配置
在类上添加@DefaultProperties注解
方法上加注解的方式,fallback方法的返回值和参数类型必须与该方法一致,否则会报错,全局的fallback方法则没有限制
3.2 配置文件配置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 33000
配置以hystrix.command开头,后面跟default,代表为全局配置,如果是针对某个方法的配置,则跟方法名,再后面是配置项
配置后还需要在方法或类上加入@HystrixCommand或@DefaultProperties注解
@HystrixCommand
@RequestMapping("getList2")
public List<ProductInfoOutPut> getProducts2(String i) {
return productClient.findListByIds(Arrays.asList(i));
}
4. Feign+Hystrix
4.1 在order服务的yml中加入下面配置
feign:
hystrix:
enabled: true
4.2在product应用中用于暴露接口的controller中做如下配置
a:FeignClient注解增加fallback,值为创建的专门用于处理fallback的类
b:在该接口中创建fallback内部类,实现该接口,注意添加@Component注解
package cn.aaralyn.sellproduct.client;
import cn.aaralyn.sellproduct.common.DecreaseStockInput;
import cn.aaralyn.sellproduct.common.ProductInfoOutPut;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Arrays;
import java.util.List;
/**
* @Auther: aaralyn
* @Date: 2018/7/27 13:42
* @Description:
*/
@FeignClient(name = "product",//name为eureka中注册的服务名称
fallback = ProductClient.ProductClientFallback.class)
public interface ProductClient {
@GetMapping("msg")
String getProductMsg();
@PostMapping("product/findListByIds")
List<ProductInfoOutPut> findListByIds(@RequestBody List<String> productIds);
@PostMapping("product/decreaseStock")
void decreaseStock(@RequestBody List<DecreaseStockInput> decreaseStockInputList);
@Component
static class ProductClientFallback implements ProductClient{
@Override
public String getProductMsg() {
return "出错啦";
}
@Override
public List<ProductInfoOutPut> findListByIds(List<String> productIds) {
ProductInfoOutPut put = new ProductInfoOutPut();
put.setProductDesrc("出错啦");
return Arrays.asList(put);
}
@Override
public void decreaseStock(List<DecreaseStockInput> decreaseStockInputList) {
}
}
}