不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

SpringCloud微服务系列(4) 服务容错保护

服务容错保护

基本介绍

为什么要服务容错保护

我们现在已经了解,微服务架构中,系统是分成好多个服务单元的,各个但隐患之间通过注册中心建立联系。
服务多了,出问题的概率同样也就增大了,问题可能来自依赖的服务也可能来自网络。不管如何肯定会导致服务调用故障或者延迟,而这些问题会直接导致依赖调用方的服务也出现问题。
这样一层影响一层,再随着请求的增加,任务的积压,最终可能导致服务瘫痪,触发雪崩。

举个例子,比如下图所示的一个电子合同服务调用:

上图调用关系非常简单,假设在用户服务调用签署服务的时候需要调用计费服务来判断当前用户资源是否允许操作时,计费服务因自身处理逻辑等原因造成了响应缓慢,所以签署服务的线程将被挂起,以等待计费服务的响应,在漫长的等待后用户服务会被告知调用签署服务失败。如果是在高并发的场景下,挂起的线程很多,使得后来的签署服务请求都被阻塞,最终导致签署服务无法使用。这样还没结束,如果签署服务无法使用,那么依赖它的合同服务和用户服务也会出现线程挂起,到阻塞到服务不可用,然后继续向外围蔓延。
你看,本来计费服务的问题,最后导致原来正常的签署服务,合同服务,用户服务都不可用,就像雪山上出现雪崩一样,连锁反应。

为了解决这个问题,微服务架构中引入了断路器模式来进行对服务容错保护。其实很像我们家里的断路器,如果没有断路器,电流过载了(例如功率过大、短路等),电路不断开,电路就会升温,甚至是烧断电路、起火。有了断路器之后,当电流过载时,会自动切断电路(跳闸或保险丝熔断),从而保护了整条电路与家庭的安全。当电流过载的问题被解决后,只要将关闭断路器,电路就又可以工作了。

Hystrix介绍

针对上述问题,SpringCloud就采用了Spring Cloud Hysrix 来实现了断路器、线程隔离等一系列保护功能。它也是基于Netflix的开源框架Hystrix来实现的,该框架的目标就是通过控制哪些访问远程系统的节点,从而对延迟或故障提供强大的容错能力。

Hystrix包含的功能有:服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等。
这里有个注意点,平时我发现很多人把Hystrix称之为断路器,其实是不准确的,Hystrix包含的远不只是一个断路器,按我的理解Hystrix包含了:断路器、命令对象、线程池、请求队列、信号量、健康检查对象等等组件。不然光光只是断路器,可实现不了上述我们介绍的那些功能。

简单示例

我们对上一章中的Ribbon服务进行改造,让其具备容错保护功能。

  • 第一步:加入依赖
compile('org.springframework.cloud:spring-cloud-starter-hystrix')
  • 第二步:加入注解,启动容错保护
@EnableCircuitBreaker //开启断路器容错保护
@EnableDiscoveryClient
@SpringBootApplication
public class ServiceRibbonApplication {

   public static void main(String[] args) {
      SpringApplication.run(ServiceRibbonApplication.class, args);
   }


   @Bean
   @LoadBalanced
   RestTemplate restTemplate() {
      return new RestTemplate();
   }
}

注意,我们其实可以用注解@SpringCloudApplication来替代上面的3个注解

  • 第三步:在调用服务方法上加上注解,指定回调方法
/**
 * 负载方法
 * @param name
 * @return
 * SERVICE-HI :虚拟主机名
 * restTemplate+Ribbon 可以应对简单的URL 如果url复杂,使用Feign,它整合了Ribbon
 */

@HystrixCommand(fallbackMethod = "hiFallback")
public String hiService(String name) {
    return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}

  • 第四步:新增指定的方法,完成降级逻辑
/**
 * 降级服务处理
 * 加入Hysrix使用回调方法,使用注解方式时必须和调用服务在一个类中
 * @return
 */
public String hiFallback(String name){//注意这里的参数要和上面的一样,否则会找不到该方法

    return "hi service has a error!";
}

我们启动测试下,架构如下:

访问测试:localhost:8764/hi?name=hzqiuxm

结果会随机出现成功和失败的情况

hi hzqiuxm, i am from port8763
hi service has a error!hzqiuxm
hi hzqiuxm, i am from port8763
hi hzqiuxm, i am from port8762

从上述结果可以看出,断路器已经设置成功了

核心

Hystrix原理分析

工作流程
  • 1、创建HystrixCommand或HystrixObservableCommand对象,以“命令”方式实现对服务调用操作封装
  • 2、执行命令
  • 3、是否被缓存,是的话缓存结果会立即返回
  • 4、断路器是否打开,打开的话立即返回
  • 5、线程池、请求队列、信号量资源判断,不够时执行第8步
  • 6、请求依赖的服务
  • 7、计算断路器的健康度,根据成功、失败、拒绝、超时等信息计算是否打开/闭合断路器
  • 8、如果需要进行,fallback降级处理
  • 9、返回成功响应

断路器原理

断路器在HystrixCommand或HystrixObservableCommand执行时,起到了关键作用,它是Hystrix的核心部件。那么它是怎么决策和计算健康度的呢?
我们先看看断路器HystrixCircuitBreaker接口中定义的主要方法和类:

boolean allowRequest(); //每个Hystrix命令的请求通过它判断是否被执行
boolean isOpen(); //判断断路器的开发/关闭状态
void markSuccess(); //从半开状态到关闭
void markNonSuccess(); //从半开状态到打开
boolean attemptExecution(); //获取断路器状态,是非幂等的

class Factory{...} //维护Hystrix和断路器的关系集合

class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker{...} //接口的一个实现类

这里面我们主要关注下HystrixCircuitBreakerImpl 中计算断路器打开的规则,规则在subscribeToStream()方法中:

public void onNext(HealthCounts hc) {
    // check if we are past the statisticalWindowVolumeThreshold
    if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) { 
        // we are not past the minimum volume threshold for the stat window,
        // so no change to circuit status.
        // if it was CLOSED, it stays CLOSED
        // if it was half-open, we need to wait for a successful command execution
        // if it was open, we need to wait for sleep window to elapse
    } else {
        if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
            //we are not past the minimum error threshold for the stat window,
            // so no change to circuit status.
            // if it was CLOSED, it stays CLOSED
            // if it was half-open, we need to wait for a successful command execution
            // if it was open, we need to wait for sleep window to elapse
        } else {
            // our failure rate is too high, we need to set the state to OPEN
            if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {
                circuitOpened.set(System.currentTimeMillis());
            }
        }

从上面代码逻辑中我们可以看到,当最大线程数超过阈值并且请求错误百分比也超过阈值的时候,断路器会通过CAS多线程保护的方式打开。

circuitBreakerRequestVolumeThreshold决定了最大线程数阈值,它的默认值由default_circuitBreakerRequestVolumeThreshold = 20决定。
circuitBreakerErrorThresholdPercentage决定了请求错误百分比阈值,它的默认值由default_circuitBreakerErrorThresholdPercentage = 50决定。
(默认值可以参考HystrixCommandProperties类中的定义)

下图是断路器的状态关系图:

当断路器处于打开状态时,如果打开时间超过了我们定义的circuitBreakerSleepWindowInMilliseconds时间(默认5000毫秒),那么断路器会切换到半开状态。
如果此时请求继续失败,断路器又变回成打开状态,等待下个circuitBreakerSleepWindowInMilliseconds时间。若请求成功,则断路器变为闭合状态。

最后附上Hystrix官方文档中断路器详细执行逻辑,大家可以在仔细理解下。

实战详解

首先大家要清楚,Hystrix对于依赖服务调用采用了依赖隔离的方式,隔离方式主要有线程隔离和信号量隔离。

  • 线程隔离:为每一个依赖服务创建一个独立的线程,性能上低于信号量隔离(我还是推荐使用这个,除非你的应用无法忍受9ms级别的延迟)。

  • 信号量隔离:用信号量控制单个依赖服务,开销远小于线程隔离,但是无法异步和设置超时。

创建请求命令

我们这边只介绍以注解的方式来创建请求命令,除了按照注解的方式,还可以以继承HystrixCommand或HystrixObservableCommand类的方式,有兴趣的同学可以参考官方文档。
创建请求命令,按照调动方式,我们可以分为三种:

  • 同步方式:最普通最常见的方式
@HystrixCommand(fallbackMethod = "hiFallback")
public String hiService(String name) {
    return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}
  • 异步方式:
@HystrixCommand
public Future<String> hiServiceAsync(final String name ){

    return new AsyncResult<String>(name){

        @Override
        public String get() throws ExecutionException {
            return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
        }
    };
}

  • 响应式方式:
public Observable<String> hiServiceObs(final String name){

    return Observable.create(new Observable.OnSubscribe<String>() {
        @Override
        public void call(Subscriber<? super String> subscriber) {

            if(!subscriber.isUnsubscribed()){

                String forObject = restTemplate.getForObject("http://SERVICE-HI/hi?name=" + name, String.class);

                subscriber.onNext(forObject);
                subscriber.onCompleted();
            }
        }
    });
}
定义服务降级

从上面可以看出,当Hystrix命令执行失败,fallback是实现服务降级处理的后备方法。使用相对比较简单,只要在HystrixCommand注解的fallbackMethod属性指定对应的方法就行。
唯一需要注意的就是一点:fallbackMethod属性指定的方法必须定义在同一个类中,并且参数保持一致。

并不是所有的服务都要去实现降级逻辑,比如一些写操作的命令,批处理的命令,离线计算的命令等。不论Hystrix命令是否实现了服务降级,命令的状态和断路器状态都会更新。

异常处理
  • 异常忽略:对某个异常,不调用fallback操作,而是抛出。实现例子如下:
@HystrixCommand(ignoreExceptions = {需要忽略的异常类.class})
  • 异常分类降级:根据不同异常,采用不同的降级处理。
    实现也很简单,只要在fallbackMethod的实现方法参数上增加Throwable对象,这样在方法内部就可以获取触发服务降级的具体异常内容了。比如:
User fallbackl{String id, Throwable e){  获取e的异常类型,采取对应的降级处理 ... }
请求缓存

微服务架构的目的之一就是应对不断增长的业务,随着每个微服务需要承受的并发压力也越来越大。都说要提高系统性能,可以采用缓存技术。那么Hystrix当仁不让的提供了。

我们可以通过注解的方式,简单的实现请求缓存。请求缓存注解有:

  • @CacheResult:标记请命令返回的结果应该被缓存,必须和@HystrixCommand注解结合使用,所用属性有:cacheKeyMethod
@CacheResult(cacheKeyMethod = "getNameByidCacheKey")
@HystrixCommand(fallbackMethod = "hiFallback")
public String hiService(String name) {
    return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}

private Long getNameByidCacheKey(Long id) {
    return id;
}

上述代码中@CacheResult代表开启缓存功能,当调用结果返回后将被Hystrix缓存,缓存的Key值不指定时就会使用该方法中的所有参数(name),当然我们可以自定义缓存Key的生成规则,上面就使用了cacheKeyMethod指定了具体的生成函数。

  • @CacheRemove:标记请求命令的缓存失效,失效的缓存根据定义的Key决定,常用属性:command、cacheKeyMethod
@CacheResult
@HystrixCommand(fallbackMethod = "hiFallback")
public String hiService(@CacheKey("name") String name) {
    return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}

@CacheRemove(commandKey = "hiService")
public void update(@CacheKey("name")User user){

    restTemplate.getForObject("http://USER-SERVICE/users",user,User.class);

}

注意,commandKey属性是必须要指定的,它用来指明需要使用请求缓存的请求命令。

  • @CacheKey:标记在请求命令的参数上,使其作为缓存的Key值。如果没有标注,则会使用所有参数。常用属性:value
@CacheResult
@HystrixCommand(fallbackMethod = "hiFallback")
public String hiService(@CacheKey("name") String name) {
    return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}

注意它的优先级最低,使用了cacheKeyMethod的话,它就不生效了。

请求合并

把一个单体应用拆分为微服务应用后,最明显的变化就是增加了通信消耗和连接数占用。在高并发的情况下,随着通信次数的增加,总的通信时间消耗将会变得不理想。

Hystrix也考虑到了这个问题,提供了HystrixCollapser来实现请求的合并,以减少通信消耗和线程数的占用。

HystrixCollapser实现的基本思想就是在HystrixCommand之前放置一个合并处理器,将处于很短时间内(默认10ms)对同一依赖服务的多个请求进行整合并以批量方式发起请求。批量的发起请求需要开发人员自己实现,并且服务的提供方也要提供相应的批量实现接口才行。

可以通过下面的图来直观了解下:

实现步骤简单如下:

  • 1.调用方需要准备2个方法,一个单个调用,一个合并调用
  • 2.在单个调用的方法上,加一个合并器

注意,请求合并会有额外的开销,因为合并的时候会有个延迟时间10ms。一般在高并发的时候才会启用,所以需要考虑请求命令本身的延迟和延迟时间窗内的并发量来统筹考虑。

配置属性

属性配置优先级

4种属性配置的优先级,由低到高分别是:
- 全局默认值:其他三个都没设时
- 全局配置属性:通过配置文件定义,可以配合动态刷新在运行期动态调整
- 实例默认值:通过代码为实例定义默认值
- 实例配置属性:通过配置文件定义,可以配合动态刷新在运行期动态调整

Command属性

Command属性主要用来控制HystrixCommand命令的行为。
- execution配置,主要功能是实现隔离,超时

execution.isolation.strategy 设置执行隔离策略,有两个值:THREAD和SEMAPHORE。

THREAD: 通过线程池隔离的策略。它在独立的线程上执行, 并且它的并发限制
受线程池中线程数量的限制。
SEMAPHORE: 通过信号量隔离的策略。它在调用线程上执行, 并且它的并发限
制受信号量计数的限制。

execution.isolation.thread.timeoutinMilliseconds :配置HystrixCommand执行的超时时间,单位为毫秒。
当HystrixCommand执行时间超过该配置值之后,Hystrix会将该执行命令标记为TIMEOUT并进入服务降级处理逻辑。

execution.timeout.enabled: 该属性用来配置HystrixCommand.run()的执行是否启用超时时间,默认为true。

execution.isolation.thread.interruptOnTimeout: 该属性用来配置当HystrixCommand.run()执行超时的时候是否要将它中断。

execution.isolation. thread.interruptOnCancel: 该属性用来配置当HystrixCommand.run()执行被取消的时候是否要将它中断。

execution.isolation.semaphore.maxConcurrentRequests: 当HystrixCommand的隔离策略使用信号量的时候,该属性用来配置信号量的大小(并发请求数)

  • fallback属性配置,主要实现并发数控制和是否降级

fallback.isolation.semaphore.maxConcurrentRequests: 该属性用来设置从调用线程允许HystrixComrnand.getFallback()方法执行的最大并发请求数。

fallback.enabled: 该属性用来设置服务降级策略是否启用

  • circuitBreaker配置,主要实现休眠时间,断路器打开条件(请求并发数,错误百分比),强制(开关)

circuitBreaker.enabled: 该属性用来确定当服务请求命令失败时,是否使用断路器来跟踪其健康指标和熔断请求,默认是true。

circuitBreaker.requestVolumeThreshold: 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数,默认20。

circuitBreaker.sleepWindowinMilliseconds: 该属性用来设置当断路器打开之后的休眠时间窗,默认5000毫秒。

circuitBreaker.errorThresholdPercentage: 该属性用来设置断路器打开的错误百分比条件,默认50%。

circuitBreaker.forceOpen: 如果将该属性设置为true, 断路器将强制进入“ 打开”状态,它会拒绝所有请求,默认false。

circuitBreaker.forceClosed: 如果将该属性设置为true,断路器将强制进入“关闭”状态,它会接收所有请求,默认false。

  • metrics配置,主要实现滚动时间窗相关配置

metrics.rollingStats.timeinMillseconds: 该属性用来设置滚动时间窗的长度,单位为毫秒,默认10000。

metrics.rollingstats.numBuckets: 该属性用来设置滚动时间窗统计指标信息时划分“桶”的数量,默认10。

metrics.rollingPercentile.enabled: 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算,默认true。

metrics.rollingPercentile.timeinMilliseconds: 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒,默认60000。
注意:该值通过动态刷新不会有效。

metics.rollingPercentile.numBuckets: 该属性用来设置百分位统计滚动窗口中使用“ 桶”的数量,默认6。
注意:该值通过动态刷新不会有效。

metrics.rollingPercentile.bucketSize: 该属性用来设置在执行过程中每个“桶”中保留的最大执行次数,默认100。
注意:该值通过动态刷新不会有效。

metrics.healthSnapshot.intervalinMilliseconds: 该属性用来设置采集影响断路器状态的健康快照(请求的成功、错误百分比)的间隔等待时间,默认500。
注意:该值通过动态刷新不会有效。

  • requestContext配置,主要实现缓存和日志相关

requestCache.enabled: 此属性用来配置是否开启请求缓存,默认true。

requestLog.enabled: 该属性用来设置Hys立ixCommand的执行和事件是否打印日志到HystrixRequestLog中,默认true。

collapser属性

合并请求相关设置
maxRequestsinBatch: 该参数用来设置一次请求合并批处理中允许的最大请求数,默认Integer.MAX VALUE。

timerDelayinMillseconds: 该参数用来设置批处理过程中每个命令延迟的时间,单位为毫秒,默认10。

requestCache.enabled: 该参数用来设置批处理过程中是否开启请求缓存,默认true。

threadPool属性

线程池的先关配置
coreSize: 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量,默认10。

maxQueueSize: 该参数用来设置线程池的最大队列大小,默认-1。
当设置为-1时,线程池将使用SynchronousQueue实现的队列,否则将使用LinkedBlockingQueue实现的队列。
注意:该值通过动态刷新不会有效。

queueSizeRejectionThreshold: 该参数用来为队列设置拒绝阈值,默认5。
注意:当maxQueueSize属性为-1 的时候,该属性不会生效。

metrics.rollingstats.timeinMilliseconds: 该参数用来设置滚动时间窗的长度,单位为毫秒,默认10000。

metrics.rollingStats.numBuckets: 该参数用来设置滚动时间窗被划分成“桶”的数量,默认10。

上面属性对应全局默认值,全局配置属性,实例默认值,实例配置属性值请在Hystrix的番外篇中查阅。

参考资料推荐

https://baijiahao.baidu.com/s?id=1593211109840459044&wfr=spider&for=pc
https://www.ebayinc.com/stories/blogs/tech/application-resiliency-using-netflix-hystrix/
https://github.com/Netflix/Hystrix/wiki
https://github.com/Netflix/Hystrix/wiki/How-it-Works

未经允许不得转载:菡萏如佳人 » SpringCloud微服务系列(4)

欢迎加入极客江湖

进入江湖关于作者