3
如何设计实现一个轻量的开放API网关之限流

文章地址: https://blog.piaoruiqing.com/blog/2019/08/26/开放api网关实践三-限流/

前言

开发高并发系统时有多重系统保护手段, 如缓存、限流、降级等. 在网关层, 限流的应用比较广泛. 很多情况下我们可以认为网关上的限流与业务没有很强的关联(与系统的承载能力有关), 且各个子系统都有限流这种需求, 将部分限流功能放到网关会比较合适.

什么是限流

众所周知, 服务器、网站应用的处理能力是有上限的, 不论配置有多高总会有一个极限, 超过极限如果放任继续接收请求, 可能会发生不可控的后果.

举个栗子🌰, 节假日网上购票, 常常会遇到排队中系统繁忙请稍后再试等提示, 这便是服务端对单位时间处理请求的数量进行了限制, 超出限制就会排队、降级甚至拒绝服务, 否则如果把系统搞崩了, 大家都买不到票了╮( ̄▽ ̄)╭.

12306系统繁忙

我们先给出限流的定义: 限流是高并发系统保护保护手段之一, 在网关层的应用很广泛. 其目的是对并发请求进行限速或限制一个时间窗口内请求的数量, 一旦达到阈值就排队等待或降级甚至拒绝服务.

其最终目的是: 在扛不住过高并发的情况下做到有损服务而不是不服务.

常用限流玩法

令牌桶

令牌桶算法, 是一个存放固定数量令牌的桶按照固定速率添加令牌. 如图:

令牌桶算法

  • 按照固定速率向桶中添加令牌.
  • 桶满时拒绝增加新令牌.
  • 每次请求消耗一个令牌(也可根据数据包大小来消耗对应的令牌数).
  • 当令牌不足时, 拒绝请求(或等待).
  • 特点: 可以应对一定程度的突发.

举个现实生活中比较常见的例子来理解, 电影院售票, 每场电影所售出的票数是一定的, 如果来晚了(后面的请求)就没票了, 要么等待下一场(等待新的令牌发放), 要么不看了(被拒绝).

漏桶

漏桶是一个底部破洞的桶, 水可以匀速流出(这时候不考虑压强, 不要杠( ̄. ̄)), 所以与令牌桶不一样的是, 漏桶算法是匀速消费, 可以用来进行流量整形流量控制. 如图:

漏桶算法

  • 固定容量的漏桶, 按照固定速率流出水(不要杠水深和压强的问题).
  • 流入水的速率固定, 溢出则被丢弃.
  • 特点: 平滑处理速率.

[版权声明]
本文发布于朴瑞卿的博客, 允许非商业用途转载, 但转载必须保留原作者朴瑞卿 及链接:blog.piaoruiqing.com. 如有授权方面的协商或合作, 请联系邮箱: piaoruiqing@gmail.com.

应用级限流

一个单体的应用程序有其承受极限, 在高并发情况下, 有必要进行过载保护, 以防过多的请求将系统弄崩. 最简单粗暴的方式就是使用计数器进行控制, 处理请求时+1, 处理完毕后-1, 除此之外我们还可以利用前文提到的令牌桶和漏桶来进行更精细的限流.如果网关是单体应用, 我们完全可以不借助其他介质, 直接在应用级别进行限流.

计数器

这种方式实现最简单粗暴,

try {
    if (counter.incrementAndGet() > limit) {
        throw new SomeException();
    }
    // do something
} finally {
    counter.decrementAndGet();
}

令牌桶

Guava提供了令牌桶算法的实现.

@Test
public void testGuavaRateLimiter() throws InterruptedException {
    RateLimiter limiter = RateLimiter.create(5);
    TimeUnit.SECONDS.sleep(1);    // 等待一秒钟发几个令牌
    for (int index = 0; index < 10; index++) {
        System.out.println(limiter.acquire()); // 打印等待时间
    }
}

输出为:

0.0
0.0
0.0
0.0
0.0
0.0
0.196108
0.194372
0.19631
0.198373

在令牌用尽后, 后面的请求都要等待有新的令牌后才能继续执行.

应用级限流实现简单, 但其局限性在于无法进行全局限流, 对于集群就无能为力了.

分布式限流

想要在集群中进行全局限流, 其关键在于将限流信息记录在共享介质中, 如Redismemcached等. 为了将限流做的精确, 写必须是原子操作.

分布式限流

Redis+Lua是一个不错的选择, 示例Lua脚本如下:

local key = KEYS[1] -- 限流的KEY
local limit = tonumber(ARGV[1])    -- 限流大小
local current = tonumber(redis.call('get', key) or '0')
if current + 1 > limit then
    return 0
else
    redis.call('INCRBY', key,'1')
    redis.call('expire', key,ARGV[2])    -- 过期时间
    return current + 1
end
  • 分布式限流将令牌的发放放到共享介质中.
  • 获取(消费)令牌操作必须是原子的.
  • 共享介质要高可用(Redis集群)

结语

网关作为内部系统外的一层屏障, 对内起到一定的保护作用, 限流便是其中之一. 网关层的限流可以简单地针对不同业务的接口进行限流, 也可考虑将限流功能做成网关的一个功能模块(如限流规则的配置、统计、针对用户维度进行统计和限流等)

推荐阅读:

欢迎关注公众号(代码如诗):
图片描述

[版权声明]
本文发布于朴瑞卿的博客, 允许非商业用途转载, 但转载必须保留原作者朴瑞卿 及链接:blog.piaoruiqing.com. 如有授权方面的协商或合作, 请联系邮箱: piaoruiqing@gmail.com.


草堂笺
34 声望3 粉丝