spring cloud zuul之路由功能和路由服務降級

1.介紹

Zuul是spring cloud中的微服務網關。網關: 是一個網絡整體系統中的前置門戶入口。請求首先通過網關,進行路徑的路由,定位到具體的服務節點上。也減少了客戶端與服務端的耦合,服務可以獨立發展,通過網關層來做映射

Zuul主要有兩大功能:路由轉發和過濾。路由轉發能夠爲全部服務提供一個唯一的入口,起到外部和內部隔離的作用,保障了後臺服務的安全性。過濾可以用來鑑權校驗,識別每個請求的權限,拒絕不符合要求的請求。

當它和config server組件配合時能夠實現動態路由,動態的將請求路由到不同的後端集羣中。

Zuul默認是實現了hystrix,ribbon和actuator

本項目使用的Spring Cloud 版本是:Hoxton.SR3

本章博客主要講以下內容:

1.zuul的路由功能

2.zuul怎麼使用Hystrix的服務斷路和服務降級

2.路由功能

要創建一個Zuul項目,很簡單:

2.1 引入pom文件

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

2.2 主類添加@EnableZuulProxy註解開啓網關服務功能

2.3 application.yml

spring:
  application:
    name: zuul-demo
server:
  port: 5555

這樣zuul就搭建完成了,然後我們可以通過在配置文件中對zuul進行配置來實現路由轉發。

2.4 傳統路由配置

2.4.1 單實例配置

在傳統的路由的單實例配置中,通過配置path和url來進行轉發,配置格式爲:zuul.routes.<routeName>.path和zuul.routes.<routeName>.url,如以下配置,<routeName>則表示api-a,該名稱可隨意定義。但是一組path和url映射關係的路由名要相同

zuul:
  routes: 
    api-a: 
      path: /apia/**
      url: http://localhost:8080/

像如上配置,當我們訪問http://localhost:5555/apia/hello時,會被轉發到http://localhost:8080/hello

2.4.2 多實例配置

 通過zuul.routes.<routeName>.path和zuul.routes.<routeName>.serviceId參數進行配置,然後利用ribbon的服務列表實現負載均衡(Zuul中自帶了對Ribbon的依賴)

zuul:
  routes:
    api-a:
      path: /apia/**
      serviceId: api-a
api-a:
  ribbon:
    listOfServers: http://localhost:8090/

傳統的路由模式中,無論是單實例還是多實例,我們都需要自己去維護路由名稱,且需要自己定義path和服務實例的列表,這樣無疑是很麻煩的。那我們可以將Eureka加進來,因爲Eureka中存在所有的服務列表。

2.5 服務路由配置(Zuul和Eureka整合)

通過Zuul和Eureka的整合,可以實現對服務實例的自動化維護,即我們不需要再爲每個serviceId指定具體的服務實例列表.但是我們依然是需要維護請求路徑的匹配表達式和服務名的映射關係,(也可不維護,會根據默認的路由規則生成)

爲Zuul項目添加Eureka的依賴,和@EnableDiscoveryClient註解。

現在我們啓動Eureka註冊中心,服務提供者端口8090,和Zuul項目

當我們將Zuul和Eureka整合後,只需要像如下配置即可:route名可隨意定義

zuul:
  routes:
    #route名,可隨意定義
    feign-service-provider:
      path: /feign-service-provider/**
      serviceId: feign-service-provider

我們也可以使用更簡單的配置方式:zuul.routes.<serviceId>=<path>,如上的配置等價於:

zuul:
  routes:
    feign-service-provider: /feign-service-provider/**

然後訪問http://localhost:5555/feign-service-provider/okuserpost,返回成功

2.5.1 通過接口的方式查看路由列表

首先需要在application.yml中添加如下配置,將actuator的/routes端口暴露出來,如果不添加配置,訪問該端口會報404

management:
  endpoints:
    web:
      exposure:
        include: "*"
 

然後訪問http://localhost:5555/actuator/routes,會出現以下json結果:

{
"/feign-service-provider/**": "feign-service-provider"
}

2.5.2 路由默認規則

由於再實際的運用過程中,大部分的路由配置規則幾乎都會採用服務名作爲外部請求的前綴,比如以下例子,其中path路徑的前綴使用了feign-service-provider,而對應的服務名也是feign-service-provider,對於這樣有規則性的配置內容,我們希望自動化的完成

zuul:
  routes:
    #route名,可隨意定義
    feign-service-provider:
      path: /feign-service-provider/**
      serviceId: feign-service-provider

當我們將Zuul和Eureka整合後,Zuul默認實現了這樣一套規則,該規則會自動的生成path和serviceId的關聯,這樣我們就不需要再爲這些服務維護這些基本的路由規則了。你啓動項目後,要稍微等一會刷新纔會有自動創建的路由列表(zuul需要向註冊中心註冊服務,並需要拉取註冊中心的 服務列表,這是需要一點時間的)

由於默認情況下所有Eureka上的服務都會被Zuul自動地創建路由,這回使得一些我們不希望對外開放的服務也會被外部訪問到,這時,我們可以使用zuul.ignored-services參數來設置一個服務名錶達式來定義不自動創建路由的規則,如當我們設置zuul.ignored-services=*時,Zuul對所有的服務都不自動創建規則。在這種情況下,我們要在配置文件中逐個爲需要路由的服務添加規則(使用zuul.routes.<serviceId>=<path>的方式)。

2.5.3 自定義路由

當我們在版本迭代時,一般都會通過服務名來判斷版本,比如userservice-v1,userservice-v2,那默認情況下Zuul自動創建的路由就爲/userservice-v1,/userservice-v2,但是這樣的表達不利於通過路徑規則進行版本管理,通常的路由表達式應該/v1/userservice,/v2/userversice.

要實現自定義,只需要增加如下Bean即可:

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

PatternServiceRouteMapper對象可以讓開發者通過正則表達式來自定義服務與路由映射的生成關係。構造函數第一個參數是用來匹配服務名稱是否符合該自定義規則的正則表達式,第二個參數是定義根據服務名中定義的內容轉換出的路徑表達式規則。當開發者在api網關中定義了PatternServiceRouteMapper實現之後,只需符合第一個參數定義規則的服務名,都會優先使用該實現構建出的表達式,如果沒有匹配上的服務規則則還是會使用默認的路由映射規則,即採用完整服務名作爲前綴的路徑表達式。

注意:name匹配的是服務名,不是路由名

2.5.4 路徑匹配

對path參數進行正則表達式匹配時,採用的時Ant風格,該風格有3中通配符

?:匹配任意單個字符,例:/user/?,可匹配/user/a,/user/b...

*: 匹配任意數量的字符,但是隻能匹配一級目錄,例:/user/*,可匹配/user/a,/user/aa....

**:匹配任意數量的字符,支持多級目錄,例:/user/**可匹配/user/a/b,/user/a/c

2.5.5 路徑優先級匹配

假設現有有兩個path:/user/**和/user/a/**

當我們請求/user/a/b時,我們肯定是希望能夠匹配上/user/a/**的,那路徑的優先級是怎樣的?

從源碼中可以發現,路由規則是通過LinkedHashMap來存儲的,即是有順序,從前往後匹配,當匹配上後,就會立即返回,並不會繼續執行,那路由規則的優先級即是在LinkedHashMap中的優先級。

在加載路徑規則時,是按照配置文件application.yml你編寫的順序來加載的,所以我們可以通過調整在配置文件中的順序來實現路徑的優先級匹配。那當我們請求/user/a/b時,我們的配置文件應該這樣來寫:

zuul:
  routes:
    userserviceA: /user/a/**
    userserviceB: /user/**

2.5.6 忽略表達式

當我們想忽略一些固定規則的path時,可使用zuul.ignored-patterns來指定Ant風格的正則表達式,例如:忽略/hello接口

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

2.5.7 路由前綴

如果想統一爲路由加個前綴,可以使用zuul.prefix屬性

zuul:
  prefix: /api

zuul.strip-prefix:

該配置是控制是否移除代理前綴。通俗來講就是當我們訪問請求地址,被zuul路由規則匹配上並轉發時,是否需要帶上前綴.

true表示移除,false表示不移除。

接下來舉個例子說明:我們先創建如下路由規則,前綴爲/service ,不需要代理前綴。假設userservice服務的端口是8080

zuul:
  prefix: /service
  strip-prefix: true
  routes: 
    userservice: /user/**

當我們訪問http://localhost:5555/service/user/hello時,其實會被代理去訪問http://localhost:8080/hello,即在代理時會除去前綴.

當strip-prefix的值設爲false時,表示在代理時需要前綴。同樣去訪問同一個上面的url,其實會被代理去訪問http://localhost:8080/service/hello

2.5.8 路由前綴的bug

在之前的版本會存在這樣一個bug(具體的版本可以測試以下,至少在Camden.SR3之前的版本存在該bug):

假設我現在設置的路由前綴爲/api,然後存在3個path:/api/a/**,/api-b/**,/cc/**

當啓動項目,路由創建路由規則時,會發現,/api/a/**,/api-b/**這兩個path創建出來的路由規則是不正確的(可通過/routes端口查看)且訪問也是錯誤的,只有/cc/**是正確的

所以這個bug的大致觸發情況就是當路由前綴和path的前綴相同是,會導致創建的路由規則錯誤。

在我使用的Hoxton.SR3版本中並沒有此bug,應該是在之前的哪個版本修復了

2.5.9 本地跳轉

zuul還支持forward形式的服務端跳轉,按如下配置即可:

zuul:
  routes:
    userservice:
      path: /api/**
      url: forward:/local

當我們訪問http://localhost:5555/api/hello時,會被轉發到本地的http://localhost:5555/local/hello

2.5.10 動態路由配置

動態路由主要是依賴config組件,我們知道,config client可以從config server上動態拉取配置。 那我們只需要在config server上的配置文件中配置路由屬性,然後由本地拉取即可,例如:我在config server上的配置文件中配置如下:

zuul:
  routes:
    userservice: /user/**
    orderservice: /order/**

在config client端即zuul項目中添加如下配置來動態刷新

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

3 Cookie

默認情況下,spring cloud zuul在請求路由時,會過濾掉http請求頭信息中一些敏感信息,防止它們被傳遞到下游的外部服務器。默認的敏感頭信息通過zuul.sensitiveHeaders參數定義,默認包括cookie,set-Cookie,authorization三個屬性。所以,我們在開發web項目時常用的cookie在spring cloud zuul網關中默認時不傳遞的,這就會引發一個常見的問題,如果我們要將使用了spring securityshiro等安全框架構建的web應用通過spring cloud zuul構建的網關來進行路由時,由於cookie信息無法傳遞,我們的web應用將無法實現登錄和鑑權。爲了解決這個問題,配置的方法有很多。

  • 通過設置全局參數爲空來覆蓋默認值,具體如下:
zuul:
  sensitive-headers: 

這種方法不推薦,雖然可以實現cookie的傳遞,但是破壞了默認設置的用意。在微服務架構的api網關之內,對於無狀態的restful api請求肯定時要遠多於這些web類應用請求的,甚至還有一些架構設計會將web類應用和app客戶端一樣歸爲api網關之外的客戶端應用。

  • 通過指定路由的參數來設置,方法有下面二種。
    方法一:對指定路由開啓自定義敏感頭。
    方法二:將指定路由的敏感頭設置爲空。
#方式一:
zuul:
  routes:
    userservice:
      custom-sensitive-headers = true
#方式二
zuul:
  routes:
    userservice:
      sensitive-headers:

比較推薦使用這二種方法,僅對指定的web應用開啓對敏感信息的傳遞,影響範圍小,不至於引起其他服務的信息泄露問題。

4. 使用Hystrix的功能來調整路由請求的超時時間等配置

由於zuul默認是依賴了Hystrix和Ribbon,所以我們在配置文件中編寫映射規則時,最好使用path+serviceId的方式

4.1  設置網關中路由轉發請求的HystrixCommand執行超時時間(單位毫秒)

當路由轉發請求的命令執行時間超過該配置值後,Hystrix會拋出異常,Zuul會對該異常進行處理並返回如下Json信息

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 1
{
    "timestamp": "2020-05-06T08:18:27.943+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "TIMEOUT"
}

4.2 設置路由轉發請求的時候,創建請求連接的超時時間

ribbon:
  SocketTimeout: 1

4.3 設置路由轉發請求的超時時間,該超時時間是對請求連接建立之後的處理時間

ribbon:
  ReadTimeout: 1

5. Zuul的服務降級

如果我們想對第4節的異常進行服務降級,需要創建一個如下的類:

public class MyFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        // 可指定路由,比如userservice,*表示針對所有的路由,如果出現Hystrix異常則進行以下的服務降級處理
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
        if (cause instanceof HystrixTimeoutException) {
            return response(HttpStatus.GATEWAY_TIMEOUT);
        } else {
            return response(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    private ClientHttpResponse response(final HttpStatus status) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return status;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return status.value();
            }

            @Override
            public String getStatusText() throws IOException {
                return status.getReasonPhrase();
            }

            @Override
            public void close() {
            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("fallback".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

然後將這個對象作爲Bean

@Bean
public MyFallbackProvider myFallbackProvider() {
	return new MyFallbackProvider();
}

接着我們再次訪問,會出現以下結果:

fallback

即直接返回一個字符串

 

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