SpringCloud系列——限流、熔斷、降級
前言
分散式環境下,服務直接相互呼叫,一個複雜的業務可能要呼叫多個服務,例如A -> B -> C -> D,當某個服務出現異常(呼叫超時、呼叫失敗等)將導致整個流程阻塞崩潰,嚴重的整個系統都會崩掉,為了實現高可用,必要的保護機制必不可少
本文記錄限流、熔斷、降級的實現處理
限流
我們採用令牌桶限流法,並自己實現一個簡單令牌桶限流
有個任務執行緒以恆定速率向令牌桶新增令牌
一個請求會消耗一個令牌,令牌桶裡的令牌大於0,才會放行,反正不允許通過
/** * 簡單的令牌桶限流 */ public class RateLimiter { /** * 桶的大小 */ private Integer limit; /** * 桶當前的token */ private static Integer tokens = 0; /** * 構造引數 */ public RateLimiter(Integer limit, Integer speed){ //初始化桶的大小,且桶一開始是滿的 this.limit = limit; tokens = this.limit; //任務執行緒:每秒新增speed個令牌 new Thread(() ->{ while (true){ try { Thread.sleep(1000L); int newTokens = tokens + speed; if(newTokens > limit){ tokens = limit; System.out.println("令牌桶滿了!!!"); }else{ tokens = newTokens; } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } /** * 根據令牌數判斷是否允許執行,需要加鎖 */ public synchronized boolean execute() { if (tokens > 0) { tokens = tokens - 1; return true; } return false; } }
main簡單測試
public static void main(String[] args) { //令牌桶限流:峰值每秒可以處理10個請求,正常每秒可以處理3個請求 RateLimiter rateLimiter = new RateLimiter(10, 3); //模擬請求 while (true){ //在控制檯輸入一個值按回車,相對於發起一次請求 Scanner scanner = new Scanner(System.in); scanner.next(); //令牌桶返回true或者false if(rateLimiter.execute()){ System.out.println("允許訪問"); }else{ System.err.println("禁止訪問"); } } }
在SpringCloud分散式下實現限流,需要把令牌桶的維護放到一個公共的地方,比如Zuul路由,當然也可以同時針對具體的每個服務進行單獨限流
另外,guava裡有現成的基於令牌桶的限流實現,引入
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>26.0-jre</version> </dependency>
具體用法這裡就不闡述了
我們找出之前的springcloud專案,在zuul-server中的AccessFilter過濾器進行限流,其他的都不變,只需要做如下修改
PS:我這裡為了方便測試,調小了令牌桶的大小,跟速率,正常情況下要伺服器的承受能力來定
/** * Zuul過濾器,實現了路由檢查 */ public class AccessFilter extends ZuulFilter { //令牌桶限流:峰值每秒可以處理10個請求,正常每秒可以處理3個請求
//PS:我這裡為了方便測試,調小了令牌桶的大小,跟速率,正常情況下按伺服器的承受能力來定 private RateLimiter rateLimiter = new RateLimiter(2, 1); //業務不變,省略其他程式碼... /** * 過濾器的具體邏輯 */ @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); HttpServletResponse response = ctx.getResponse(); //限流 if(!rateLimiter.execute()){ try { ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(200); //直接寫入瀏覽器 response.setContentType("text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); writer.println("系統繁忙,請稍後在試!<br/>System busy, please try again later!"); writer.flush();return null; } catch (Exception e) { e.printStackTrace(); } } //業務不變,省略其他程式碼.. } }
按照我們設定的值,一秒能處理一個請求,峰值一秒能處理兩個請求,下面瘋狂重新整理進行測試
熔斷
yml配置開啟Hystrix熔斷功能,進行容錯處理
feign: hystrix: enabled: true
設定Hystrix的time-out時間
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 5000 #毫秒 #或者設定從不超時 #timeout: # enabled: false
在使用Feign呼叫服務提供者時配置@FeignClient的 fallback,進行容錯處理(服務提供者發生異常),如果需要獲取到異常資訊,則要配置fallbackFactory<T>
@FeignClient(name = "sso-server", path = "/",/*fallback = SsoFeign.SsoFeignFallback.class,*/fallbackFactory = SsoFeign.SsoFeignFallbackFactory.class)
/** * 容錯處理(服務提供者發生異常,將會進入這裡) */ @Component public class SsoFeignFallback implements SsoFeign { @Override public Boolean hasKey(String key) { System.out.println("呼叫sso-server失敗,進行SsoFeignFallback.hasKey處理:return false;"); return false; } }
/** * 只打印異常,容錯處理仍交給 SsoFeignFallback */ @Component public class SsoFeignFallbackFactory implements FallbackFactory<SsoFeign> { private final SsoFeignFallback ssoFeignFallback; public SsoFeignFallbackFactory(SsoFeignFallback ssoFeignFallback) { this.ssoFeignFallback = ssoFeignFallback; } @Override public SsoFeign create(Throwable cause) { cause.printStackTrace(); return ssoFeignFallback; } }
FallbackFactory也可以這樣寫
/** * 容錯處理 */
@Component
public class SsoFeignFallbackFactory implements FallbackFactory<SsoFeign> { @Override public SsoFeign create(Throwable cause) { //列印異常 cause.printStackTrace(); return new SsoFeign() { @Override public Boolean hasKey(String key) { System.out.println("呼叫sso-server失敗:return false;"); return false; } }; } }
因為我們沒有啟動Redis,報錯,但我們進行容錯處理,所以還是返回了false
降級
當呼叫服務傳送異常,容錯處理的方式有多種,我們可以:
1、重連,比如服務進行了限流,本次連線被限制,重連一次或N次就可以得到資料
2、直接返回一個友好提示
3、降級呼叫備用服務、返回快取的資料等
後記
降級也可以叫做“備胎計劃&rdquo