Spring Cloud(十二):Spring Cloud Zuul 限流詳解(附源碼)(轉)

前面已經介紹了不少zuul的功能,本篇繼續介紹它的另外一大功能。在高併發的應用中,限流每每是一個繞不開的話題。本文詳細探討在Spring Cloud中如何實現限流。html

在 Zuul 上實現限流是個不錯的選擇,只須要編寫一個過濾器就能夠了,關鍵在於如何實現限流的算法。常見的限流算法有漏桶算法以及令牌桶算法。這個可參考 https://www.cnblogs.com/LBSer/p/4083131.html ,寫得通俗易懂,你值得擁有,我就不拽文了。redis

GoogleGuava 爲咱們提供了限流工具類 RateLimiter ,因而乎,咱們能夠擼代碼了。算法

簡單示例

@Component
public class RateLimitZuulFilter extends ZuulFilter {

   private final RateLimiter rateLimiter = RateLimiter.create(1000.0);

   @Override
   public String filterType() {
       return FilterConstants.PRE_TYPE;
   }

   @Override
   public int filterOrder() {
       return Ordered.HIGHEST_PRECEDENCE;
   }

   @Override
   public boolean shouldFilter() {
       // 這裏能夠考慮弄個限流開啓的開關,開啓限流返回true,關閉限流返回false,你懂的。
       return true;
   }


   @Override
   public Object run() {
       try {
           RequestContext currentContext = RequestContext.getCurrentContext();
           HttpServletResponse response = currentContext.getResponse();
           if (!rateLimiter.tryAcquire()) {
               HttpStatus httpStatus = HttpStatus.TOO_MANY_REQUESTS;
               response.setContentType(MediaType.TEXT_PLAIN_VALUE);
               response.setStatus(httpStatus.value());
               response.getWriter().append(httpStatus.getReasonPhrase());
               currentContext.setSendZuulResponse(false);
               throw new ZuulException(
                       httpStatus.getReasonPhrase(),
                       httpStatus.value(),
                       httpStatus.getReasonPhrase()
               );
           }
       } catch (Exception e) {
           ReflectionUtils.rethrowRuntimeException(e);
       }
       return null;
   }
}

 

如上,咱們編寫了一個 pre 類型的過濾器。對Zuul過濾器有疑問的可參考個人博客:spring

Spring Cloud內置的Zuul過濾器詳解:http://www.itmuch.com/spring-cloud/zuul/zuul-filter-in-spring-cloud
Spring Cloud Zuul過濾器詳解:http://www.itmuch.com/spring-cloud/zuul/spring-cloud-zuul-filter

在過濾器中,咱們使用 GuavaRateLimiter 實現限流,若是已經達到最大流量,就拋異常數據庫

 

分佈式場景下的限流

以上單節點Zuul下的限流,但在生產中,咱們每每會有多個Zuul實例。對於這種場景如何限流呢?咱們能夠藉助Redis實現限流。併發

使用redis實現,存儲兩個key,一個用於計時,一個用於計數。請求每調用一次,計數器增長1,若在計時器時間內計數器未超過閾值,則能夠處理任務app

if(!cacheDao.hasKey(TIME_KEY)) {
   cacheDao.putToValue(TIME_KEY, 0, 1, TimeUnit.SECONDS);
}      

if(cacheDao.hasKey(TIME_KEY) && cacheDao.incrBy(COUNTER_KEY, 1) > 400) {
   // 拋個異常什麼的
}

 

 

實現微服務級別的限流

一些場景下,咱們可能還須要實現微服務粒度的限流。此時能夠有兩種方案:分佈式

方式一:在微服務自己實現限流。

和在Zuul上實現限流相似,只需編寫一個過濾器或者攔截器便可,比較簡單,不做贅述。我的不太喜歡這種方式,由於每一個微服務都得編碼,感受成本很高啊。ide

加班那麼多,做爲程序猿的咱們,應該學會偷懶,這樣纔可能有時間孝順父母、抱老婆、逗兒子、遛狗養鳥、聊天打屁、追求人生信仰。好了不扯淡了,看方法二吧。微服務

方法二:在Zuul上實現微服務粒度的限流。

在講解以前,咱們不妨模擬兩個路由規則,兩種路由規則分別表明Zuul的兩種路由方式。

zuul:
 routes:
   microservice-provider-user: /user/**
   user2:
     url: http://localhost:8000/
     path: /user2/**

 

如配置所示,在這裏,咱們定義了兩個路由規則, microservice-provider-user 以及 user2 ,其中 microservice-provider-user 這個路由規則使用到Ribbon + Hystrix,走的是 RibbonRoutingFilter ;而 user2 這個路由用不上Ribbon也用不上Hystrix,走的是 SipleRoutingFilter 。若是你搞不清楚這點,請參閱個人博客:

Spring Cloud內置的Zuul過濾器詳解:http://www.itmuch.com/spring-cloud/zuul/zuul-filter-in-spring-cloud

Spring Cloud Zuul過濾器詳解:http://www.itmuch.com/spring-cloud/zuul/spring-cloud-zuul-filter

 

搞清楚這點以後,咱們就能夠擼代碼了:

@Component
public class RateLimitZuulFilter extends ZuulFilter {

   private Map<String, RateLimiter> map = Maps.newConcurrentMap();

   @Override
   public String filterType() {
       return FilterConstants.PRE_TYPE;
   }

   @Override
   public int filterOrder() {
       // 這邊的order必定要大於org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter的order
       // 也就是要大於5
       // 不然,RequestContext.getCurrentContext()裏拿不到serviceId等數據。
       return Ordered.LOWEST_PRECEDENCE;
   }


   @Override
   public boolean shouldFilter() {
       // 這裏能夠考慮弄個限流開啓的開關,開啓限流返回true,關閉限流返回false,你懂的。
       return true;
   }


   @Override
   public Object run() {
       try {
           RequestContext context = RequestContext.getCurrentContext();
           HttpServletResponse response = context.getResponse();
           String key = null;
           // 對於service格式的路由,走RibbonRoutingFilter
           String serviceId = (String) context.get(SERVICE_ID_KEY);
           if (serviceId != null) {
               key = serviceId;
               map.putIfAbsent(serviceId, RateLimiter.create(1000.0));
           }

           // 若是壓根不走RibbonRoutingFilter,則認爲是URL格式的路由
           else {
               // 對於URL格式的路由,走SimpleHostRoutingFilter
               URL routeHost = context.getRouteHost();
               if (routeHost != null) {
                   String url = routeHost.toString();
                   key = url;
                   map.putIfAbsent(url, RateLimiter.create(2000.0));
               }
           }

           RateLimiter rateLimiter = map.get(key);
           if (!rateLimiter.tryAcquire()) {
               HttpStatus httpStatus = HttpStatus.TOO_MANY_REQUESTS;
               response.setContentType(MediaType.TEXT_PLAIN_VALUE);
               response.setStatus(httpStatus.value());
               response.getWriter().append(httpStatus.getReasonPhrase());
               context.setSendZuulResponse(false);
               throw new ZuulException(
                       httpStatus.getReasonPhrase(),
                       httpStatus.value(),
                       httpStatus.getReasonPhrase()
               );
           }
       } catch (Exception e) {
           ReflectionUtils.rethrowRuntimeException(e);
       }
       return null;
   }
}

 

 

簡單講解一下這段代碼:

對於 microservice-provider-user 這個路由,咱們能夠用 context.get(SERVICE_ID_KEY); 獲取到serviceId,獲取出來就是 microservice-provider-user

而對於 user2 這個路由,咱們使用 context.get(SERVICE_ID_KEY); 得到是null,可是呢,能夠用 context.getRouteHost() 得到路由到的地址,獲取出來就是 http://localhost:8000/ 。接下來的事情,大家懂的。

改進與提高

實際項目中,除以上實現的限流方式,還可能會:

1、在上文的基礎上,增長配置項,控制每一個路由的限流指標,並實現動態刷新,從而實現更加靈活的管理

2、基於CPU、內存、數據庫等壓力限流(感謝平安常浩智)提出。。

下面,筆者藉助Spring Boot Actuator提供的 Metrics 能力進行實現基於內存壓力的限流——當可用內存低於某個閾值就開啓限流,不然不開啓限流。

@Component
public class RateLimitZuulFilter extends ZuulFilter {

   @Autowired
   private SystemPublicMetrics systemPublicMetrics;

   @Override
   public boolean shouldFilter() {
       // 這裏能夠考慮弄個限流開啓的開關,開啓限流返回true,關閉限流返回false,你懂的。
       Collection<Metric<?>> metrics = systemPublicMetrics.metrics();
       Optional<Metric<?>> freeMemoryMetric = metrics.stream()
               .filter(t -> "mem.free".equals(t.getName()))
               .findFirst();

       // 若是不存在這個指標,穩妥起見,返回true,開啓限流
       if (!freeMemoryMetric.isPresent()) {
           return true;
       }

       long freeMemory = freeMemoryMetric.get()
               .getValue()
               .longValue();

       // 若是可用內存小於1000000KB,開啓流控
       return freeMemory < 1000000L;
   }

   // 省略其餘方法
}

 

 

3、實現不一樣維度的限流

例如:

  1. 對請求的目標URL進行限流(例如:某個URL每分鐘只容許調用多少次)

  2. 對客戶端的訪問IP進行限流(例如:某個IP每分鐘只容許請求多少次)

  3. 對某些特定用戶或者用戶組進行限流(例如:非VIP用戶限制每分鐘只容許調用100次某個API等)

  4. 多維度混合的限流。此時,就須要實現一些限流規則的編排機制。與、或、非等關係。

參考文檔

  1. 分佈式環境下限流方案的實現:http://blog.csdn.net/Justnow_/article/details/53055299

相關文章
相關標籤/搜索