一、资源隔离

分布式环境中我们不能确保调用的依赖服务不发生问题。诸如基础设施服务器宕机(数据库、缓存),外部依赖服务挂掉,网络抖动引起的连接超时。当这些情况发生时,大量请求迅速涌到链路上的故障节点,就会像繁忙的高速公路上发生交通事故短时间内就会造成交通拥堵,直至瘫痪。

不能让局部服务的故障不断扩散蔓延导致全局系统不可用,造成重大生产事故,所以要对外部依赖进行资源隔离,将故障控制在小范围内。

hystrix两种隔离策略

线程池隔离策略(默认)

创建副线程去调用依赖服务, 执行依赖代码的线程与请求线程(比如Tomcat线程)分离 ,当副线程请求调用失败、超时等异常情况而阻塞时不会影响到主线程。

可以利用线程可设置超时的特性给通过网络进行的服务间调用设置超时时间。

线程池机制的优缺点

线程池机制的优点:

(1) 任何一个依赖服务都可以被隔离在自己的线程池内,即使自己的线程池资源填满了,也不会影响任何其他的服务调用;

(2) 服务可以随时引入一个新的依赖服务,因为即使这个新的依赖服务有问题,也不会影响其他任何服务的调用;

(3) 当一个故障的依赖服务重新变好的时候,可以通过清理掉线程池,瞬间恢复该服务的调用,而如果是tomcat线程池被占满,再恢复就很麻烦;

(4) 如果一个client调用配置有问题,线程池的健康状况随时会报告,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机;

(5) 如果一个服务本身发生了修改,需要重新调整配置,此时线程池的健康状况也可以随时发现,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机;

(6) 基于线程池的异步本质,可以在同步的调用之上,构建一层异步调用层;

线程池机制的缺点:

(1) 线程池机制最大的缺点就是增加了cpu的开销。

信号量隔离策略

用于隔离本地代码,利用信号量限制同时运行的线程数量,不会创建副线程,开销较小(复杂算法、多重循环,时间复杂度高的情况)。

二、限流

hystrix利用线程池的工作原理来进行限流。

线程池的工作原理
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize 为线程池的基本大小。
  • maximumPoolSize 为线程池最大线程大小。
  • keepAliveTimeunit 则是线程空闲后的存活时间。
  • workQueue 用于存放任务的阻塞队列。
  • handler 当队列和最大线程池都满了之后的饱和策略。

188580-202ba87b6a285694.jpg

四种拒绝策略

2018110923282372.png

AbortPolicy:不处理,直接抛出异常。
CallerRunsPolicy:若线程池还没关闭,调用当前所在线程来运行任务,r.run()执行。
DiscardOldestPolicy:LRU策略,丢弃队列里最近最久不使用的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉,不抛出异常。

三、主要参数配置

    @HystrixCommand(commandKey = "getCompanyInfoById",
                    groupKey = "company-info",
                    threadPoolKey = "company-info",
                    fallbackMethod = "fallbackMethod",
                    threadPoolProperties = {
                        @HystrixProperty(name = "coreSize", value = "30"),
                        @HystrixProperty(name = "maxQueueSize", value = "101"),
                        @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                        @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
                     })
  • commandKey: 代表一个接口, 如果不配置,默认是@HystrixCommand注解修饰的函数的函数名。
  • groupKey: 代表一个服务,一个服务可能会暴露多个接口。 Hystrix会根据组来组织和统计命令的告、仪表盘等信息。Hystrix命令默认的线程划分也是根据命令组来实现。默认情况下,Hystrix会让相同组名的命令使用同一个线程池,所以我们需要在创建Hystrix命令时为其指定命令组来实现默认的线程池划分。
  • threadPoolKey: 对线程池进行更细粒度的配置,默认等于groupKey的值。如果依赖服务中的某个接口耗时较长,需要单独特殊处理,最好单独用一个线程池,这时候就可以配置threadpool key。也可以多个服务接口设置同一个threadPoolKey构成线程组。
  • fallbackMethod:@HystrixCommand注解修饰的函数的回调函数,@HystrixCommand修饰的函数必须和这个回调函数定义在同一个类中,因为定义在了同一个类中,所以fackback method可以是public/private均可。
  • 线程池配置:coreSize表示核心线程数,hystrix默认是10;maxQueueSize表示线程池的最大队列大小; keepAliveTimeMinutes表示非核心线程空闲时最大存活时间;queueSizeRejectionThreshold:该参数用来为队列设置拒绝阈值。通过该参数,即使队列没有达到最大值也能拒绝请求。

四、降级

服务提前配置备用措施,当故障发生时无缝启用备用方案(或者返回一些默认值),用户无感知,最终目的都是为了提供7*24小时稳定服务,这样对用户来说才是高价值、可信赖的优质服务。

线程run()抛出异常,超时,线程池或信号量满了,或短路了,都会触发fallback机制。

五、熔断

当请求调用失败率达到阀值自动触发降级(如因网络故障/超时造成的失败率高),熔断器触发的快速失败会进行快速恢复。

工作流程:

  1. 如果经过断路器的流量超过了一定的阈值,HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()配置,默认20。比如,要求在10s内,经过短路器的流量必须达到20个才会去判断要不要短路;
  2. 如果断路器统计到的异常调用的占比超过了一定的阈值,HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()配置。如果达到了上面的要求,比如说在10s内,经过短路器的请求达到了30个;同时其中异常的访问数量,占到了一定的比例(默认50%),比如60%的请求都是异常(报错,timeout,reject),会开启短路;
  3. 然后断路器从close状态转换到open状态;
  4. 断路器打开的时候,所有经过该断路器的请求全部被短路,不调用后端服务,直接走fallback降级逻辑;
  5. 经过了一段时间之后,HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()配置,断路器会half-open状态,让一条请求经过短路器,看能不能正常调用。如果调用成功了,那么断路器就自动恢复,转到close状态。

六、监控

根据监控结果可以看出合理的超时时长配置和计算出服务集群部署的规模大小。

u=2568138430,3468427254&fm=173&app=49&f=JPEG.jpg

计算公式:

服务集群部署的规模大小 = 服务在健康状态时每秒支撑的最大请求数 * 第99百分位延迟时间(以秒为单位)+ 少量用于缓冲的额外线程

举个例子:某个服务需要支撑的QPS为1000,即每秒需要处理1000个请求。假设通过统计得到百分之99的服务耗时为100ms,那一个线程1秒内能处理10个请求,一台机器的线程池大小一般就使用默认配置为10,即一台机器1秒内能处理 10 * 10 = 100个请求,最后得出该服务总共需要部署 1000/100 = 10台机器。

七、请求缓存、合并

请求缓存

在一次请求中,如果有多个command,参数都是一样的,调用的接口也是一样的,其实结果可以认为也是一样的。这个时候,可以让第一次command执行返回的结果,被缓存在内存中,然后在这个请求上下文中,后续的其他对这个依赖的调用全部从内存中取用缓存结果就可以了。

https://blog.csdn.net/zhuchua...

请求合并

高并发的场景下,将一个时间窗口内多个相同请求合并成一个请求,较少网络连接次数。

http://blog.didispace.com/spr...

八、开发中遇到的问题

默认情况下,hystrix不会将父线程的上下文传播到有hystrix命令管理的线程中。例如,在默认情况下,对被父线程调用并由@HystrixCommand保护的方法而言,在父线程中设置为ThreadLocal的值是不会自动传递到子线程的。实际场景中的例子,调用通用UserUtil工具类从请求头获取不到userId, jwt串, 租户id。

两种方法:

(1) 手动设置

​ 手动从父线程ThreadLocal中取出需要的key-value设置到子线程中。

(2) 代码配置

​ 自定义HystrixConcurrencyStrategy

https://www.cnblogs.com/duanx...


xiaweiqiang
12 声望4 粉丝

不忘初心