转载自:https://blog.youkuaiyun.com/xiaomin1991222/article/details/84827800
在高并发系统时有三把利器用来保护系统:缓存、降级和限流。
缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发 / 请求量,即限流。
限流的目的是通过对并发访问 / 请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。
一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如 nginx 的 limit_conn 模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如 Guava 的 RateLimiter 、 nginx 的 limit_req 模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制 MQ 的消费速率。另外还可以根据网络连接数、网络流量、 CPU 或内存负载等来限流。
限流算法
计数器
计数器是最简单粗暴的算法。比如某个服务最多只能每秒钟处理100个请求。那么可以设置一个1秒钟的滑动窗口,窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数。
内存中需要保存10次的次数。可以用数据结构LinkedList来实现。格子每次移动的时候判断一次,当前访问次数和LinkedList中最后一个相差是否超过100,如果超过就需要限流了。
很明显,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

示例代码如下:
//服务访问次数,可以放在Redis中,实现分布式系统的访问计数
Long counter = 0L;
//使用LinkedList来记录滑动窗口的10个格子。
LinkedList<Long> ll = new LinkedList<Long>();
public static void main(String[] args){
Counter counter = new Counter();
counter.doCheck();
}
private void doCheck(){
while (true){
ll.addLast(counter);
if (ll.size() > 10){
ll.removeFirst();
}
//比较最后一个和第一个,两者相差一秒
if ((ll.peekLast() - ll.peekFirst()) > 100){
//To limit rate
}
Thread.sleep(100);
}
}
令牌桶算法(Token Bucket)
令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。即按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token,如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。
漏桶算法(Leaky Bucket)
漏桶作为计量工具时,可以用于流量整形和流量控制,漏桶算法是一个固定容量的漏桶,按照常量固定速率流出水滴,可以以任意速率流入水滴到漏桶,如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
令牌桶和漏桶对比
令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿 3 个令牌, 4 个令牌),并允许一定程度突发流量;漏桶限制的是常量流出速率(即流出速率是一个固定常量值),从而平滑突发流入速率;
应用级限流
限流总并发/连接/请求数
对于一个应用系统来说一定会有极限并发/请求数,即总有一个 TPS/QPS 阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此最好进行过载保护,防止大量请求涌入击垮系统。
如果使用过 Tomcat ,其 Connector 其中一种配置有如下几个参数:
acceptCount: 如果 Tomcat 的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接;
maxConnections: 瞬时最大连接数,超出的会排队等待;
maxThreads: Tomcat 能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数则可能会僵死。
限流总资源数
如果有的资源是稀缺资源(如数据库连接、线程),而且可能有多个系统都会去使用它,那么需要限制应用;可以使用池化技术来限制总资源数:连接池、线程池。比如分配给每个应用的数据库连接是 100 ,那么本应用最多可以使用 100 个资源,超出了可以等待或者抛异常。
限流某个接口的总并发 / 请求数
如果接口可能会有突发访问情况,但又担心访问量太大造成崩溃,如抢购业务;这个时候就需要限制这个接口的总并发 / 请求数总请求数了;因为粒度比较细,可以为每个接口都设置相应的阀值。可以使用 Java 中的 AtomicLong 进行限流:
try {
if(atomic.incrementAndGet() > 限流数) {
//拒绝请求
}
//处理请求
} finally {
atomic.decrementAndGet();
}
适合对业务无损的服务或者需要过载保护的服务进行限流,如抢购业务,超出了大小要么让用户排队,要么告诉用户没货了,对用户来说是可以接受的。而一些开放平台也会限制用户调用某个接口的试用请求量,也可以用这种计数器方式实现。这种方式也是简单粗暴的限流,没有平滑处理,需要根据实际情况选择使用。
限流某个接口的时间窗请求数
即一个时间窗口内的请求数,如想限制某个接口 / 服务每秒 / 每分钟 / 每天的请求数/ 调用量。如一些基础服务会被很多其他系统调用,比如商品详情页服务会调用基础商品服务调用,但是怕因为更新量比较大将基础服务打挂,这时要对每秒 / 每分钟的调用量进行限速;一种实现方式如下所示:
LoadingCache<Long, AtomicLong> counter =
CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicLong>() {
@Override
public AtomicLong load(Long seconds) throws Exception {
return new AtomicLong(0);
}
});
long limit = 1000;
while(true) {
//得到当前秒long currentSeconds = System.currentTimeMillis() / 1000;
if(counter.get(currentSeconds).incrementAndGet() > limit) {
System.out.println("限流了:"+ currentSeconds);
continue;
}
//业务处理
}
使用 Guava 的 Cache 来存储计数器,过期时间设置为 2 秒(保证 1 秒内的计数器是有的),然后获取当前时间戳然后取秒数来作为 KEY 进行计数统计和限流,这种方式也是简单粗暴,刚才说的场景够用了。
平滑限流某个接口的请求数
之前的限流方式都不能很好地应对突发请求,即瞬间请求可能都被允许从而导致一些问题;因此在一些场景中需要对突发请求进行整形,整形为平均速率请求处理(比如 5r/s ,则每隔 200 毫秒处理一个请求,平滑了速率)。这个时候有两种算法满足我们的场景:令牌桶和漏桶算法。 Guava 框架提供了令牌桶算法实现,可直接拿来使用。
Guava RateLimiter 提供了令牌桶算法实现:平滑突发限流 (SmoothBursty) 和平滑预热限流 (SmoothWarmingUp) 实现。
// SmoothBursty
RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire());
RateLimiter.create(5) 表示桶容量为 5 且每秒新增 5 个令牌,即每隔 200 毫秒新增一个令牌;
limiter.acquire() 表示消费一个令牌,如果当前桶中有足够令牌则成功(返回值为 0 ),如果桶中没有令牌则暂停一段时间,比如发令牌间隔是 200 毫秒,则等待200 毫秒后再去消费令牌,这种实现将突发请求速率平均为了固定请求速率。
SmoothBursty 允许一定程度的突发,会有人担心如果允许这种突发,假设突然间来了很大的流量,那么系统很可能扛不住这种突发。因此需要一种平滑速率的限流工具,从而系统冷启动后慢慢的趋于平均固定速率(即刚开始速率小一些,然后慢慢趋于设置的固定速率)。 Guava 也提供了 SmoothWarmingUp 来实现这种需求,其可以认为是漏桶算法,但是在某些特殊场景又不太一样。
SmoothWarmingUp 创建方式:
RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit)
permitsPerSecond 表示每秒新增的令牌数, 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) 。
高并发系统限流策略
751

被折叠的 条评论
为什么被折叠?



