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-Data
和Spring-Security
)和同步程式設計模式在Spring Cloud Gateway
中並不適用,所以最好先閱讀一下上面提到的三個框架的文件。
Spring Cloud Gateway
依賴於Spring Boot
和Spring WebFlux
提供的基於Netty
的執行時環境,它並非構建為一個WAR包或者執行在傳統的Servlet
容器中。
專有名詞
- 路由(Route):路由是閘道器的基本元件。它由ID,目標URI,謂詞(Predicate)集合和過濾器集合定義。如果謂詞聚合判斷為真,則匹配路由。
- 謂詞(Predicate):使用的是Java8中基於函數語言程式設計引入的java.util.Predicate。使用謂詞(聚合)判斷的時候,輸入的引數是
ServerWebExchange
型別,它允許開發者匹配來自HTTP請求的任意引數,例如HTTP請求頭、HTTP請求引數等等。 - 過濾器(Filter):使用的是指定的
GatewayFilter
工廠所建立出來的GatewayFilter
例項,可以在傳送請求到下游之前或者之後修改請求(引數)或者響應(引數)。
其實Filter
還包括了GlobalFilter
,不過在官方文件中沒有提到。
工作原理
客戶端向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.SR1
,Greenwich
可以在倫敦地鐵站的地圖查到這個站點,對應的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-WebFlux
的HandlerMapping
元件基礎設施的一部分,也就是HandlerMapping
進行匹配的時候,會把配置好的路由規則也納入匹配機制之中。Spring Cloud Gateway
自身包含了很多內建的路由謂詞工廠。這些謂詞分別匹配一個HTTP請求的不同屬性。多個路由謂詞工廠可以用and
的邏輯組合在一起。
目前Spring Cloud Gateway
提供的內建的路由謂詞工廠如下:
指定日期時間規則路由謂詞
按照配置的日期時間指定的路由謂詞有三種可選規則:
- 匹配請求在指定日期時間之前。
- 匹配請求在指定日期時間之後。
- 匹配請求在指定日期時間之間。
值得注意的是,配置的日期時間必須滿足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:Bar
,application.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過濾器鏈的底層。示意圖如下:
例如要實現負載均衡的功能,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是目前網路程式設計框架的主流方向,最好要跟上主流的步伐掌握這些框架的使用,有能力最好成為它們的貢獻者。
目前常見的反應式程式設計框架有:
- Reactor和
RxJava2
,其中Reactor
在後端的JVM應用比較常見,RxJava2
在安卓編寫的APP客戶端比較常見。 Reactor-Netty
,這個是基於Reactor
和Netty
封裝的。Spring-WebFlux
和Spring-Cloud-Gateway
,其中Spring-Cloud-Gateway
依賴Spring-WebFlux
,而Spring-WebFlux
底層依賴於Reactor-Netty
。
根據這個鏈式關係,最好系統學習一下Reactor
和Netty
。
參考資料:
附錄
選用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:www.throwable.club/2019/05/04/…
- Coding Page:throwable.coding.me/2019/05/04/…
(本文完 c-3-d e-a-20190504)