在高併發的系統中,每每須要在系統中作限流,一方面是爲了防止大量的請求使服務器過載,致使服務不可用,另外一方面是爲了防止網絡攻擊。react
常見的限流方式,好比Hystrix適用線程池隔離,超過線程池的負載,走熔斷的邏輯。在通常應用服務器中,好比tomcat容器也是經過限制它的線程數來控制併發的;也有經過時間窗口的平均速度來控制流量。常見的限流緯度有好比經過Ip來限流、經過uri來限流、經過用戶訪問頻次來限流。redis
通常限流都是在網關這一層作,好比Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也能夠在應用層經過Aop這種方式去作限流。算法
本文詳細探討在 Spring Cloud Gateway 中如何實現限流。spring
常見的限流算法tomcat
計數器算法bash
計數器算法採用計數器實現限流有點簡單粗暴,通常咱們會限制一秒鐘的可以經過的請求數,好比限流qps爲100,算法的實現思路就是從第一個請求進來開始計時,在接下去的1s內,每來一個請求,就把計數加1,若是累加的數字達到了100,那麼後續的請求就會被所有拒絕。等到1s結束後,把計數恢復成0,從新開始計數。具體的實現能夠是這樣的:對於每次服務調用,能夠經過AtomicLong#incrementAndGet()方法來給計數器加1並返回最新值,經過這個最新值和閾值進行比較。這種實現方式,相信你們都知道有一個弊端:若是我在單位時間1s內的前10ms,已經經過了100個請求,那後面的990ms,只能眼巴巴的把請求拒絕,咱們把這種現象稱爲「突刺現象」服務器
漏桶算法網絡
漏桶算法爲了消除"突刺現象",能夠採用漏桶算法實現限流,漏桶算法這個名字就很形象,算法內部有一個容器,相似生活用到的漏斗,當請求進來時,至關於水倒入漏斗,而後從下端小口慢慢勻速的流出。無論上面流量多大,下面流出的速度始終保持不變。無論服務調用方多麼不穩定,經過漏桶算法進行限流,每10毫秒處理一次請求。由於處理的速度是固定的,請求進來的速度是未知的,可能忽然進來不少請求,沒來得及處理的請求就先放在桶裏,既然是個桶,確定是有容量上限,若是桶滿了,那麼新進來的請求就丟棄。併發
在算法實現方面,能夠準備一個隊列,用來保存請求,另外經過一個線程池(ScheduledExecutorService)來按期從隊列中獲取請求並執行,能夠一次性獲取多個併發執行。app
這種算法,在使用事後也存在弊端:沒法應對短期的突發流量。
令牌桶算法
從某種意義上講,令牌桶算法是對漏桶算法的一種改進,桶算法可以限制請求調用的速率,而令牌桶算法可以在限制調用的平均速率的同時還容許必定程度的突發調用。在令牌桶算法中,存在一個桶,用來存放固定數量的令牌。算法中存在一種機制,以必定的速率往桶中放令牌。每次請求調用須要先獲取令牌,只有拿到令牌,纔有機會繼續執行,不然選擇選擇等待可用的令牌、或者直接拒絕。放令牌這個動做是持續不斷的進行,若是桶中令牌數達到上限,就丟棄令牌,因此就存在這種狀況,桶中一直有大量的可用令牌,這時進來的請求就能夠直接拿到令牌執行,好比設置qps爲100,那麼限流器初始化完成一秒後,桶中就已經有100個令牌了,這時服務還沒徹底啓動好,等啓動完成對外提供服務時,該限流器能夠抵擋瞬時的100個請求。因此,只有桶中沒有令牌時,請求才會進行等待,最後至關於以必定的速率執行。
實現思路:能夠準備一個隊列,用來保存令牌,另外經過一個線程池按期生成令牌放到隊列中,每來一個請求,就從隊列中獲取一個令牌,並繼續執行。
Spring Cloud Gateway限流
在Spring Cloud Gateway中,有Filter過濾器,所以能夠在「pre」類型的Filter中自行實現上述三種過濾器。可是限流做爲網關最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory這個類,適用Redis和lua腳本實現了令牌桶的方式。具體實現邏輯在RequestRateLimiterGatewayFilterFactory類中,lua腳本在以下圖所示的文件夾中:
具體源碼不打算在這裏講述,讀者能夠自行查看,代碼量較少,先以案例的形式來說解如何在Spring Cloud Gateway中使用內置的限流過濾器工廠來實現限流。
首先在工程的pom文件中引入gateway的起步依賴和redis的reactive依賴,代碼以下:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId></dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifatId>spring-boot-starter-data-redis-reactive</artifactId></dependency>複製代碼
在配置文件中作如下的配置:
server: port: 8081spring: cloud: gateway: routes: - id: limit_route uri: http://httpbin.org: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複製代碼
在上面的配置文件,指定程序的端口爲8081,配置了 redis的信息,並配置了RequestRateLimiter的限流過濾器,該過濾器須要配置三個參數:
KeyResolver須要實現resolve方法,好比根據Hostname進行限流,則須要用hostAddress去判斷。實現完KeyResolver以後,須要將這個類的Bean註冊到Ioc容器中。
public class HostAddrKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } } @Bean public HostAddrKeyResolver hostAddrKeyResolver() { return new HostAddrKeyResolver(); }複製代碼
能夠根據uri去限流,這時KeyResolver代碼以下:
public class UriKeyResolver implements KeyResolver { @Override public Mono<String> 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去循環請求lcoalhost:8081,循環間隔1s。從壓測的結果上看到有部分請求經過,由部分請求失敗。經過redis客戶端去查看redis中存在的key。以下:
可見,RequestRateLimiter是使用Redis來進行限流的,並在redis中存儲了2個key。關注這兩個key含義能夠看lua源代碼。
以上就是本文的所有內容,但願對你們的學習有所幫助
你們以爲不錯能夠點個贊在關注下,之後還會分享更多文章!