1. 程式人生 > >Spring Cloud第十四篇 | Api閘道器Zuul

Spring Cloud第十四篇 | Api閘道器Zuul

本文是Spring Cloud專欄的第十四篇文章,瞭解前十三篇文章內容有助於更好的理解本文:

  1. Spring Cloud第一篇 | Spring Cloud前言及其常用元件介紹概覽

  2. Spring Cloud第二篇 | 使用並認識Eureka註冊中心

  3. Spring Cloud第三篇 | 搭建高可用Eureka註冊中心

  4. Spring Cloud第四篇 | 客戶端負載均衡Ribbon

  5. Spring Cloud第五篇 | 服務熔斷Hystrix

  6. Spring Cloud第六篇 | Hystrix儀表盤監控Hystrix Dashboard

  7. Spring Cloud第七篇 | 宣告式服務呼叫Feign

  8. Spring Cloud第八篇 | Hystrix叢集監控Turbin

  9. Spring Cloud第九篇 | 分散式服務跟蹤Sleuth

  10. Spring Cloud第十篇 | 分散式配置中心Config

  11. Spring Cloud第十一篇 | 分散式配置中心高可用

  12. Spring Cloud第十二篇 | 訊息匯流排Bus

  13. Spring Cloud第十三篇 | Spring Boot Admin服務監控

一、閘道器分類

開放Api

    開放api(openApi) 企業需要將自身資料、能力等作為開發平臺向外開放,通常會以rest的方式向外提供,最好的例子就是淘寶開放平臺、騰訊公司的QQ開發平臺、微信開放平臺。 Open API開放平臺必然涉及到客戶應用的接入、API許可權的管理、呼叫次數管理等,必然會有一個統一的入口進行管理,這正是API閘道器可以發揮作用的時候。

微服務閘道器

    微服務的概念最早在2012年提出,在Martin Fowler的大力推廣下,微服務在2014年後得到了大力發展。 在微服務架構中,有一個元件可以說是必不可少的,那就是微服務閘道器,微服務閘道器處理了負載均衡,快取,路由,訪問控制,服務代理,監控,日誌等。API閘道器在微服務架構中正是以微服務閘道器的身份存在。

API服務管理平臺

    上述的微服務架構對企業來說有可能實施上是困難的,企業有很多遺留系統,要全部抽取為微伺服器改動太大,對企業來說成本太高。但是由於不同系統間存在大量的API服務互相呼叫,因此需要對系統間服務呼叫進行管理,清晰地看到各系統呼叫關係,對系統間呼叫進行監控等。 API閘道器可以解決這些問題,我們可以認為如果沒有大規模的實施微服務架構,那麼對企業來說微服務閘道器就是企業的API服務管理平臺。

二、閘道器設計

開放API介面

    1、對於OpenAPI使用的API閘道器來說,一般合作伙伴要以應用的形式接入到OpenAPI平臺,合作伙伴需要到 OpenAPI平臺申請應用。因此在OpenAPI閘道器之外,需要有一個面向合作伙伴的使用的平臺用於合作伙伴,這就要求OpenAPI閘道器需要提供API給這個使用者平臺進行訪問。如下架構:

    當然如果是在簡單的場景下,可能並不需要提供一個面向合作伙伴的門戶,只需要由公司的運營人員直接新增合作伙伴應用id/金鑰等,這種情況下也就不需要合作伙伴門戶子系統。

內網API介面

    2、對於內網的API閘道器,在起到的作用上來說可以認為是微服務閘道器,也可以認為是內網的API服務治理平臺。當企業將所有的應用使用微服務的架構管理起來,那麼API閘道器就起到了微服務閘道器的作用。而當企業只是將系統與系統之間的呼叫使用rest api的方式進行訪問時使用API閘道器對呼叫進行管理,那麼API閘道器起到的就是API服務治理的作用。架構參考如下:

    3、對於公司內部公網應用(如APP、公司的網站),如果管理上比較細緻,在架構上是可能由獨立的API閘道器來處理這部分內部公網應用,如果想比較簡單的處理,也可以是使用面向合作伙伴的API閘道器。如果使用獨立的API閘道器,有以下的好處:

面向合作伙伴和麵向公司主體業務的優先順序不一樣,不同的API閘道器可以做到業務影響的隔離。

    內部API使用的管理流程和麵向合作伙伴的管理流程可能不一樣。

    內部的API在功能擴充套件等方面的需求一般會大於OpenAPI對於功能的要求。

    基於以上的分析,如果公司有能力,那麼還是建議分開使用合作伙伴OPEN API閘道器和內部公網應用閘道器。

三、閘道器框架

  • Tyk:Tyk是一個開放原始碼的API閘道器,它是快速、可擴充套件和現代的。Tyk提供了一個API管理平臺,其中包括API閘道器、API分析、開發人員門戶和API管理面板。Try 是一個基於Go實現的閘道器服務。https://tyk.io

  • Kong:Kong是一個可擴充套件的開放原始碼API Layer(也稱為API閘道器或API中介軟體)。Kong 在任何RESTful API的前面執行,通過外掛擴充套件,它提供了超越核心平臺的額外功能和服務,是基於Nginx+Lua進行二次開發的方案。https://konghq.com

  • Orange:Orange和Kong類似也是基於OpenResty的一個API閘道器程式,是由國人開發的。 http://orange.sumory.com

  • Netflix Zuul:Zuul是Netflix公司的開源專案,提供動態路由、監視、彈性、安全性等功能的邊緣服務。Zuul是Netflix出品的一個基於JVM路由和服務端的負載均衡器,Spring Cloud在Netflix專案中也已經集成了Zuul。https://github.com/Netflix/zuul

  • GateWay:GateWay是Spring Cloud的一個子專案,構建於Spring5+,基於Spring Boot 2.x 響應式的、非阻塞式的 API。https://spring.io/projects/spring-cloud-gateway

四、閘道器作用

    閘道器的作用,可以實現負載均衡、路由轉發、日誌、許可權控制、監控等。

五、閘道器與過濾器區別

    閘道器是攔截所有伺服器請求進行控制

    過濾器攔截某單個伺服器請求進行控制

六、Nginx與Zuul區別

    Nginx是採用伺服器負載均衡進行轉發

    Zuul依賴Ribbon和Eureka實現本地負載均衡轉發
    相對來說Nginx功能比Zuul功能更加強大,能夠整合其他語言比如Lua指令碼實現強大的功能,同時Nginx可以更好的抗高併發,Zuul閘道器適用於請求過濾和攔截等。

七、閘道器

Zuul是Spring Cloud推薦的一個元件:https://github.com/Netflix/zuul

1、使用Zuul實現反向代理

1-1、在springcloud-zuul模組中新增依賴

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

1-2、application.yml配置檔案內容如下:

spring:
  application:
    name: springcloud-zuul
server:
  port: 9999
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8700/eureka
    #客戶端每隔30秒從Eureka服務上更新一次服務資訊
    registry-fetch-interval-seconds: 30
    #需要將我的服務註冊到eureka上
    register-with-eureka: true
    #需要檢索服務
    fetch-registry: true
  #心跳檢測檢測與續約時間
  instance:
    #告訴服務端,如果我10s之內沒有給你發心跳,就代表我故障了,將我剔除掉,預設90s
    #Eureka服務端在收到最後一次心跳之後等待的時間上限,單位為秒,超過則剔除(客戶端告訴服務端按照此規則等待自己)
    lease-expiration-duration-in-seconds: 10
    #每隔2s向服務端傳送一次心跳,證明自已依然活著,預設30s
    #Eureka客戶端向服務端傳送心跳的時間間隔,單位為秒(客戶端告訴服務端自己會按照該規則)
    lease-renewal-interval-in-seconds: 2
#以/api-a/ 開頭的請求都轉發給springcloud-service-consumer服務
#以/api-b/開頭的請求都轉發給springcloud-service-feign服務
zuul:
  routes:
    api-a:
      path: /api-a/**
      serviceId: springcloud-service-consumer
    api-b:
      path: /api-b/**
      serviceId: springcloud-service-feign

1-3、在主類上添加註解

@EnableZuulProxy //開啟Zuul的支援
@EnableEurekaClient //開啟Eureka客戶端支援

完成上面的操作之後,我們可以啟動相應的服務,啟動相應服務如下:

    然後訪問springcloud-service-consumer服務中的介面:http://localhost:9999/api-a/consumer/hello,同理springcloud-service-feign的服務中介面也是這樣 http://localhost:9999/api-b/feign/hello路徑中的api-a,和api-b分別被路由到響應的服務上去,你也可以配置忽略api-a,api-b等等其他配置

2、Zuul對Ribbon和Hystrix的支援

    從依賴上可以看出來,Zuul自身會依賴Ribbon和Hystrix的依賴,所以Zuul本身就擁有執行緒隔離和斷路器的自我保護功能,以及對服務呼叫的客戶端負載均衡功能,但是僅限於我們path和serviceId的組合使用

zuul.routes.<route>.path
zuul.routes.<route>.serviceId

不支援path和url的組合使用

zuul.routes.<route>.path
zuul.routes.<route>.url

3、使用Zuul過濾器

    微服務數量多的情況下,我們為每個服務都加上安全校驗和許可權控制,是非常麻煩的,這樣的做法並不可取,它會增加後期系統的維護難度,因為每一個系統中的各種校驗邏輯很多情況下大致相同或者類似,然後這些非業務的邏輯程式碼分散到各個服務中,產生的冗餘程式碼是我們不想看到的,所以通常的做法是通過閘道器服務來完成這些非業務性質的校驗。

3-1、Filter的生命週期

    Filter的生命週期有4個,分別是 “PRE”、“ROUTING”、“POST” 和“ERROR”,整個生命週期可以用下圖來表示

Zuul大部分功能都是通過過濾器來實現的,這些過濾器型別對應於請求的典型生命週期。

  • PRE:這種過濾器在請求被路由之前呼叫。我們可利用這種過濾器實現身份驗證、在叢集中選擇請求的微服務、記錄除錯資訊等。

  • ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建傳送給微服務的請求,並使用 Apache HttpClient 或 Netfilx Ribbon 請求微服務。

  • POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應新增標準的 HTTP Header、收集統計資訊和指標、將響應從微服務傳送給客戶端等。

  • ERROR:在其他階段發生錯誤時執行該過濾器。

在Zuul閘道器中,我們需要自定義一個類來繼承ZuulFilter抽象類並實現4個相應的抽象方法即可。

簡單例項,驗證請求有沒有userToken引數:

@Component
public class TokenFilter  extends ZuulFilter {
     
    // 過濾器型別 pre 表示在 請求之前進行攔截
    @Override
    public String filterType() {
        return "pre";
    }
​
    // 過濾器的執行順序。當請求在一個階段的時候存在多個多個過濾器時,需要根據該方法的返回值依次執行
    @Override
    public int filterOrder() {
        return 0;
    }
​
    // 判斷過濾器是否生效
    @Override
    public boolean shouldFilter() {
        return true;
    }
​
    @Override
    public Object run() throws ZuulException {
        // 獲取上下文
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        String userToken = request.getParameter("userToken");
        if (StringUtils.isEmpty(userToken)) {
            //setSendZuulResponse(false)令zuul過濾該請求,不進行路由
            currentContext.setSendZuulResponse(false);
            //設定返回的錯誤碼
            currentContext.setResponseStatusCode(401);
            currentContext.setResponseBody("userToken is null");
            return null;
        }
        // 否則正常執行業務邏輯.....
        return null;
    }
}

3-2、重啟springcloud-zuul服務,訪問:

    http://localhost:9999/api-b/feign/hello返回:userToken is null

    http://localhost:9999/api-b/feign/hello?userToken=""返回:spring cloud provider-01 hello world

在上面實現的過濾器程式碼中,我們通過繼承ZuulFilter抽象類並重寫了下面的四個方法來實現自定義的過濾器。這四個方法分別定義了:

  • filterType():過濾器的型別,它決定過濾器在請求的哪個生命週期中執行。這裡定義為pre,代表會在請求被路由之前執行。

  • filterOrder():過濾器的執行順序。當請求在一個階段中存在多個過濾器時,需要根據該方法返回的值來依次執行。通過數字指定,數字越大,優先順序越低。

  • shouldFilter():判斷該過濾器是否需要被執行。這裡我們直接返回了true,因此該過濾器對所有請求都會生效。實際運用中我們可以利用該函式來指定過濾器的有效範圍。

  • run():過濾器的具體邏輯。這裡我們通過currentContext.setSendZuulResponse(false)令 Zuul 過濾該請求,不對其進行路由,然後通過currentContext.setResponseStatusCode(401)設定了其返回的錯誤碼,當然我們也可以進一步優化我們的返回,比如,通過currentContext.setResponseBody(body)對返回 body 內容進行編輯等。

4、Zuul路由規則

4-1、預設路由規則

    由於Zuul引入Eureka閘道器的時候,會為每一個服務創鍵一個預設的路由規則,預設情況下例項名作為請求的字首,這樣不對外開放的服務也會被外界訪問到,我們可以控制一下為哪些服務創鍵路由規則。

zuul.ignored-services: *

*表示不為所有的服務建立預設的路由規則,則需要我們自己配置路由規則。

4-2、自定義路由規則

    為了相容客戶端不同版本,有時候需要我們為一組互相配合的微服務定義一個版本標識來方便管理,它們的版本關係,根據這個標識我們很容易的知道這些服務需要一起啟動並配合使用,比如我們的服務都採用以版本這樣的命名方式,例如:consumer-v1,consumer-v2分版本訪問服務的話,我們可以使用自定義路由規則,注入PatternServiceRouteMapper物件即可自動的構建類似 /v1/consumer/** 的路由規則

@Bean
public PatternServiceRouteMapper patternServiceRouteMapper(){
  return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)",
      "${version}/${name}");
}

    PatternServiceRouteMapper物件可以通過正則表示式來自定義服務與路由對映的生成關係。其中建構函式的

第一個引數: 用來匹配服務名稱是否符合該自定義規則的正則表示式,

第二個引數: 用來定義根據服務名中定義的內容轉換出的路徑表示式規則。

    當開發者在API閘道器中定義了PatternServiceRouteMapper實現之後,只要符合第一個引數定義規則的服務名,都會優先使用該實現構建出的路徑表示式,如果沒有匹配上的服務則還是會使用預設的路由對映規則,即採用完整服務名作為字首的路徑表示式。

5、路徑匹配

在上面案例上我們看到了使用萬用字元作為匹配路徑,一共有三種萬用字元,如下:

萬用字元萬用字元含義
? 匹配單個字元,如:/consumer/a,/consumer/b
* 匹配任意數量的字元,如:/consumer/abc,/consumer/def
** 匹配任意數量的字元,支援多級路徑,如:/consumer/abc/def,/consumer/ghi/gkl

    我們在使用的時候,比如我們的consumer服務路由路徑為:/consumer/**,由於發展還需要再次拆分出另外一個consumer-ext服務,路由規則為/consumer/ext/**,由於**匹配多級目錄這時候我們需要區別這些服務路徑,properties配置無法保證配置的載入順序,但在YML配置檔案中我們可以使用/consumer/ext/**配置在/consumer/**前面則可以保證consumer-ext服務的正常路由

6、忽略表示式

zuul.ignored-patterns: /**/hello/**

該配置表示忽略路由路徑包含hello的路徑

7、路由字首

zuul.prefix: /api
zuul.strip-prefix: true

    prefix:字首,當請求匹配字首時會進行代理

    strip-prefix:代理字首預設(true)會從請求路徑中移除,可以設定為false關閉移除代理字首動作,也可以通過zuul.routes.<route>.strip-prefix=false來對指定路由關閉移除代理字首動作。

    但是在《Spring Cloud微服務實戰》中指出Brixton.SR7和Camden.SR3中有Bug,該案例版本為Finchley.SR4未發現Bug

8、Zuul安全與Header

    敏感的Header設定,一般來說同一個系統中的服務之間共享Header,不過Zuul防止一些敏感的Header外洩,防止它們被傳遞到下游伺服器,如果我們需要傳遞Cookie,Set-Cookie,Authorization 這些資訊,我們可以這樣做

做法一全域性設定:將 zuul.sensitive-headers 的值設定為空
做法二指定路由設定:
zuul.routes.<route>.sensitiveHeaders: ''
zuul.routes.<route>.custom-sensitive-headers: true

9、忽略 Header

    可用 zuul.ignoredHeaders 屬性丟棄一些 Header,這樣設定後 Cookie 將不會傳播到其它微服務中

zuul.ignored-headers: Cookie

10、禁用指定的 Filter

zuul.<SimpleClassName>.<filterType>.disable=true

具體詳細配置參考Spring官網:https://cloud.spring.io/spring-cloud-static/Finchley.SR4/single/spring-cloud.html#_router_and_filter_zuul

八、Zuul的動態路由

    Zuul作為服務的統一入口,傳統方式將路由規則配置在配置檔案中,如果路由規則發生了改變,需要重啟伺服器,這就會對外界停止服務。這時候我們結合SpringCloud Config分散式配置中心《Spring Cloud第十篇 | 分散式配置中心Config》實現動態路由規則,此處不再演示訊息匯流排Bus《Spring Cloud第十二篇 | 訊息匯流排Bus》的使用。

1、配置中心服務端

1-1、為了保證以前配置中心服務端模組(springcloud-config-server)的整潔性,此處新建一個配置中心服務端模組命名為(springcloud-zuul-config-server)
1-2、新增依賴

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

1-3、配置application.yml檔案

spring:
  application:
    name: springcloud-zuul-config-server
  cloud:
    config:
      server:
        git:
          #配置git倉庫地址
          uri: https://gitee.com/coding-farmer/config-center
          #配置倉庫路徑
          search-paths: "{profile}"
          #訪問git倉庫的使用者名稱
          username:
          #訪問git倉庫的密碼
          password:
          #配置中心通過git從遠端git庫,有時本地的拷貝被汙染,
          #這時配置中心無法從遠端庫更新本地配置,設定force-pull=true,則強制從遠端庫中更新本地庫
          force-pull: true
          #預設從git倉庫克隆下載的在C:/Users/<當前使用者>/AppData/Local/Temp
          #basedir:
server:
  port: 8888
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8700/eureka
    #客戶端每隔30秒從Eureka服務上更新一次服務資訊
    registry-fetch-interval-seconds: 30
    #需要將我的服務註冊到eureka上
    register-with-eureka: true
    #需要檢索服務
    fetch-registry: true
  #心跳檢測檢測與續約時間
  instance:
    #告訴服務端,如果我10s之內沒有給你發心跳,就代表我故障了,將我剔除掉,預設90s
    #Eureka服務端在收到最後一次心跳之後等待的時間上限,單位為秒,超過則剔除(客戶端告訴服務端按照此規則等待自己)
    lease-expiration-duration-in-seconds: 10
    #每隔2s向服務端傳送一次心跳,證明自已依然活著,預設30s
    #Eureka客戶端向服務端傳送心跳的時間間隔,單位為秒(客戶端告訴服務端自己會按照該規則)
    lease-renewal-interval-in-seconds: 2
    # 啟用ip配置 這樣在註冊中心列表中看見的是以ip+埠呈現的
    prefer-ip-address: true
    # 例項名稱  最後呈現地址:ip:2002
    instance-id: ${spring.cloud.client.ip-address}:${server.port}

1-4、在啟動類上添加註解

@EnableConfigServer
@EnableEurekaClient

到此配置中心服務端搭建完成

2、修改zuul服務模組

2-1、新增配置中心客戶端依賴

​<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-config-client</artifactId>
</dependency>

2-2、將application.yml檔案改名為application-bak.yml(為了application.yml檔案),配置bootstrap.yml檔案

server:
  port: 9999
spring:
  application:
    name: springcloud-zuul
  cloud:
    config:
      #uri則表示配置中心的地址
      #uri: http://localhost:8888
      #注:config 客戶端在沒有 spring.cloud.config.name屬性的時候,服務端{application} 獲取的是客戶端
      #spring.application.name的值,否則,獲取的是 spring.cloud.config.name的值。
      #1)、當沒有spring.cloud.config.name時,客戶端獲取的是spring.application.name 所對應的git庫中的檔案,並且只能
      #獲取一個檔案,
      #2)、當一個專案中有需求要獲取多個檔案時,就需要用到spring.cloud.config.name這個屬性,以逗號分割
      name: configzuul
      profile: dev
      #label對應了label部分
      label: master
#      username:
#      password:
      discovery:
        #表示開啟通過服務名來訪問config-server
        enabled: true
        #則表示config-server的服務名
        service-id: springcloud-zuul-config-server
      #失敗快速響應
      fail-fast: true
      retry:
        #配置重試次數,預設為6
        max-attempts: 6
        #初始重試間隔時間,預設1000ms
        initial-interval: 1000
        #間隔乘數,預設1.1
        multiplier: 1.1
        #最大間隔時間,預設2000ms
        max-interval: 2000
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8700/eureka
    #客戶端每隔30秒從Eureka服務上更新一次服務資訊
    registry-fetch-interval-seconds: 30
    #需要將我的服務註冊到eureka上
    register-with-eureka: true
    #需要檢索服務
    fetch-registry: true
  #心跳檢測檢測與續約時間
  instance:
    #告訴服務端,如果我10s之內沒有給你發心跳,就代表我故障了,將我剔除掉,預設90s
    #Eureka服務端在收到最後一次心跳之後等待的時間上限,單位為秒,超過則剔除(客戶端告訴服務端按照此規則等待自己)
    lease-expiration-duration-in-seconds: 10
    #每隔2s向服務端傳送一次心跳,證明自已依然活著,預設30s
    #Eureka客戶端向服務端傳送心跳的時間間隔,單位為秒(客戶端告訴服務端自己會按照該規則)
    lease-renewal-interval-in-seconds: 2
    # 啟用ip配置 這樣在註冊中心列表中看見的是以ip+埠呈現的
    prefer-ip-address: true
    # 例項名稱  最後呈現地址:ip:2002
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
management:
  endpoints:
    web:
      exposure:
        include: ["info","health","refresh"]

2-3、配置重新整理類

@Configuration
public class Config {
​
    @RefreshScope
    @ConfigurationProperties("zuul")
    public ZuulProperties zuulProperties() {
        return new ZuulProperties();
    }
}

3、在程式碼倉庫新增配configzuul-dev.yml置檔案

4、啟動相關服務

    Eureka服務(springcloud-eureka-server),zuul的配置中心服務(springcloud-zuul-config-server)、提供者服務(springcloud-service-provider)、消費者服務(springcloud-service-consumer)、zuul服務(springcloud-zuul)

    訪問消費者服務介面:http://localhost:9999/api-a/consumer/hello?userToken=""

    訪問配置中心服務端:http://localhost:8888/configzuul-dev.yml,檢視配置結果如圖:

    修改倉庫config-center的configzuul-dev.yml配置為api-c接著在訪問配置中心倉庫配置結果,http://localhost:8888/configzuul-dev.yml,注意api-a的key不要修改

    然後傳送post請求zuul的refresh端點進行配置重新整理http://localhost:9999/actuator/refresh

    然後你會發現http://localhost:9999/api-a/consumer/hello?userToken=’‘路徑訪問不通了,訪問http://localhost:9999/api-c/consumer/hello?userToken=’'結果如圖,顯示頁面為

    到此zuul的動態重新整理完成,此處動態重新整理就是使用了配置中心的功能,不瞭解的可以參考《Spring Cloud第十篇 | 分散式配置中心Config》

 

詳細參考案例原始碼:https://gitee.com/coding-farmer/spirngcloud-learn