天天看點

六. SpringCloud網關

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通訊架構

源碼架構:

六. SpringCloud網關
1.2 整個微服務架構中網關的位置

可以看到網關是所有微服務的入口

六. SpringCloud網關
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核心概念

  • 路由 Route

    路由是建構網關的基本子產品,它由ID、目标URI、一系列的斷言和過濾器組成,如果斷言為true則比對路由。
  • 斷言 Predicate

    參考的是Java8的

    java.util.function.Predicate

    ,開發人員可以比對HTTP請求中的所有内容(例如請求頭或請求參數), 如果請求與斷言比對則進行路由。
  • 過濾器 Filter

    Spring架構中GatewayFilter的執行個體,使用過濾器,可以在請求被路由之前或者之後對請求進行修改。客戶當發送Web請求,通過一些比對條件,定位到真正的服務節點,并在這個轉發過程的前後,進行一些精細化的控制,而斷言就是這些比對條件,過濾器可以了解為一個無所不能的攔截器,用來實作這些精細化的控制,有了斷言和過濾器,再加上目标的URI,就可以實作一個具體的路由。
六. SpringCloud網關

3. 網關工作流程

用戶端向Spring Cloud Gateway送出請求。如果 網關處理程式映射(Gateway Handler Mapping)确定請求與路由比對,則将其發送到 網關Web處理程式(Gateway Web Handler)。該處理程式通過特定于請求的

過濾器鍊

來運作請求。

過濾器器由虛線分隔的原因是,過濾器可以在發送代理請求

之前

之後

運作邏輯。所有“前置”過濾器邏輯均被執行。然後發出代理請求。發出代理請求後,将運作“後置”過濾器邏輯。圖中虛線左邊的對應于前置過濾器,虛線右邊的對應于

後置過濾器

六. SpringCloud網關

前置過濾器可以做參數校驗、權限校驗、流量監控、日志輸出、協定轉換等

後置過濾器可以做響應内容、響應頭的修改、日志的輸出、流量監控等

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網關

這樣的話用網關對微服務進行路由通路,就可以不再對外暴露微服務的真實位址,而是統一暴露為網關的位址。

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

六. SpringCloud網關

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進行處理。
六. SpringCloud網關
  • 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
六. SpringCloud網關
帶Cookies的通路 curl http://localhost:9588/paymentInfo --cookie "username=zzyy"
六. SpringCloud網關
  • Header Route Predicate
predicates:
    - Header=X-Request-Id,\d+ # 請求頭要由X-Request-Id屬性并且值為正數的正規表達式
           

兩個參數:一個是屬性名稱和一個正規表達式,這個屬性值和正規表達式比對則執行。

curl http://localhost:9588/paymentInfo -H "X-Reqeust-Id:123"

六. SpringCloud網關
  • 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會使該自定義過濾器幾乎永遠先被執行。

繼續閱讀