5、 spring cloud:Hystrix

Hystrix使用:

使用方式一:自定义Hystrix请求命令的方式

com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect
com.netflix.hystrix.HystrixCommand

public class BookCommand extends HystrixCommand<Book> {
    private RestTemplate restTemplate;
    private Long id;

    @Override
    protected Book getFallback() {
        Throwable executionException = getExecutionException();
        System.out.println(executionException.getMessage());
        return new Book("宋诗选注", 88, "钱钟书", "三联书店");
    }

    @Override
    protected Book run() throws Exception {
        return restTemplate.getForObject("http://HELLO-SERVICE/getbook5/{1}", Book.class,id);
    }

    public BookCommand(Setter setter, RestTemplate restTemplate,Long id) {
        super(setter);
        this.restTemplate = restTemplate;
        this.id = id;
    }

    @Override
    protected String getCacheKey() {
        return String.valueOf(id);
    }
}

调用方式:

    BookCommand bc1 = new BookCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HELLO-SERVICE")).andCommandKey(commandKey), restTemplate, 1l);
    Book e1 = bc1.execute();

方式二:Spring boot使用

@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

    @Bean
    @LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}
@Service
public class UserService {
    @Autowired
    RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "userFallback", commandKey = "userKey")
    public User user(String name) {
        User user = restTemplate.getForEntity("http://HELLO-SERVICE/hello1?name={1}", User.class, name).getBody();
        return user;
    }
    public String userFallback() {
        return "error";
    }
}

Spring Cloud中Hystrix的请求缓存 - 个人文章 - SegmentFault 思否
hystrix缓存功能的使用 - CSDN博客
通过方法重载开启缓存

Hystrix原理

策略配置:Hystrix有两种降级模型,即信号量(同步)模型和线程池(异步)模型,这两种模型所有可定制的部分都体现在了HystrixCommandProperties和HystrixThreadPoolProperties两个类中。Hystrix提供了配置修改的入口,没有将配置界面化。
数据统计:为了计算断路器的健康度,使用滑动窗口对数据进行“平滑”统计
断路器:断路器可以说是Hystrix内部最重要的状态机,是它决定着每个Command的执行过程。

Hystrix原理.png

深入理解Hystrix之文档翻译 - CSDN博客

断路器

hystrix流程图.png

断路器HystrixCircuitBreaker有三个状态,

CLOSED关闭状态:允许流量通过。
OPEN打开状态:不允许流量通过,即处于降级状态,走降级逻辑。
HALF_OPEN半开状态:允许某些流量通过,并关注这些流量的结果,如果出现超时、异常等情况,将进入OPEN状态,如果成功,那么将进入CLOSED状态。

  1. CLOSED -> OPEN :
    时间窗口内(默认10秒)请求量大于请求量阈值(即circuitBreakerRequestVolumeThreshold,默认值是20),并且该时间窗口内错误率大于错误率阈值(即circuitBreakerErrorThresholdPercentage,默认值为50,表示50%),那么断路器的状态将由默认的CLOSED状态变为OPEN状态
// 检查是否超过了我们设置的断路器请求量阈值
if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
    // 如果没有超过统计窗口的请求量阈值,则不改变断路器状态,
    // 如果它是CLOSED状态,那么仍然是CLOSED.
    // 如果它是HALF-OPEN状态,我们需要等待请求被成功执行,
    // 如果它是OPEN状态, 我们需要等待睡眠窗口过去。
} else {
    if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
        //如果没有超过统计窗口的错误率阈值,则不改变断路器状态,,
        // 如果它是CLOSED状态,那么仍然是CLOSED.
        // 如果它是HALF-OPEN状态,我们需要等待请求被成功执行,
        // 如果它是OPEN状态, 我们需要等待【睡眠窗口】过去。
    } else {
        // 如果错误率太高,那么将变为OPEN状态
        if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {
            // 因为断路器处于打开状态会有一个时间范围,所以这里记录了变成OPEN的时间
            circuitOpened.set(System.currentTimeMillis());
        }
    }

这里的错误率是个整数,即errorPercentage= (int) ((double) errorCount / totalCount * 100);

  1. OPEN ->HALF_OPEN:
    前面说过,当进入OPEN状态后,会进入一段睡眠窗口,即只会OPEN一段时间,所以这个睡眠窗口过去,就会“自动”从OPEN状态变成HALF_OPEN状态,这种设计是为了能做到弹性恢复,这种状态的变更,并不是由调度线程来做,而是由请求来触发,每次请求都会进行如下检查:
@Override
public boolean attemptExecution() {
    if (properties.circuitBreakerForceOpen().get()) {
        return false;
    }
    if (properties.circuitBreakerForceClosed().get()) {
        return true;
    }
    // circuitOpened值等于1说明断路器状态为CLOSED
    if (circuitOpened.get() == -1) {
        return true;
    } else {
        if (isAfterSleepWindow()) {
            // 睡眠窗口过去后只有第一个请求能被执行
            // 如果执行成功,那么状态将会变成CLOSED
            // 如果执行失败,状态仍变成OPEN
            if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}
 
// 睡眠窗口是否过去
private boolean isAfterSleepWindow() {
    // 还记得上面CLOSED->OPEN时记录的时间吗?
    final long circuitOpenTime = circuitOpened.get();
    final long currentTime = System.currentTimeMillis();
    final long sleepWindowTime = properties.circuitBreakerSleepWindowInMilliseconds().get();
    return currentTime > circuitOpenTime + sleepWindowTime;

}
  1. HALF_OPEN ->CLOSED :

变为半开状态后,会放第一笔请求去执行,并跟踪它的执行结果,如果是成功,那么将由HALF_OPEN状态变成CLOSED状态:

@Override
public void markSuccess() {
    if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {
        //This thread wins the race to close the circuit - it resets the stream to start it over from 0
        metrics.resetStream();
        Subscription previousSubscription = activeSubscription.get();
        if (previousSubscription != null) {
            previousSubscription.unsubscribe();
        }
        Subscription newSubscription = subscribeToStream();
        activeSubscription.set(newSubscription);
        // 已经进入了CLOSED阶段,所以将OPEN的修改时间设置成-1
        circuitOpened.set(-1L);
    }
}
  1. HALF_OPEN ->OPEN :

变为半开状态时,如果第一笔被放去执行的请求执行失败(资源获取失败、异常、超时等),就会由HALP_OPEN状态再变为OPEN状态:

@Override
public void markNonSuccess() {
    if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) {
        // This thread wins the race to re-open the circuit - it resets the start time for the sleep window
        circuitOpened.set(System.currentTimeMillis());
    }
}

滑动窗口

上面提到的断路器需要的时间窗口请求量和错误率这两个统计数据,都是指固定时间长度内的统计数据,断路器的目标,就是根据这些统计数据来预判并决定系统下一步的行为,Hystrix通过滑动窗口来对数据进行“平滑”统计,默认情况下,一个滑动窗口包含10个桶(Bucket),每个桶时间宽度是1秒,负责1秒的数据统计。滑动窗口包含的总时间以及其中的桶数量都是可以配置的,来张官方的截图认识下滑动窗口:
滑动窗口.png

上图的每个小矩形代表一个桶,可以看到,每个桶都记录着1秒内的四个指标数据:成功量、失败量、超时量和拒绝量,这里的拒绝量指的就是上面流程图中【信号量/线程池资源检查】中被拒绝的流量。10个桶合起来是一个完整的滑动窗口,所以计算一个滑动窗口的总数据需要将10个桶的数据加起来。

线程池/信号量模式

hystrix提供两种模式:线程池和信号量模式。

线程池隔离模式:使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理),缺点主要是就是增加了 CPU 的开销。
信号量隔离模式:使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃改类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)。

Hystrix 采用了 Bulkhead Partition 舱壁隔离技术,来将外部依赖进行资源隔离,进而避免任何外部依赖的故障导致本服务崩溃。

舱壁隔离,是说将船体内部空间区隔划分成若干个隔舱,一旦某几个隔舱发生破损进水,水流不会在其间相互流动,如此一来船舶在受损时,依然能具有足够的浮力和稳定性,进而减低立即沉船的危险。
Hystrix 对每个外部依赖用一个单独的线程池,这样的话,如果对那个外部依赖调用延迟很严重,最多就是耗尽那个依赖自己的线程池而已,不会影响其他的依赖调用。
可以用 Hystrix semaphore 技术来实现对某个依赖服务的并发访问量的限制,而不是通过线程池/队列的大小来限制流量。Semaphore 技术可以用来限流和削峰,但是不能用来对调研延迟的服务进行 timeout 和隔离。Execution.isolation.strategy 设置为 SEMAPHORE,那么 Hystrix 就会用 semaphore 机制来替代线程池机制,来对依赖服务的访问进行限流。如果通过 semaphore 调用的时候,底层的网络调用延迟很严重,那么是无法 timeout 的,只能一直 block 住。一旦请求数量超过了 semaphore 限定的数量之后,就会立即开启限流。

Semaphore 不能设置超时和实现异步访问,所以只有在依赖的服务是足够可靠的情况下才使用信号量,所以使用hystrix时一般使用线程池隔离。

Hystrix中的信号量使用TryableSemaphore实现

com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy#SEMAPHORE/THREAD
com.netflix.hystrix.AbstractCommand.TryableSemaphore
com.netflix.hystrix.AbstractCommand#applyHystrixSemantics
com.netflix.hystrix.AbstractCommand#executeCommandWithSpecifiedIsolation

TryableSemaphore:
Semaphore that only supports tryAcquire and never blocks and that supports a dynamic permit count.
Using AtomicInteger increment/decrement instead of java.util.concurrent.Semaphore since we don't need blocking and need a custom implementation to get the dynamic permit count and since AtomicInteger achieves the same behavior and performance without the more complex implementation of the actual Semaphore class using AbstractQueueSynchronizer.
Hystrix中的信号量没有使用java中的Semaphore实现,由于不需要阻塞,只是使用AtomicInteger代替相对复杂的AbstractQueueSynchronizer。

总结

  1. 线程池隔离/信号量 用于限流;
  2. hystrix使用滑动窗口用于统计信息用于降级;
  3. 由于Semaphore 不能设置超时和实现异步访问,hystrix时一般使用线程池隔离,线程池隔离有一定的性能损耗;
  4. Hystrix只提供了配置修改的入口,没有将配置界面化,如果想在页面上动态调整配置,还需要自己实现。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容