当前位置: 首页 > >

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

发布时间:

前言

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


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

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

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


正文
限流的目的

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


限流的方式

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

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

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

    限制 远程接口 调用速率

    限制 MQ 的消费速率

    可以根据 网络连接数网络流量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 提供了 令牌桶算法实现


    *滑突发限流 (SmoothBursty)

    *滑预热限流 (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 可以进行更复杂的限流场景。



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







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



友情链接: