2流高手速成記(之九):基於SpringCloudGateway實現服務閘道器功能

2022-11-22 15:00:51

咱們接上回 

上一節我們基於Sentinel實現了微服務體系下的限流和熔斷,使得整個微服務架構的安全性和穩定性上升了一個臺階

篇尾我們引出了一個問題,眾多的微服務節點,我們如何部署才能滿足使用者端簡潔高效的存取需求?

—— 今天我們就來引入服務閘道器的概念

什麼是服務閘道器?

服務閘道器是微服務體系下唯一的流量入口,對內實現內部架構統合,所有外來請求都要經由閘道器路由到對應的微服務節點,進而實現完整的業務邏輯

由於是每個外部請求的必經之路,因此除了路由之外,服務閘道器還可以勝任幾乎所有的橫切面功能,比如我們上一節提到的限流、熔斷,以及統一的認證服務、紀錄檔監控等

使用服務閘道器的優勢:

1. 使用者端簡化 —— 加入服務閘道器之後,使用者端只需要知道服務閘道器的存取地址即可,而不再需要了解每個微服務的存取埠

2. 降低耦合度 —— 其他微服務節點有變動,我們只需要靈活調整服務閘道器的路由即可,不必每次都去修改使用者端

3. 提升可維護性 —— 由服務閘道器統一實現路由、灰度釋出、負載均衡、限流熔斷等機制,開發人員更專注於業務實現

引入服務閘道器帶來的弊端:

微服務架構講求去中心化,而閘道器的引入使之變成了唯一的單點,其高可用性和可維護性變成了必須要解決的課題

如下是服務閘道器的基本功能以及常見的幾種服務閘道器的對比:(圖示來自:CSDN 張維鵬,感謝)

 我們本節重點關注 SpringCloudGateway 的用法,其他感興趣大家可以自行了解

新建nacos-sentinel-gateway模組

我們首先建立一個新模組並引入如下依賴項:

很多依賴項我們已經很熟悉了,這裡重新說明一下他們的用途:

Nacos Service Discovery —— 基於Nacos實現服務發現

Nacos Configuration —— 基於Nacos實現設定中心

Spring Cloud Alibaba Sentinel —— Sentinel限流熔斷元件,我們上一節的主要內容

Cloud Bootstrap —— bootstrap.yml 載入機制,實現Nacos雲設定的關鍵

Gateway —— 即 SpringCloudGateway,我們本節關注的重點

Spring Cloud Alibaba Sentinel Gateway —— Sentinel元件對於SpringCloudGateway的適配機制,本節後半部分我們要用到

值得注意的是:SpringCloudGateway內部直接包含了spring-boot-starter-web依賴,如果你的工程中同時包含二者的參照,會報衝突錯誤

解決的辦法是新增spring-cloud-starter-gateway的同時排除掉spring-boot-starter-web

<!-- 引入gateway閘道器 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </exclusion>
    </exclusions>
</dependency>

由於本節中不牽扯spring-boot-starter-web的使用,所以不存在這個問題,如下是本節的設計到的各個依賴項在pom中的宣告:

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </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</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

專案工程目錄如下:

   

本節相較於之前第一個不同點來了,組態檔不再是.properties,而是變成了.yml,後者比前者的表達能力更強,且比xml更簡潔

.yml是yaml的一個變種(基本完全一樣),非常適合作為組態檔,它不但可以表示變數,還可以宣告陣列及字典

這裡給大家提供一個可以線上將.properties轉換為.yml的工具,非常的方便,感謝BeJSON站長

====================================

線上properties轉yaml、yml工具 - BeJSON.com

====================================

如下是轉換完畢後的bootstrap.yml

spring:
  cloud:
    nacos:
      config:
        username:nacos
        password:nacos
        contextPath:/nacos
        server-addr:127.0.0.1:8848

內容依然僅是nacos的宣告,為便於測試效果,本節使用了原生的application.yml,因此bootstrap.yml中並未包含spring.application.name的宣告

而如果我們打算將application.yml託管給nacos,則必須在bootstrap.yml中宣告spring.application.name

然後我們來看application.yml的內容,這將是本節的重點

spring:
  application:
    name: nacos-sentinel-gateway
  cloud:
    gateway:
      enabled: true
      routes:
        - id: dubbo-nacos-consumer
          uri: http://127.0.0.1:8080
          order: 1
          predicates:
            - Path=/consumer/**
          filters:
            - StripPrefix=1
server:
  port: 8081

到這裡,閘道器服務就可以直接跑起來了

啥?還沒寫程式碼呢!是的,一句程式碼不用寫,來一個組態檔,你的服務閘道器就已經搭建起來了,非常方便對不?

我們分析下gateway的相關設定,不難看出其中的端倪:

1. route(路由)是gateway的基本設定單元,說白了就是搭建閘道器從設定路由規則開始

2. 每個route都包含四部分:

id —— 路由標識,自行命名,不重複即可

uri —— 請求轉發的真實目標地址

order —— 優先順序,數位越小代表優先順序越高

predicates —— 斷言(陣列),用於判定轉發規則是否成立

filters —— 過濾器(陣列),請求url ---> 過濾器處理 --->目標url,定義了url的變換規則

我們以此來解讀一下上述的閘道器路由到底定義了一個怎樣的轉發規則

id —— 和我們之前構建的dubbo-nacos-consumer同名,那下述轉發規則自然與其有關

uri —— dubbo-nacos-consumer監聽8080埠,因此我們打算將請求轉發給dubbo-nacos-consumer服務

order —— 1代表高優先順序

predicates —— Path=/consumer/**,代表請求路徑中需要包含/consumer/的字首,也就說我們會將路徑中包含/consumer/的請求轉發給dubbo-nacos-consumer服務

filters —— StripPrefix=1,是我們以後經常會遇到的一種過濾器,含義為過濾一級字首,這裡的字首就是上邊提到的/consumer字首

這是什麼意思?

我們閘道器監聽的埠是8081,

假定我們的請求為 http://127.0.0.1:8081/consumer/person/select ,

因為滿足包含字首/consumer/的設定,請求被轉發到 http://127.0.0.1:8080/consumer/person/select ,

而過濾器會過濾掉/consumer字首,於是請求路徑變為 http://127.0.0.1:8080/person/select

而這正是dubbo-nacos-consumer中的PersonController.select方法的有效對映

我們依次啟動先前建立的 dubbo-nacos-provider 、dubbo-nacos-consumer 以及我們剛建立好的 nacos-sentinel-gateway

開啟post執行請求驗證下結果

結果證實了我們的猜想,這樣一來我們就簡單實現了服務閘道器的路由功能

雖然功能是實現了,但是你有沒有發現,我們是直接指定了轉發的目標地址,這種模式設定的路由規則存在幾個非常大的弊端:

1. 閘道器必須知道所有微服務的地址,並且逐一設定

2. 微服務地址有任何變動,閘道器必須做同步更改

3. 無法實現負載均衡

那麼有沒有其他設定方式可以避開這些不足?當然是有的!

基於Nacos服務發現實現負載均衡

我們將app.yml的內容做如下改動:

spring:
  application:
    name: nacos-sentinel-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      enabled: true
      routes:
        - id: dubbo-nacos-consumer
          uri: lb://dubbo-nacos-consumer
          predicates:
            - Path=/consumer/**
          filters:
            - StripPrefix=1
server:
  port: 8081

相比於前一個版本,我們有兩個地方的改動

首先,我們加入了基於Nacos的服務發現設定:

  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

其次,我們原有固定的轉發地址改為了另一種形式:uri: lb://dubbo-nacos-consumer

這種模式遵循的固定格式為:lb://service-name

lb —— load balancing的縮寫,即負載均衡

service-name —— Nacos中註冊的服務名稱,注意這裡是名稱,而並非ip

lb://dubbo-nacos-consumer 代表如果斷言成立,則請求將均衡的分發至各個dubbo-nacos-consumer的服務範例

我們先在post中執行驗證下結果:

 

結果跟前一個版本是一樣的,印證了這種設定模式是有效的

但是由於我們的dubbo-nacos-cosumer服務只有一個範例,所以看不出來負載均衡的效果

幸運的是,在idea之下我們可以非常方便的設定另一個啟動範例

 

 

為了便於對比,我們先把consumer的application.properties組態檔遷移回本地,並刪除nacos的雲端設定

而後我們建立另一個新的組態檔application-anotherr.properties,內容如下:

# 應用名稱
spring.application.name=dubbo-nacos-consumer
# dubbo 協定
dubbo.protocol.id=dubbo
dubbo.protocol.name=dubbo
# dubbo 協定埠( -1 表示自增埠,從 20880 開始)
dubbo.protocol.port=-1
# Dubbo 消費端訂閱伺服器端的應用名,多個服務提供者用逗號分隔
dubbo.cloud.subscribed-services=dubbo-nacos-provider
# dubbo 服務掃描基準包
dubbo.scan.base-packages=com.example.dubbonacosconsumer
# Nacos幫助檔案: https://nacos.io/zh-cn/docs/concepts.html
# Nacos認證資訊
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
# Nacos 服務發現與註冊設定,其中子屬性 server-addr 指定 Nacos 伺服器主機和埠
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 註冊到 nacos 的指定 namespace,預設為 public
spring.cloud.nacos.discovery.namespace=public

# 應用服務 WEB 存取埠
server.port=8098

# sentinel看板設定
spring.cloud.sentinel.transport.dashboard = 127.0.0.1:9999
# 開啟對sentinel看板的飢餓式載入。sentinel預設是懶載入機制,只有存取過一次的資源才會被監控,通過關閉懶載入,在專案啟動時就連線sentinel控制檯
spring.cloud.sentinel.eager = true

內容與application.properties基本相同,區別僅在於

# 應用服務 WEB 存取埠
server.port=8098

我們知道,同一臺機器,同一個監聽埠智慧使用一次,因此如果想借用idea直接在開發環境下起另一個服務範例,則需要開啟另一個不同的埠

然後我們建立一個新的啟動設定

 

 

 

 

最關鍵的一句設定:

--spring.profiles.active=another

代表我們指定程式的啟動組態檔為:application-anotherr.properties

為了與原始的consumer做區分,我們先啟動consumer,然後改一下consumer的PersonController內容,再啟動another-consumer

    @GetMapping("/select")
    @SentinelResource(value = "person/select", blockHandler = "selectBlock")
    public SelectRetVo select() {
        SelectRetVo vo = new SelectRetVo();
        vo.setPersons(service.select());
        vo.setError("ok-another"); // another的區別
        return vo;
    }

此刻我們開啟Nacos的服務中心,會看到dubbo-nacos-consumer存在兩個執行中的範例

 

然後我們再次通過Post存取閘道器驗證結果

 

 

返回結果中的 ok 和 ok-another 會交替出現

看到效果了嗎?原始consumer和another-consumer都是動態啟動的,而基於Nacos服務發現機制,

gateway在無感知的情況下,完全不做任何改動,就實現了多個consumer範例的動態負載均衡,是不是非常便捷?

那除了 lb://service-name 這種方式,還有沒有更簡單的辦法?當然是有的!

基於Naocs的全自動路由設定

我們再次改動gateway的app.yml組態檔內容

spring:
  application:
    name: nacos-sentinel-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      enabled: true
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
server:
  port: 8081

這一次我們沒有特別指定下游節點的ip,也沒有設定相關的負載均衡,那這樣寫實現了什麼效果?

我先給大家看Post的測試結果:

 

我們來和前邊的負載均衡範例做下對比:

前次的存取地址:http://localhost:8081/consumer/person/select

原生的存取地址:http://localhost:8081/dubbo-nacos-consumer/person/select

看到區別了嗎?這樣寫相當於 lb://預設spring.application.name

僅是這樣嗎?其實還不止!仔細看,本次設定中我們並沒有指定特定的service-name開負載均衡!

也就是說這種模式之下,所有註冊到Nacos服務中心的外部可存取Web節點範例均啟動負載均衡設定!

不信你可以仿照剛剛的consumer,自己再起一組支援web存取的範例,隨便寫幾個controller測試方法,結果一試便知,gateway同樣滿足無感知存取

 

本節中我們對SpringCloudGateway分別進行了 從特定ip設定,到指定服務的負載均衡,再到全自動動態負載均衡 的一個統一的介紹,但SpringCloudGateway的使用其實還遠不止這些

對 斷言 和 過濾器 更加細化的客製化還可以幫我們過濾掉各種場景下的外部非法存取,從而對整個內部叢集的穩定性起到一個更加的有效的防護作用

但是我們要思考一個問題 —— 微服務體系下的不穩定因素僅來自於叢集外部嗎?答案顯然不是!我們面臨的業務需求是豐富多樣的,叢集內部同樣可能出現效能消耗的熱點

那麼當問題發生在微服務體系內部時,我們如何實現問題及熱點的精準定位呢?請看下回 —— 基於Spring Cloud Sleuth的鏈路追蹤,敬請期待~