1. Gateway概述
1.1 Gateway是什麼
服務網關還可以用Zuul網關,但是Zuul網關由于一些維護問題,是以這裡我們學習Gateway網關,SpringCloud全家桶裡有個很重要的元件就是網關, 在1.x的版本中都是采用Zuul網關;但在2.x版本中,Zuul的更新一直跳票,SpringCloud最後自己研發了一個網關代替Zuul,也就是說SpringCloud Gateway是原Zuul1.x版的替代品。
SpringCloud Gateway是在Spring生态系統之上建構的API網關服務,基于Spring5,SpringBoot2和Project Reactor等技術。
Gateway旨在提供一種簡單而有效的方式來對API進行路由,以及提供一些強大的過濾功能,例如熔斷、限流、重試等。
SpringCloud Gateway作為SpringCloud生态系統中的網關,目的是替代Zuul,在SpringCloud2.0以上版本中,沒有對新版本的Zuul2.0以上最新高新能版本進行內建,仍然使用的是Zuul 1.x非Reactor模式的老版本。而為了提升網關的性能,SpringCloud Gateway是基于WebFlux架構實作的,而WebFlux架構底層則使用了高性能的Reactor模式通信架構Netty。
SpringCloud Gateway的目标是提供統一的路由方式且基于Filter鍊的方式提供了網關基本的功能,例如:安全、監控/名額、限流等。
一句話:Spring Cloud Gateway使用的Webflux中的reactor-netty響應式程式設計元件,底層使用了Netty通訊架構
源碼架構:
1.2 整個微服務架構中網關的位置
可以看到網關是所有微服務的入口
1.3 Gateway能做什麼
反向代理、鑒權、流量控制、熔斷、日志監控等。
SpringCloud Gateway具有如下特性:
- 基于Spring Framework 5, Project Reactor和SpringBoot 2.x進行建構
- 動态路由:能夠比對任何請求屬性
- 可以對路由指定Predicate(斷言)和Filter(過濾器)
- 內建Hystrix的熔斷器功能
- 內建SpringCloud服務發現功能
- 請求限流功能
- 支援路徑重寫等待
1.4 Gateway與Zuul的差別
在SpringCloud Finchley正式版以前,SpringCloud推薦的網關是Netflix提供的Zuul:
- Zuul 1.x 是一個基于阻塞I/O的API Gateway
- Zuul 1.x 基于Servlet 2.5使用阻塞架構,它不支援任何長連接配接(如WebSocket),Zuul的設計模式和Nginx比較像,每次I/O操作都是從工作線程中選擇一個執行,請求線程被阻塞到工作線程完成,但是差别是Nginx是用C++實作的,Zuul用Java實作,而JVM本身會有第一次加載較慢的情況,使得Zuul的性能相對較差
- Zuul 2.x 理念更先進,想基于Netty非阻塞和支援長連接配接,但是SpringCloud目前還沒有整合。Zuul 2.x 的性能較Zull 1.x有很大提升。根據官方提供的基準測試,SpringCloud Gateway的RPS(每秒請求數)是Zuul的1.6倍
- SpringCloud Gateway基于Spring Framework 5, Project Reactor和SpringBoot 2.x進行建構,使用非阻塞API,還支援WebSocket,并且與Spring Cloud緊密內建有更好的開發體驗
1.5 模型對比
Zuul1.x模型:
SpringCloud中所內建的Zuul版本,采用的是Tomcat容器,使用的是傳統的Servlet IO處理模型.
Servlet的聲明周期?=> Servlet由Servlet Container進行生命周期管理,Container啟動時構造Servlet對象并調用Servlet init() 進行初始化,Container運作時接受請求,并為每一個請求配置設定一個線程(一般從線程池中擷取空閑線程)然後調用Service()方法,Container關閉時嗲用Servlet destory() 銷毀Servlet。
上述模式的缺點:Servlet是一個簡單的網絡IO模型,當請求進入Servlet Container時,Servlet Container就會為其綁定一個線程,在 并發不高的場景下 這種模型是适合的,但是一旦高并發(比如使用jemeter加壓),線程數量就會上漲,而線程資源代價是昂貴的(上下文切換,記憶體消耗大)嚴重影響請求的處理時間。在一些簡單業務場景下,不希望為每個request配置設定一個線程,隻需要1個或幾個線程就能應對極大并發的請求,這種業務場景下Servlet模型沒有優勢。
是以Zuul 1.x 是 基于Servlet之上的一個阻塞式處理模型,即Spring實作了處理所有request請求的一個Servlet(DispatcherServlet)并由該Servlet阻塞式處理處理。是以SpringCloud Zuul無法擺脫Servlet模型的弊端。
Gateway模型:
傳統的Web架構,如struts2,SpringMVC等都是基于Servlet API與Servlet容器基礎之上運作的。
但是 在Servlet3.1之後又了異步非阻塞的支援,而WebFlux是一個典型非阻塞異步的架構,它的核心是基于Reactor的相關API實作的。相對于傳統的Web架構來說,它可以運作在諸如Netty,Undertow及支援Servlet3.1的容器上。非阻塞+函數式程式設計(Spring5必須使用Java8)。
Spring WebFlux是Spring 5.0引入的新的響應式架構,差別于Spring MVC,它不需要依賴于Servlet API,它是完全異步非阻塞的,并且基于Reactor來實作響應式流規範。
2. Gateway核心概念
-
路由是建構網關的基本子產品,它由ID、目标URI、一系列的斷言和過濾器組成,如果斷言為true則比對路由。路由 Route
-
參考的是Java8的斷言 Predicate
,開發人員可以比對HTTP請求中的所有内容(例如請求頭或請求參數), 如果請求與斷言比對則進行路由。java.util.function.Predicate
-
Spring架構中GatewayFilter的執行個體,使用過濾器,可以在請求被路由之前或者之後對請求進行修改。客戶當發送Web請求,通過一些比對條件,定位到真正的服務節點,并在這個轉發過程的前後,進行一些精細化的控制,而斷言就是這些比對條件,過濾器可以了解為一個無所不能的攔截器,用來實作這些精細化的控制,有了斷言和過濾器,再加上目标的URI,就可以實作一個具體的路由。過濾器 Filter
3. 網關工作流程
用戶端向Spring Cloud Gateway送出請求。如果 網關處理程式映射(Gateway Handler Mapping)确定請求與路由比對,則将其發送到 網關Web處理程式(Gateway Web Handler)。該處理程式通過特定于請求的
過濾器鍊
來運作請求。
過濾器器由虛線分隔的原因是,過濾器可以在發送代理請求
之前
和
之後
運作邏輯。所有“前置”過濾器邏輯均被執行。然後發出代理請求。發出代理請求後,将運作“後置”過濾器邏輯。圖中虛線左邊的對應于前置過濾器,虛線右邊的對應于
後置過濾器
。
前置過濾器可以做參數校驗、權限校驗、流量監控、日志輸出、協定轉換等
後置過濾器可以做響應内容、響應頭的修改、日志的輸出、流量監控等
SpringCloud Gateway的核心邏輯其實就是 路由轉發和執行過濾器鍊
4. 配置案例(兩種方式)
yml配置檔案實作SpringCloud Gateway的路由配置
建立Module:cloud-gateway-gateway9527作為網關微服務,在網關子產品的POM檔案在服務注冊中心必要的依賴以外還需要 引入SpringCloud Gateway的依賴(版本号已經在父工程中統一配置),需要注意的是,網關微服務 不需要引入Web啟動器(gateway做的是網關不需要web):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
老規矩編寫其配置類application.yml,以9527端口,服務名為cloud-gateway将自己注冊進Eureka服務注冊中心
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
網關嘛,就是擋在微服務之前看大門的,是以先不編寫其業務類。
編寫網關的主啟動類:
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class);
}
}
設定9527網關要對服務提供方提供的8001端口微服務進行路由處理,我們不想暴露8001端口,希望在8001端口外面套上一層網關的9527端口,在8001微服務中有如下的兩個服務:
@GetMapping("/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Integer id);
@GetMapping("/payment/lb")
public String getPaymentLB();
為了配置對上面兩個服務的路由,我們修改9527網關微服務的配置檔案application.yml,修改後的配置檔案如下:
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh # payment_route # 路由的ID,沒有固定規則但要求唯一,建議配合服務名
uri: http://localhost:8001 # 比對後提供服務的路由位址
predicates:
- Path=/payment/get/** # 斷言,路徑相比對的進行路由
- id: payment_routh2 # payment_route # 路由的ID,沒有固定規則但要求唯一,建議配合服務名
uri: http://localhost:8001 # 比對後提供服務的路由位址
predicates:
- Path=/payment/lb/** # 斷言,路徑相比對的進行路由
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
在路由routes中我們配置了兩個路由,一個是針對8001的getPaymentById服務,另一個是針對8001的getPaymentLB服務,然後我們分别啟動Eureka服務注冊中心、服務提供方8001微服務、9527網關微服務。我們發現,在添加網關前,我們需要通過http://localhost:8001/payment/get/1 來通路服務提供方的服務,但是現在不僅通路提供服務方可以通路其服務,我們通過通路網關也可以通路到服務提供方的微服務:
這樣的話用網關對微服務進行路由通路,就可以不再對外暴露微服務的真實位址,而是統一暴露為網關的位址。
SpringCloud Gateway的網關路由有兩種配置方式,一種就是上面通過配置檔案application.yml進行網關路由配置,還可以 在代碼中注入RouteLocator的Bean進行配置,下面實作用編碼的方式,實作通過9527網關對百度新聞的通路。
非配置檔案,編碼方式實作SpringCloud Gateway的路由配置
案例:通過9527網關通路到外網的百度新聞位址
在9527網關微服務中編寫如下的配置類:
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route",
r -> r.path("/guonei")
.uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
}
上面的配置類注入
RouteLocator
的Bean,其配置的一個id為"path_route"的路由,當通路位址http://localhost:9527/guonei 時,該路由會将通路自動轉發到http://news.baidu.com/guonei
5. 通過微服務名實作動态路由
在上面的yml配置檔案中,我們把路由位址寫死了,這明顯應該是程式中避免的。是以更好的方式是通過微服務名來實作動态路由。
預設情況下 Gateway會根據注冊中心注冊的服務清單,以注冊中心上微服務名為路徑建立動态路由進行轉發,進而實作動态路由的功能。為了示範SpringCloud Gateway實作的動态路由,我們啟用服務提供方叢集,使8001/8002微服務都啟動,讓
CLOUD-PAYMENT-SERVICE
服務對應于兩個具體服務執行個體。
為了實作網關根據微服務名對服務進行動态路由,需要在網關微服務配置檔案中開啟從注冊中心動态建立路由的功能,添加如下配置:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 開啟從注冊中心動态建立路由的功能,利用微服務名進行路由
然後将原來配置檔案中寫死的路由位址改為注冊中心的服務名:
需要注意的是uri的協定為lb,,表示啟用Gateway的負載均衡功能。
uri: lb://cloud-payment-service # 比對後提供服務的路由位址
然後通過網關我們通路
CLOUD-PAYMENT-SERVICE
服務,可以看到網關同之前用過的Ribbon一樣,可以實作動态路由,可以看出其預設的負載均衡算法也是輪詢負載均衡。
6. Predicate詳解
6.1 predicate是什麼
啟動gateway9527
Gateway将路由比對作為Spring WebFlux HandlerMapping基礎架構的一部分。
Gateway包括許多内置的Route Predicate工廠。所有這些Predicate都與HTTP請求的不同屬性比對。多個Route Predicate工廠可以進行組合。
Gateway 建立Route對象時,使用RoutePredicateFactory建立Predicate對象,Predicate對象可以指派給Route。Gateway包括許多内置的Route Predicate Factories。
所有這些謂詞都比對HTTP請求的不同屬性。多種謂詞工廠可以組合,并通過邏輯and。
6.2 常用的Route Predicate
說白了,Predicate就是為了實作一組比對規則,讓請求過來找到對應的Route進行處理。
- After Route Predicate
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.ort
predicates:
- After=2021-03-04T09:07:25.143+08:00[Asia/Shanghai]
After好了解,但是時間串怎麼獲得?↓
public static void main(String[] args) {
ZonedDateTime zbj = ZonedDateTime.now();//預設時區
System.out.println(zbj);//2021-03-04T09:07:25.143+08:00[Asia/Shanghai]
}
- Before Route Predicate
- Between Route Predicate
- Cookie Route Predicate
Cookie Route Predicate需要兩個參數,一個是Cookie name,一個是正規表達式。路由規則會通過擷取對應的Cookie name值和正規表達式去比對,如果比對上就會執行路由,如果沒有比對上則不執行
predicates:
- Cookie=username,zzyy
不帶Cookie的通路 curl http://localhost:9588/paymentInfo
帶Cookies的通路 curl http://localhost:9588/paymentInfo --cookie "username=zzyy"
- Header Route Predicate
predicates:
- Header=X-Request-Id,\d+ # 請求頭要由X-Request-Id屬性并且值為正數的正規表達式
兩個參數:一個是屬性名稱和一個正規表達式,這個屬性值和正規表達式比對則執行。
curl http://localhost:9588/paymentInfo -H "X-Reqeust-Id:123"
- Host Route Predicate
Host Route Predicate接收一組參數,一組比對的域名清單,這個模闆是一個ant 分割的模闆,用 .
号作為分割符。它通過參數中的主機位址作為比對規則。
predicates:
- Host=**.somehost.org,**.anotherhost.org
- Method Route Predicate
predicates:
- Method=GET
- Path Route Predicate
- Query Route Predicate
支援傳入兩個參數,一個是屬性名,一個是屬性值,屬性值可以是正規表達式
http: //localhost:9527/payment/lb?username=1
predicates:
- Query=username,\d+ # 要有參數名username并且值還要是整數才能路由
- Remote Addr Route Predicate
- Weight Route Predicate
7. Filter詳解
7.1 Filter是什麼
路由過濾器可用于修改進入的HTTP請求和傳回的HTTP響應,路由過濾器隻能指定路由進行使用。
Gateway内置了多種路由過濾器,它們都是由GatewayFilter的工廠類來産生。
7.2 Gateway的Filter
生命周期
- pre
- post
種類
- GatewayFilter (31個)
- GlobalFilter (10個)
7.3 常用的GatewayFilter
- AddRequestParameter
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.ort
filters:
# 過濾器工廠會在比對的請求頭加上一對請求頭,名為X-Reqeust-Id,值為1024
- AddRequestParameter=X-Reqeust-Id,1024
predicates:
- After=2021-03-04T09:07:25.143+08:00[Asia/Shanghai]
- ...
7.4 自定義過濾器
一般我們使用自定義過濾器
SpringCloud Gateway中自定義過濾器要是實作兩個接口
org.springframework.cloud.gateway.filter.GlobalFilter
org.springframework.core.Ordered
前者實作了全局過濾器,而後者規定了過濾器的執行順序,該順序數字越小,過濾器越先被執行。
下面編寫具體的過濾器并實作上面兩個接口的方法:
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("===> 全局過濾器:" + new Date());
//獲得uname屬性
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
//如果不包含該屬性,則過濾器對請求進行攔截
if (uname == null) {
log.info("===> 使用者名為null,非法使用者");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/** 加載過濾器的順序,數字越小優先級越高 */
@Override
public int getOrder() {
return 3;
}
}
我們對服務進行通路我們可以發現,當我們包含請求參數uname時,可以正常通路,如果不含該請求參數,則通路無法正常進行。
這裡需要注意的是,在getOrder()方法中,為了保證過濾器可拓展,盡量不要用0/1 這種拓展性不夠好的數字,用0/1會使該自定義過濾器幾乎永遠先被執行。