微服務怎麼限流?算法+框架+實戰!

作者:lipengxs<br> 來源:https://my.oschina.net/lipengxs/blog/4733443

背景

隨着微服務的流行,服務和服務之間的穩定性變得越來越重要。緩存、降級和限流是保護微服務系統運行穩定性的三大利器。

  • 緩存:提升系統訪問速度和增大系統能處理的容量
  • 降級:當服務出問題或者影響到核心流程的性能則需要暫時屏蔽掉
  • 限流:解決服務雪崩,級聯服務發生阻塞時,及時熔斷,防止請求堆積消耗佔用系統的線程、IO等資源,造成其他級聯服務所在服務器的崩潰

這裏我們主要說一下限流,限流的目的應當是通過對併發訪問/請求進行限速或者一個時間窗口內的的請求進行限速來保護系統,一旦達到限制速率就可以拒絕服務、等待、降級。 首先,我們需要去了解最基本的兩種限流算法。

限流算法

  • 漏桶算法
  • 令牌桶算法
  • 計算器算法

限流框架

下面說一下現有流行的限流工具

guava

Google的Guava工具包中就提供了一個限流工具類——RateLimiter。

RateLimiter是基於“令牌通算法”來實現限流的。

hystrix

hystrix主要是通過資源池以及信號量來限流,暫時能支持簡單的限流

sentinel

限流比較主流的三種算法:漏桶,令牌桶,滑動窗口。而Sentinel採用的是最後一種,滑動窗口來實現限流的。當然sentinel不僅僅侷限於限流,它是一個面向分佈式服務架構的高可用流量防護組件,主要以流量爲切入點,從限流、流量整形、熔斷降級、系統負載保護、熱點防護等多個維度來幫助開發者保障微服務的穩定性。

限流實戰

有很多應用都是可以直接在調用端、代理、網關等中間層進行限流,下面簡單介紹下集中中間件限流方式

nginx限流

nginx限流方式有三種

  • limit_conn_zone
  • limit_req_zone
  • ngx_http_upstream_module

但是nginx限流不夠靈活,不好動態配置。

zuul限流

除了zuul引入限流相關依賴

<dependency>
     <groupid>com.marcosbarbero.cloud</groupid>
     <artifactid>spring-cloud-zuul-ratelimit</artifactid>
     <version>2.0.0.RELEASE</version>
</dependency>

相關配置如下:

zuul:

    ratelimit:

        key-prefix: your-prefix  #對應用來標識請求的key的前綴

        enabled: true

        repository: REDIS  #對應存儲類型(用來存儲統計信息)默認是IN_MEMORY

        behind-proxy: true  #代理之後

        default-policy: #可選 - 針對所有的路由配置的策略,除非特別配置了policies

             limit: 10 #可選 - 每個刷新時間窗口對應的請求數量限制

             quota: 1000 #可選-  每個刷新時間窗口對應的請求時間限制(秒)

              refresh-interval: 60 # 刷新時間窗口的時間,默認值 (秒)

               type: #可選 限流方式

                    - user

                    - origin

                    - url

          policies:

                myServiceId: #特定的路由

                      limit: 10 #可選- 每個刷新時間窗口對應的請求數量限制

                      quota: 1000 #可選-  每個刷新時間窗口對應的請求時間限制(秒)

                      refresh-interval: 60 # 刷新時間窗口的時間,默認值 (秒)

                      type: #可選 限流方式

                          - user

                          - origin

                          - url

注意這裏的倉庫如果是針對全侷限流,那麼可以考慮存到redis中,這裏的zuul.ratelimit.repository可以設置爲redis,但是如果擴容後則需要動態調整,不過靈活,所以這裏我建議還是選擇本地內存(INM_MOMERY)或者不設置,這樣伸縮容後可以自動擴展,不用變更配置,

如果需要動態更新,可以集成apollo配置進行動態更新,

public class ZuulPropertiesRefresher implements ApplicationContextAware {


    private ApplicationContext applicationContext;

    @Autowired
    private RouteLocator routeLocator;

    @ApolloConfigChangeListener(interestedKeyPrefixes = "zuul.",value="zuul.yml")
    public void onChange(ConfigChangeEvent changeEvent) {
        refreshZuulProperties(changeEvent);
    }

    private void refreshZuulProperties(ConfigChangeEvent changeEvent) {
        log.info("Refreshing zuul properties!");

        /**
         * rebind configuration beans, e.g. ZuulProperties
         * @see org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent
         */
        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));

        /**
         * refresh routes
         * @see org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration.ZuulRefreshListener#onApplicationEvent
         */
        this.applicationContext.publishEvent(new RoutesRefreshedEvent(routeLocator));

        log.info("Zuul properties refreshed!");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

springcloud 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
</artifatid></dependency>

複製代碼在配置文件中做以下的配置:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
  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

配置了 redis的信息,並配置了RequestRateLimiter的限流過濾器,該過濾器需要配置三個參數:

  • burstCapacity,令牌桶總容量。
  • replenishRate,令牌桶每秒填充平均速率。
  • key-resolver,用於限流的鍵的解析器的 Bean 對象的名字。它使用 SpEL 表達式根據#{@beanName}從 Spring 容器中獲取 Bean 對象。

可以通過KeyResolver來指定限流的Key,比如我們需要根據用戶來做限流,IP來做限流等等。

1)IP限流

@Bean
public KeyResolver ipKeyResolver() {
    return exchange -&gt; Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}

2)用戶限流

@Bean
KeyResolver userKeyResolver() {
    return exchange -&gt; Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}

3)接口限流

@Bean
KeyResolver apiKeyResolver() {
    return exchange -&gt; Mono.just(exchange.getRequest().getPath().value());
}

這裏只是針對單節點限流,如果需要可以自定義全侷限流

sentinel 限流

sentinel限流這裏不做詳細描述,大家想了解可以參考下面文檔:https://mp.weixin.qq.com/s/4LjnzDg9uNQIJML6MIriEg

應用限流

這裏springboot應用服務需要限流的話,這裏給的方案是集成google的guava類庫,大家在網上能搜索到很多demo,我這裏不做詳細描述,主要是下面api的使用:

 RateLimiter.create(callerRate);

現在容器比較火,現在如果部署在容器或者虛擬機上,我們需要動態調整資源數後,那麼限流也會跟着變化,這裏說一下如何實現動態限流。第一步肯定是集成配置中心實現配置動態更新,至於說生效方式有幾種 方案一: 增加監聽器,當配置變動時重新創建限流對象

方案二: 限流對象定時創建,這裏引入了應用緩存框架,下面給個demo

import com.ctrip.framework.apollo.Config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;


@Slf4j
public class RateLimitInterceptor implements HandlerInterceptor {

    private Config config;

    private static final String RATE_TYPE_GLOBAL = "global";
    private static final String RATE_TYPE_URL = "url";

    //全侷限流
    public RateLimitInterceptor(Config config) {
        this.config = config;
    }

    Cache<object, ratelimiter> rateLimiterCache = Caffeine.newBuilder()
            .initialCapacity20
            .expireAfterWrite(2, TimeUnit.MINUTES)
            .maximumSize100
            .softValues()
            .recordStats()
            .build();


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (StringUtils.isBlank(request.getRequestURI()) || request.getRequestURI().startsWith("/actuator/")
                || request.getRequestURI().startsWith("/srch-recommend/fault-tolerant/health")||request.getRequestURI().startsWith("/health")) {
            return true;
        }
         try {
            boolean rateLimitEnabled=config.getBooleanProperty("ratelimit.enabled", false);
            if(!rateLimitEnabled){
                return true;
            }
            if (!do(RATE_TYPE_GLOBAL, StringUtils.EMPTY, "ratelimit.global")) {
                return false;
            }
            String url = request.getRequestURI();
            if (StringUtils.isNotBlank(url)) {
                return do(RATE_TYPE_URL, url, "ratelimit.url.");
            }
            return true;
        } catch (Exception e) {
            log.warn("RateLimitInterceptor error message:{}", e.getMessage(), e);
            return true;
        }
    }

    private boolean doRateLimiter(String rateType, String key, String configPrefix) {
        String cacheKey = rateType + "-" + key;
        RateLimiter rateLimiter = rateLimiterCache.getIfPresent(cacheKey);
        if (rateLimiter == null) {
            int callerRate = config.getIntProperty(configPrefix + uniqueKey, 0);
            if (callerRate &gt; 0) {
                rateLimiter = RateLimiter.create(callerRate);
                rateLimiterCache.put(cacheKey, rateLimiter);
            }
        }
        return rateLimiter == null || rateLimiter.tryAcquire();
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    }

}

當然這裏如果有業務相關的限流可以根據參考上面的demo自己來實現限流。

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2021最新版)

2.別在再滿屏的 if/ else 了,試試策略模式,真香!!

3.臥槽!Java 中的 xx ≠ null 是什麼新語法?

4.Spring Boot 2.5 重磅發佈,黑暗模式太炸了!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章