SpringCloud微服務(wù)網(wǎng)關(guān)做邊緣服務(wù)限流方案
點擊上方藍色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達
在高并發(fā)的系統(tǒng)中,往往需要在系統(tǒng)中做限流,一方面是為了防止大量的請求使服務(wù)器過載,導(dǎo)致服務(wù)不可用,另一方面是為了防止網(wǎng)絡(luò)攻擊。?
常見的限流方式,比如Hystrix適用線程池隔離,超過線程池的負載,走熔斷的邏輯。在一般應(yīng)用服務(wù)器中,比如tomcat容器也是通過限制它的線程數(shù)來控制并發(fā)的;也有通過時間窗口的平均速度來控制流量。常見的限流緯度有比如通過Ip來限流、通過uri來限流、通過用戶訪問頻次來限流。
一般限流都是在網(wǎng)關(guān)這一層做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也可以在應(yīng)用層通過Aop這種方式去做限流。這里主要講講常用的限流方式。
一、常見的限流算法
目前常用的限流算法有兩個:漏桶算法和令牌桶算法。
1·計數(shù)器算法
計數(shù)器算法采用計數(shù)器實現(xiàn)限流有點簡單粗暴,一般我們會限制一秒鐘的能夠通過的請求數(shù),比如限流qps為100,算法的實現(xiàn)思路就是從第一個請求進來開始計時,在接下去的1s內(nèi),每來一個請求,就把計數(shù)加1,如果累加的數(shù)字達到了100,那么后續(xù)的請求就會被全部拒絕。等到1s結(jié)束后,把計數(shù)恢復(fù)成0,重新開始計數(shù)。具體的實現(xiàn)可以是這樣的:對于每次服務(wù)調(diào)用,可以通過AtomicLong#incrementAndGet()方法來給計數(shù)器加1并返回最新值,通過這個最新值和閾值進行比較。這種實現(xiàn)方式,相信大家都知道有一個弊端:如果我在單位時間1s內(nèi)的前10ms,已經(jīng)通過了100個請求,那后面的990ms,只能眼巴巴的把請求拒絕,我們把這種現(xiàn)象稱為“突刺現(xiàn)象”
2.漏桶算法
漏桶算法的原理比較簡單,請求進入到漏桶中,漏桶以一定的速率漏水。當(dāng)請求過多時,水直接溢出??梢钥闯?,漏桶算法可以強制限制數(shù)據(jù)的傳輸速度。但高并發(fā)情況下最容易出現(xiàn)瓶頸。
3.令牌桶算法
令牌桶算法的原理是系統(tǒng)以一定速率向桶中放入令牌,如果有請求時,請求會從桶中取出令牌,如果能取到令牌,則可以繼續(xù)完成請求,否則等待或者拒絕服務(wù)。這種算法可以應(yīng)對突發(fā)程序的請求,因此比漏桶算法好。

在Wikipedia上,令牌桶算法是這么描述的:
·每秒會有r個令牌放入桶中,或者說,每過1/r 秒桶中增加一個令牌
·桶中最多存放b個令牌,如果桶滿了,新放入的令牌會被丟棄
·當(dāng)一個n字節(jié)的數(shù)據(jù)包到達時,消耗n個令牌,然后發(fā)送該數(shù)據(jù)包
·如果桶中可用令牌小于n,則該數(shù)據(jù)包將被緩存或丟棄
Spring Cloud Gateway限流
在Spring Cloud Gateway中,有Filter過濾器,因此可以在“pre”類型的Filter中自行實現(xiàn)上述三種過濾器。但是限流作為網(wǎng)關(guān)最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory這個類,適用Redis和lua腳本實現(xiàn)了令牌桶的方式。具體實現(xiàn)邏輯在RequestRateLimiterGatewayFilterFactory類中,lua腳本在如下圖所示的文件夾中:
讀者可以自行查看,先以案例的形式來講解如何在Spring Cloud Gateway中使用內(nèi)置的限流過濾器工廠來實現(xiàn)限流。
首先在工程的pom文件中引入gateway的起步依賴和redis的reactive依賴,代碼如下:
?
????org.springframework.cloud
????spring-cloud-starter-gateway
????org.springframework.boot
????spring-boot-starter-data-redis-reactive
在配置文件中做以下的配置:
server:
??port:?8088
spring:
??cloud:
????gateway:
??????routes:
??????-?id:?limit_route
????????uri:?http://httpbin.org.com:80/get
????????predicates:
????????-?After=2017-01-20T17:42:47.789-07:00[America/Denver]
????????filters:
????????-?name:?RequestRateLimiter
??????????args:
????????????key-resolver:?'#{@hostAddrKeyResolver}'
????????????redis-rate-limiter.replenishRate:?1
????????????redis-rate-limiter.burstCapacity:?3
??application:
????name:?gateway-limiter
??redis:
????host:?localhost
????port:?6379
????database:?0
在上面的配置文件,指定程序的端口為8088,配置了 redis的信息,并配置了RequestRateLimiter的限流過濾器,該過濾器需要配置三個參數(shù):
burstCapacity,令牌桶總?cè)萘俊?br style="outline: 0px;">replenishRate,令牌桶每秒填充平均速率。
key-resolver,用于限流的鍵的解析器的 Bean 對象的名字。它使用 SpEL 表達式根據(jù)#{@beanName}從 Spring 容器中獲取 Bean 對象。
KeyResolver需要實現(xiàn)resolve方法,比如根據(jù)Hostname進行限流,則需要用hostAddress去判斷。實現(xiàn)完KeyResolver之后,需要將這個類的Bean注冊到Ioc容器中。
public class HostAddrKeyResolver implements KeyResolver {
@Override
public?Mono?resolve(ServerWebExchange?exchange)?{
????return?Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
return new HostAddrKeyResolver();
}
可以根據(jù)uri去限流,這時KeyResolver代碼如下:
public class UriKeyResolver implements KeyResolver {
@Override
public?Mono?resolve(ServerWebExchange?exchange)?{
????return?Mono.just(exchange.getRequest().getURI().getPath());
}
}
@Bean
public UriKeyResolver uriKeyResolver() {
return new UriKeyResolver();
}
也可以以用戶的維度去限流:
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst(“user”));
}
用jmeter進行壓測,配置10thread去循環(huán)請求lcoalhost:8088,循環(huán)間隔1s。從壓測的結(jié)果上看到有部分請求通過,由部分請求失敗。通過redis客戶端去查看redis中存在的key。
可見,RequestRateLimiter是使用Redis來進行限流的,并在redis中存儲了2個key。關(guān)注這兩個key含義可以看lua源代碼。
使用Guava的RateLimiter做限流
Guava中開源出來一個令牌桶算法的工具類RateLimiter,可以輕松實現(xiàn)限流的工作。RateLimiter對簡單的令牌桶算法做了一些工程上的優(yōu)化,具體的實現(xiàn)是SmoothBursty。需要注意的是,RateLimiter的另一個實現(xiàn)SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。也許是出于簡單起見,RateLimiter中的時間窗口能且僅能為1S,如果想搞其他時間單位的限流,只能另外造輪子。
RateLimiter有一個有趣的特性是[前人挖坑后人跳],也就是說RateLimiter允許某次請求拿走了超出剩余令牌數(shù)的令牌,但是下一次請求將為此付出代價,一直等到令牌虧空補上,并且桶中有足夠本次請求使用的令牌為止。這里面就涉及到一個權(quán)衡,是讓前一次請求干等到令牌夠用才走掉呢,還是讓它走掉后面的請求等一等呢?Guava的設(shè)計者選擇的是后者,先把眼前的活干了,后面的事后面再說。
測試代碼:
public?class?RateLimiterMain?{
????public?static?void?main(String[]?args)?{
????????RateLimiter?rateLimiter?=?RateLimiter.create(2);
????????System.out.println(rateLimiter.acquire(5));
????????System.out.println(rateLimiter.acquire(2));
????????System.out.println(rateLimiter.acquire(1));
????}
}
輸出內(nèi)容:
0.0
2.496889
0.992149
可以看出,令牌桶每秒只能產(chǎn)生2個令牌,我們可以第一次取出5個,但是第二次再去取令牌的時候,需要等2.5s,也就是第一次令牌取完后,需要等2.5s才能取到令牌。同樣的,第三次取1個令牌的時候,也需要等待第二次的1s的時間。也就是,取的速率可以超過令牌產(chǎn)生的速率,但是下一次再次去取的時候,需要阻塞等待。
當(dāng)然也可以使用tryAcquire來非阻塞的獲取,可以實時返回結(jié)果。另外tryAcquire也可以傳入?yún)?shù),也就是等待的時間,超時直接返回false。這點等同于常見的lock,tryLock。
并發(fā)控制Semapphore
一般來說,在網(wǎng)關(guān)系統(tǒng)中,還有一個參數(shù)叫并發(fā)控制,就是某一個資源可以被同時訪問的個數(shù)。這種情況下,我們可以使用Semaphore來控制。
Semaphore不同于互斥鎖?;コ怄i是某個資源只能支持同時一個訪問,而Semaphore可以支持多個訪問,但是加上了總數(shù)的控制。
示例
4.1 在pom中加入guava依賴
?com.google.guava
?guava
?18.0
把限流服務(wù)封裝到一個類中LimitService,提供tryAcquire()方法,用來嘗試獲取令牌,返回true表示獲取到,如下所示:
@Service
public?class?LimitService?{
????//每秒只發(fā)出5個令牌
????RateLimiter?rateLimiter?=?RateLimiter.create(5.0);
????/**
?????*?嘗試獲取令牌
?????*?@return
?????*/
????public?boolean?tryAcquire(){
????????return?rateLimiter.tryAcquire();
????}
}
調(diào)用方是個普通的controller,每次收到請求的時候都嘗試去獲取令牌,獲取成功和失敗打印不同的信息,如下:
@Controller
public?class?HelloController?{
????private?static?SimpleDateFormat?sdf?=?new?SimpleDateFormat("yyyy-MM-dd?HH:mm:ss");
????@Autowired
????private?LimitService?limitService;
????@RequestMapping("/access")
????@ResponseBody
????public?String?access(){
????????//嘗試獲取令牌
????????if(limitService.tryAcquire()){
????????????//模擬業(yè)務(wù)執(zhí)行500毫秒
????????????try?{
????????????????Thread.sleep(500);
????????????}catch?(InterruptedException?e){
????????????????e.printStackTrace();
????????????}
????????????return?"aceess?success?["?+?sdf.format(new?Date())?+?"]";
????????}else{
????????????return?"aceess?limit?["?+?sdf.format(new?Date())?+?"]";
????????}
????}
}
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。
本文鏈接:
https://blog.csdn.net/weixin_39654286/article/details/109688116
粉絲福利:實戰(zhàn)springboot+CAS單點登錄系統(tǒng)視頻教程免費領(lǐng)取
???
?長按上方微信二維碼?2 秒 即可獲取資料
感謝點贊支持下哈?
