# 097. 深入理解线程池隔离技术的设计原则以及动手实战接口限流实验

  1. command 的创建和执行:资源隔离

  2. request cache:请求缓存

  3. fallback:优雅降级

  4. circuit breaker:短路器,快速熔断(一旦后端服务故障,立刻熔断,阻止对其的访问)

    把一个分布式系统中的某一个服务,打造成一个高可用的服务

    资源隔离,优雅降级,熔断

  5. 判断,线程池或者信号量的容量是否已满,reject,有限流做用

    限流,限制对后端的服务的访问量,比如说你对 mysql,redis,zookeeper,各种后端的中间件的资源,访问,其实为了避免过大的流浪打死后端的服务,线程池,信号量,限流

    限制服务对后端的资源的访问

# 设计原则

官网教程 (opens new window)

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

线程池隔离,学术名称:bulkhead 舱壁隔离

外部依赖的调用在单独的线程中执行,这样就能跟调用线程隔离开来,避免外部依赖调用 timeout 耗时过长,导致调用线程被卡死

Hystrix 对每个外部依赖用一个单独的线程池,这样的话,如果对那个外部依赖调用延迟很严重, 最多就是耗尽那个依赖自己的线程池而已,不会影响其他的依赖调用

当然可以不使用线程池,但这需要客户端被信任非常快速地失败(网络连接/读取超时和重试配置)并始终表现良好。

Netflix 在其 Hystrix 设计中选择使用线程和线程池来实现隔离,原因有很多,其中包括:

  1. 每个服务可能都会调用数十个依赖服务,然而那些依赖服务通常是由很多不同的团队开发的

  2. 每个后端服务都提供自己的 client 库

    比如说用 thrift 的话,就会提供对应的 thrift 依赖

  3. client 调用库随时会变更

  4. client 调用库随时可能会增加新的网络请求的逻辑

  5. client 调用库可能会包含诸如自动重试,数据解析,内存中缓存等逻辑

  6. client 调用库一般都对调用者来说是个黑盒,包括实现细节,网络访问,默认配置,等等

  7. 在真实的生产环境中经常会出现调用者突然间惊讶的发现 client 调用库发生了某些变化

  8. 即使 client 调用库没有改变,依赖服务本身可能有会发生逻辑上的变化

  9. 有些依赖的 client 调用库可能还会拉取其他的依赖库,而且可能那些依赖库配置的不正确

  10. 大多数网络请求都是同步调用的

  11. 调用失败和延迟,也有可能会发生在 client 调用库本身的代码中,不一定就是发生在网络请求中

简单来说,就是你必须默认 client 调用库就很不靠谱,而且随时可能各种变化,所以就要用强制隔离的方式来确保任何服务的故障不能影响当前服务; 我不知道在学习这个课程的学员里,有多少人真正参与过一些复杂的分布式系统的开发,在一些大公司里,做一些复杂的项目的话,如广告计费系统特别复杂, 可能涉及多个团队,总共三四十个人,五六十个人,一起去开发一个系统,每个团队负责一块儿; 每个团队里的每个人,负责一个服务,或者几个服务,比较常见的大公司的复杂分布式系统项目的分工合作的一个流程

# 线程池的好处

  1. 任何一个依赖服务都可以被隔离在自己的线程池内,即使自己的线程池资源填满了,也不会影响任何其他的服务调用
  2. 服务可以随时引入一个新的依赖服务,因为即使这个新的依赖服务有问题,也不会影响其他任何服务的调用
  3. 当一个故障的依赖服务重新变好的时候,可以通过清理掉线程池,瞬间恢复该服务的调用,而如果是 tomcat 线程池被占满,再恢复就很麻烦
  4. 如果一个 client 调用库配置有问题,线程池的健康状况随时会报告,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机
  5. 如果一个服务本身发生了修改,需要重新调整配置,此时线程池的健康状况也可以随时发现,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机
  6. 基于线程池的异步本质,可以在同步的调用之上,构建一层异步调用层

简单来说,最大的好处就是资源隔离,确保说,任何一个依赖服务故障,不会拖垮当前的这个服务

注意

尽管隔离是一个单独的线程提供的,但您的底层客户端代码也应该有超时和/或响应线程中断, 这样它就无法无限期地阻塞并使 Hystrix 线程池饱和。

怎么理解注意事项?我工作中就遇到过,调用网站的服务,调用的那个线程永远阻塞住, 我猜想是他内部出错了,一直阻塞没有返回,而我自己服务中的那个线程就被占用, 所以就算是使用 hystrix,你也应该在调用第三方服务的时候进行超时配置。

多线程这一块不是很熟悉,所以也是懵懵懂懂的

# 线程池的缺点

线程池的主要缺点是它们增加了计算开销。每个命令执行都涉及在单独的线程上运行命令所涉及的排队,调度和上下文切换。

Hystrix 官方自己做了一个多线程异步带来的额外开销,通过对比多线程异步调用+同步调用得出, Netflix API 每天通过 hystrix 执行 10亿 次调用,每个服务实例有 40 个以上的线程池, 每个线程池有 10 个左右的线程;最后发现说,用 hystrix 的额外开销,就是给请求带来了 3ms 左右的延时, 最多延时在 10ms 以内,相比于可用性和稳定性的提升,这是可以接受的

# 信号量

我们可以用 hystrix semaphore 技术来实现对某个依赖服务的并发访问量的限制,而不是通过线程池/队列的大小来限制流量

sempahore 技术可以用来限流和削峰,但是不能用来对调研延迟的服务进行 timeout 和隔离

execution.isolation.strategy,设置为 SEMAPHORE,那么 hystrix 就会用 semaphore 机制来替代线程池机制,来对依赖服务的访问进行限流

如果通过 semaphore 调用的时候,底层的网络调用延迟很严重,那么是无法 timeout 的,只能一直 block 住

一旦请求数量超过了 semephore 限定的数量之后,就会立即开启限流

# 接口限流实验

其实在前面我自己记录笔记的时候就尝试过这个 实验

package cn.mrcode.cachepdp.eshop.cache.ha;

import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixThreadPoolProperties;

import org.junit.Test;

import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * ${todo}
 *
 * @author : zhuqiang
 * @date : 2019/6/5 22:15
 */
public class CommandLimit extends HystrixCommand<String> {
    public CommandLimit() {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("test-group"))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                        // 配置线程池大小,同时并发能力个数
                        .withCoreSize(2)
                        // 配置等待线程个数;如果不配置该项,则没有等待,超过则拒绝
                        .withMaxQueueSize(5)
                        // 由于 maxQueueSize 是初始化固定的,该配置项是动态调整最大等待数量的
                        // 可以热更新;规则:只能比 MaxQueueSize 小,
                        .withQueueSizeRejectionThreshold(2)
                )
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withExecutionTimeoutInMilliseconds(2000)) // 修改为 2 秒超时
        );
    }

    @Override
    protected String run() throws Exception {
        TimeUnit.MILLISECONDS.sleep(800);
        return "success";
    }

    @Override
    protected String getFallback() {
        return "降级";
    }

    @Test
    public void test() throws InterruptedException {
        int count = 13;
        CountDownLatch downLatch = new CountDownLatch(count);
        for (int i = 0; i < count; i++) {
            int finalI = i;
            new Thread(() -> {
                CommandLimit commandLimit = new CommandLimit();
                String execute = commandLimit.execute();
                System.out.println(Thread.currentThread().getName() + " " + finalI + " : " + execute + "  :  " + new Date());
                downLatch.countDown();
            }).start();
        }
        downLatch.await();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

输出日志为

Thread-0 0 : 降级  :  Wed Jun 05 23:07:16 CST 2019
Thread-11 11 : 降级  :  Wed Jun 05 23:07:16 CST 2019
Thread-1 1 : 降级  :  Wed Jun 05 23:07:16 CST 2019
Thread-6 6 : 降级  :  Wed Jun 05 23:07:16 CST 2019
Thread-12 12 : 降级  :  Wed Jun 05 23:07:16 CST 2019
Thread-10 10 : 降级  :  Wed Jun 05 23:07:16 CST 2019
Thread-3 3 : success  :  Wed Jun 05 23:07:17 CST 2019
Thread-5 5 : success  :  Wed Jun 05 23:07:17 CST 2019
Thread-4 4 : success  :  Wed Jun 05 23:07:18 CST 2019
Thread-7 7 : success  :  Wed Jun 05 23:07:18 CST 2019
Thread-2 2 : 降级  :  Wed Jun 05 23:07:18 CST 2019
Thread-8 8 : 降级  :  Wed Jun 05 23:07:18 CST 2019
Thread-9 9 : 降级  :  Wed Jun 05 23:07:18 CST 2019
1
2
3
4
5
6
7
8
9
10
11
12
13

看到只有 4 个被执行成功了;

特别注意:withQueueSizeRejectionThreshold 是热更新 withMaxQueueSize 配置的; 在该测试中,休眠和超时很重要,因为:

  • 休眠少了,那么执行速度过快,输出日志可能大于 withCoreSize + withQueueSizeRejectionThreshold 数量;
  • 休眠多了,那么排队中被释放出来的时候发现已经超时就走降级机制了,而不是还去请求;