Spring Cloud 學(xué)習(xí)(22) --- Zuul(四) 限流、動(dòng)態(tài)路由概念

之前利用 Hystrix,通過(guò)熔斷器實(shí)現(xiàn)了通過(guò)某個(gè)閾值來(lái)對(duì)異常流量進(jìn)行降級(jí)處理。除了對(duì)異常流量進(jìn)行降級(jí)之外,還可以通過(guò) 流量排隊(duì)限流、分流等操作,防止系統(tǒng)出錯(cuò)。

限流算法

限流算法一般分為 漏桶、令牌桶 兩種。

漏桶

漏桶的圓形是一個(gè)底部有漏孔的桶,桶的上方有一個(gè)入水口,水不斷流進(jìn)桶內(nèi),桶下方的漏孔會(huì)以一個(gè)相對(duì)恒定的速度漏水,在入大于出的情況下,桶在一段時(shí)間內(nèi)就會(huì)被裝滿,這時(shí)候多余的水就會(huì)溢出;而在入小于出的情況下,漏桶起不到任何作用。

當(dāng)請(qǐng)求或者具有一定體量的數(shù)據(jù)進(jìn)入系統(tǒng)時(shí),在漏桶作用下,流量被整形,不能滿足要求的部分被削減掉,漏桶算法能強(qiáng)制限定流量速度。溢出的流量可以被再次利用起來(lái),并非完全丟棄,可以把溢出的流量收集到一個(gè)隊(duì)列中,做流量排隊(duì),盡量合理利用所有資。


leaky-bucket

令牌桶

令牌桶與漏桶的區(qū)別是,桶里放的是令牌而不是流量,令牌以一個(gè)恒定的速度被加入桶內(nèi),可以積壓,可以溢出。當(dāng)流量涌入時(shí),量化請(qǐng)求用于獲取令牌,如果取到令牌則方形,同時(shí)桶內(nèi)丟掉這個(gè)令牌;如果取不到令牌,則請(qǐng)求被丟棄。
由于桶內(nèi)可以存一定量的令牌,那么就可能會(huì)解決一定程度的流量突發(fā)。這個(gè)也是漏桶與令牌桶的適用場(chǎng)景不同之處。

token-bucket

限流實(shí)例

在 Zuul 中實(shí)現(xiàn)限流,最簡(jiǎn)單的方式是使用 Filter 加上相關(guān)的限流算法,其中可能會(huì)考慮到 Zuul 多節(jié)點(diǎn)部署。因?yàn)樗惴ǖ脑?,這是需要一個(gè) K/V 存儲(chǔ)工具(Redis等)。

spring-cloud-zuul-ratelimit 是一個(gè)針對(duì) Zuul 的限流庫(kù)
限流粒度的策略:

  • user:認(rèn)證用戶名或匿名,針對(duì)某用戶粒度進(jìn)行限流
  • origin:客戶機(jī) ip,針對(duì)請(qǐng)求客戶機(jī) ip 粒度進(jìn)行限流
  • url:特定 url,針對(duì)某個(gè)請(qǐng)求 url 粒度進(jìn)行限流
  • serviceId:特定服務(wù),針對(duì)某個(gè)服務(wù) id 粒度進(jìn)行限流

限流粒度臨時(shí)變量存儲(chǔ)方式:

  • IN_MEMORY:基于本地內(nèi)存,底層是 ConcurrentHashMap
  • REDIS:基于 Redis K/V 存儲(chǔ)
  • CONSUL:基于 Consul K/V 存儲(chǔ)
  • JPA:基于 SpringData JPA,數(shù)據(jù)庫(kù)存儲(chǔ)
  • BUKET4J:使用 Java 編寫的基于令牌桶算法的限流庫(kù),四種模:JCache、HazelcastApache Ignite、Inifinispan,后面三種支持異步

源碼:https://gitee.com/laiyy0728/spring-cloud/tree/master/spring-cloud-zuul/spring-cloud-zuul-ratelimit

Zuul Server

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>com.marcosbarbero.cloud</groupId>
        <artifactId>spring-cloud-zuul-ratelimit</artifactId>
        <version>2.0.6.RELEASE</version>
    </dependency>
</dependencies>
server:
  port: 5555
spring:
  application:
    name: spring-cloud-ratelimit-zuul-server
eureka:
  instance:
    instance-id: ${spring.application.name}:${server.port}
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
zuul:
  routes:
    spring-cloud-ratelimit-provider-service:
      path: /provider/**
      serviceId: spring-cloud-ratelimit-provider-service
  ratelimit:
    key-prefix: springcloud # 按粒度拆分的臨時(shí)變量 key 的前綴
    enabled: true # 啟用開關(guān)
    repository: in_memory # key 的存儲(chǔ)類型,默認(rèn)是 in_memory
    behind-proxy: true # 表示代理之后
    default-policy:
      limit: 2 # 在一個(gè)單位時(shí)間內(nèi)的請(qǐng)求數(shù)量
      quota: 1 # 在一個(gè)單位時(shí)間內(nèi)的請(qǐng)求時(shí)間限制
      refresh-interval: 3 # 單位時(shí)間窗口
      type: 
        - user # 可指定用戶粒度
        - origin # 可指定客戶端地址粒度
        - url # 可指定 url 粒度

Provider

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>
spring:
  application:
    name: spring-cloud-ratelimit-provider-service
server:
  port: 7070
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${server.port}

驗(yàn)證

快速訪問(wèn)幾次 http://localhost:5555/provider/get-result ,返回值如下:

{
    "timestamp": "2019-02-20T06:52:51.220+0000",
    "status": 429,
    "error": "Too Many Requests",
    "message": "429"
}

控制臺(tái)打印異常如下:

com.netflix.zuul.exception.ZuulException: 429
    at com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitExceededException.<init>(RateLimitExceededException.java:13) ~[spring-cloud-zuul-ratelimit-core-2.0.6.RELEASE.jar:2.0.6.RELEASE]
    at com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPreFilter.lambda$run$0(RateLimitPreFilter.java:106) ~[spring-cloud-zuul-ratelimit-core-2.0.6.RELEASE.jar:2.0.6.RELEASE]
    at java.util.ArrayList.forEach(ArrayList.java:1257) ~[na:1.8.0_171]
    at com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPreFilter.run(RateLimitPreFilter.java:79) ~[spring-cloud-zuul-ratelimit-core-2.0.6.RELEASE.jar:2.0.6.RELEASE]
    at com.netflix.zuul.ZuulFilter.runFilter(ZuulFilter.java:117) ~[zuul-core-1.3.1.jar:1.3.1]
    at com.netflix.zuul.FilterProcessor.processZuulFilter(FilterProcessor.java:193) ~[zuul-core-1.3.1.jar:1.3.1]
    at com.netflix.zuul.FilterProcessor.runFilters(FilterProcessor.java:157) ~[zuul-core-1.3.1.jar:1.3.1]
    ...

正常訪問(wèn)結(jié)果如下:

zuul rate limit result !

動(dòng)態(tài)路由

之前配置路由映射規(guī)則的方式,為“靜態(tài)路由”。如果在迭代過(guò)程中,可能需要?jiǎng)討B(tài)將路由映射規(guī)則寫入內(nèi)存。在“靜態(tài)路由”配置中,需要重啟 Zuul 應(yīng)用。
不需要重啟 Zuul,又能修改映射規(guī)則的方式,稱為“動(dòng)態(tài)路由”。

  • SpringCloud Config + Bus,動(dòng)態(tài)刷新配置文件。好處是不用 Zuul 維護(hù)映射規(guī)則,可以隨時(shí)修改,隨時(shí)生效。缺點(diǎn)是需要單獨(dú)集成一些使用并不頻繁的組件。SpringCloud Config 沒(méi)有可視化界面,維護(hù)也麻煩
  • 重寫 Zuul 配置讀取方式,采用事件刷新機(jī)制,從數(shù)據(jù)庫(kù)讀取路由映射規(guī)則。此方式基于數(shù)據(jù)庫(kù),可輕松實(shí)現(xiàn)管理頁(yè)面,靈活度高。

動(dòng)態(tài)路由實(shí)現(xiàn)原理

動(dòng)態(tài)路由原理核心類依賴圖

DiscoveryClientRouteLocator

public class DiscoveryClientRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
    
    // 省略其他方法

    // 路由
    protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
        // 省略方法實(shí)現(xiàn)
    }

    // 刷新
    public void refresh() {
        this.doRefresh();
    }
}

locateRoutes 方法繼承自 SimpleRouteLocator 類,并重寫規(guī)則,該方法主要的功能就是將配置文件中的映射規(guī)則信息包裝成 LinkedHashMap<String, ZuulRoute>,鍵為路徑 path,值 ZuulRoute 是配置文件的封裝類。之前的映射配置信息就是使用 ZuulRoute 封裝的。
refresh 實(shí)現(xiàn)自 RefreshableRouteLocator 接口,添加刷新功能必須實(shí)現(xiàn)此方法,doRefresh 方法來(lái)自 SimpleRouteLocator

SimpleRouteLocator

SimpleRouteLocatorDiscoveryClientRouteLocator 的父類,此類基本實(shí)現(xiàn)了 RouteLocator 接口,對(duì)讀取配置文件信息做一些處理,提供方法 doRefresh、locateRoutes 供子類實(shí)現(xiàn)刷新策略與映射規(guī)則加載策略

/**
 * Calculate all the routes and set up a cache for the values. Subclasses can call
 * this method if they need to implement {@link RefreshableRouteLocator}.
 */
protected void doRefresh() {
    this.routes.set(locateRoutes());
}

/**
 * Compute a map of path pattern to route. The default is just a static map from the
 * {@link ZuulProperties}, but subclasses can add dynamic calculations.
 */
protected Map<String, ZuulRoute> locateRoutes() {
    LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
    for (ZuulRoute route : this.properties.getRoutes().values()) {
        routesMap.put(route.getPath(), route);
    }
    return routesMap;
}

這兩個(gè)方法都是 protectted 修飾,是為了讓子類不用維護(hù)此類一些成員變量就能實(shí)現(xiàn)刷新或讀取路由的功能。從注釋上可以看到,調(diào)用 doRedresh 方法需要實(shí)現(xiàn) RefreshableRouteLocator;locateRoutes 默認(rèn)是一個(gè)靜態(tài)的映射讀取方法,如果需要?jiǎng)討B(tài)記載映射,需要子類重寫此方法。

ZuulServerAutoConfiguration

ZuulServerAutoConfiguration 是 Spring Cloud Zuul 的配置類,主要目的是注冊(cè)各種過(guò)濾器、監(jiān)聽器以及其他功能。Zuul 在注冊(cè)中心新增服務(wù)后刷新監(jiān)聽器也是在這個(gè)類中注冊(cè)的,底層是 Spring 的 ApplicationListener

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
public class ZuulServerAutoConfiguration {

    // 省略其他功能注冊(cè)

    // Zuul 刷新監(jiān)聽器
    private static class ZuulRefreshListener
            implements ApplicationListener<ApplicationEvent> {

        @Autowired
        private ZuulHandlerMapping zuulHandlerMapping;

        private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();

        @Override
        public void onApplicationEvent(ApplicationEvent event) {
            if (event instanceof ContextRefreshedEvent
                    || event instanceof RefreshScopeRefreshedEvent
                    || event instanceof RoutesRefreshedEvent
                    || event instanceof InstanceRegisteredEvent) {
                reset();
            }
            else if (event instanceof ParentHeartbeatEvent) {
                ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
                resetIfNeeded(e.getValue());
            }
            else if (event instanceof HeartbeatEvent) {
                HeartbeatEvent e = (HeartbeatEvent) event;
                resetIfNeeded(e.getValue());
            }
        }

        private void resetIfNeeded(Object value) {
            if (this.heartbeatMonitor.update(value)) {
                reset();
            }
        }

        private void reset() {
            this.zuulHandlerMapping.setDirty(true);
        }
    }
}

其中,由方法 onApplicationEvent可知,Zuul 會(huì)接收 4 種事件通知 ContextRefreshedEvent、RefreshScopeRefreshedEvent、RoutesRefreshedEventInstanceRegisteredEvent,這四種通知都會(huì)去刷新路由映射配置信息,此外,心跳續(xù)約監(jiān)視器 HeartbeatEvent 也會(huì)觸發(fā)這個(gè)動(dòng)作

ZuulHandlerMapping

ZuulServerAutoConfiguration#ZuulRefreshListener 中,注入了 ZuulHandlerMapping,此類是將本地配置的映射關(guān)系,映射到遠(yuǎn)程的過(guò)程控制器

/**
 * MVC HandlerMapping that maps incoming request paths to remote services.
 */
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {

    // 省略其他配置

    private volatile boolean dirty = true;

    public void setDirty(boolean dirty) {
        this.dirty = dirty;
        if (this.routeLocator instanceof RefreshableRouteLocator) {
            ((RefreshableRouteLocator) this.routeLocator).refresh();
        }
    }
}

dirty 屬性很重要,它是用來(lái)控制當(dāng)前是否需要重新加載映射配置信息的標(biāo)記,在 Zuul 每次進(jìn)行路由操作的時(shí)候都會(huì)檢查這個(gè)值。如果為 true,則會(huì)觸發(fā)配置信息的重新加載,同時(shí)再將其審核制為 false。由 setDirty 方法體可知,啟動(dòng)刷新動(dòng)作必須實(shí)現(xiàn) RefreshableRouteLocator,否則會(huì)出現(xiàn)類轉(zhuǎn)換異常。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容