服务雪崩效应是一种因“服务提供者的不可用”(原因)导致“服务调用者不可用”(结果),并将不可用逐渐放大的现象。
解释上面这句话:服务提供者不可用,比如,服务提供者A服务的访问压力过大,或者是网络原因,硬件原因等等多种因素,造成了服务提供者的不可访问。此时,相应的服务调用者B服务,就无法成功调用其提供的接口,并且造成线程阻塞,挤压线程。随着调用次数的增多,挤压的线程越来越多,那么这个服务调用者的抗并发量,就越来越少,直至最后崩掉。依次类推,B服务还为C服务提供了接口,那么B服务崩掉了,C服务的线程也开始挤压,直至C服务崩掉。依次类推,最后的效果就是所有的微服务都崩掉了。这就是服务雪崩效应。
造成服务雪崩的原因有很多,包括硬件原因,网络原因,软件原因等等。这里,我们只谈从软件方面,解决服务雪崩的几种解决方案和解决套路,以此来了解微服务架构中相关的技术栈的由来以及使用理由。
超时:
造成线程的挤压都是因为同步远程调用长时间没有得到回复造成的,所以设置超时时间,可以减少线程的长时间占用,避免线程挤压。
限流:
对服务进行限流,避免服务因并发量过大而造成服务崩掉。
熔断:
对已经挂掉的服务,直接不再进行调用,而是直接返回结果,这就是熔断。以此避免已经挂掉的服务对调用者造成的影响。
隔离:
下面详细介绍
服务降级:
下面详细介绍
限流,顾名思义,就是控制微服务的流量。多余的请求,直接拒绝掉。以此保护微服务不至于压力过大而崩溃。
已经有很多优秀的框架实现了限流策略,这里介绍一下限流常用的几个算法。
计数器算法:
1.固定窗口计算器算法:
固定单位时间内处理多少请求。比如,1秒固定处理100个请求,那么1秒内,多余100个的请求,直接拒绝掉。
这种算法的缺点就是无法平稳的控制请求流量。比如,在第一秒前面一直没有请求进来,而在最后时刻进来100个请求,然后马上进入了第二秒。第二秒就又可以容纳100个请求,此时,第二秒刚开始就来了100个请求,那么也不会限流。这就相当于是在短时间内接收了两个限流级别的并发请求,服务很容易崩掉。
这里的100个请求只是打个比方,一般我们设置阈值一定是设置一个服务最大承受并发的阈值,可以设置几千甚至几万。试想一下,采用固定窗口算法,当两个阈值级别的请求一下子过来后,该服务很容易会崩掉。
2.滑动窗口计算器算法:
针对上面固定窗口算法带来的问题,出现了滑动窗口算法。他是把单位时间划分成更小的粒度级别,如1秒内接收100个请求,那么,把1秒平分成10份,那么就是10个窗口,每个窗口最多只能接收10个请求。
漏桶算法:
漏桶算法是加了一个桶(容器),一方面,发来的请求存入桶内。直至桶里放满,则再来的请求直接丢弃。另一方面,桶内请求以一定的速率出去,到达目标服务中去。这样的缺点就是系统只能以一定的速率接收到请求,无法应对突然增大的并发量。
令牌桶算法:
令牌桶算法是对漏桶算法的优化,往桶里添加的不再是请求,而是令牌,以一定的速率往桶里放请求。当请求来时,桶里的令牌会发送给请求,请求拿着令牌才会到达目标服务。如果桶里令牌没有令牌,则执行限流。这样,令牌桶是以一定速率生成,而消费令牌桶,则不是以一定速率,而是根据请求数量的多少而决定消费的快慢,这样,就可以抗住一些突发量的大并发请求。
隔离分为进程隔离,线程隔离和信号量隔离。隔离机制的本质就是将服务调用的粒度划分的更小,以此来减少服务生产崩溃而对服务调用带来的影响,避免服务雪崩现象产生。
进程隔离:
进程是传统操作系统中的重要隔离机制,每一个进程拥有独立的地址空间,提供操作系统级别的保护区。一个进程出现问题不会影响其他进程的正常运行,一个应用出错也不会对其他应用产生副作用。
随着微服务和容器技术的发展,我们说微服务的最佳载体正是容器。容器的本质就是一个进程,Docker鼓励一个容器只运行一个进程。这种方式非常适合以进程为粒度的微服务架构。也有人利用Docker容器作为轻量级的虚拟化方案,在单个容器中同时运行多个进程,这种使用方式往往会给应用带来隔离性问题和运行隐患。
进程与进程之间的互相隔离实现了容器之间互不影响的特性。在启动一个容器时,本质上就是启动了一个进程,Linux通过Namespace技术实现容器之间的隔离,通过Cgroups来实现容器的资源控制。用户的应用进程实际上就是容器里PID=1的进程,也是其他后续创建的所有进程的父进程,这意味着没有办法同时运行两个不同的应用,除非你能事先找到一个公共的PID=1的程序来充当两个不同应用的父进程。
容器作为一种沙箱技术,提供了类似集装箱的功能,把应用封装起来,这样被装进集装箱的应用就可以方便地被复制和移动。容器具备了天然的“不共享任何资源”的特性,所以可以认为容器是独立的服务主体。容器在与其他容器交互时,需要使用基于网络的消息通信机制,摆脱了模块之间的强依赖耦合。我们在本书后面的内容中会进一步介绍容器隔离机制的细节。
线程隔离:
进程虽然具有较好的隔离性,但是进程之间交互需要跨进程边界,进程在数据共享方面存在数据传输的开销。而多线程并发编程模式可以最大限度地提高程序的并行度,线程作为操作系统最小的调度单元可以更好地利用多处理器的优势,恰当地使用线程可以降低开发和维护的开销,并且可以提高复杂应用的性能,当前在软件系统中大量使用了多线程编程模型。
线程隔离主要指线程池的隔离,在应用系统内部,将不同请求分类发送给不同的线程池,当某个服务出现故障时,可以根据预先设定的熔断策略阻断线程的继续执行。来看如下图所示的案例。
在Consumer模块中,接口A和接口C共用相同的线程池,当接口A的访问量激增时,接口A因为与接口C共用相同的线程池,所以势必影响接口C的效率,进而可能产生雪崩效应。如果我们使用线程隔离机制,那么可以将接口A和接口C做一个很好的隔离,如下图所示。
使用线程隔离机制将使线程池内可能出现问题的线程和其他线程隔离运行在一个独立的线程池中,一旦此线程出现问题,不会影响其他线程的运行,防止雪崩效应的产生。
信号量隔离:
信号量semaphore是一个并发工具类,用来控制可同时并发的线程数。其内部维护了一组虚拟许可,通过构造器指定许可的数量,每次线程执行操作时先通过acquire方法获得许可,执行完毕再通过release方法释放许可。如果无可用许可,那么acquire方法将一直阻塞,直到其他线程释放许可。在信号量隔离机制下,接收请求和执行下游依赖在同一个线程内完成,不存在线程上下文切换所带来的性能开销。
信号量的资源隔离只是起到开关的作用,比如,服务A的信号量大小为10,那么就是说它同时只允许有10个Tomcat线程来访问服务A,其他请求都会被拒绝,从而达到资源隔离和限流保护的作用。
有点类似于限流的思想。
参考文章:精华内容
首先,先知道什么是服务熔断和降级。
服务熔断就是实现一个熔断器。熔断器的实现需要考虑如下因素:
第一、熔断器默认是关闭状态。关闭状态的熔断器,请求是畅通无阻的,可以正常到达调用的服务去。那熔断器什么时候处于打开状态,什么时候处于半开状态呢?需要有一个算法来支撑。通常的做法是利用滑动时间窗口,记录每个时间片内,相关的指标数据,根据这些指标数据,来判断熔断器的状态是否需要切换。
每个时间片段,称之为bucket,滑动窗口默认维护10个bucket,单位时间为1s。即每秒生成一个bucket,最大维护10个bucket,超过10个后,最早的那个bucket被抛弃,始终保持10个bucket。每个bucket记录请求的总数,成功数,超时数,拒绝数等指标数据。默认错误超过50%进行中断拦截(总之会根据滑动窗口内指标数据的综合考虑,进行熔断器状态的设置)。
第二、熔断恢复。正常情况下,熔断器是出于闭合状态,当熔断指标到达阈值时,熔断器状态变为打开状态。此时,所有的请求直接返回或抛出异常。熔断器处于打开状态一段时间后,会转换成半开状态,进行一部分请求的放行,尝试被隔离的服务,是否已经正常,如果正常,则熔断器变为闭合状态,请求继续通过,如果不正常,则熔断器继续变为打开状态。
第三、熔断后,如何处理请求。断路器处于打开状态后,请求直接返回。业务系统如何知道发生了什么情况呢?可向业务层抛出特定的异常,用于标识断路器打开状态,然后,就用到了服务降级技术。熔断器通常通过降级措施来处理,提供提供降级接口或实现,由熔断器自行处理降级措施,开发人员只需要实现降级逻辑即可,其它事情就交给熔断器来处理。
参考文章:微服务熔断隔离机制及注意事项