Spring Cloud限流思路及解決方案

轉自: http://blog.csdn.net/zl1zl2zl3/article/details/78683855html

在高併發的應用中,限流每每是一個繞不開的話題。本文詳細探討在Spring Cloud中如何實現限流。redis

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

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

代碼示例

  1. @Component數據庫

  2. public class RateLimitZuulFilter extends ZuulFilter {併發

  3.  

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

  5.  

  6.    @Override分佈式

  7.    public String filterType() {ide

  8.        return FilterConstants.PRE_TYPE;微服務

  9.    }

  10.  

  11.    @Override

  12.    public int filterOrder() {

  13.        return Ordered.HIGHEST_PRECEDENCE;

  14.    }

  15.  

  16.    @Override

  17.    public boolean shouldFilter() {

  18.        // 這裏能夠考慮弄個限流開啓的開關,開啓限流返回true,關閉限流返回false,你懂的。

  19.        return true;

  20.    }

  21.  

  22.    @Override

  23.    public Object run() {

  24.        try {

  25.            RequestContext currentContext = RequestContext.getCurrentContext();

  26.            HttpServletResponse response = currentContext.getResponse();

  27.            if (!rateLimiter.tryAcquire()) {

  28.                HttpStatus httpStatus = HttpStatus.TOO_MANY_REQUESTS;

  29.  

  30.                response.setContentType(MediaType.TEXT_PLAIN_VALUE);

  31.                response.setStatus(httpStatus.value());

  32.                response.getWriter().append(httpStatus.getReasonPhrase());

  33.  

  34.                currentContext.setSendZuulResponse(false);

  35.  

  36.                throw new ZuulException(

  37.                        httpStatus.getReasonPhrase(),

  38.                        httpStatus.value(),

  39.                        httpStatus.getReasonPhrase()

  40.                );

  41.            }

  42.        } catch (Exception e) {

  43.            ReflectionUtils.rethrowRuntimeException(e);

  44.        }

  45.        return null;

  46.    }

  47. }

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

  •  

    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,若在計時器時間內計數器未超過閾值,則能夠處理任務

  1. if(!cacheDao.hasKey(TIME_KEY)) {

  2.    cacheDao.putToValue(TIME_KEY, 0, 1, TimeUnit.SECONDS);

  3. }      

  4. if(cacheDao.hasKey(TIME_KEY) && cacheDao.incrBy(COUNTER_KEY, 1) > 400) {

  5.    // 拋個異常什麼的

  6. }

實現微服務級別的限流

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

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

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

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

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

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

  1. zuul:

  2.  routes:

  3.    microservice-provider-user: /user/**

  4.    user2:

  5.      url: http://localhost:8000/

  6.      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

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

  1. @Component

  2. public class RateLimitZuulFilter extends ZuulFilter {

  3.  

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

  5.  

  6.    @Override

  7.    public String filterType() {

  8.        return FilterConstants.PRE_TYPE;

  9.    }

  10.  

  11.    @Override

  12.    public int filterOrder() {

  13.        // 這邊的order必定要大於org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter的order

  14.        // 也就是要大於5

  15.        // 不然,RequestContext.getCurrentContext()裏拿不到serviceId等數據。

  16.        return Ordered.LOWEST_PRECEDENCE;

  17.    }

  18.  

  19.    @Override

  20.    public boolean shouldFilter() {

  21.        // 這裏能夠考慮弄個限流開啓的開關,開啓限流返回true,關閉限流返回false,你懂的。

  22.        return true;

  23.    }

  24.  

  25.    @Override

  26.    public Object run() {

  27.        try {

  28.            RequestContext context = RequestContext.getCurrentContext();

  29.            HttpServletResponse response = context.getResponse();

  30.  

  31.            String key = null;

  32.            // 對於service格式的路由,走RibbonRoutingFilter

  33.            String serviceId = (String) context.get(SERVICE_ID_KEY);

  34.            if (serviceId != null) {

  35.                key = serviceId;

  36.                map.putIfAbsent(serviceId, RateLimiter.create(1000.0));

  37.            }

  38.            // 若是壓根不走RibbonRoutingFilter,則認爲是URL格式的路由

  39.            else {

  40.                // 對於URL格式的路由,走SimpleHostRoutingFilter

  41.                URL routeHost = context.getRouteHost();

  42.                if (routeHost != null) {

  43.                    String url = routeHost.toString();

  44.                    key = url;

  45.                    map.putIfAbsent(url, RateLimiter.create(2000.0));

  46.                }

  47.            }

  48.            RateLimiter rateLimiter = map.get(key);

  49.            if (!rateLimiter.tryAcquire()) {

  50.                HttpStatus httpStatus = HttpStatus.TOO_MANY_REQUESTS;

  51.  

  52.                response.setContentType(MediaType.TEXT_PLAIN_VALUE);

  53.                response.setStatus(httpStatus.value());

  54.                response.getWriter().append(httpStatus.getReasonPhrase());

  55.  

  56.                context.setSendZuulResponse(false);

  57.  

  58.                throw new ZuulException(

  59.                        httpStatus.getReasonPhrase(),

  60.                        httpStatus.value(),

  61.                        httpStatus.getReasonPhrase()

  62.                );

  63.            }

  64.        } catch (Exception e) {

  65.            ReflectionUtils.rethrowRuntimeException(e);

  66.        }

  67.        return null;

  68.    }

  69. }

簡單講解一下這段代碼:

對於 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 能力進行實現基於內存壓力的限流——當可用內存低於某個閾值就開啓限流,不然不開啓限流。

  1. @Component

  2. public class RateLimitZuulFilter extends ZuulFilter {

  3.    @Autowired

  4.    private SystemPublicMetrics systemPublicMetrics;

  5.    @Override

  6.    public boolean shouldFilter() {

  7.        // 這裏能夠考慮弄個限流開啓的開關,開啓限流返回true,關閉限流返回false,你懂的。

  8.        Collection<Metric<?>> metrics = systemPublicMetrics.metrics();

  9.        Optional<Metric<?>> freeMemoryMetric = metrics.stream()

  10.                .filter(t -> "mem.free".equals(t.getName()))

  11.                .findFirst();

  12.        // 若是不存在這個指標,穩妥起見,返回true,開啓限流

  13.        if (!freeMemoryMetric.isPresent()) {

  14.            return true;

  15.        }

  16.        long freeMemory = freeMemoryMetric.get()

  17.                .getValue()

  18.                .longValue();

  19.        // 若是可用內存小於1000000KB,開啓流控

  20.        return freeMemory < 1000000L;

  21.    }

  22.    // 省略其餘方法

  23. }

3、實現不一樣維度的限流,例如:

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

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

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

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

參考文檔

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

相關文章
相關標籤/搜索