哈喽大家好啊,我是Hydra,今天来和大家聊聊服务的限流。
(资料图片)
服务限流,是指通过控制请求的速率或次数来达到保护服务的目的,在微服务中,我们通常会将它和熔断、降级搭配在一起使用,来避免瞬时的大量请求对系统造成负荷,来达到保护服务平稳运行的目的。下面就来看一看常见的6种限流方式,以及它们的实现与使用。
固定窗口算法固定窗口算法通过在单位时间内维护一个计数器,能够限制在每个固定的时间段内请求通过的次数,以达到限流的效果。
算法实现起来也比较简单,可以通过构造方法中的参数指定时间窗口大小以及允许通过的请求数量,当请求进入时先比较当前时间是否超过窗口上边界,未越界且未超过计数器上限则可以放行请求。
@Slf4jpublic class FixedWindowRateLimiter { // 时间窗口大小,单位毫秒 private long windowSize; // 允许通过请求数 private int maxRequestCount; // 当前窗口通过的请求计数 private AtomicInteger count=new AtomicInteger(0); // 窗口右边界 private long windowBorder; public FixedWindowRateLimiter(long windowSize,int maxRequestCount){ this.windowSize = windowSize; this.maxRequestCount = maxRequestCount; windowBorder = System.currentTimeMillis()+windowSize; } public synchronized boolean tryAcquire(){ long currentTime = System.currentTimeMillis(); if (windowBorder < currentTime){ log.info("window reset"); do { windowBorder += windowSize; }while(windowBorder < currentTime); count=new AtomicInteger(0); } if (count.intValue() < maxRequestCount){ count.incrementAndGet(); log.info("tryAcquire success"); return true; }else { log.info("tryAcquire fail"); return false; } }}
进行测试,允许在1000毫秒内通过5个请求:
void test() throws InterruptedException { FixedWindowRateLimiter fixedWindowRateLimiter = new FixedWindowRateLimiter(1000, 5); for (int i = 0; i < 10; i++) { if (fixedWindowRateLimiter.tryAcquire()) { System.out.println("执行任务"); }else{ System.out.println("被限流"); TimeUnit.MILLISECONDS.sleep(300); } }}
运行结果:
固定窗口算法的优点是实现简单,但是可能无法应对突发流量的情况,比如每秒允许放行100个请求,但是在0.9秒前都没有请求进来,这就造成了在0.9秒到1秒这段时间内要处理100个请求,而在1秒到1.1秒间可能会再进入100个请求,这就造成了要在0.2秒内处理200个请求,这种流量激增就可能导致后端服务出现异常。
滑动窗口算法滑动窗口算法在固定窗口的基础上,进行了一定的升级改造。它的算法的核心在于将时间窗口进行了更精细的分片,将固定窗口分为多个小块,每次仅滑动一小块的时间。
并且在每个时间段内都维护了单独的计数器,每次滑动时,都减去前一个时间块内的请求数量,并再添加一个新的时间块到末尾,当时间窗口内所有小时间块的计数器之和超过了请求阈值时,就会触发限流操作。
看一下算法的实现,核心就是通过一个int类型的数组循环使用来维护每个时间片内独立的计数器:
@Slf4jpublic class SlidingWindowRateLimiter { // 时间窗口大小,单位毫秒 private long windowSize; // 分片窗口数 private int shardNum; // 允许通过请求数 private int maxRequestCount; // 各个窗口内请求计数 private int[] shardRequestCount; // 请求总数 private int totalCount; // 当前窗口下标 private int shardId; // 每个小窗口大小,毫秒 private long tinyWindowSize; // 窗口右边界 private long windowBorder; public SlidingWindowRateLimiter(long windowSize, int shardNum, int maxRequestCount) { this.windowSize = windowSize; this.shardNum = shardNum; this.maxRequestCount = maxRequestCount; shardRequestCount = new int[shardNum]; tinyWindowSize = windowSize/ shardNum; windowBorder=System.currentTimeMillis(); } public synchronized boolean tryAcquire() { long currentTime = System.currentTimeMillis(); if (currentTime > windowBorder){ do { shardId = (++shardId) % shardNum; totalCount -= shardRequestCount[shardId]; shardRequestCount[shardId]=0; windowBorder += tinyWindowSize; }while (windowBorder < currentTime); } if (totalCount < maxRequestCount){ log.info("tryAcquire success,{}",shardId); shardRequestCount[shardId]++; totalCount++; return true; }else{ log.info("tryAcquire fail,{}",shardId); return false; } }}
进行一下测试,对第一个例子中的规则进行修改,每1秒允许100个请求通过不变,在此基础上再把每1秒等分为10个0.1秒的窗口。
void test() throws InterruptedException { SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter(1000, 10, 10); TimeUnit.MILLISECONDS.sleep(800); for (int i = 0; i < 15; i++) { boolean acquire = slidingWindowRateLimiter.tryAcquire(); if (acquire){ System.out.println("执行任务"); }else{ System.out.println("被限流"); } TimeUnit.MILLISECONDS.sleep(10); }}
查看运行结果:
程序启动后,在先休眠了一段时间后再发起请求,可以看到在0.9秒到1秒的时间窗口内放行了6个请求,在1秒到1.1秒内放行了4个请求,随后就进行了限流,解决了在固定窗口算法中相邻时间窗口内允许通过大量请求的问题。
滑动窗口算法通过将时间片进行分片,对流量的控制更加精细化,但是相应的也会浪费一些存储空间,用来维护每一块时间内的单独计数,并且还没有解决固定窗口中可能出现的流量激增问题。
漏桶算法为了应对流量激增的问题,后续又衍生出了漏桶算法,用专业一点的词来说,漏桶算法能够进行流量整形和流量控制。
漏桶是一个很形象的比喻,外部请求就像是水一样不断注入水桶中,而水桶已经设置好了最大出水速率,漏桶会以这个速率匀速放行请求,而当水超过桶的最大容量后则被丢弃。
看一下代码实现:
@Slf4jpublic class LeakyBucketRateLimiter { // 桶的容量 private int capacity; // 桶中现存水量 private AtomicInteger water=new AtomicInteger(0); // 开始漏水时间 private long leakTimeStamp; // 水流出的速率,即每秒允许通过的请求数 private int leakRate; public LeakyBucketRateLimiter(int capacity,int leakRate){ this.capacity=capacity; this.leakRate=leakRate; } public synchronized boolean tryAcquire(){ // 桶中没有水,重新开始计算 if (water.get()==0){ log.info("start leaking"); leakTimeStamp = System.currentTimeMillis(); water.incrementAndGet(); return water.get() < capacity; } // 先漏水,计算剩余水量 long currentTime = System.currentTimeMillis(); int leakedWater= (int) ((currentTime-leakTimeStamp)/1000 * leakRate); log.info("lastTime:{}, currentTime:{}. LeakedWater:{}",leakTimeStamp,currentTime,leakedWater); // 可能时间不足,则先不漏水 if (leakedWater != 0){ int leftWater = water.get() - leakedWater; // 可能水已漏光,设为0 water.set(Math.max(0,leftWater)); leakTimeStamp=System.currentTimeMillis(); } log.info("剩余容量:{}",capacity-water.get()); if (water.get() < capacity){ log.info("tryAcquire success"); water.incrementAndGet(); return true; }else { log.info("tryAcquire fail"); return false; } }}
进行一下测试,先初始化一个漏桶,设置桶的容量为3,每秒放行1个请求,在代码中每500毫秒尝试请求1次:
void test() throws InterruptedException { LeakyBucketRateLimiter leakyBucketRateLimiter =new LeakyBucketRateLimiter(3,1); for (int i = 0; i < 15; i++) { if (leakyBucketRateLimiter.tryAcquire()) { System.out.println("执行任务"); }else { System.out.println("被限流"); } TimeUnit.MILLISECONDS.sleep(500); }}
查看运行结果,按规则进行了放行:
但是,漏桶算法同样也有缺点,不管当前系统的负载压力如何,所有请求都得进行排队,即使此时服务器的负载处于相对空闲的状态,这样会造成系统资源的浪费。由于漏桶的缺陷比较明显,所以在实际业务场景中,使用的比较少。
令牌桶算法令牌桶算法是基于漏桶算法的一种改进,主要在于令牌桶算法能够在限制服务调用的平均速率的同时,还能够允许一定程度内的突发调用。
它的主要思想是系统以恒定的速度生成令牌,并将令牌放入令牌桶中,当令牌桶中满了的时候,再向其中放入的令牌就会被丢弃。而每次请求进入时,必须从令牌桶中获取一个令牌,如果没有获取到令牌则被限流拒绝。
假设令牌的生成速度是每秒100个,并且第一秒内只使用了70个令牌,那么在第二秒可用的令牌数量就变成了130,在允许的请求范围上限内,扩大了请求的速率。当然,这里要设置桶容量的上限,避免超出系统能够承载的最大请求数量。
Guava中的RateLimiter就是基于令牌桶实现的,可以直接拿来使用,先引入依赖:
com.google.guava guava 29.0-jre
进行测试,设置每秒产生5个令牌:
void acquireTest(){ RateLimiter rateLimiter=RateLimiter.create(5); for (int i = 0; i < 10; i++) { double time = rateLimiter.acquire(); log.info("等待时间:{}s",time); }}
运行结果:
可以看到,每200ms左右产生一个令牌并放行请求,也就是1秒放行5个请求,使用RateLimiter能够很好的实现单机的限流。
那么再回到我们前面提到的突发流量情况,令牌桶是怎么解决的呢?RateLimiter中引入了一个预消费的概念。在源码中,有这么一段注释:
* It is important to note that the number of permits requested never affects the * throttling of the request itself (an invocation to {@code acquire(1)} and an invocation to {@code * acquire(1000)} will result in exactly the same throttling, if any), but it affects the throttling * of the next request. I.e., if an expensive task arrives at an idle RateLimiter, it will be * granted immediately, but it is the next request that will experience extra throttling, * thus paying for the cost of the expensive task.
大意就是,申请令牌的数量不同不会影响这个申请令牌这个动作本身的响应时间,acquire(1)和acquire(1000)这两个请求会消耗同样的时间返回结果,但是会影响下一个请求的响应时间。
如果一个消耗大量令牌的任务到达空闲的RateLimiter,会被立即批准执行,但是当下一个请求进来时,将会额外等待一段时间,用来支付前一个请求的时间成本。
至于为什么要这么做,通过举例来引申一下。当一个系统处于空闲状态时,突然来了1个需要消耗100个令牌的任务,那么白白等待100秒是毫无意义的浪费资源行为,那么可以先允许它执行,并对后续请求进行限流时间上的延长,以此来达到一个应对突发流量的效果。
看一下具体的代码示例:
void acquireMultiTest(){ RateLimiter rateLimiter=RateLimiter.create(1); for (int i = 0; i <3; i++) { int num = 2 * i + 1; log.info("获取{}个令牌", num); double cost = rateLimiter.acquire(num); log.info("获取{}个令牌结束,耗时{}ms",num,cost); }}
运行结果:
可以看到,在第二次请求时需要3个令牌,但是并没有等3秒后才获取成功,而是在等第一次的1个令牌所需要的1秒偿还后,立即获得了3个令牌得到了放行。同样,第三次获取5个令牌时等待的3秒是偿还的第二次获取令牌的时间,偿还完成后立即获取5个新令牌,而并没有等待全部重新生成完成。
除此之外RateLimiter还具有平滑预热功能,下面的代码就实现了在启动3秒内,平滑提高令牌发放速率到每秒5个的功能:
void acquireSmoothly(){ RateLimiter rateLimiter=RateLimiter.create(5,3, TimeUnit.SECONDS); long startTimeStamp = System.currentTimeMillis(); for (int i = 0; i < 15; i++) { double time = rateLimiter.acquire(); log.info("等待时间:{}s, 总时间:{}ms" ,time,System.currentTimeMillis()-startTimeStamp); }}
查看运行结果:
可以看到,令牌发放时间从最开始的500ms多逐渐缩短,在3秒后达到了200ms左右的匀速发放。
总的来说,基于令牌桶实现的RateLimiter功能还是非常强大的,在限流的基础上还可以把请求平均分散在各个时间段内,因此在单机情况下它是使用比较广泛的限流组件。
中间件限流前面讨论的四种方式都是针对单体架构,无法跨JVM进行限流,而在分布式、微服务架构下,可以借助一些中间件进行限。Sentinel是Spring Cloud Alibaba中常用的熔断限流组件,为我们提供了开箱即用的限流方法。
使用起来也非常简单,在service层的方法上添加@SentinelResource注解,通过value指定资源名称,blockHandler指定一个方法,该方法会在原方法被限流、降级、系统保护时被调用。
@Servicepublic class QueryService { public static final String KEY="query"; @SentinelResource(value = KEY, blockHandler ="blockHandlerMethod") public String query(String name){ return "begin query,name="+name; } public String blockHandlerMethod(String name, BlockException e){ e.printStackTrace(); return "blockHandlerMethod for Query : " + name; }}
配置限流规则,这里使用直接编码方式配置,指定QPS到达1时进行限流:
@Componentpublic class SentinelConfig { @PostConstruct private void init(){ List rules = new ArrayList<>(); FlowRule rule = new FlowRule(QueryService.KEY); rule.setCount(1); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setLimitApp("default"); rules.add(rule); FlowRuleManager.loadRules(rules); }}
在application.yml中配置sentinel的端口及dashboard地址:
spring: application: name: sentinel-test cloud: sentinel: transport: port: 8719 dashboard: localhost:8088
启动项目后,启动sentinel-dashboard:
java -Dserver.port=8088 -jar sentinel-dashboard-1.8.0.jar
在浏览器打开dashboard就可以看见我们设置的流控规则:
进行接口测试,在超过QPS指定的限制后,则会执行blockHandler()方法中的逻辑:
Sentinel在微服务架构下得到了广泛的使用,能够提供可靠的集群流量控制、服务断路等功能。在使用中,限流可以结合熔断、降级一起使用,成为有效应对三高系统的三板斧,来保证服务的稳定性。
网关限流网关限流也是目前比较流行的一种方式,这里我们介绍采用Spring Cloud的gateway组件进行限流的方式。
在项目中引入依赖,gateway的限流实际使用的是Redis加lua脚本的方式实现的令牌桶,因此还需要引入redis的相关依赖:
org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-data-redis-reactive
对gateway进行配置,主要就是配一下令牌的生成速率、令牌桶的存储量上限,以及用于限流的键的解析器。这里设置的桶上限为2,每秒填充1个令牌:
spring: application: name: gateway-test cloud: gateway: routes: - id: limit_route uri: lb://sentinel-test predicates: - Path=/sentinel-test/** filters: - name: RequestRateLimiter args: # 令牌桶每秒填充平均速率 redis-rate-limiter.replenishRate: 1 # 令牌桶上限 redis-rate-limiter.burstCapacity: 2 # 指定解析器,使用spEl表达式按beanName从spring容器中获取 key-resolver: "#{@pathKeyResolver}" - StripPrefix=1 redis: host: 127.0.0.1 port: 6379
我们使用请求的路径作为限流的键,编写对应的解析器:
@Slf4j@Componentpublic class PathKeyResolver implements KeyResolver { public Mono resolve(ServerWebExchange exchange) { String path = exchange.getRequest().getPath().toString(); log.info("Request path: {}",path); return Mono.just(path); }}
启动gateway,使用jmeter进行测试,设置请求间隔为500ms,因为每秒生成一个令牌,所以后期达到了每两个请求放行1个的限流效果,在被限流的情况下,http请求会返回429状态码。
除了上面的根据请求路径限流外,我们还可以灵活设置各种限流的维度,例如根据请求header中携带的用户信息、或是携带的参数等等。当然,如果不想用gateway自带的这个Redis的限流器的话,我们也可以自己实现RateLimiter接口来实现一个自己的限流工具。
gateway实现限流的关键是spring-cloud-gateway-core包中的RedisRateLimiter类,以及META-INF/scripts中的request-rate-limiter.lua这个脚本,如果有兴趣可以看一下具体是如何实现的。
总结总的来说,要保证系统的抗压能力,限流是一个必不可少的环节,虽然可能会造成某些用户的请求被丢弃,但相比于突发流量造成的系统宕机来说,这些损失一般都在可以接受的范围之内。前面也说过,限流可以结合熔断、降级一起使用,多管齐下,保证服务的可用性与健壮性。
标签:
哈喽大家好啊,我是Hydra,今天来和大家聊聊服务的限流。服务限流,是指通过控制请求的速率或次数来达到保
维持对其H股“买入”评级,目标价由15 84港元上调至18 19港元。
格隆汇5月10日丨科大讯飞(002230)(002230 SZ)于2023年5月9日接受机构调研,就“讯飞星火大模型体验效果很好
编者按:时间进入5月份,在北京做开发的房企都在忙一件事:准备拿地。从各家房企的存货来看,如今是“地主
1、可以存放1-4天左右,这期间控偶感和营养成分不会发生太大改变。一般情况下,要是不立即食用的话,最好是
雪白的梨花傲立枝头,淡淡的花瓣在空中飘然舞动,风吹尘扬混合着梨花清香,4月22日,2023“中国梦——文化进万
5月10日消息,PingPong官微发布产品上新公告,为帮助商家拓展印尼市场业务,PingPong已在“开店通”新增Tik
IU被警方告发涉嫌剽窃6首歌曲今天的热度非常高,现在也是在热搜榜上了,那么具体的IU被警方告发涉嫌剽窃6首
海思科近期接受投资者调研时称,HSK3486适应症麻醉诱导(IGA)的第一项美国Ⅲ期临床试验已达到了预设主要终
“强制扫码才能消费”到底该不该?北京青年报连续数日追踪报道引发行业关注。5月9日上午,北京市消协发布对
App5月10日消息,医疗信息化板块快速走高,朗玛信息涨超10%,本月累计涨幅超50%,万达信息大涨7%,国新健康
中原网讯(郑报全媒体记者侯爱敏)2023年中国品牌日活动今日在上海开幕,记者昨日从市发展改革委获悉,郑州14
图为共青团山西省第十六次代表大会代表郭子涵接受采访。马杰摄黄河新闻网讯(记者周昱丽)郭子涵,来自陵川
“五一”假期后,国内飞往泰国的机票迅速降价。九元航空小程序显示,5月12日,广州白云机场直飞曼谷廊曼机
1、天启坦克是有原形的,是前苏联设计的,样车也有,有4条履带、两门128mm的52倍口径炮,前装甲厚达430mm,
5月9日,百联股份(600827)融资买入4275 51万元,融资偿还3369 64万元,融资净买入905 87万元,融资余额6 48亿元。
Z世代打工人观察:从整顿职场到寻找价值,00后真的如网上所说的那样在整顿职场吗?
近日杭州网友苗苗告诉记者,最近她在乘坐杭州地铁4号线经过新风站时,收到一条陌生iPhone手机发来的隔空投
76人与凯尔特人的天王山战役今天7点30将在波士顿开打。76人主帅老里弗斯在赛前接受了采访,里弗斯谈到了球
拟增资主体为格力地产控股股东珠海投资控股有限公司。
1、是这首不?飞--孟颖看下是否是需要的版本如果对您有帮助请采纳。2、谢谢!!!。本文到此分享完毕,希望
1、【解释】:绿叶茂盛,花渐凋谢。2、指暮春时节。3、也形容春残的景象。4、【出自】:宋·李清照《如梦令
上工申贝:拟择机处置所持长江传媒股票:上工申贝(600843)公告,公司拟择机处置持有的长江传媒(600757)
1、省宣传部副部长这个职务的级别为副厅级(个人高配除外)省宣传部常务副部长(现在一般都是高配正厅级干部担
翻倍大牛股中国科传5月9日晚间公告,因自身需要,股东人民邮电出版社有限公司(简称“人邮社”)、电子工业
5月9日,记者从四川省总工会获悉,建功十四五奋进新征程2023年四川省高校教职工网球比赛日前在自贡落幕。
1、运单号查询物流需要到物流公司的官网最准确。2、因为每个公司的单号编码规则不同,找到公司的官网,如果
鞭牛士5月9日消息,据媒体报道,5月7日,河南许昌的网友发现在胖东来超市出售的特价水果上有不明的切口,怀
➤➤2023天津智能大会智能驾驶挑战赛活动地点答:天津世界智能驾驶挑战赛赛事地点为天津市东丽湖WIDC(世界
新农合医保看牙科可以报销吗?新农合医保看牙科可以报销多少?怎么报销?社保网小编为您整理了以下最新资讯
Copyright © 2015-2022 人人服装网版权所有 备案号:粤ICP备18023326号-36 联系邮箱:8557298@qq.com