Spring Cloud Zuul 快速入門

服務網關和Zuul

爲什麼要有服務網關:

我們都知道在微服務架構中,系統會被拆分爲很多個微服務。那麼作爲客戶端要如何去調用這麼多的微服務呢?難道要一個個的去調用嗎?很顯然這是不太實際的,我們需要有一個統一的接口與這些微服務打交道,這就是我們需要服務網關的原因。

我們已經知道,在微服務架構中,不同的微服務可以有不同的網絡地址,各個微服務之間通過互相調用完成用戶請求,客戶端可能通過調用N個微服務的接口完成一個用戶請求。比如:用戶查看一個商品的信息,它可能包含商品基本信息、價格信息、評論信息、折扣信息、庫存信息等等,而這些信息獲取則來源於不同的微服務,諸如產品系統、價格系統、評論系統、促銷系統、庫存系統等等,那麼要完成用戶信息查看則需要調用多個微服務,這樣會帶來幾個問題:

  1. 客戶端多次請求不同的微服務,增加客戶端代碼或配置編寫的複雜性
  2. 認證繁雜,訪問每個服務都要進行一次認證
  3. 每個服務都通過http訪問,導致http請求增加,效率不高拖慢系統性能
  4. 多個服務存在跨域請求問題,處理起來比較複雜

如下圖所示:
Spring Cloud Zuul 快速入門

我們該如何解決這些問題呢?我們可以嘗試想一下,不要讓前端直接知道後臺諸多微服務的存在,我們的系統本身就是從業務領域的層次上進行劃分,形成多個微服務,這是後臺的處理方式。對於前臺而言,後臺應該仍然類似於單體應用一樣,一次請求即可,於是我們可以在客戶端和服務端之間增加一個API網關,所有的外部請求先通過這個微服務網關。它只需跟網關進行交互,而由網關進行各個微服務的調用。

這樣的話,我們就可以解決上面提到的問題,同時開發就可以得到相應的簡化,還可以有如下優點:

  1. 減少客戶端與微服務之間的調用次數,提高效率
  2. 便於監控,可在網關中監控數據,可以做統一切面任務處理
  3. 便於認證,只需要在網關進行認證即可,無需每個微服務都進行認證
  4. 降低客戶端調用服務端的複雜度

這裏可以聯想到一個概念,面向對象設計中的門面模式,即對客戶端隱藏細節,API網關也是類似的東西,只不過叫法不同而已。它是系統的入口,封裝了應用程序的內部結構,爲客戶端提供統一服務,一些與業務本身功能無關的公共邏輯可以在這裏實現,諸如認證、鑑權、監控、緩存、負載均衡、流量管控、路由轉發等等。示意圖:
Spring Cloud Zuul 快速入門

總結一下,服務網關大概就是四個功能:統一接入、流量管控、協議適配、安全維護。而在目前的網關解決方案裏,有Nginx+ Lua、Kong、Tyk以及Spring Cloud Zuul等等。這裏以Zuul爲例進行說明,它是Netflix公司開源的一個API網關組件,Spring Cloud對其進行二次封裝做到開箱即用。同時,Zuul還可以與Spring Cloud中的Eureka、Ribbon、Hystrix等組件配合使用。

可以說,Zuul實現了兩個功能,路由轉發和過濾器:

  • 路由轉發:接受請求,轉發到後端服務
  • 過濾器:提供一系列過濾器完成權限、日誌、限流等切面任務。
  • 可以說路由+過濾器=Zuul

服務網關的要素:

  • 網關作爲唯一的入口,所以穩定性和高可用是跑不了了
  • 以及具備良好的併發性能
  • 安全性,確保服務不被惡意訪問
  • 擴展性,網關容易成爲吞吐量的瓶頸,所以需要便於擴展

Zuul的四種過濾器API:

  • 前置(Pre)
  • 路由(Route)
  • 後置(Post)
  • 錯誤(Error)

zuul前後置過濾器的典型應用場景:

  • 前置(Pre)
    • 限流
    • 鑑權
    • 參數校驗調整
  • 後置(Post)
    • 統計
    • 日誌

Zuul的核心是一系列過濾器,開發者通過實現過濾器接口,可以做大量切面任務,即AOP思想的應用。Zuul的過濾器之間沒有直接的相互通信,而是通過本地ThreadLocal變量進行數據傳遞的。Zuul架構圖:
Spring Cloud Zuul 快速入門

在Zuul裏,一個請求的生命週期:
Spring Cloud Zuul 快速入門


Zuul:路由轉發,排除和自定義

本小節我們來學習如何使用服務網關,也就是Spring Cloud Zuul這個組件,首先新建一個項目,選擇如下模塊:
Spring Cloud Zuul 快速入門

pom.xml配置的依賴如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.SR1</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    <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>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

項目創建好後,將application.properties改爲bootstrap.yml,編輯內容如下:

spring:
  application:
    name: api-gateway
  cloud:
    config:
      discovery:
        enabled: true
        service-id: CONFIG
      profile: dev
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

注:我這裏使用了配置中心,若對此不熟悉的話,可以參考我另一篇文章:Spring Cloud Config - 統一配置中心

在啓動類中,加上@EnableZuulProxy註解,代碼如下:

package org.zero.springcloud.apigateway;

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

@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

完成以上配置後啓動這個項目,我這裏項目啓動是正常的。然後我們來通過這個網關訪問一下商品服務中獲取商品列表的接口。如下:
Spring Cloud Zuul 快速入門

訪問地址說明:

  • 該zuul項目跑在8951端口上
  • 第一個/product是需要訪問的服務的名稱
  • 後面跟的/buyer/product/list是商品服務中獲取商品列表的接口地址

只要是在eureka上註冊的服務都能夠通過zuul進行轉發,例如我通過zuul來訪問config的配置文件:
Spring Cloud Zuul 快速入門

如上,可以看到,報錯了,網關超時。這是因爲默認情況下,zuul的熔斷機制超時時間是2秒,當一個服務響應的時間較長就會報網關超時錯誤。

我們在配置文件中,加上如下超時時間的配置即可:
Spring Cloud Zuul 快速入門

ribbon.ReadTimeout, ribbon.SocketTimeout這兩個就是ribbon超時時間設置,當在yml寫時,應該是沒有提示的,給人的感覺好像是不是這麼配的一樣,其實不用管它,直接配上就生效了。

還有zuul.host.connect-timeout-millis, zuul.host.socket-timeout-millis這兩個配置,這兩個和上面的ribbon都是配超時的。區別在於,如果路由方式是serviceId的方式,那麼ribbon的生效,如果是url的方式,則zuul.host開頭的生效。(此處重要!使用serviceId路由和url路由是不一樣的超時策略)

如果你在zuul配置了熔斷fallback的話,熔斷超時也要配置,即hystrix那段配置。不然如果你配置的ribbon超時時間大於熔斷的超時,那麼會先走熔斷,相當於你配的ribbon超時就不生效了。

現在重啓項目,再次訪問之前的地址,就不會出現網關超時的錯誤了:
Spring Cloud Zuul 快速入門

之前我們訪問的都是GET類型的接口,我們來看看POST類型的是否能夠正常訪問。如下:
Spring Cloud Zuul 快速入門

每次請求某個服務的接口,都需要帶上這個服務的名稱。有沒有辦法可以自定義這個規則呢?答案是有的,在配置文件中,增加路由的自定義配置:

zuul:
  routes:
    myProduct:
      path: /myProduct/**
      serviceId: product

說明:

  • myProduct 自定義的前綴
  • path 匹配的地址
  • product 路由到哪個服務

重啓項目,測試如下:
Spring Cloud Zuul 快速入門

在項目啓動的時候,我們也可以在控制檯中查看到zuul所有的路由規則:
Spring Cloud Zuul 快速入門

如果我們有些服務的接口不希望對外暴露,只希望在服務間調用,那麼就可以在配置文件中,增加路由排除的配置。例如我不希望listForOrder被外部訪問,則在配置文件中,增加如下配置即可:

zuul:
  ...
  ignored-patterns:
    - /myProduct/buyer/product/listForOrder
    - /product/buyer/product/listForOrder

重啓項目,這時訪問就會報404了。如下:
Spring Cloud Zuul 快速入門

還可以使用通配符進行匹配。如下示例:

zuul:
  ...
  ignored-patterns:
    - /**/buyer/product/listForOrder

Zuul:Cookie和動態路由

我們在web開發中,經常會利用到cookie來保存用戶的登錄標識。但我們使用了zuul組件後,默認情況下,cookie是無法直接傳遞給服務的,因爲cookie默認被列爲敏感的headers。所以我們需要在配置文件中,將sensitiveHeaders的值置空。如下:

zuul:
  ...
  routes:
    myProduct:
      path: /myProduct/**
      serviceId: product
      sensitiveHeaders:  # 置空該屬性的值即可

我們每次配置路由信息都需要重啓項目,顯得很麻煩,在線上環境也不能這樣隨便重啓項目。所以我們得實現動態路由的功能,實現動態路由其實就利用一下我們之前實現的動態刷新配置文件的功能即可。首先把Zuul路由相關的配置剪切到git上,如下:
Spring Cloud Zuul 快速入門

注:我這裏使用了配置中心,若對此不熟悉的話,可以參考我另一篇文章:Spring Cloud Config - 統一配置中心

在pom.xml文件中,增加如下依賴項:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
</dependency>

然後在bootstrap.yml中,增加rabbitmq的配置。如下:

spring:
  application:
    name: api-gateway
  cloud:
    config:
      discovery:
        enabled: true
        service-id: CONFIG
      profile: dev
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

最後在項目中創建一個config包,在該包中創建一個ZuulConfig配置類,用於加載配置文件中的配置。代碼如下:

package org.zero.springcloud.apigateway.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.stereotype.Component;

/**
 * @program: api-gateway
 * @description: 網關路由配置類
 * @author: 01
 * @create: 2018-08-25 15:51
 **/
@Component
public class ZuulConfig {

    @RefreshScope
    @ConfigurationProperties("zuul")
    public ZuulProperties zuulProperties(){
        return new ZuulProperties();
    }
}

完成以上配置後,重啓項目,即可實現動態路由了,例如我現在把myProduct改成yourProduct,如下:
Spring Cloud Zuul 快速入門

此時無需重啓項目,訪問新的地址即可。如下:
Spring Cloud Zuul 快速入門


Zuul的高可用

  • 因爲Zuul也屬於一個微服務,所以我們將多個Zuul節點註冊到Eureka Server即可實現Zuul的高可用性
  • 將Nginx和Zuul “混搭”,利用nginx做負載均衡,轉發到多個Zuul上
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章