SpringCloud Gateway微服務閘道器實戰與原始碼分析-上

2022-07-09 15:00:37

概述

定義

Spring Cloud Gateway 官網地址 https://spring.io/projects/spring-cloud-gateway/ 最新版本3.1.3

Spring Cloud Gateway 檔案地址 https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/

Spring Cloud Gateway GitHub原始碼地址 https://github.com/spring-cloud/spring-cloud-gateway

Spring Cloud Gateway使用了WebFlux技術,而WebFlux技術底層又基於高效能的Reactor模式通訊框架Netty。Spring Cloud Gateway基於Spring 5、Spring Boot 2和project Reactor技術上構建非同步非阻塞的高吞吐量API閘道器,提供一種簡單且有效的方式來路由到API,併為它們提供橫切關注點如安全性、監控/指標和彈性等。Spring Cloud Gateway特性如下:

Spring Cloud Gateway特性如下:

  • 能夠在任何請求屬性上匹配路由。
  • 謂詞和過濾器是特定於路由的。
  • 整合斷路器。
  • 整合Spring Cloud DiscoveryClient
  • 編寫謂詞和過濾器編寫易用。
  • 限制請求速率。
  • 路徑重寫

閘道器作為系統的唯一流量入口,封裝內部系統的架構,所有請求都先經過閘道器,由閘道器將請求路由到合適的微服務,優勢如下:

  • 簡化使用者端的工作。閘道器將微服務封裝起來後,使用者端只需同閘道器互動,而不必呼叫各個不同服務。
  • 降低函數間的耦合度。 一旦服務介面修改,只需修改閘道器的路由策略,不必修改每個呼叫該函數的使用者端,從而減少了程式間的耦合性。
  • 解放開發人員把精力專注於業務邏輯的實現。由閘道器統一實現服務路由(灰度與ABTest)、負載均衡、存取控制、流控熔斷降級等非業務相關功能,而不需要每個服務 API 實現時都去考慮。
  • 現在前後端分離大趨勢下,目前大部分瀏覽器安全同源策略出現前端請求的跨域問題,閘道器也是解決跨域問題的一種較完美方式。

流量閘道器與微服務閘道器

流量閘道器(如典型Nginx閘道器)是指提供全域性性的、與後端業務應用無關的策略,例如 HTTPS證書解除安裝、Web防火牆、全域性流量監控等。而微服務閘道器(如Spring Cloud Gateway)是指與業務緊耦合的、提供單個業務域級別的策略,如服務治理、身份認證等。也就是說,流量閘道器負責南北向流量排程及安全防護,微服務閘道器負責東西向流量排程及服務治理。

主流閘道器

  • Kong 閘道器:Kong 的效能非常好,非常適合做流量閘道器,但是對於複雜系統不建議業務閘道器用 Kong,主要是工程性方面的考慮。基於OpenResty或Nginx+Lua實現。
  • Zuul1.x 閘道器:Zuul 1.0 的落地經驗豐富,但是效能差、基於同步阻塞IO,適合中小架構,不適合並行流量高的場景,因為容易產生執行緒耗盡,導致請求被拒絕的情況。
  • Gateway 閘道器:功能強大豐富,效能好,官方基準測試 RPS (每秒請求數)是Zuul的1.6倍,能與 SpringCloud 生態很好相容,單從流式程式設計+支援非同步上也足以讓開發者選擇它了。
  • Zuul 2.x:效能與 gateway 差不多,基於非阻塞的,支援長連線,但 SpringCloud 沒有整合 zuul2 的計劃,並且 Netflix 相關元件都宣佈進入維護期,前景未知。

從發展趨勢上看,Spring Cloud Gateway作為Spring Cloud生態體系中的閘道器,目標替代Netflix的Zuul且勢在必行。

術語

進一步研究 Spring Cloud Gateway 的設定及其使用之前,我們先了解幾個 Spring Cloud Gateway 的核心術語

  • Route(路由):閘道器的基本組成部分。它由一個ID、一個目標URI、一組謂詞和一組過濾器定義。如果聚合謂詞或者說斷言為真,則匹配路由。
  • Predicate(謂詞):這是一個Java 8函數謂詞,輸入型別是Spring Framework serverwebexchange,匹配HTTP請求中的任何內容,如頭或引數。
  • Filter(過濾器):這些是使用特定工廠構建的GatewayFilter範例,可以在傳送下游請求之前或之後修改請求和響應。

工作流程

  • 使用者端向Spring Cloud Gateway發出請求。
  • 如果閘道器處理程式對映決定一個請求匹配一個路由,它被傳送到閘道器Web處理程式。
  • 此處理程式通過特定於該請求的過濾器鏈執行請求。用虛線分隔過濾器的原因是,過濾器可以在傳送代理請求之前和之後執行邏輯。
  • 執行所有「預」篩選邏輯。然後發出代理請求。
  • 發出代理請求後,執行「post」過濾器邏輯。

實戰

加依賴

專案或模組中加入spring-cloud-starter-gateway

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

如果引入了啟動器,但又不希望啟用閘道器,則可以通過設定spring.cloud.gateway.enabled=false來禁用。全部詳細設定可以查閱官網,Spring Cloud Gateway詳細設定說明 https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/appendix.html

官網提供兩種設定謂詞和過濾器的方法,分別是shortcuts and fully expanded arguments,譯為快捷方式和完全擴充套件的引數方式,後續例子我們都使用快捷方式,這種方式簡潔舒暢,官方的例子也大都是使用快捷方式。

路由設定Route 主要由路由id、目標uri、斷言集合和過濾器集合組成

  • id:路由標識,要求唯一,名稱任意(預設值 uuid,一般不用,需要自定義)
  • uri:請求最終被轉發到的目標地址
  • order: 路由優先順序,數位越小,優先順序越高
  • predicates:斷言陣列,即判斷條件,如果返回值是boolean,則轉發請求到 uri 屬性指定的服務中
  • filters:過濾器陣列,在請求傳遞過程中,對請求做一些修改

閘道器路由初體驗

利用前面庫存微服務提供的deduct介面,埠為4080,啟動庫存微服務,存取http://localhost:4080/deduct,顯示成功

建立閘道器微服務模組,pom檔案依賴如下,由於後面我們有Gateway整合Nacos和Sentinel的範例,所以這裡把其他依賴也先加進來

<?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>simple-ecommerce</artifactId>
        <groupId>cn.itxs</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>ecom-gateway</artifactId>
    <packaging>jar</packaging>
    <version>1.0</version>
    <name>ecom-gateway</name>
    <description>a simple electronic commerce platform demo tutorial for gateway service</description>

    <properties>
        <spring-cloud-loadbalancer.version>3.1.3</spring-cloud-loadbalancer.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
            <version>${spring-cloud-loadbalancer.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
    </dependencies>

</project>

bootstrap.yml組態檔加入建議路由設定如下

server:
  port: 4090
spring:
  application:
    name: ecom-gateway
  cloud:
    gateway:
      routes:
        - id: storage_route
          uri: http://localhost:4080
          predicates:
            - Path=/storage-service/**
          filters:
            - StripPrefix=1

啟動閘道器微服務

存取閘道器提供api介面http://localhost:4090/storage-service/deduct,匹配storage-service為真後通過過濾器去掉一層也即是storage-service路徑去掉,然後轉發至uri地址,最終轉發url為http://localhost:4080/deduct ,成功返回結果

整合Nacos

本地組態檔bootstrap.yml改為如下,commons-dev.yaml包含服務註冊的組

spring:
  application:
    name: ecom-gateway
  profiles:
    active: dev
  cloud:
    nacos:
      # 註冊中心資訊放在設定中心上,每個程式一般只設定設定中心的資訊
      server-addr: 192.168.50.95:8848
      config:
        server-addr: ${spring.cloud.nacos.server-addr}
        file-extension: yaml
        namespace: a2b1a5b7-d0bc-48e8-ab65-04695e61db01
        group: gateway-group
        extension-configs:
          - dataId: commons-dev.yaml
            group: commons-group
            refresh: true
        username: itsx
        password: itxs123
        enabled: true # 預設為true,設定false 來完全關閉 Spring Cloud Nacos Config
        refresh-enabled: true # 預設為true,當變更設定時,應用程式中能夠獲取到最新的值,設定false來關閉動態重新整理,我們使用註冊中心場景大部分就是動態感知,因此基本使用預設的

將路由設定也一併放在Nacos中設定ecom-gateway-dev.yaml,內容如下,uri這裡使用的是庫存微服務名稱,lb是做負載均衡處理

server:
  port: 4090
spring:
  cloud:
    gateway:
      routes:
        - id: storage_route
          uri: lb://ecom-storage-service
          predicates:
            - Path=/storage-service/**
          filters:
            - StripPrefix=1

Nacos中commons-dev.yaml的關於Nacos註冊中心使用設定如下,庫存微服務也是使用這個,服務註冊和發現都在ecom-group組

spring:
  cloud:
    nacos:
      discovery:
        server-addr: ${spring.cloud.nacos.server-addr}
        group: ecom-group
        namespace: a2b1a5b7-d0bc-48e8-ab65-04695e61db01      
        username: itsx
        password: itxs123

啟動庫存微服務和閘道器微服務,都註冊到同一個組裡面

再次存取http://localhost:4090/storage-service/deduct ,正常返回結果,到此我們已經成功整合Nacos

路由斷言工廠

Route Predicate Factories為路由斷言工廠,官網提供12種路由工廠,如果都沒有滿足你的需求,還可以自定義路由斷言工廠

我們先設定一個未來時間的after斷言- After=2022-07-09T23:42:47.789-08:00[Asia/Shanghai]

可以直接存取本機IP,返回一個錯誤的頁面

將after斷言改為- Before=2022-07-09T23:42:47.789-08:00[Asia/Shanghai]後則可以正常存取。

  • 其他還有許多規則詳細可以查閱官網,如下面,各位可以自己一一嘗試
    • Between=2022-07-08T23:42:47.789-08:00[Asia/Shanghai], 2022-07-09T23:42:47.789-08:00[Asia/Shanghai]
    • Cookie=chocolate, ch.p
    • Header=X-Request-Id, \d+
    • Host=**.somehost.org,anotherhost.org
    • Method=GET,POST
    • Query=green
    • RemoteAddr=192.168.1.1/24
    • Weight=group1, 8 Weight=group1, 2
    • XForwardedRemoteAddr=192.168.1.1/24

自定義路由斷言工廠

當官方提供的所有斷言工廠無法滿足業務需求時,還可以自定義斷言工廠。新增自定義斷言工廠類自定斷言工廠主要注意一下幾點:

  • 需要宣告是Springboot的Bean,新增註解@Component,名稱必須以RoutePredicateFactory結尾,這個是命名約束。如果不按照命名約束來命名,那麼就會找不到該斷言工廠。字首就是設定中設定的斷言。
  • 可以直接複製Gateway中已經實現的斷言工廠,修改對應的內容,避免踩坑。
  • 繼承父類別AbstractRoutePredicateFactory,並重寫方法。
  • 需要定義一個Config靜態內部類,宣告屬性來接收 組態檔中對應的斷言的資訊。
  • 在重寫的shortcutFieldOrder方法中,繫結Config中的屬性。傳入陣列的內容需要與Config中的屬性一致。
  • 在重寫的apply方法中,實現具體驗證邏輯, true就是匹配成功 false匹配失敗。

新建一個庫存數量的路由斷言工廠QuantityRoutePredicateFactory.java,如庫存在100和200之間可以存取

package cn.itxs.ecom.gateway.factory;

import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

// 自定義路由斷言工廠
@Component
public class QuantityRoutePredicateFactory extends AbstractRoutePredicateFactory<QuantityRoutePredicateFactory.Config>{


    public QuantityRoutePredicateFactory() {
        super(QuantityRoutePredicateFactory.Config.class);
    }

    // 將組態檔中的值按返回集合的順序,賦值給設定類
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(new String[]{"minQuantity", "maxQuantity"});
    }

    @Override
    public Predicate<ServerWebExchange> apply(Consumer<Config> consumer) {
        return super.apply(consumer);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        // 建立閘道器斷言物件
        // 檢查
        return serverWebExchange -> {
            // TODO 獲取請求引數age,判斷是否滿足如設定的[100, 200)
            MultiValueMap<String, String> queryParams = serverWebExchange.getRequest().getQueryParams();
            String quantity = queryParams.getFirst("quantity");
            if (StringUtils.hasText(quantity) && quantity.matches("[0-9]+")) {
                int iQuantity = Integer.parseInt(quantity);
                if (iQuantity >= config.getMinQuantity() && iQuantity < config.getMaxQuantity()) {
                    return true;
                }
            }
            return false;
        };
    }


    // 設定類,屬性用於接收組態檔中的值
    @Validated
    public static class Config {
        private int minQuantity;
        private int maxQuantity;

        public int getMinQuantity() {
            return minQuantity;
        }

        public void setMinQuantity(int minQuantity) {
            this.minQuantity = minQuantity;
        }

        public int getMaxQuantity() {
            return maxQuantity;
        }

        public void setMaxQuantity(int maxQuantity) {
            this.maxQuantity = maxQuantity;
        }
    }
}

Nacos閘道器的設定中增加自定義路由斷言工廠設定Quantity

server:
  port: 4090
spring:
  cloud:
    gateway:
      routes:
        - id: storage_route
          uri: lb://ecom-storage-service
          predicates:
            - Path=/storage-service/**
            - Quantity=100,200
          filters:
            - StripPrefix=1

啟動閘道器微服務,存取http://localhost:4090/storage-service/deduct?quantity=99 ,沒有匹配路由策略

而存取http://localhost:4090/storage-service/deduct?quantity=100 ,能夠正確返回庫存微服務介面結果

**本人部落格網站 **IT小神 www.itxiaoshen.com