前言:

在大量并发的环境下,为了防止由于请求暴涨,导致系统崩溃从而引起雪崩,一般会对流量做一定的限制操作。比如等待、排队、降级、拒绝服务、限流等。

我们这节主要讲的是限流,限流的措施有很多,我这里分为单机限流和集群限流。

(1)单机限流:在java中我们可以使用AtomicInteger,RateLimiter或Semaphore来实现,但是上述方案都不支持集群限流。

(2)集群限流:集群限流的应用场景有两个。

一个是网关限流:常用的方案有Nginx限流和Lua+Redis(如:Spring Cloud Gateway),

另一个场景是接口限流:与外部或者下游服务接口的交互,因为接口限制必须进行限流。

一、单机限流


1、java后台代码限流

Java单机限流可以使用AtomicInteger,RateLimiter或Semaphore都可以用来限流。我们说一下RateLimiter限流例子:

引入guava包:

<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>28.2-jre</version>
</dependency>

Guava 实现限流示例:

public class RateLimiterExample {public static void main(String[] args) {// 每秒产生 10 个令牌(每 100 ms 产生一个)RateLimiter rt = RateLimiter.create(10);for (int i = 0; i < 11; i++) {new Thread(() -> {// 获取 1 个令牌rt.acquire();System.out.println("正常执行方法,ts:" + Instant.now());}).start();}}
}

二、集群限流(网关限流)


1、Nginx限流

Nginx限流可以使用其自带的2个模块:连接数限流模块(ngx_http_limit_conn_module)和漏桶算法实现的请求限流模块(ngx_http_limit_req_module)

我们通过修改Nginx的配置文件可以实现。

(1)ngx_http_limit_conn_module

limit_conn是用来对某个key对应的总的网络连接数进行限流。可以按照IP、host维度进行限流。不是每个请求都会被计数器统计,只有被Nginx处理并且已经读取了整个请求头的连接才会被计数。

//IP维度限流:
http {# 用来配置限流key及存放key对应信息的内存区域大小。此处的key是“$binary_remote_addr”,表示IP地址。也可以使用$server_name作为keylimit_conn_zone $binary_remote_addr zone=addr:10m; # 被限流后的日志级别limit_conn_log_level error; # 被限流后返回的状态码limit_conn_status 503; ...server {...location /limit {# 要配置存放key和计数器的共享内存区域和指定key的最大连接数。此处表示Nginx最多同时并发处理1个连接limit_conn addr 1; }
...//host维度:
http {limit_conn_zone $server_name zone zone=hostname:10m;# 被限流后的日志级别limit_conn_log_level error; # 被限流后返回的状态码limit_conn_status 503; ...server {...location /limit {limit_conn hostname 1;}
...
}

流程如下:

(2)ngx_http_limit_req_module

limit_req是漏桶算法,对于指定key对应的请求进行限流。

http {# 配置限流key、存放key对应信息的共享内存区域大小、固定请求速率。此处的key是“$binary_remote_addr”(IP地址)。固定请求速率使用rate配置,支持10r/s和60r/m。limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; limit_conn_log_level error;limit_conn_status 503;...server {...location /limit {# 配置限流区域、桶容量(突发容量,默认为0)、是否延迟模式(默认延迟)limit_req zone=one burst=5 nodelay; }...}
}

limit_req的主要执行过程如下:

  1. 请求进入后首先判断上一次请求时间相对于当前时间是否需要限流,如果需要则执行步骤2,否则执行步骤3.

  2. 如果没有配置桶容量(burst=0),按照固定速率处理请求。如果请求被限流了,直接返回503;
    如果配置了桶容量(burst>0),及延迟模式(没有配置nodelay)。如果桶满了,则新进入的请求被限流。如果没有满,则会以固定速率被处理;
    如果配置了桶容量(burst>0),及非延迟模式(配置了nodelay)。则不会按照固定速率处理请求,而是允许突发处理请求。如果桶满了,直接返回503.

  3. 如果没有被限流,则正常处理请求。

  4. Nginx会在响应时间选择一些(3个节点)限流key进行过期处理,进行内存回收。

2、Lua+Redis限流(SpringCloud Gateway)

Redis执行Lua脚本会以原子性方式进行,单线程的方式执行脚本,在执行脚本时不会再执行其他脚本或命令。并且,Redis只要开始执行Lua脚本,就会一直执行完该脚本再进行其他操作,所以Lua脚本中不能进行耗时操作。使用Lua脚本,还可以减少与Redis的交互,减少网络请求的次数。

SpringCloud中的网关组件Gateway它是基于Redis和Lua实现了令牌桶算法的限流功能,我们可以通过他的原理和细节来了解一下lua+Redis的实现。

Gateway基于Filter模式,提供了限流过滤器 RequestRateLimiterGatewayFilterFactory,我们来看一下该类中的处理Filter的方法。

@SuppressWarnings("unchecked")@Overridepublic GatewayFilter apply(Config config) {KeyResolver resolver = getOrDefault(config.keyResolver, defaultKeyResolver);RateLimiter<Object> limiter = getOrDefault(config.rateLimiter, defaultRateLimiter);boolean denyEmpty = getOrDefault(config.denyEmptyKey, this.denyEmptyKey);HttpStatusHolder emptyKeyStatus = HttpStatusHolder.parse(getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap(key -> {if (EMPTY_KEY.equals(key)) {if (denyEmpty) {setResponseStatus(exchange, emptyKeyStatus);return exchange.getResponse().setComplete();}return chain.filter(exchange);}String routeId = config.getRouteId();if (routeId == null) {Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);routeId = route.getId();}return limiter.isAllowed(routeId, key).flatMap(response -> {for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());}if (response.isAllowed()) {return chain.filter(exchange);}setResponseStatus(exchange, config.getStatusCode());return exchange.getResponse().setComplete();});});}

我们可以看到,允不允许过,主要是通过limiter.isAllowed(routeId, key)这个方法过滤,limiter的默认实现类是RedisRateLimiter。我们看一下RedisRateLimiter中的isAllowed方法。

public Mono<Response> isAllowed(String routeId, String id) {       //routeId是ip地址,id是使用KeyResolver获取的限流维度id,比如说基于uri,IP或者用户等等。  Config routeConfig = loadConfiguration(routeId);    // 每秒能够通过的请求数    int replenishRate = routeConfig.getReplenishRate();    // 最大流量    int burstCapacity = routeConfig.getBurstCapacity();try {        // 组装Lua脚本的KEY        List<String> keys = getKeys(id);        // 组装Lua脚本需要的参数,1是指一次获取一个令牌        List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",Instant.now().getEpochSecond() + "", "1");        // 调用Redis,tokens_left = redis.eval(SCRIPT, keys, args)        Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);.....// 省略
}static List<String> getKeys(String id) {    String prefix = "request_rate_limiter.{" + id;    String tokenKey = prefix + "}.tokens";    String timestampKey = prefix + "}.timestamp";    return Arrays.asList(tokenKey, timestampKey);
}

备注:

getKeys函数的prefix包含了"{id}",这是为了解决Redis集群键值映射问题。

Redis的KeySlot算法中,如果key包含{},就会使用第一个{}内部的字符串作为hash key,这样就可以保证拥有同样{}内部字符串的key就会拥有相同slot。Redis要求单个Lua脚本操作的key必须在同一个节点上,但是Cluster会将数据自动分布到不同的节点,使用这种方法就解决了上述的问题。

然后我们来看一下Lua脚本的实现,该脚本就在Gateway项目的resource文件夹下。它就是如同 Guava的 RateLimiter一样,实现了令牌桶算法,只不过不在需要进行线程休眠,而是直接返回是否能够获取。

local tokens_key = KEYS[1]
-- request_rate_limiter.${id}.tokens 令牌桶剩余令牌数的KEY值
local timestamp_key = KEYS[2]
-- 令牌桶最后填充令牌时间的KEY值local rate = tonumber(ARGV[1])
-- replenishRate 令令牌桶填充平均速率
local capacity = tonumber(ARGV[2])
-- burstCapacity 令牌桶上限
local now = tonumber(ARGV[3])
-- 得到从 1970-01-01 00:00:00 开始的秒数
local requested = tonumber(ARGV[4])
-- 消耗令牌数量,默认 1 local fill_time = capacity/rate
-- 计算令牌桶填充满令牌需要多久时间
local ttl = math.floor(fill_time*2)
-- *2 保证时间充足local last_tokens = tonumber(redis.call("get", tokens_key))
-- 获得令牌桶剩余令牌数
if last_tokens == nil then
-- 第一次时,没有数值,所以桶时满的last_tokens = capacity
endlocal last_refreshed = tonumber(redis.call("get", timestamp_key))
-- 令牌桶最后填充令牌时间
if last_refreshed == nil then  last_refreshed = 0
endlocal delta = math.max(0, now-last_refreshed)
-- 获取距离上一次刷新的时间间隔
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
-- 填充令牌,计算新的令牌桶剩余令牌数 填充不超过令牌桶令牌上限。local allowed = filled_tokens >= requested
local new_tokens = filled_tokenslocal allowed_num = 0
if allowed then
-- 若成功,令牌桶剩余令牌数(new_tokens) 减消耗令牌数( requested ),
-- 并设置获取成功( allowed_num = 1 ) 。new_tokens = filled_tokens - requested allowed_num = 1
end
-- 设置令牌桶剩余令牌数( new_tokens ) ,令牌桶最后填充令牌时间(now)
-- ttl是超时时间
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
-- 返回数组结果
return { allowed_num, new_tokens }

至此,就完成了lua+redis中使用令牌桶解决IP限流问题。如果要自己写的话,其原理及结构也大同小异。

二、集群限流(接口限流)


接口限流也可以使用集群限流中的网关限流,其他接口限流针对不同场景(接口限流场景比较,多依据不同环境做使用不同方法),主要有以下几个方向:

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

(2)限制 远程接⼝口 调⽤用速率

(3)限制  MQ 的消费速率

备注:补充一下漏桶算法和令牌桶算法。

(1)漏桶算法:漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

它的主要目的是:控制数据注入到网络的速率,平滑网络上的突发流量。

基本过程:

(1)到达的数据包(网络层的PDU)被放置在底部具有漏孔的桶中(数据包缓存)

(2)漏桶最多可以排队b个字节,漏桶的这个尺寸受限于有效的系统内存。如果数据包漏桶已经满了,那么数据包应被丢弃

(3)数据包从漏桶中漏出,以常量速率(r字节/秒)注入网络,因此平滑了突发流量

(4)漏桶算法强调的是在桶中缓存数据包,然后以一定的速率从桶中取出数据包,从而实现了限流,防止突发流量

使用场景:

这是用来保护他人,也就是保护他所调用的系统。

当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。

例如保护数据库的限流,先把对数据库的访问加入到木桶中,worker再以db能够承受的qps从木桶中取出请求,去访问数据库。

(2)令牌桶算法:令牌桶算法和 漏桶算法效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定时间间隔往桶里加入令牌(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个令牌,如果没有令牌可拿了就阻塞或者拒绝服务.

主要目的是:用来控制发送到网络上的数据的数目,并允许突发数据的发送。

基本过程:

(1)假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中

(2)假设桶最多可以存发b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃

(3)当一个n个字节的数据包到达时,就从令牌桶中删除n个令牌,并且数据包被发送到网络

(4)如果令牌桶中少于n个令牌,那么不会删除令牌,并且认为这个数据包在流量限制之外

(5)算法允许最长b个字节的突发,但从长期运行结果看,数据包的速率被限制成常量r。对于在流量限制外的数据包可以以不同的方式处理

(6)它们可以被丢弃

(7)它们可以排放在队列中以便当令牌桶中累积了足够多的令牌时再传输

(8)它们可以继续发送,但需要做特殊标记,网络过载的时候将这些特殊标记的包丢弃

使用场景:

令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。

所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制。

适合电商抢购或者微博出现热点事件这种场景,因为在限流的同时可以应对一定的突发流量。如果采用漏桶那样的均匀速度处理请求的算法,在发生热点时间的时候,会造成大量的用户无法访问,对用户体验的损害比较大。

从原理上看,令牌桶算法和漏桶算法是相反的,一个“进水”,一个是“漏水”。

总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证被别人的系统不被打垮,用漏桶算法。

使用限流方式限制流量,完美应对高并发中请求暴涨难题相关推荐

  1. 小米开源监控系统OpenFalcon应对高并发7种手段

    2019独角兽企业重金招聘Python工程师标准>>> 小米开源监控系统OpenFalcon应对高并发7种手段 原创 2016-04-01 秦晓辉 高可用架构 编者按:本文是秦晓辉在 ...

  2. spring cloud微服务间限流,使用jMeter性能测试高并发

    有关网关限流方式查看上一篇博客:spring cloud网关(zuul)限流,使用jMeter性能测试高并发 在网关限流后,有可能有些微服务与网关山的限流不一致,比如网关限流100QPS,而微服务只能 ...

  3. 程序员过关斩将--应对高并发系统有没有通用的解决方案呢?

    " 灵魂拷问: 应对高并发系统有没有一些通用的解决方案呢? 这些方案解决了什么问题呢? 这些方案有那些优势和劣势呢? 对性能孜孜不倦的追求是互联网技术不断发展的根本驱动力,从最初的大型机到现 ...

  4. 高并发中的 限流、熔断、降级、预热、背压你都知道是什么意思吗?

    首先,我们需要明确一下这几个名词出现的场景:分布式高并发环境.如果你的产品卖相不好,没人鸟它,那它就用不着这几个属性.不需要任何加成,低并发系统就能工作的很好. 分布式系统是一个整体,调用关系错综复杂 ...

  5. 应对高并发的几个常见方法

    我们在应对高并发大流量时也会采用类似"抵御洪水"的方案,归纳起来共有三种方法. Scale-out(横向扩展):分而治之是一种常见的高并发系统设计方法,采用分布式部署的方式把流量分 ...

  6. 如何应对高并发问题?

    如何应对高并发问题? 我觉着可以从两方面来着手,第一方面是硬件,第二方面是软件.当然硬件方面做的比较少,从软件方面来说做的就比较多了. 硬件方面的话,我们可以购买性能更好的服务器,或者是提升现有服务器 ...

  7. 京东云携手达达构建行业首个跨云双活平台 应对高并发需求

    达达快送平台的孙壮不经意间刷新了自己配送时效的记录. 2020年6月18日,他只用了十几分钟就将达达集团旗下即时零售平台京东到家的订单送到了用户手中.并且还是在层层山峦,蜿蜒崎岖的山城重庆. 跟孙壮一 ...

  8. 视频直播系统开发:如何应对高并发访问?

    视频直播开发的挑战 随着移动互联网的快速普及,视频直播已经成为了新的社交媒体形式,也成为了一种新的商业模式.现在,越来越多的人开始使用视频直播来展示自己的生活和工作,同时,越来越多的企业开始将视频直播 ...

  9. 如何理解高并发中的协程?协程的实现和历史

    <Libco是一个C/C++协程库,在微信服务中广泛使用> <协程到底是什么?> <如何理解高并发中的协程?协程的实现和历史> 目录 普通的函数 从普通函数到协程 ...

最新文章

  1. 二级c语言上机编程技巧,二级C语言上机编程题技巧总结
  2. boost::hana::make_tuple用法的测试程序
  3. 9.为包含指针的关联容器指定比较类型
  4. djagno-oscar的页面出现Error 10605 - Receiving country does not support the transaction currency.
  5. 警惕成教自考_不,保持警惕不会伤害Java。 关于Java许可的评论。
  6. 《操作系统》OS学习(十):进程控制
  7. python *args和**kwargs以及序列解包
  8. LeetCode 1792. 最大平均通过率(优先队列)
  9. mysql新建用户只能查看试图_Sql Server:创建用户并指定该用户只能看指定的视图,除此之外的都不让查看。...
  10. emacs latex_如何使用Emacs创建LaTeX文档
  11. java在线打开xml文件_java实现简单解析XML文件功能示例
  12. 3月2日 PCA的应用示例
  13. python在材料方面的应用_Python数据科学(一)- python与数据科学应用(Ⅰ)
  14. android viewpager监听,viewPager的OnPageChangeListener监听器的方法调用顺序
  15. 算法导论第三版第六章 答案
  16. SiO2 介孔二氧化硅纳米颗粒的明胶包裹/二氧化硅杂化凝胶固定化醇脱氢酶/白蛋白和壳聚糖包裹的二氧化硅微球/二氧化硅包覆的量子点荧光编码微球
  17. 从单体式架构迁移到微服务架构,简述java程序的运行原理
  18. cheng-06、初始化失败_Destroying singletons报错问题
  19. html页面顶部横条,CSS:页面顶部的精简横幅(如本页中的橙色横幅)
  20. 0基础单片机入门知识:怎么使用数字万用表以及注意事项

热门文章

  1. 掌控机遇秘诀!多位百万富翁的创业真经!
  2. 用11行Python代码,实现动态二维码制作
  3. 邮票的孔怎么做出来的_邮票怎么做防潮工作,防潮柜有用吗【中华古画古物邮票吧】...
  4. jwt token注销_【原创精选】OAuth 2.0+JWT+spring security完成认证授权-生产级-附带源码...
  5. 升级bigsur_2020年升级MacBook Pro 2015
  6. 为什么说云计算运维是新“贵”?
  7. 如何root安卓手机_如何判断自己的安卓10系统手机是不是动态分区+获取magisk 完美root和解锁system分区有什么用途...
  8. 范成法加工matlab_基于Matlab的渐开线齿轮的范成法仿真.doc
  9. 各种品牌的主板和笔记本的BIOS快捷启动按键
  10. 设计模式----代理模式