Spring Cloud筆記-Gateway新一代網關(十二)

1.概述簡介

Gateway官方文檔:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/

在Spring Cloud 1.x中,網關使用的是Zuul 1,在Spring Cloud 2.x中,網關使用的是Gateway,因爲Zuul 2版本進展緩慢,所以Spring Cloud自己研發了網關,Gateway是原Zuul 1的替代版。Gateway採用異步非阻塞模型開發,性能上不需要擔心,雖然Netflix發佈了Zuul 2版本,但是Spring Cloud並沒有整合的計劃,所以才自己推出了Gateway的方案。

Spring Cloud Gateway是Spring Cloud的一個全新項目,基於Spring 5+Spring Boot 2.x+Project Reactor等技術開發的網關,旨在爲微服務架構提供一種簡單有效的統一API路由管理方式。

Spring Cloud Gateway作爲Spring Cloud生態系統中的網關,目標是替代Zuul 1,在Spring Cloud 2.x版本中,沒有對新版本Zuul 2最新高性能版本進行集成,仍然使用的Zuul 1非Reactor模式的老版本,爲了提升網關性能,Spring Cloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層使用了高性能的Reactor模式通信框架Netty。

Spring Cloud Gateway目標是提供統一的路由方式,並且基於Filter鏈方式提供網關基本功能:安全、監控、指標、限流。

網關的作用:反向代理、鑑權、流量控制、熔斷、日誌監控等。

在微服務架構中,網關的位置,位於各個微服務的上一層,通過網關後,就訪問到了具體的微服務了。

Spring Cloud Gateway特性:

  • 基於Spring Framework 5+Project Reactor+Spring Boot 2.x進行構建
  • 動態路由:能夠匹配任何請求屬性
  • 支持路由指定Predicate(斷言)和Filter(過濾器),而且編寫容易
  • 集成了Hystrix斷路器功能
  • 集成Spring Cloud服務發現功能
  • 支持請求限流功能
  • 支持路徑重寫功能

Spring Cloud Gateway和Zuul的區別:

  • Zuul 1採用的是阻塞I/O的API網關
  • Zuul 1使用Servlet2.5的阻塞架構實現,不支持任何長連接(如WebSocket),Zuul的設計模式和Nginx比較像,每次I/O都從工作線程中選擇一個執行,請求線程在工作線程完成之前一直是阻塞的,Nginx是C++實現,Zuul是Java實現,JVM在第一次的加載時候,會比較慢,所以Zuul的性能比較差
  • Zuul 2設計理念跟先進,基於Netty非阻塞和支持長連接,但是目前Spring Cloud沒有整合它,Zuul 2的性能比Zuul 1性能有較大提升,Spring Cloud Gateway的性能也不錯,官方測試數據表示,Spring Cloud Gateway的RPS(每秒請求數)是Zuul 1的1.6倍
  • Spring Cloud Gateway建立在Spring Framework 5+Project Reactor+Spring Boot 2.x之上,使用非阻塞API
  • Spring Cloud Gateway還支持WebSocket,與Spring緊密集成擁有更好的開發體驗

Zuul 1模型缺點:Zuul 1採用傳統Servlet I/O處理模型,但請求進入servlet容器時,servlet容器會爲其綁定一個線程,在併發不高的情況下是適用的,當併發量增加,線程數就會增加,但是線程資源是非常昂貴的(線程上下文切換內存消耗大),會影響到請求處理時間。有些簡單的業務場景,並不需要每個請求分配一個線程,簡單業務的高併發下,實際可能只需要幾個線程就能扛得住,因此,每個請求分配一個線程,在高併發環境下並沒有優勢。

Gateway模型:Gateway模型採用的是WebFlux框架,這個框架是一個典型的非阻塞異步的框架,並且在Servlet 3.1後,支持了異步非阻塞,框架的核心是基於Reactor相關API實現的。相對於傳統Web框架,它可以運行在支持Servlet 3.1容器上的組件中(如Netty、Undertow等)。Spring WebFlux是Spring 5引入的新的響應式框架,區別於Spring MVC,不需要依賴Servlet API,完全異步非阻塞,並且基於Reactor來實現響應式流規範。

Spring WebFlux可以參考官網:https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html

2.三大核心概念

Route(路由)

路由是構建網關的基本模塊,由ID,目標URI一系列斷言和過濾器組成,如果斷言爲true,則匹配該路由。

Predicate(斷言)

參考Java8的java.util.function.Predicate,開發人員可以匹配HTTP請求中的所有內容(比如請求頭,請求參數等),如果請求與斷言匹配,則進行路由。

Filter(過濾)

類似於Web開發中的過濾器,這裏指的是Spring框架中GatewayFilter實例,使用過濾器可以在請求被路由之前或之後對請求進行修改。

總結

路由的功能是由斷言和過濾組合來實現的,一個Web請求發送後,先經過網關,網關裏的斷言和過濾用來判斷這個請求是否需要路由轉發,當斷言爲true,過濾器放行時候,這個請求進行路由轉發,此時,請求才到達具體的微服務模塊。

3.Gateway工作流程

Spring Cloud Gateway Diagram

客戶端向Spring Cloud Gateway發送請求,然後在Gateway Handler Mapping中找到與請求相匹配的路由,將其Web Handler。Handler通過指定的過濾器鏈將請求發送到實際服務,執行業務邏輯,然後返回。如果有post類型的過濾器,執行過濾器邏輯。

pre類型的過濾器可以做參數校驗、權限校驗、流量監控、日誌輸出、協議轉換等功能。

post類型的過濾器可以做響應內容、響應頭的修改、日誌輸出、流量監控等功能。

4.入門配置

創建cloud-gateway-gateway9527模塊,修改pom.xml,加入spring-cloud-starter-gateway和spring-cloud-starter-netflix-eureka-client的座標。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>com.atguigu.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>cloud-gateway-gateway9527</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

添加application.yml配置文件。

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    register-with-eureka: true # true:將自己註冊進Eureka
    fetch-registry: true # true:需要去註冊中心獲取其他服務地址
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

添加主啓動類。

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GatewayMain9527.class, args);
    }
}

通過yml給Gateway添加路由規則。

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
        # - 是yml語法,代表數組的意思
        - id: payment_route1 # 路由ID,沒有固定規則,需要保證唯一
          uri: http://localhost:8001 # 路由到哪個地址,實際提供微服務的地址
          predicates:
            - Path=/payment/get/** # 路徑匹配斷言
        - id: payment_route2 # 路由ID,沒有固定規則,需要保證唯一
          uri: http://localhost:8001 # 路由到哪個地址,實際提供微服務的地址
          predicates:
            - Path=/payment/loadbalance # 路徑匹配斷言
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    register-with-eureka: true # true:將自己註冊進Eureka
    fetch-registry: true # true:需要去註冊中心獲取其他服務地址
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

相比之前的yml,這裏的yml多了Gateway結點,配置這個的目的在於隱藏localhost:8001的地址,如果想訪問payment8001服務的/payment/get/**地址,之前需要訪問http://localhost:8001/payment/get/1,配置了路由之後,訪問http://localhost:9527/payment/get/1也可以實現,於是,原來的8001端口就隱藏了。

配置路由有兩種方式,一種是上面的yml配置文件,另一種是是編碼方式配置,uri中記得帶http,否則訪問不到,這裏的uri可以寫任意地址,代表一個路由規則,訪問path路徑的時候,被路由到uri地址。

package com.atguigu.springcloud.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
        return routeLocatorBuilder.routes()
                // 當訪問http://localhost:9527/payment/get/**的時候,請求會被路由到http://localhost:8001/payment/get/**
                .route("payment_route1", r -> r.path("/payment/get/**").uri("http://localhost:8001/payment/get/**"))
                // 當訪問http://localhost:9527/payment/loadbalance的時候,請求會被路由到http://localhost:8001/payment/loadbalance
                .route("payment_route2", r -> r.path("/payment/loadbalance").uri("http://localhost:8001/payment/loadbalance"))
                .build();
    }
}

5.通過微服務名實現動態路由

現在存在的問題:真實服務地址寫死了,我們應該通過服務名來找服務,而不是通過服務地址找服務。

默認情況下,Gateway會根據註冊中心註冊的服務列表,以註冊中心上微服務名爲路徑創建動態路由轉發,從而實現動態路由的功能。

爲了演示網關的負載均衡,修改Provider8001和Provider8002的yml配置文件,將它們的註冊地址改成eureka7001的地址。啓動Eureka7001和Provider8001和Provider8002服務。修改Gateway9527模塊的application.yml配置文件,修改爲如下內容。因爲要修改application.yml,所以先把GatewayConfig類進行屏蔽,避免產生影響。

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
        # - 是yml語法,代表數組的意思
        - id: payment_route1 # 路由ID,沒有固定規則,需要保證唯一
          # uri: http://localhost:8001 # 路由到哪個地址,實際提供微服務的地址
          uri: lb://cloud-payment-service # 使用微服務名稱查找路由地址,lb是用於識別負載均衡的前綴,不能修改
          predicates:
            - Path=/payment/get/** # 路徑匹配斷言
        - id: payment_route2 # 路由ID,沒有固定規則,需要保證唯一
          # uri: http://localhost:8001 # 路由到哪個地址,實際提供微服務的地址
          uri: lb://cloud-payment-service # 使用微服務名稱查找路由地址,lb是用於識別負載均衡的前綴,不能修改
          predicates:
            - Path=/payment/loadbalance # 路徑匹配斷言
      discovery:
        locator:
          enabled: true # 開啓從註冊中心動態創建路由的功能,利用微服務名稱進行路由
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    register-with-eureka: true # true:將自己註冊進Eureka
    fetch-registry: true # true:需要去註冊中心獲取其他服務地址
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

通過瀏覽器訪問http://localhost:9527/payment/loadbalance,在頁面端可以看到端口號的不斷變化,如果要實現負載均衡,注意uri中的lb是固定的,它是識別開啓負載均衡的標誌。

6.Predicate的使用

在Gateway9527啓動的時候,在Console可以看到如下內容,代表執行加載Predicate,我們目前使用的Predicate是Path。

2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [After]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Before]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Between]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Cookie]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Header]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Host]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Method]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Path]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Query]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [ReadBodyPredicateFactory]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [RemoteAddr]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Weight]
2020-06-17 07:31:15.502  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [CloudFoundryRouteService]

官方文檔:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#gateway-request-predicates-factories

Spring Cloud Gateway將路由匹配作爲Spring WebFlux HandlerMapping基礎架構的一部分。

Spring Cloud Gateway包括許多內置的Route Predicate Factory,這些Predicate鬥魚HTTP請求的不同屬性匹配,多個Route Predicate Factory可以進行組合。

Spring Cloud Gateway創建Route對象時,使用Route Predicate Factory創建Predicate對象,Predicate對象可以賦值給Route。

根據官方文檔裏的說明,在application.yml裏配置上即可生效,這相當於加了一個條件,條件滿足就做路由轉發,不滿足就不路由轉發。

簡單說下吧,After Route Predicate、Before Route Predicate、Between Route Predicate需要用到一個DateTime值,這個值要使用ZonedDateTime類來獲取。Cookie Route Predicate、Header Route Predicate、Host Route Predicate的測試使用cmd來測試,通過發送curl命令即可完成,具體可以看一下curl命令的參數介紹。

測試Cookie Route Predicate
curl http://localhost:9527/payment/loadbalance --cookie "username=wangshaoyang"
測試Header Route Predicate
curl http://localhost:9527/payment/loadbalance -H "X-Request-Id:123"
測試Header Route Predicate
curl http://localhost:9527/payment/loadbalance -H "Host: www.atguigu.com"

Method Route Predicate是根據GET或POST做斷言,Path Route Predicate就是我們最初使用的方式,Query Route Predicate是根據請求裏所帶Query參數進行判斷的,RemoteAddr Route Predicate是對請求發起ip做的校驗,Weight Route Predicate是給負載均衡分配權重的,可以指定請求發到某一個uri的權重。

所以說,Predicate可以提供一組匹配規則,每當一個請求過來的時候,去檢驗這些規則,如果滿足規則,進行路由,否則,就提示出錯。

7.Filter的使用

路由過濾器可用於修改進入的HTTP請求和返回的HTTP響應,路由過濾器只能指定路由進行使用,Spring Cloud Gateway內置了多種路由過濾器,它們是通過Gateway Filter Factory產生的。

從生命週期來分類,可以分爲Pre和Post,類似於Spring AOP的前置通知和後置通知。從種類上來分類,可以分爲Gateway Filter和Global Filter。具體可以參考官網,這個的配置也是在application.yml里加配置即可。

Gateway Filter:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#gatewayfilter-factories

Global Filter:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#global-filters

下面來介紹自定義全局過濾器,我們可以根據業務需求做定製化處理。

package com.atguigu.springcloud.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class MyGatewayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 獲取Request,獲取參數
        MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
        System.out.println(queryParams);
        // 這裏可以對queryParams裏的值做一些校驗
        if ("".equals(queryParams.getFirst("username"))) {
            System.out.println("非法用戶");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            // 表示該請求已經處理完成
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    /**
     * 過濾器的優先級,值越小優先級越高
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章