限流,顾名思义,限制系统的流量,防止用户过多地访问系统的资源,甚至是恶意地访问,比如恶意爬虫,DDOS 等;同时也防止系统承载过多流量而崩溃,从而对系统运行资源做到一个有效的管理
在分布式系统中,节点之间需要相互调用,如果调用链中一个节点宕机,将会导致整个链路都无法访问,从而造成雪崩问题,使整个系统出现不可用状态,为了解决这种问题,保持高可用的性质,常见处理方式有:超时返回,调用方调用超时直接返回不再无效等待;舱壁模式,限定每个业务能使用的线程数,当多个线程阻塞于同一个服务,后续的线程将不能调用该服务,也就不会被阻塞;熔断降级,将被调用的业务进行熔断降级;以及 限流。
前三种更多是在节点宕机被发现后的应对措施,而限流更偏向于一种预防措施
分布式系统中,主流的限流方案有两种:
计数限流应该是最简单的限流算法。例如系统限制同时只能处理 100 个请求,那么就维护一个计数器,接受一个请求时计数器加一,处理完毕时计数器减一,且每次请求到来时先判断计数器的值是否超出阈值,是的话拒绝请求
根据系统是单体系统还是分布式系统又可细分为单机限流跟分布式限流。单机限流的话可以使用 Java 中的原子整数作为计数器;分布式限流的可以使用 Redis 中的 incr 命令
计数限流算法的缺陷在于,只考虑了流量的阈值,没有考虑流量的突发性。一小时达到 100 个请求的压力跟 1 秒内达到 100 个请求的压力是完全不同的,后者这种突发性的流量对系统的压力是非常大的,因此需要限制一定时间间隔内的流量大小,引入时间窗口,比如最简单的 固定窗口限流
固定窗口算法相比于传统计数限流多了一个时间窗口的概念,计数器在每个固定的时间窗口内,如果计数器的值小于请求阈值,就允许请求访问,同时计数器增一;否则就不允许。每经过一个时间窗口的长度计数器就重置,进入到下一个时间窗口
这种算法看起来确实可以确保每个窗口内的请求数不超出阈值,但却不能确保两个窗口之间的交界区域中的限流,比如阈值设置为 每个窗口为 1 s,然后每秒最多 100 个请求,那么如果在第 i 个窗口的后半段有 51 个请求,进入到第 i + 1 个窗口时计数器重置,在第 i + 1 个窗口的前半段有 51 个请求,那么根据算法,这 102 个请求都是会被允许的,但是在第 i 个窗口的后半段跟第 i + 1 个窗口的前半段组成的这个同样为 1s 的时间窗口中却有 102 个请求被允许,不符合限流的阈值要求
所以时间窗口不能固定,需要使用滑动窗口
滑动窗口中不需要记录每次时间窗口的起始边界,而是在每个请求到来时,根据时间戳来减去时间窗口长度,比如 1 s,然后动态地得到窗口的边界,再判断到达时间戳处于该窗口内的请求数是否超出阈值,从而确定允许或者拒绝该请求。所以该算法需要保存每个请求的到达时间戳,同时需要清除掉窗口长度之前的过时请求的到达时间戳,如果一个窗口长度内的请求数很多,需要花费一定的内存存储开销
这种方法的问题在于:
为了解决流量突发导致流量不平滑的问题,我们设置一个漏桶,当请求到来时不是直接判断允许或者拒绝,而是先放到桶中,如果桶内存放的请求数量达到桶的容量了再拒绝后续的请求,然后漏桶定时地将桶内的请求放出,由后端服务拿去处理
可以看出,在这种算法中,无论请求产生的速率多大,后端服务拿去处理的速率都是固定的,从而使流量平滑,跟消息队列很类似,削峰填谷。在这里,计算机中的一大定理 —— 一切问题都能通过增加一个中间层来解决,再一次发挥作用
但是 绝对的流量平滑并不一定是好事。有些突发请求,我们是可以接受的,因为需要为了满足用户的体验而尽快处理,只要在系统可以平稳运行的前提下即可。为此,需要令牌桶算法
令牌桶算法中同样需要一个漏桶,但放入桶中的不是请求了,而是令牌。令牌会定时地放入漏桶,如果桶中令牌数量超出桶容量,则后续的令牌被丢弃。当有请求到来时,需要先向桶索取令牌,索取成功则被允许可以处理,否则被拒绝。这个思路跟 信号量 很类似,可以控制某种资源被同时访问的对象数目
当多个请求突发时,假设桶内有充足的令牌,那么这些突发的请求都可以马上获取令牌然后被处理,而不像漏桶算法那样只能以永远固定的速率被处理,所以在应对突发流量时,令牌桶算法的表现更佳
令牌桶算法有个问题就是,在系统刚开始运行时,桶中是没有令牌的,那么一开始的请求就获取不到足够的令牌,无法被处理,但系统刚开始运行时应该是有充足的资源来处理请求的。处理方案就是一开始应该进行令牌桶的 预热,预先放入几个令牌,确保系统刚开始运行时的请求能及时被处理
Hystrix 是 SpringCloud 框架中的一个组件,可以使用信号量跟线程池来进行限流
Nginx 优秀的代理,路由和负载均衡功能使其成为网关中的首选,而且它也提供了限流的功能,因此选择网关限流的话可以使用 Nginx。Nginx 提供了两种限流方法,分别是控制速率以及控制并发连接数,采用的算法是漏桶算法
Sentinel 是阿里开源的用于服务容错的综合性解决方案,支持限流熔断降级等功能
对资源的 限流阈值 可以设置为 QPS 或者 每秒的线程数
默认的 限流模式 为 直接模式,限流效果为快速失败。此外还支持其它限流模式,如关联模式和链路模式。
在 关联模式 中,对于某个资源,可以设置它的关联资源,当它的关联资源达到限流阈值时,它自己就会被限流。这种操作适用于为了确保某些重要资源可用而限制其它资源的 应用让步 的场景,例如订单服务有读取订单信息跟写入订单信息两个接口,在高并发的场景下,可能两个接口都会占用系统资源,这种情况下我们可能想优先保障写入信息接口的使用,那就可以使用关联模式,对读取信息接口开启关联模式,设置关联资源为写入信息接口,这样当写入信息接口的请求增多,读取信息的接口就会被限制,从而使系统资源倾向于写入信息接口,确保写入信息接口的可用。
链路模式 基于指定链路的入口进行限流,不同的资源接口可能有不同的或相同的入口,对某个入口的限流不会影响在其它入口进入的请求
限流效果 除了 快速失败 还有 Warm Up 和 排队等待。
Warm Up 主要应对于系统长期处于低水位状态下,遇到流量突然暴增,直接把系统抬高到高水位使得系统有可能被压垮的情况。通过冷启动和预热,避免冷系统被压垮。基于的是令牌桶算法。具体到实现来说,如果某个资源选择的是 Warm Up 的限流效果,那么请求的阈值一开始会是初始阈值除以一个冷却因子,默认为 3,即 一开始的阈值只有用户设置的阈值的三分之一,然后 经过预热时长,把阈值逐渐升至设定的阈值,从而完成冷启动。
排队等待主要用于严格限制请求通过系统的速率,以匀速方式经过,对应的是漏桶算法
我们知道,在电路中,保险丝用于保护电路,当电路电流过大时,保险丝就会熔断,从而避免器件损坏。而应用系统中的熔断也类似如此。服务熔断是指调用方访问服务时通过一个断路器作为代理进行调用,而断路器会持续观察被调用服务返回的状态是成功亦或是失败,当失败次数超过设置的阈值时断路器打开,请求就不能到达服务了,从而避免调用方阻塞于调用过程
断路器有三种状态:
在服务被熔断后,一般会让后续的请求走事先配置好的处理方法,这个处理方法就是一个降级逻辑。通常是在系统高并发时,为了使重要的核心业务正常运行,对非核心,非关键的业务不再让其正常地占有部分资源,进行降级处理,从而让出系统资源给核心业务执行