大家好,我是姚半仙,慕课网《Java架构师成长直通车》课程架构师讲师团成员之一。今天我们一起来学习线程隔离。线程隔离是个什么概念呢?简单的说,就是将用户请求线程和服务执行线程分割开来,同时约定了每个服务最多可用线程数。说也说不明白,我就举个例子吧。
假设我们的服务器就是六扇门总部,那每个用户请求都是来访参观的皇亲国戚。首先我们的Web容器有个线程池用来接收请求,我们把这个线程池可以看做是六扇门的大堂,所有来访的用户都要先到大堂接待。接下来,我们六扇门提供了各种各样不同的服务满足这些官老爷们,比如按摩服务,蒸桑拿服务,美容服务。按照老规矩的话,这些服务都是在大堂现场开展,假如大堂只能接待20个人,结果来了这20人全是要按摩的,而按摩服务耗时又比较久,那么后面如果再来个想要蒸桑拿的可就没地方去了。
现在我们六扇门有了新规矩-线程隔离,就是说每个服务单独设置一个小房间(独立线程池),把大厅区域和服务区域隔离开来,每个服务房间也有接待数量限制,比如我设置了按摩房最多接纳10人,桑拿房最多5人,美容护理室也是5人。这样,即便来了20个人喊着要按摩,我们也只能接待10人,剩下的10个人就会收到Thread Pool Rejects。如此一来,也不会耽搁六扇门为用户提供其他服务。
这下明白了吧?那我们就来看一看线程隔离方案的全景图。
一波三折 - 线程隔离的三道坎
在线程隔离的完整链路里,需要经历三道坎(降级)才能取得真经
- 线程池拒绝:这一步是线程隔离机制直接负责的,假如当前商品服务分配了10个线程,那么当线程池已经饱和的时候就可以拒绝服务,调用请求会收到Thread Pool Rejects,然后将被转到对应的fallback逻辑中。其实控制线程池中线程数量是由多个参数共同作用的,我们分别看一下
- coreSize:核心线程数(默认为10)
- maximumSize:设置最大允许的线程数(默认也是10),这个属性需要打开allowMaximumSizeToDivergeFromCoreSize之后才能生效,后面这个属性允许线程池中的线程数扩展到maxinumSize个
- queueSizeRejectionThreshold:这个属性经常会被忽略,这是控制队列最大阈值的,Hystrix默认设置了5。即便把maximumSize改大,但因为线程队列阈值的约束,你的程序依然无法承载很多并发量。所以当你想改大线程池的时候,需要这两个属性一同增大
- keepAliveTimeMinutes:这个属性和线程回收有关,我们知道线程池可以最大可以扩展到maximumSize,当线程池空闲的时候,多余的线程将会被回收,这个属性就指定了线程被回收前存活的时间。默认2分钟,如果设置的过小,可能会导致系统频繁回收/新建线程,造成资源浪费
- 线程Timeout:我们通常情况下认为延迟只会发生在网络请求上,其实不然,在Netflix设计Hystrix的时候,就有一个设计理念:调用失败和延迟也可能发生在远程调用之前(比如说一次超长的Full GC导致的超时,或者方法只是一个本地业务计算,并不会调用外部方法),这个设计理念也可以在Hystrix的Github文档里也有提到。因此在方法调用过程中,如果同样发生了超时,则会产生Thread Timeout,调用请求被流转到fallback
- 服务异常/超时:这就是我们前面学习的的服务降级,在调用远程方法后发生异常或者连接超时等情况,直接进入fallback
Hystrix的业务流程非常复杂,降级、熔断和线程隔离之间还相互影响,大家如果有时间可以尝试着画一幅Hystrix的全家福,把前面学到的Hystrix核心功能的主线脉络全部梳理在一张图上,这就要靠大家深入研究源代码才能理得清楚了。
线程隔离的方式
Hystrix提供了两种线程隔离的方式,分别是线程池技术和信号量技术。这两种方式在业务流程上是一致的,在默认情况下,Hystrix使用线程池的方式。可以使用如下配置参数切换到信号量方式:
execution.isolation.strategy = ExecutionIsolationStrategy.SEMAPHORE
线程池 vs 信号量的优缺点比较
前面我们学习了Hystrix中的线程隔离,通常我们都采用基于线程池的实现方式,这也是最容易理解的方案。Hystrix还提供了另一种底层实现,那就是信号量隔离。
小时候我们就知道“红灯停,绿灯行”,跟着交通信号的指示过马路。信号量也是这么一种放行、禁行的开关作用。它和线程池技术一样,控制了服务可以被同时访问的并发数量,乍一看好像两种技术并没有多大区别,我们接下来比较一下它们在应用场景上的不同之处。
大家来找茬
线程隔离原理
- 线程池技术:它使用Hystrix自己内建的线程池去执行方法调用,而不是使用Tomcat的容器线程
- 信号量技术:它直接使用Tomcat的容器线程去执行方法,不会另外创建新的线程,信号量只充当开关和计数器的作用。获取到信号量的线程就可以执行方法,没获取到的就转到fallback
从性能角度看
- 线程池技术:涉及到线程的创建、销毁和任务调度,而且CPU在执行多线程任务的时候会在不同线程之间做切换,我们知道在操作系统层面CPU的线程切换是一个相对耗时的操作,因此从资源利用率和效率的角度来看,线程池技术会比信号量慢
- 信号量技术:由于直接使用Tomcat容器线程去访问方法,信号量只是充当一个计数器的作用,没有额外的系统资源消费,所以在性能方面具有明显的优势
超时判定
- 线程池技术:相当于多了一层保护机制(Hystrix自建线程),因此可以直接对“执行阶段”的超时进行判定
- 信号量技术:只能等待诸如网络请求超时等“被动超时”的情况
使用场景
我这里引用Hystrix的官方文档,里面特地讲到了信号量的使用场景:
Generally the only time you should use semaphore isolation for HystrixCommands is when the call is so high volume (hundreds per second, per instance) that the overhead of separate threads is too high; this typically only applies to non-network calls
当请求量非常密集,导致线程隔离的开销比较高的时候,建议使用信号量的方式降低负荷,这种情况通常用来应对非网络请求(不需要调用外部服务)。
根据官方建议,信号量适用在超高并发的非外部接口调用上(还是中文言简意赅),注意“the only time”,意思是官方只建议在上述场景中应用信号量技术,在其他场景上尽量使用线程池做线程隔离。
除此之外,线程池技术要特别注意ThreadLocal的数据传递作用,由于前后调用不在同一个线程内,也不在父子线程内,所以如果你在业务层面声明了ThreadLocal变量,将无法获取正确的值。
学习Tips:往往一个开源组件在提供一个功能的时候,不会仅仅提供一种技术方案,比方说Spring在动态代理方面既支持JDKDynamic也支持CGLIB,而Hystrix在资源隔离这里不仅提供了线程池技术,也有基于信号量(Semaphore)的方案。所谓存在即有道理,我们可以把自己放在开源组件开发者的角度,分析一下为什么设计这些不同的方案,如果你是设计者的话会提供怎样的方案,久而久之,我们就能把自己的视野从“使用者”拔高到“设计者”。