对于高并发的系统,有三把利器用来保护系统:缓存、降级 和 限流。限流常见的应用场景是秒杀、下单和评论等 突发性 并发问题。
缓存 的目的是提升 系统访问速度 和 系统吞吐量。
降级 是当服务 出问题 或者影响到核心流程的性能,则需要 暂时屏蔽掉,待 高峰 或者 问题解决后 再打开。
有些场景并不能用 缓存 和 降级 来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(最新的评论)。因此需有一种手段来限制这些场景的 并发/请求量,即 限流。
限流的目的是通过对 并发访问/请求进行 限速,或者一个 时间窗口 内的的请求进行限速来 保护系统,一旦达到限制速率则可以 拒绝服务(定向到错误页或告知资源没有了)、排队 或 等待(比如秒杀、评论、下单)、降级(返回托底数据或默认数据,如商品详情页库存默认有货)。
限制 总并发数(比如 数据库连接池、线程池)
限制 瞬时并发数(如 nginx
的 limit_conn
模块,用来限制 瞬时并发连接数)
限制 时间窗口内的平均速率(如 Guava
的 RateLimiter
、nginx
的 limit_req
模块,限制每秒的平均速率)
限制 远程接口 调用速率
限制 MQ
的消费速率
可以根据 网络连接数、网络流量、CPU
或 内存负载 等来限流
有时候还可以使用 计数器 来进行限流,主要用来限制 总并发数,比如 数据库连接池、线程池、秒杀的并发数。通过 全局总请求数 或者 一定时间段的总请求数 设定的 阀值 来限流。这是一种 简单粗暴 的限流方式,而不是 平均速率限流。
令牌桶限制的是 平均流入速率,允许突发请求,并允许一定程度 突发流量。
漏桶限制的是 常量流出速率,从而平滑 突发流入速率。
可以使用池化技术来限制总资源数:连接池、线程池。比如分配给每个应用的数据库连接是 100
,那么本应用最多可以使用 100
个资源,超出了可以 等待 或者 抛异常。
如果你使用过 Tomcat
,其 Connector
其中一种配置有如下几个参数:
maxThreads: Tomcat
能启动用来处理请求的 最大线程数,如果请求处理量一直远远大于最大线程数,可能会僵死。
maxConnections: 瞬时最大连接数,超出的会 排队等待。
acceptCount: 如果 Tomcat
的线程都忙于响应,新来的连接会进入 队列排队,如果 超出排队大小,则 拒绝连接。
使用 Java
中的 AtomicLong
,示意代码:
try{
if(atomic.incrementAndGet() > 限流数) {
//拒绝请求
} else {
//处理请求
}
} finally {
atomic.decrementAndGet();
}
使用 Guava
的 Cache
,示意代码:
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;
}
// 业务处理
}
之前的限流方式都不能很好地应对 突发请求,即 瞬间请求 可能都被允许从而导致一些问题。因此在一些场景中需要对突发请求进行改造,改造为 平均速率 请求处理。
Guava RateLimiter
提供了 令牌桶算法实现:
平滑突发限流 (SmoothBursty
)
平滑预热限流 (SmoothWarmingUp
) 实现
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
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);
速率是 梯形上升 速率的,也就是说 冷启动 时会以一个比较大的速率慢慢到平均速率;然后趋于 平均速率(梯形下降到平均速率)。可以通过调节 warmupPeriod
参数实现一开始就是平滑固定速率。
分布式限流最关键的是要将 限流服务 做成 原子化,而解决方案可以使用 redis + lua
或者 nginx + lua
技术进行实现。
接入层 通常指请求流量的入口,该层的主要目的有:
对于 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
可以进行更复杂的限流场景。
欢迎关注技术公众号: 零壹技术栈
本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。