Spring Cloud Gateway入坑記

Throwable發表於2019-05-04

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時的請求才能正常路由。

Cookie路由謂詞

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
複製程式碼

Header路由謂詞

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/{id} /actuator/gateway/routes/{id} GET 展示對應ID已經定義的路由配置
routes/{id} /actuator/gateway/routes/{id} POST 新增一個新的路由配置
routes/{id} /actuator/gateway/routes/{id} 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

原文連結

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

相關文章