Spring Cloud Gateway入坑記


Spring Cloud Gateway入坑記

前提

最近在做老系統的重構,重構完成后新系統中需要引入一個網關服務,作為新系統和老系統接口的適配和代理。之前,很多網關應用使用的是Spring-Cloud-Netfilx基於Zuul1.x版本實現的那套方案,但是鑒於Zuul1.x已經停止迭代,它使用的是比較傳統的阻塞(B)IO + 多線程的實現方案,其實性能不太好。后來Spring團隊干脆自己重新研發了一套網關組件,這個就是本次要調研的Spring-Cloud-Gateway

簡介

Spring Cloud Gateway依賴於Spring Boot 2.0, Spring WebFlux,和Project Reactor。許多熟悉的同步類庫(例如Spring-DataSpring-Security)和同步編程模式在Spring Cloud Gateway中並不適用,所以最好先閱讀一下上面提到的三個框架的文檔。

Spring Cloud Gateway依賴於Spring BootSpring WebFlux提供的基於Netty的運行時環境,它並非構建為一個WAR包或者運行在傳統的Servlet容器中。

專有名詞

  • 路由(Route):路由是網關的基本組件。它由ID,目標URI,謂詞(Predicate)集合和過濾器集合定義。如果謂詞聚合判斷為真,則匹配路由。
  • 謂詞(Predicate):使用的是Java8中基於函數式編程引入的java.util.Predicate。使用謂詞(聚合)判斷的時候,輸入的參數是ServerWebExchange類型,它允許開發者匹配來自HTTP請求的任意參數,例如HTTP請求頭、HTTP請求參數等等。
  • 過濾器(Filter):使用的是指定的GatewayFilter工廠所創建出來的GatewayFilter實例,可以在發送請求到下游之前或者之后修改請求(參數)或者響應(參數)。

其實Filter還包括了GlobalFilter,不過在官方文檔中沒有提到。

工作原理

s-c-g-e-1.png

客戶端向Spring Cloud Gateway發出請求,如果Gateway Handler Mapping模塊處理當前請求如果匹配到一個目標路由配置,該請求就會轉發到Gateway Web Handler模塊。Gateway Web Handler模塊在發送請求的時候,會把該請求通過一個匹配於該請求的過濾器鏈。上圖中過濾器被虛線分隔的原因是:過濾器的處理邏輯可以在代理請求發送之前或者之后執行。所有pre類型的過濾器執行之后,代理請求才會創建(和發送),當代理請求創建(和發送)完成之后,所有的post類型的過濾器才會執行。

見上圖,外部請求進來后如果落入過濾器鏈,那么虛線左邊的就是pre類型的過濾器,請求先經過pre類型的過濾器,再發送到目標被代理的服務。目標被代理的服務響應請求,響應會再次經過濾器鏈,也就是走虛線右側的過濾器鏈,這些過濾器就是post類型的過濾器。

注意,如果在路由配置中沒有明確指定對應的路由端口,那么會使用如下的默認端口:

  • HTTP協議,使用80端口。
  • HTTPS協議,使用443端口。

引入依賴

建議直接通過Train版本(其實筆者考究過,Train版本的代號其實是倫敦地鐵站的命名,像當前的Spring Cloud最新版本是Greenwich.SR1Greenwich可以在倫敦地鐵站的地圖查到這個站點,對應的SpringBoot版本是2.1.x)引入Spring-Cloud-Gateway,因為這樣可以跟上最新穩定版本的Spring-Cloud版本,另外由於Spring-Cloud-Gateway基於Netty的運行時環境啟動,不需要引入帶Servlet容器的spring-boot-starter-web

父POM引入下面的配置:

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.4.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
</dependencyManagement>

子模塊或者需要引入Spring-Cloud-Gateway的模塊POM引入下面的配置:

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

創建一個啟動類即可:

@SpringBootApplication
public class RouteServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(RouteServerApplication.class, args);
	}
}

網關配置

網關配置最終需要轉化為一個RouteDefinition的集合,配置的定義接口如下:

public interface RouteDefinitionLocator {
	Flux<RouteDefinition> getRouteDefinitions();
}

通過YAML文件配置或者流式編程式配置(其實文檔中還有配合Eureka的DiscoveryClient進行配置,這里暫時不研究),最終都是為了創建一個RouteDefinition的集合。

Yaml配置

配置實現是PropertiesRouteDefinitionLocator,關聯着配置類GatewayProperties

spring:
  cloud:
    gateway:
      routes:
       - id: datetime_after_route    # <------ 這里是路由配置的ID
        uri: http://www.throwable.club  # <------ 這里是路由最終目標Server的URI(Host)
        predicates:                     # <------ 謂詞集合配置,多個是用and邏輯連接
         - Path=/blog    # <------- Key(name)=Expression,鍵是謂詞規則工廠的ID,值一般是匹配規則的正則表示

編程式流式配置

編程式和流式編程配置需要依賴RouteLocatorBuilder,目標是構造一個RouteLocator實例:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route(r -> r.path("/blog")
                .uri("http://www.throwable.club")
            )
            .build();
}

路由謂詞工廠

Spring Cloud Gateway將路由(Route)作為Spring-WebFluxHandlerMapping組件基礎設施的一部分,也就是HandlerMapping進行匹配的時候,會把配置好的路由規則也納入匹配機制之中。Spring Cloud Gateway自身包含了很多內建的路由謂詞工廠。這些謂詞分別匹配一個HTTP請求的不同屬性。多個路由謂詞工廠可以用and的邏輯組合在一起。

目前Spring Cloud Gateway提供的內置的路由謂詞工廠如下:

s-c-g-e-2.png

指定日期時間規則路由謂詞

按照配置的日期時間指定的路由謂詞有三種可選規則:

  • 匹配請求在指定日期時間之前。
  • 匹配請求在指定日期時間之后。
  • 匹配請求在指定日期時間之間。

值得注意的是,配置的日期時間必須滿足ZonedDateTime的格式:

//年月日和時分秒用'T'分隔,接着-07:00是和UTC相差的時間,最后的[America/Denver]是所在的時間地區
2017-01-20T17:42:47.789-07:00[America/Denver]

例如網關的應用是2019-05-01T00:00:00+08:00[Asia/Shanghai]上線的,上線之后的請求都路由奧www.throwable.club,那么配置如下:

server 
  port: 9090
spring:
  cloud:
    gateway:
      routes:
       - id: datetime_after_route
        uri: http://www.throwable.club
        predicates:
         - After=2019-05-01T00:00:00+08:00[Asia/Shanghai]

此時,只要請求網關http://localhost:9090,請求就會轉發到http://www.throwable.club

如果想要只允許2019-05-01T00:00:00+08:00[Asia/Shanghai]之前的請求,那么只需要改為:

server 
  port: 9091
spring:
  cloud:
    gateway:
      routes:
       - id: datetime_before_route
        uri: http://www.throwable.club
        predicates:
         - Before=2019-05-01T00:00:00+08:00[Asia/Shanghai]

如果只允許兩個日期時間段之間的時間進行請求,那么只需要改為:

server 
  port: 9090
spring:
  cloud:
    gateway:
      routes:
       - id: datetime_between_route
        uri: http://www.throwable.club
        predicates:
         - Between=2019-05-01T00:00:00+08:00[Asia/Shanghai],2019-05-02T00:00:00+08:00[Asia/Shanghai]

那么只有2019年5月1日0時到5月2日0時的請求才能正常路由。

CookieRoutePredicateFactory需要提供兩個參數,分別是Cookie的name和一個正則表達式(value)。只有在請求中的Cookie對應的name和value和Cookie路由謂詞中配置的值匹配的時候,才能匹配命中進行路由。

server 
  port: 9090
spring:
  cloud:
    gateway:
      routes:
       - id: cookie_route
        uri: http://www.throwable.club
        predicates:
         - Cookie=doge,throwable

請求需要攜帶一個Cookie,name為doge,value需要匹配正則表達式"throwable"才能路由到http://www.throwable.club

這里嘗試本地搭建一個訂單Order服務,基於SpringBoot2.1.4搭建,啟動在9091端口:

// 入口類
@RestController
@RequestMapping(path = "/order")
@SpringBootApplication
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

    @GetMapping(value = "/cookie")
    public ResponseEntity<String> cookie(@CookieValue(name = "doge") String doge) {
        return ResponseEntity.ok(doge);
    }
}

訂單服務application.yaml配置:

spring:
  application:
    name: order-service
server:
  port: 9091

網關路由配置:

spring:
  application:
    name: route-server
  cloud:
    gateway:
      routes:
        - id: cookie_route
          uri: http://localhost:9091
          predicates:
            - Cookie=doge,throwable
curl http://localhost:9090/order/cookie --cookie "doge=throwable"

//響應結果
throwable

HeaderRoutePredicateFactory需要提供兩個參數,分別是Header的name和一個正則表達式(value)。只有在請求中的Header對應的name和value和Header路由謂詞中配置的值匹配的時候,才能匹配命中進行路由。

訂單服務中新增一個/header端點:

@RestController
@RequestMapping(path = "/order")
@SpringBootApplication
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

    @GetMapping(value = "/header")
    public ResponseEntity<String> header(@RequestHeader(name = "accessToken") String accessToken) {
        return ResponseEntity.ok(accessToken);
    }
}

網關的路由配置如下:

spring:
  cloud:
    gateway:
      routes:
        - id: header_route
          uri: http://localhost:9091
          predicates:
            - Header=accessToken,Doge
curl -H "accessToken:Doge" http://localhost:9090/order/header

//響應結果
Doge

Host路由謂詞

HostRoutePredicateFactory只需要指定一個主機名列表,列表中的每個元素支持Ant命名樣式,使用.作為分隔符,多個元素之間使用,區分。Host路由謂詞實際上針對的是HTTP請求頭中的Host屬性。

訂單服務中新增一個/header端點:

@RestController
@RequestMapping(path = "/order")
@SpringBootApplication
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

    @GetMapping(value = "/host")
    public ResponseEntity<String> host(@RequestHeader(name = "Host") String host) {
        return ResponseEntity.ok(host);
    }
}

網關的路由配置如下:

spring:
  cloud:
    gateway:
      routes:
        - id: host_route
          uri: http://localhost:9091
          predicates:
            - Host=localhost:9090
curl http://localhost:9090/order/host

//響應結果
localhost:9091  # <--------- 這里要注意一下,路由到訂單服務的時候,Host會被修改為localhost:9091

其實可以定制更多樣化的Host匹配模式,甚至可以支持URI模板變量。

- Host=www.throwable.**,**.throwable.**

- Host={sub}.throwable.club

請求方法路由謂詞

MethodRoutePredicateFactory只需要一個參數:要匹配的HTTP請求方法。

網關的路由配置如下:

spring:
  cloud:
    gateway:
      routes:
        - id: method_route
          uri: http://localhost:9091
          predicates:
            - Method=GET

這樣配置,所有的進入到網關的GET方法的請求都會路由到http://localhost:9091

訂單服務中新增一個/get端點:

@GetMapping(value = "/get")
public ResponseEntity<String> get() {
    return ResponseEntity.ok("get");
}
curl http://localhost:9090/order/get

//響應結果
get 

請求路徑路由謂詞

PathRoutePredicateFactory需要PathMatcher模式路徑列表和一個可選的標志位參數matchOptionalTrailingSeparator。這個是最常用的一個路由謂詞。

spring:
  cloud:
    gateway:
      routes:
        - id: path_route
          uri: http://localhost:9091
          predicates:
            - Path=/order/path
@GetMapping(value = "/path")
public ResponseEntity<String> path() {
    return ResponseEntity.ok("path");
}
curl http://localhost:9090/order/path

//響應結果
path 

此外,可以通過{segment}占位符配置路徑如/foo/1/foo/bar/bar/baz,如果通過這種形式配置,在匹配命中進行路由的時候,會提取路徑中對應的內容並且將鍵值對放在ServerWebExchange.getAttributes()集合中,KEY為ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE,這些提取出來的屬性可以供GatewayFilter Factories使用。

請求查詢參數路由謂詞

QueryRoutePredicateFactory需要一個必須的請求查詢參數(param的name)以及一個可選的正則表達式(regexp)。

spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: http://localhost:9091
        predicates:
        - Query=doge,throwabl.

這里配置的param就是doge,正則表達式是throwabl.

@GetMapping(value = "/query")
public ResponseEntity<String> query(@RequestParam("name") String doge) {
  return ResponseEntity.ok(doge);
}
curl http://localhost:9090/order/query?doge=throwable

//響應結果
throwable 

遠程IP地址路由謂詞

RemoteAddrRoutePredicateFactory匹配規則采用CIDR符號(IPv4或IPv6)字符串的列表(最小值為1),例如192.168.0.1/16(其中192.168.0.1是遠程IP地址並且16是子網掩碼)。

spring:
  cloud:
    gateway:
      routes:
      - id: remoteaddr_route
        uri: http://localhost:9091
        predicates:
        - RemoteAddr=127.0.0.1
@GetMapping(value = "/remote")
public ResponseEntity<String> remote() {
  return ResponseEntity.ok("remote");
}
curl http://localhost:9090/order/remote

//響應結果
remote 

關於遠程IP路由這一個路由謂詞其實還有很多擴展手段,這里暫時不展開。

多個路由謂詞組合

因為路由配置中的predicates屬性其實是一個列表,可以直接添加多個路由規則:

spring:
  cloud:
    gateway:
      routes:
      - id: remoteaddr_route
        uri: http://localhost:9091
        predicates:
        - RemoteAddr=xxxx
        - Path=/yyyy
        - Query=zzzz,aaaa

這些規則是用and邏輯組合的,例如上面的例子相當於:

request = ...
if(request.getRemoteAddr == 'xxxx' && request.getPath match '/yyyy' && request.getQuery('zzzz') match 'aaaa') {
    return true;
}
return false;

GatewayFilter工廠

路由過濾器GatewayFilter允許修改進來的HTTP請求內容或者返回的HTTP響應內容。路由過濾器的作用域是一個具體的路由配置。Spring Cloud Gateway提供了豐富的內建的GatewayFilter工廠,可以按需選用。

因為GatewayFilter工廠類實在太多,筆者這里舉個簡單的例子。

如果我們想對某些請求附加特殊的HTTP請求頭,可以選用AddRequestHeaderX-Request-Foo:Barapplication.yml如下:

spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: https://example.org
        filters:
        - AddRequestHeader=X-Request-Foo,Bar

那么所有的從網關入口的HTTP請求都會添加一個特殊的HTTP請求頭:X-Request-Foo:Bar

目前GatewayFilter工廠的內建實現如下:

ID 類名 類型 功能
StripPrefix StripPrefixGatewayFilterFactory pre 移除請求URL路徑的第一部分,例如原始請求路徑是/order/query,處理后是/query
SetStatus SetStatusGatewayFilterFactory post 設置請求響應的狀態碼,會從org.springframework.http.HttpStatus中解析
SetResponseHeader SetResponseHeaderGatewayFilterFactory post 設置(添加)請求響應的響應頭
SetRequestHeader SetRequestHeaderGatewayFilterFactory pre 設置(添加)請求頭
SetPath SetPathGatewayFilterFactory pre 設置(覆蓋)請求路徑
SecureHeader SecureHeadersGatewayFilterFactory pre 設置安全相關的請求頭,見SecureHeadersProperties
SaveSession SaveSessionGatewayFilterFactory pre 保存WebSession
RewriteResponseHeader RewriteResponseHeaderGatewayFilterFactory post 重新響應頭
RewritePath RewritePathGatewayFilterFactory pre 重寫請求路徑
Retry RetryGatewayFilterFactory pre 基於條件對請求進行重試
RequestSize RequestSizeGatewayFilterFactory pre 限制請求的大小,單位是byte,超過設定值返回413 Payload Too Large
RequestRateLimiter RequestRateLimiterGatewayFilterFactory pre 限流
RequestHeaderToRequestUri RequestHeaderToRequestUriGatewayFilterFactory pre 通過請求頭的值改變請求URL
RemoveResponseHeader RemoveResponseHeaderGatewayFilterFactory post 移除配置的響應頭
RemoveRequestHeader RemoveRequestHeaderGatewayFilterFactory pre 移除配置的請求頭
RedirectTo RedirectToGatewayFilterFactory pre 重定向,需要指定HTTP狀態碼和重定向URL
PreserveHostHeader PreserveHostHeaderGatewayFilterFactory pre 設置請求攜帶的屬性preserveHostHeader為true
PrefixPath PrefixPathGatewayFilterFactory pre 請求路徑添加前置路徑
Hystrix HystrixGatewayFilterFactory pre 整合Hystrix
FallbackHeaders FallbackHeadersGatewayFilterFactory pre Hystrix執行如果命中降級邏輯允許通過請求頭攜帶異常明細信息
AddResponseHeader AddResponseHeaderGatewayFilterFactory post 添加響應頭
AddRequestParameter AddRequestParameterGatewayFilterFactory pre 添加請求參數,僅僅限於URL的Query參數
AddRequestHeader AddRequestHeaderGatewayFilterFactory pre 添加請求頭

GatewayFilter工廠使用的時候需要知道其ID以及配置方式,配置方式可以看對應工廠類的公有靜態內部類XXXXConfig

GlobalFilter工廠

GlobalFilter的功能其實和GatewayFilter是相同的,只是GlobalFilter的作用域是所有的路由配置,而不是綁定在指定的路由配置上。多個GlobalFilter可以通過@Order或者getOrder()方法指定每個GlobalFilter的執行順序,order值越小,GlobalFilter執行的優先級越高。

注意,由於過濾器有pre和post兩種類型,pre類型過濾器如果order值越小,那么它就應該在pre過濾器鏈的頂層,post類型過濾器如果order值越小,那么它就應該在pre過濾器鏈的底層。示意圖如下:

s-c-g-e-3.png

例如要實現負載均衡的功能,application.yml配置如下:

spring:
  cloud:
    gateway:
      routes:
      - id: myRoute
        uri: lb://myservice   # <-------- lb特殊標記會使用LoadBalancerClient搜索目標服務進行負載均衡
        predicates:
        - Path=/service/**

目前Spring Cloud Gateway提供的內建的GlobalFilter如下:

類名 功能
ForwardRoutingFilter 重定向
LoadBalancerClientFilter 負載均衡
NettyRoutingFilter Netty的HTTP客戶端的路由
NettyWriteResponseFilter Netty響應進行寫操作
RouteToRequestUrlFilter 基於路由配置更新URL
WebsocketRoutingFilter Websocket請求轉發到下游

內建的GlobalFilter大多數和ServerWebExchangeUtils的屬性相關,這里就不深入展開。

跨域配置

網關可以通過配置來控制全局的CORS行為。全局的CORS配置對應的類是CorsConfiguration,這個配置是一個URL模式的映射。例如application.yaml文件如下:

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "https://docs.spring.io"
            allowedMethods:
            - GET

在上面的示例中,對於所有請求的路徑,將允許來自docs.spring.io並且是GET方法的CORS請求。

Actuator端點相關

引入spring-boot-starter-actuator,需要做以下配置開啟gateway監控端點:

management.endpoint.gateway.enabled=true 
management.endpoints.web.exposure.include=gateway

目前支持的端點列表:

ID 請求路徑 HTTP方法 描述
globalfilters /actuator/gateway/globalfilters GET 展示路由配置中的GlobalFilter列表
routefilters /actuator/gateway/routefilters GET 展示綁定到對應路由配置的GatewayFilter列表
refresh /actuator/gateway/refresh POST 清空路由配置緩存
routes /actuator/gateway/routes GET 展示已經定義的路由配置列表
routes/ /actuator/gateway/routes/ GET 展示對應ID已經定義的路由配置
routes/ /actuator/gateway/routes/ POST 添加一個新的路由配置
routes/ /actuator/gateway/routes/ DELETE 刪除指定ID的路由配置

其中/actuator/gateway/routes/{id}添加一個新的路由配置請求參數的格式如下:

{
  "id": "first_route",
  "predicates": [{
    "name": "Path",
    "args": {"doge":"/throwable"}
  }],
  "filters": [],
  "uri": "https://www.throwable.club",
  "order": 0
}

小結

筆者雖然是一個底層的碼畜,但是很久之前就向身邊的朋友說:

反應式編程結合同步非阻塞IO或者異步非阻塞IO是目前網絡編程框架的主流方向,最好要跟上主流的步伐掌握這些框架的使用,有能力最好成為它們的貢獻者。

目前常見的反應式編程框架有:

  • ReactorRxJava2,其中Reactor在后端的JVM應用比較常見,RxJava2在安卓編寫的APP客戶端比較常見。
  • Reactor-Netty,這個是基於ReactorNetty封裝的。
  • Spring-WebFluxSpring-Cloud-Gateway,其中Spring-Cloud-Gateway依賴Spring-WebFlux,而Spring-WebFlux底層依賴於Reactor-Netty

根據這個鏈式關系,最好系統學習一下ReactorNetty

參考資料:

附錄

選用Spring-Cloud-Gateway不僅僅是為了使用新的技術,更重要的是它的性能有了不俗的提升,基准測試項目spring-cloud-gateway-bench的結果如下:

代理組件(Proxy) 平均交互延遲(Avg Latency) 平均每秒處理的請求數(Avg Requests/Sec)
Spring Cloud Gateway 6.61ms 32213.38
Linkered 7.62ms 28050.76
Zuul(1.x) 12.56ms 20800.13
None(直接調用) 2.09ms 116841.15

原文鏈接

  • Github Page:http://www.throwable.club/2019/05/04/spring-cloud-gateway-guide
  • Coding Page:http://throwable.coding.me/2019/05/04/spring-cloud-gateway-guide

(本文完 c-3-d e-a-20190504)

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM