前言

在微服务架构中,服务与服务之间的依赖和调用越来越复杂,为了保证服务的稳定性,应对不同的业务场景,通常需要引入限流、熔断降级等其他系统负载保护来保证服务的稳定性;

限流

顾名思义,就是对请求或者并发数进行限制;通过监控应用流量QPS或者并发数等指标,当达到指令阈值就对流量进行限制,避免瞬时流量高峰压垮服务,导致资源耗尽而造成服务崩溃;

常见场景:双十一秒杀,热点新闻等

其中:

阈值:在一个单位时间内允许的请求量。如 QPS 限制为10,说明 1 秒内最多接受 10 次请求。

拒绝策略:超过阈值的请求的拒绝策略,常见的拒绝策略有直接拒绝、排队等待等。

常见限流算法

计数器算法(固定窗口)

计数器算法:维护一个计数器,以单位时间段作为一个窗口,当一次请求访问某个资源时候,判断计数器与阈值大小,小于阈值允许访问,并且计数器+1,否则触发限流策略,拒绝访问,当前时间窗口过去计数器清零。如下:

 实现方式:

使用redis中incr命令自增+过期时间即可;伪代码如下:

local count = redis.call("incr",KEYS[1])
if count == 1 then
  redis.call('expire',KEYS[1],ARGV[2])
end
if count > tonumber(ARGV[1]) then
  return 0
end
return 1

优点:实现简单;

缺点:存在临界值问题,假设设置资源每10s负载能力为100,即每个10s周期内访问量限制不超过100,前一周期的最后1s和下一周期的前1s内请求量都为100,虽然满足每个周期不超过阈值,但是2s内已经有200请求量,超过系统负载,可能导致服务崩溃;

滑动窗口算法

为了解决固定窗口临界值的问题,引入滑动窗口算法,滑动窗口算法将一个时间周期分成n个小周期,分别记录每个小周期内的访问次数,并且根据时间滑动删除过期的小周期;

假设时间周期为10s,阈值100,将10s在分为10个小周期,即每个小周期(1s)的访问阈值为10,超过10则出发限流策略,可以避免上述临界值问题;

 从图中不难看出,滑动窗口算法就是固定窗口的升级版。将计时窗口划分成一个小窗口,滑动窗口算法就退化成了固定窗口算法。而滑动窗口算法其实就是对请求数进行了更细粒度的限流,窗口划分的越多,则限流越精准。

实现方式:

使用redis中zset数据结构;伪代码如下:

--KEYS[1]: 限流 key
--ARGV[1]: 时间窗口值,如1000ms
--ARGV[2]: 当前时间毫秒值(作为zset中的score)
--ARGV[3]: 阈值
--ARGV[4]: 唯一id,作为zset中的member;
-- 1. 移除时间窗口之前的数据
redis.call('zremrangeByScore', KEYS[1], 0, ARGV[1])
-- 2. 统计当前元素数量
local res = redis.call('zcard', KEYS[1])
-- 3. 是否超过阈值
if (res == nil) or (res < tonumber(ARGV[3])) then
    redis.call('zadd', KEYS[1], ARGV[2], ARGV[4])
    return 1
else
    return 0
end

优点:实现简单;避免固定窗口的临界问题

缺点:滑动窗口和固定窗口都无法解决短时间之内集中流量的突击。无法平滑的处理流量,如每秒限制100个请求,理想情况是希望每10ms一个请求,但是实际情况可能5ms内就有100个请求,后续的请求会被粗暴拒绝;无法精准控制频率,当然可以设置多条限流规则,如同时设置限流每秒100个请求+每10ms不超过2个请求;

漏桶算法

为了解决滑动窗口突发流量平滑处理问题和粗暴拒绝请求,可以使用漏桶算法;

漏桶算法将请求先放入漏桶中,如漏桶容量已达到阈值(限流值),则丢弃(触发拒绝策略),并且漏桶以固定的速率进行释放请求(即执行请求),直到漏桶为空时阻塞;漏桶算法处理限流更加平滑和柔性,处理速率固定且不会粗暴拒绝请求;示意图如下:

实现方式:

优点:平滑处理,柔性策略,类似队列思想

缺点:面对突发请求,服务的处理速度和平时是一样的可能不是我们想要的,我们可能希望在面对突发流量时候在系统平稳的同时,提升用户体验即能更快的处理请求,而不是和正常流量一样;

令牌桶算法

令牌桶算法是对漏斗算法的一种改进,除了能够起到限流的作用外,还允许一定程度的流量突发。

令牌桶算法定义限流大小,按照一定速率往令牌桶添加令牌,如果桶内令牌数超过阈值则丢弃该令牌,请求时先通过令牌桶获取令牌,拿到令牌后处理请求,否则执行拒绝策略;

实现方式:

Google 的 Java 开发工具包 Guava 中的限流工具类 RateLimiter 就是令牌桶的一个实现,直接使用 RateLimiter 即可,引入依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>27.0-jre</version>
</dependency>

代码使用

RateLimiter rateLimiter = RateLimiter.create(2);/QPS=2
for (int i = 0; i < 10; i++) {
    String time = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME);
    System.out.println(time + ":" + rateLimiter.tryAcquire());
    Thread.sleep(250);
}

优点:令牌桶在应对突发流量时处理更好,如桶内有 100 个令牌,那么这 100 个令牌可以马上被取走,而不像漏桶那样匀速的消费;

缺点:实现复杂

总结

限流算法各有优缺点,按照实际情况(落地)和系统需求(请求流量、是否有突发流量等)灵活选用;

市面上也有比较成熟的限流工具和框架。如Google的Guava中基于令牌桶实现的限流组件,以及alibaba开源的流量控制框架Sentinel,基于不同策略和模式使用不同的限流算法实现,如:匀速排队方式会严格控制请求通过的间隔时间,即让请求以均匀的速度通过,对应的是漏桶算法。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐