并发三剑客之限流方案总结

6,436 阅读5分钟

前言

对于高并发的系统,有三把利器用来保护系统:缓存降级限流。限流常见的应用场景是秒杀、下单和评论等 突发性 并发问题。

  1. 缓存 的目的是提升 系统访问速度系统吞吐量

  2. 降级 是当服务 出问题 或者影响到核心流程的性能,则需要 暂时屏蔽掉,待 高峰 或者 问题解决后 再打开。

  3. 有些场景并不能用 缓存降级 来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(最新的评论)。因此需有一种手段来限制这些场景的 并发/请求量,即 限流

正文

限流的目的

限流的目的是通过对 并发访问/请求进行 限速,或者一个 时间窗口 内的的请求进行限速来 保护系统,一旦达到限制速率则可以 拒绝服务(定向到错误页或告知资源没有了)、排队等待(比如秒杀、评论、下单)、降级(返回托底数据或默认数据,如商品详情页库存默认有货)。

限流的方式

  1. 限制 总并发数(比如 数据库连接池线程池

  2. 限制 瞬时并发数(如 nginxlimit_conn 模块,用来限制 瞬时并发连接数

  3. 限制 时间窗口内的平均速率(如 GuavaRateLimiternginxlimit_req 模块,限制每秒的平均速率)

  4. 限制 远程接口 调用速率

  5. 限制 MQ 的消费速率

  6. 可以根据 网络连接数网络流量CPU内存负载 等来限流

限流的算法

1. 令牌桶

2. 漏桶

3. 计数器

有时候还可以使用 计数器 来进行限流,主要用来限制 总并发数,比如 数据库连接池线程池秒杀的并发数。通过 全局总请求数 或者 一定时间段的总请求数 设定的 阀值 来限流。这是一种 简单粗暴 的限流方式,而不是 平均速率限流

令牌桶 vs 漏桶

令牌桶限制的是 平均流入速率,允许突发请求,并允许一定程度 突发流量

漏桶限制的是 常量流出速率,从而平滑 突发流入速率

应用级别限流

1. 限流总资源数

可以使用池化技术来限制总资源数:连接池线程池。比如分配给每个应用的数据库连接是 100,那么本应用最多可以使用 100 个资源,超出了可以 等待 或者 抛异常

2. 限流总并发/连接/请求数

如果你使用过 Tomcat,其 Connector 其中一种配置有如下几个参数:

  • maxThreads: Tomcat 能启动用来处理请求的 最大线程数,如果请求处理量一直远远大于最大线程数,可能会僵死。

  • maxConnections: 瞬时最大连接数,超出的会 排队等待

  • acceptCount: 如果 Tomcat 的线程都忙于响应,新来的连接会进入 队列排队,如果 超出排队大小,则 拒绝连接

3. 限流某个接口的总并发/请求数

使用 Java 中的 AtomicLong,示意代码:

try{
    if(atomic.incrementAndGet() > 限流数) {
        //拒绝请求
    } else {
        //处理请求
    }
} finally {
    atomic.decrementAndGet();
}

4. 限流某个接口的时间窗请求数

使用 GuavaCache,示意代码:

LoadingCache counter = CacheBuilder.newBuilder()
    .expireAfterWrite(2, TimeUnit.SECONDS)
    .build(newCacheLoader() {
        @Override
        public AtomicLong load(Long seconds) throws Exception {
            return newAtomicLong(0);
        }
    });

longlimit =1000;
while(true) {
    // 得到当前秒
    long currentSeconds = System.currentTimeMillis() /1000;
    if(counter.get(currentSeconds).incrementAndGet() > limit) {
        System.out.println("限流了: " + currentSeconds);
        continue;
    }
    // 业务处理
}

5. 平滑限流某个接口的请求数

之前的限流方式都不能很好地应对 突发请求,即 瞬间请求 可能都被允许从而导致一些问题。因此在一些场景中需要对突发请求进行改造,改造为 平均速率 请求处理。

Guava RateLimiter 提供了 令牌桶算法实现

  1. 平滑突发限流 (SmoothBursty)

  2. 平滑预热限流 (SmoothWarmingUp) 实现

平滑突发限流(SmoothBursty)

RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());

将得到类似如下的输出:

0.0
0.198239
0.196083
0.200609
0.199599
0.19961

平滑预热限流(SmoothWarmingUp)

RateLimiter limiter = RateLimiter.create(5, 1000,  TimeUnit.MILLISECONDS);
for(inti = 1; i < 5; i++) {
    System.out.println(limiter.acquire());
}

Thread.sleep(1000L);
for(inti = 1; i < 5; i++) {
    System.out.println(limiter.acquire());
}

将得到类似如下的输出:

0.0
0.51767
0.357814
0.219992
0.199984
0.0
0.360826
0.220166
0.199723
0.199555

SmoothWarmingUp 的创建方式:

RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit);
  • permitsPerSecond: 表示 每秒新增 的令牌数
  • warmupPeriod: 表示在从 冷启动速率 过渡到 平均速率 的时间间隔

速率是 梯形上升 速率的,也就是说 冷启动 时会以一个比较大的速率慢慢到平均速率;然后趋于 平均速率(梯形下降到平均速率)。可以通过调节 warmupPeriod 参数实现一开始就是平滑固定速率。

分布式限流

分布式限流最关键的是要将 限流服务 做成 原子化,而解决方案可以使用 redis + lua 或者 nginx + lua 技术进行实现。

接入层限流

接入层 通常指请求流量的入口,该层的主要目的有:

  • 负载均衡
  • 非法请求过滤
  • 请求聚合
  • 缓存、降级、限流
  • A/B测试
  • 服务质量监控

对于 Nginx 接入层限流 可以使用 Nginx 自带了两个模块:连接数限流模块 ngx_http_limit_conn_module漏桶 算法实现的 请求限流模块 ngx_http_limit_req_module。还可以使用 OpenResty 提供的 Lua 限流模块 lua-resty-limit-traffic 进行 更复杂的 限流场景。

  • limit_conn: 用来对某个 KEY 对应的 总的网络连接数 进行限流,可以按照如 IP域名维度 进行限流。

  • limit_req: 用来对某个 KEY 对应的 请求的平均速率 进行限流,并有两种用法:平滑模式delay)和 允许突发模式 (nodelay)。

OpenResty 提供的 Lua 限流模块 lua-resty-limit-traffic 可以进行更复杂的限流场景。


欢迎关注技术公众号: 零壹技术栈

零壹技术栈

本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。