最近項目中 spring cloud zuul 運用到限流功能,打算配置一下就直接使用,不過在壓測與調優過程中遇到一些沒有預測到的問題,附上排查與解析結果
yml、pom配置
強烈推薦,按最新github上的文檔配,可以避免搜到一些已經廢棄不用的配置方式!
https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit
我的一些配置,可以直接套用:
zuul: routes: #路由、重試等zuul其他配置省略 #限流 ratelimit: enabled: true # 開啟限流功能 behind-proxy: true # 開啟則限流與業務訪問是異步的,相當於rateLimitFilter先放過;默認是false repository: REDIS # 可選REDIS、CONSUL、JPA等,老版本還有本地內存可選 policy-list: myProject1: - limit: 5 # 這種配置方式相當於:10分鍾內允許5個請求訪問/api/test/info接口 refresh-interval: 10 type: - url_pattern=/api/test/info myProject2: - limit: 3000 refresh-interval: 1 # 更常見的配置是這種,一秒允許3k個,相當於配qps限制 type: - url_pattern=/api/test2/info
- limit: 300 refresh-interval: 1 # 如果同一個服務有多個需要限流的url,可以這樣 type: - url_pattern=/api/test2/info2
pom需要:
<dependency> <groupId>com.marcosbarbero.cloud</groupId> <artifactId>spring-cloud-zuul-ratelimit</artifactId> <version>${latest-version}</version> </dependency>
如果 repository 選擇用 REDIS,還需要:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
啟動,檢查限流功能生效,並且不影響不限流的其他接口!至此解決了限流有無的問題!
返回處理
我們可能需要對觸發限流的情況做監控、報警等,需要識別由限流導致的異常返回
- 推薦使用默認的 RateLimiterErrorHandler,直接寫自己需要的處理就行
- zuul觸發限流后會拋出 http 429,可以針對這個錯誤碼對response包裝
- 也可以在全局異常處理中,判斷出現的異常是否是 RateLimitExceededException
其他配置方式
目前項目中只用到了對特定url的qps限流,zuul ratelimit 還提供對user、http method、url正則等
性能分析
限流配置之前,單實例壓測,qps大概能到2500;配個2300的限流,開開心心啟動服務,啟動壓測!
WTF!限流對性能的影響已經超過了限流配置本身。。一定是我哪里不對TAT
開始排查問題,查實現原理
zuul 限流的入口是zuul的 RateLimitPreFilter
其中 rateLimitKeyGenerator.key 所生成的redis key較長,規則為 前綴(默認為springBoot項目名) + : + zuul項目名 + : + matcher(和限流策略有關,這里是URL_PATTERN) + : + matcher(再來一遍)
如果項目名和url較長,可能出現key例如:my-test-project-gateway:my-sub-project:/api/test2/info2:/api/test2/info2
不過監控看redis暫不是短板,繼續查
限流的實現,通過 rateLimiter.consume 方法
繼續往下看,calcRemainingLimit 方法,內部調用了calcRemaining 方法:
原理不難,利用redis incr命令,每次計算當前過期窗口內還剩幾次,來決定是否限流,安全又高效
但是,上一張圖 rateLimiter.consume 方法增加了 synchornized,懷疑是這個原因
其中 redisTemplate.opsForValue().increment(key, usage) 已經沒有並發問題了,這里感覺不用再 synchornized +_+*
發現也有同僚遇到了這個問題,建議是重寫這個類,去掉synchornized
https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit/issues/96
討論中有人主張去掉synchornized,有人主張保留
決定還是要去掉synchornized,壓測
完美!!!
再驗證一下對無限流的接口是否無影響,確保可用
其他網關限流方式
目前剛開始接觸,也還在探索中,可以一起討論下
1. spring cloud gateway:
- 也是成熟的技術,並且大部分文章分析 Finchley版本的 gateway比 zuul 1.x系列的性能和功能整體要好
- 目前 spring cloud 沒集成 Zuul 2.x,雖然zuul 2.x使用了異步無阻塞式的 API,性能改善明顯
- 實現思想很簡潔,令牌桶,只有50行lua,其中有4次redis調用
- 可以通過monitor命令看出:
request_rate_limiter.lua
測試配置為:
redis-rate-limiter.replenishRate: 500 允許每秒500個請求
redis-rate-limiter.burstCapacity: 5000 令牌桶容量5000
1. 查詢當前桶里剩余令牌數
1590493327.354684 [0 lua] "get" "request_rate_limiter.{/app/test/info}.tokens"
2. 查詢上次取牌的時間
注意:通過當前時間,和上次取牌的時間差,即可在lua中計算出這段時間內新補充進桶里的令牌數,不用每秒真正補充進桶 1590493327.354696 [0 lua] "get" "request_rate_limiter.{/app/test/info}.timestamp"
3. 把這次取的1個令牌,和這段時間內需要重新補充進桶的令牌整合,更新最新令牌數
注意:超時時間為 (brustCapacity/replenishRate)*2,感覺不乘2也行,只要保證超時時間 >= 桶重新裝滿的時間就夠了,乘2是否完全是為了保險? 1590493327.354712 [0 lua] "setex" "request_rate_limiter.{/app/test/info}.tokens" "20" "4999"
4. 更新最新取牌時間 1590493327.354727 [0 lua] "setex" "request_rate_limiter.{/app/test/info}.timestamp" "20" "1590493327"
2. 自己用filter+redis、guava rate limiter等實現
可以做單機緩存、自己定制規則
總結
目前的測試是單機壓測,集群下壓測或許還會在別的地方遇到瓶頸
單實例配置、zuul進程數和其他配置、redis集群性能、業務代碼,都有提升的空間
還需要繼續排查與優化