基於Spring Cloud Gateway的路由實踐

基本介紹

Spring Cloud Gateway(下文以SCG代替), 顧名思義這是由Spring 官方出品的一款網關產品,是Spring Cloud的子項目。

This project provides a library for building an API Gateway on top of Spring MVC. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.

官方介紹主要突出了路由功能的簡單有效,同時可以在安全、監控以及擴展性方面提供不錯的支持,畢竟靠着Spring Cloud這棵大樹。

架構理解

基於Spring Cloud Gateway的路由實踐
這是官方網站的工作原理示意圖,從上圖可以看出SCG在整個流程中主要擔任反向代理的角色。客戶端請求抵達SCG後,SCG通過Handler Mapping將請求路由到Web Handler,Web Handler再通過Filter對原始請求進行處理,最終發送到被代理的服務端。

技術對比

在研究SCG之前,我們發現Spring Cloud下面已經有一個成熟的API套件Spring Cloud Netflix,提供了服務註冊發現(Eureka),熔斷器(Hystrix),智能路由(Zuul)和客戶端負載均衡(Ribbon)等特性,其中就有我們需要的路由功能Zuul。
那爲什麼在集成一個路由功能後,Spring Cloud還要自己開發一個用於路由的Gateway項目呢?我們來看看他們的一些對比,由於Spring Cloud只集成了Zuul1.0,所以比較也集中在Zuul1.0和SCG之間。

連接方式 支持服務器 功能
Zuul1.0 Servlet API Tomcat,undertow 基本路由規則,僅支持Path的路由
SCG Reactor Netty 較多路由規則,可以支持header,cookie,query,method等豐富的predict定義

從上面的對比來看,SCG基於Project Reactor可以獲得更優秀的吞吐,在功能方面相當於Zuul的優化,更加靈活的配置可以滿足幾乎所有的網關路由需求。
雖然說Zuul2.0也是基於Netty開發,並增強了路由和過濾器功能,然而他的多次跳票最終讓Spring下決心自己做一款網關路由產品,並表示不會將Zuul2.0集成進以後的Spring Cloud中,也算一段趣聞吧。

網關實踐

下面我們實際動手實現一個網關,結合過程中遇到的問題來熟悉SCG的各項特性。

初始化

我們新建一個基於Spring Boot的Maven項目,添加SCG的依賴,主要是下面兩個

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

這裏選擇的是最新的Spring Boot Release版本(2.1.4)以及支持2.1的Spring Cloud分支Greenwich。

最後建一個SpringBootApplication,參照首頁的Demo去掉Hystrix和RateLimit相關的內容就可以跑起來了(https://spring.io/projects/spring-cloud-gateway)。

動態路由

光有個Demo肯定不行,我們的網關是要實際投產使用的,在分析了實際需求之後我們發現急需的第一個功能是動態路由。
在文檔中提供了兩種方式的路由配置方式

  1. 通過java API
    直接通過RouteLocatorBuilder構建如下:

    builder.routes().route("path_route", 
        r -> r.path("/get").uri("http://httpbin.org")).build();
  2. 通過配置文件
    通過YAML文件構建路由如下:

        spring:
                cloud:
                    gateway:
                        routes:
                        - id: host_route
                            uri: https://example.org
                            predicates:
                            - Path=/foo/{segment},/bar/{segment}

但是實際需求中存在動態分配路由的場景,以上兩種方式顯然都不能滿足需求。

通過查看源代碼發現SCG加載路由是通過RouteDefinitionLocator接口實現,有以下默認實現(框掉的部分可以暫時忽略,這是我們自己的實現):

基於Spring Cloud Gateway的路由實踐

在GatewayAutoConfiguration中通過Primary的方式指定CompositeRouteDefinitionLocator作爲路由定義加載的入口,通過組合模式將所有的RouteDefinitionLocator代理。最終通過CompositeRouteDefinitionLocator的getRouteDefinitions方法將所有定義加載出來。

  @Bean
    @Primary
    public RouteDefinitionLocator routeDefinitionLocator(
            List<RouteDefinitionLocator> routeDefinitionLocators) {
        return new CompositeRouteDefinitionLocator(
                Flux.fromIterable(routeDefinitionLocators));
    }
public class CompositeRouteDefinitionLocator implements RouteDefinitionLocator {

    private final Flux<RouteDefinitionLocator> delegates;

    public CompositeRouteDefinitionLocator(Flux<RouteDefinitionLocator> delegates) {
        this.delegates = delegates;
    }

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        return this.delegates.flatMap(RouteDefinitionLocator::getRouteDefinitions);
    }

}

通過源代碼的解讀,我們發現如果需要定義新的路由加載方式,只需要增加一個RouteDefinitionLocator的實現即可,在實際操作中爲了方便路由更新我們仿照已有的實現
InMemoryRouteDefinitionRepository進行實現,類圖如下:
基於Spring Cloud Gateway的路由實踐

我們通過新增了一個抽象類類完成RouteDefinitionRepository的擴展,在抽象類裏我們實現了基本的get, save, delete方法,另外新增了refresh方法用於刷新緩存,而緩存的實現參考了InMemory的實現方式。
在需要進行擴展的時候我們可以通過繼承AbstractRoutConfigure來增加我們自己的configure loader,再通過Configuration方式注入即可:
基於Spring Cloud Gateway的路由實踐

最終的實現效果是我們通過數據庫變更配置後,通過restful接口來調用refresh方法即可完成路由的動態刷新。

服務路由

通過上面動態路由的基本實現,我們數據庫中的配置是這樣的
基於Spring Cloud Gateway的路由實踐
但是我們是要做微服務和集羣的網關,直接寫地址顯然是不行的。
針對這種情況,SCG提供了一種URI的格式:lb://main-service,其中main-service是我們微服務在註冊中心的name。
當URI以lb開頭,則在進行URI解析的時候會去尋找zookeeper,consul,eureka 對應的客戶端實現。我們使用的是eureka,並且在數據庫中加上以下配置
基於Spring Cloud Gateway的路由實踐
這樣我們就可以成功代理微服務提供的接口了。

容錯管理

容錯管理從以下兩方面進行考慮
1, 路由未定義
針對路由未找到的情況,提供有意義的報錯信息進行有效反饋。
實現層面主要通過定義一個NotFound的路由,通過設置order確保NotFound路由在所有的路由之後執行,這樣當所有的路由都沒有匹配上的時候就會被路由到NotFound路由,從而反饋有意義的報錯信息。
數據庫定義,
基於Spring Cloud Gateway的路由實踐
代碼部分:

/**未找到路由的時候提示錯誤信息*/
    @RequestMapping(value = "/notfoundcontroller")
    public Mono<Map<String, String>> notFoundController() {
        Map<String, String> res = new HashMap<>();
        res.put("code", "-404");
        res.put("data", "route definition not found");
        return Mono.just(res);
    }

2, 熔斷器Hystrix
熔斷器主要應用於請求超時,服務端錯誤等使用場景,SCG提供了Hystrix的集成,我們只需要在YAML配置文件裏面配置default filter並加入fallbackUri的實現即可。

YAML

spring: 
  cloud:
    gateway:
      default-filters:
      - name: Hystrix
        args:
          name: fallbackcmd
          fallbackUri: forward:/fallbackcontroller

fallbackUri

/**斷路器對應的服務降級地址,對於請求失敗進行處理*/
    @RequestMapping(value = "/fallbackcontroller")
    public Mono<Map<String, String>> fallBackController() {
        Map<String, String> res = new HashMap<>();
        res.put("code", "-100");
        res.put("data", "service not available");
        return Mono.just(res);
    }

通過上面兩點的配置,我們在請求出錯如超時、服務宕機的情況都可以得到對應的錯誤信息,確保了網關服務的魯棒性。

限流機制

SCG使用的限流機制(Rate Limiter)基於令牌桶算法,我們先大致瞭解一下令牌桶算法。
基於Spring Cloud Gateway的路由實踐

從上圖可以看出,令牌桶算法的主要數據結構是個緩衝區。通過勻速生成的令牌來填充緩衝區相當於生產者,而實際流量則相當於消費者來消費緩衝區中的令牌。

我們再結合SCG中的實現來看看令牌桶算法如何限流的。
SCG使用RateLimiter需要引入spring-boot-starter-data-redis-reactive,所以SCG的令牌桶實現是基於Redis的,這樣可以滿足分佈式的要求。
SCG在使用過程中需要設置三個參數replenishRate ,burstCapacity和KeyResolver。
 replenishRate表示的是裝桶的速率,也就是令牌生成的速率;
 burstCapacity表示瞬間高爆發的容量,官方文檔解釋是一秒內允許的最大流量又補充了一句是令牌桶可以裝下的令牌數。
 KeyResolver很好理解,通過key的定義可以明確規定限流的層級,用戶級還是IP級別等等。
對於burstCapacity的理解,只有當replenishRate和burstCapacity相等時也就是請求處理基本是勻速的情況下,burstCapacity才表示一秒內允許的最大流量,否則解釋爲令牌桶的容量更加貼切。

代碼實現主要通過RedisRateLimiter.class和request_rate_limiter.lua兩個文件,而主要邏輯是通過腳本文件實現。

基於Spring Cloud Gateway的路由實踐
這裏主要獲取java傳過來的參數,計算出ttl,ttl的邏輯是桶裝滿所需時間的兩倍。

基於Spring Cloud Gateway的路由實踐
上面這段代碼是實現限流的關鍵,每次都會通過當前時間和上次刷新時間的間隔計算填充的令牌,只有填充後的令牌 >= 請求的令牌數才符合條件允許令牌獲取。
當新的請求獲取令牌後,更新令牌桶的令牌數和最後刷新時間。

在實際引用中我們根據我們服務器的壓力來設定rate和capacity,通過不停的調節來尋求吞吐和負載的平衡。
基於Spring Cloud Gateway的路由實踐

日誌配置

日誌配置方面除了基本的logback配置,需要加入access_log的配置,根據官方文檔我們需要在logback配置文件中加入logger和appender的配置。

<appender name="accessLog" class="ch.qos.logback.core.FileAppender">
        <file>access_log.log</file>
        <encoder>
            <pattern>%msg%n</pattern>
        </encoder>
    </appender>

    <appender name="async" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="accessLog" />
    </appender>

    <logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false">
        <appender-ref ref="async"/>
    </logger>

如上所示,通過定義logger接收netty的AccessLog,通過異步發射器發送到accessLog Appender。

這裏需要注意的是Netty AccessLog的配置要到reactor-netty0.7.9之後才支持,所以在使用這個功能之前需要確保我們netty的版本滿足要求,項目目前使用的spring版本如下,對應的reactor-netty版本爲0.8.6。
基於Spring Cloud Gateway的路由實踐

配置了這麼多,然而access.log文件還是空空如也,因爲你漏掉了很重要的一步:
在啓動參數中添加 -Dreactor.netty.http.server.accessLogEnabled=true. 注意這個屬性是java系統屬性而不是spring配置屬性,也就是說只能通過啓動參數注入。

小結

我們通過一些簡單的介紹瞭解了SCG的出現背景,然後通過實際的網關搭建實踐來一步步的理解SCG的架構理念和實現細節。
通過動態路由部分我們見證了SCG的可擴展性架構,在服務路由和容錯管理部分我們主要和Spring Cloud已有組件(eureka, hystrix)進行集成,而在限流機制部分我們通過閱讀源代碼理解了基於令牌桶的限流算法以及如何結合Redis實現分佈式系統限流,在日誌配置部分主要是結合Netty的日誌機制來完成網關的訪問日誌配置。
在我們的實踐中我們沒有用上SCG的所有特性,但是就目前的情況用於我們自己的API 網關已經夠用。

周邊花絮

HikariDataSource

在啓動spring boot程序的時候發現了下面兩句話

2019-05-20 13:56:32,381 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2019-05-20 13:56:32,875 [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.

於是懷着好奇心去看了看這個HikariDataSource是何方神聖。
不看不知道一看嚇一跳,這個從來沒聽過的東西居然是spring boot默認的連接池,作爲連接池他的性能居然能超過druid。限於篇幅,我們會另開一篇來研究一下。

如何處理多個locator

在前面的源碼分析中我們看到,RouteDefinitionLocator是採用的代理模式,通過一個組合器將所有代理locator中定義的route加載出來,核心代碼:
基於Spring Cloud Gateway的路由實踐
所以當我們定義了多個locator的時候(如MySql的locator, Json的locator),SCG是如何對這些locator進行merge的呢?
通過GatewayAutoConfiguration.class 我們發現定義爲Primary的RouteLocator是CachingRouteLocator。
基於Spring Cloud Gateway的路由實踐
而在CachingRouteLocator中通過裝飾者模式對所有locator獲得的route進行了排序,排序的依據是order字段。
基於Spring Cloud Gateway的路由實踐

綜上所述,如果定義了多個routeDefinitionLocator,則對於裏面的route會根據order進行排序,如果order未定義則按照默認order爲0處理。
排序完成後按照先後順序逐個匹配請求,如果滿足則不繼續匹配,也就是說全局來說定義的order越小則優先級越高,不管出自哪個locator。

Hystrix circuit short-circuited and is OPEN錯誤

這個錯誤是配置了斷路器之後出現的,當配置的fallbackuri沒有定義或者無法匹配的時候會出現。我們實踐中的起因是配置了fallbackuri的method爲GET,而實際引起錯誤的請求是通過POST發送過來的。

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