Zuul服務網關二個功能請求的路由和過濾器使用

Zuul服務網關

1.理解Zuul

​ Zuul 是從設備和網站到應用程序後端的所有請求的前門。作爲邊緣服務應用程序,Zuul 旨在實現動
態路由,監視,彈性和安全性。Zuul 包含了對請求的路由和過濾兩個最主要的功能。

Zuul 是 Netflix 開源的微服務網關,它可以和 Eureka、Ribbon、Hystrix 等組件配合使用。Zuul 的核心是一系列的過濾器,這些過濾器可以完成以下功能

  • 審查與監控:在邊緣位置追蹤有意義的數據和統計結果,從而帶來精確的生產試圖
  • 壓力測試:逐漸增加只想集羣的流量,以瞭解性能
  • 靜態響應處理:在邊緣位置直接建立部份響應,從而避免其轉發到內部集羣\
  • 化,以及讓系統的邊緣更貼近系統的使用者
  • 身份認證與安全:識別每個資源的驗證要求,並拒絕那些與要求不符的請求
  • 動態路由:動態地將請求路由到不同的後端集羣
  • 負載分配:爲每一種負載類型分配對應容量,並棄用超出限定值的請求
  • 多區域彈性:跨越AWS Region進行請求路由,旨在實現ELB(Elastic Load Balancing)使用的多樣

2.什麼是服務網關

API Gateway(APIGW / API 網關),顧名思義,是出現在系統邊界上的一個面向 API 的、串行集中式的強管控服務,這裏的邊界是企業 IT 系統的邊界,可以理解爲 企業級應用防火牆 ,主要起到 隔離外部訪問與內部系統的作用 。在微服務概念的流行之前,API 網關就已經誕生了,例如銀行、證券等領域常見的前置機系統,它也是解決訪問認證、報文轉換、訪問統計等問題的。

API 網關是一個服務器,是系統對外的唯一入口。API 網關封裝了系統內部架構,爲每個客戶端提供
定製的 API。所有的客戶端和消費端都通過統一的網關接入微服務,在網關層處理所有非業務功能。API
網關並不是微服務場景中必須的組件,如下圖,不管有沒有 API 網關,後端微服務都可以通過 API 很好
地支持客戶端的訪問

但對於服務數量衆多、複雜度比較高、規模比較大的業務來說,引入 API 網關也有一系列的好處

  • 聚合接口使得服務對調用者透明,客戶端與後端的耦合度降低
  • 聚合後臺服務,節省流量,提高性能,提升用戶體驗
  • 提供安全、流控、過濾、緩存、計費、監控等 API 管理功能

3.爲什麼要使用網關

**單體應用:**瀏覽器發起請求到單體應用所在的機器,應用從數據庫查詢數據原路返回給瀏覽器,對
於單體應用來說是不需要網關的。
**微服務:**微服務的應用可能部署在不同機房,不同地區,不同域名下。此時客戶端(瀏覽器/手機/
軟件工具)想要請求對應的服務,都需要知道機器的具體 IP 或者域名 URL,當微服務實例衆多
時,這是非常難以記憶的,對於客戶端來說也太複雜難以維護。此時就有了網關,客戶端相關的請
求直接發送到網關,由網關根據請求標識解析判斷出具體的微服務地址,再把請求轉發到微服務實
例。這其中的記憶功能就全部交由網關來操作了。
在這裏插入圖片描述

總結

如果讓客戶端直接與各個微服務交互:

  • 客戶端會多次請求不同的微服務,增加了客戶端的複雜性
  • 存在跨域請求,在一定場景下處理相對複雜
  • 身份認證問題,每個微服務需要獨立身份認證
  • 難以重構,隨着項目的迭代,可能需要重新劃分微服務
  • 某些微服務可能使用了防火牆/瀏覽器不友好的協議,直接訪問會有一定的困難

因此,我們需要網關介於客戶端與服務器之間的中間層,所有外部請求率先經過微服務網關,客戶端只
需要與網關交互,只需要知道網關地址即可。這樣便簡化了開發且有以下優點

  • 易於監控,可在微服務網關收集監控數據並將其推送到外部系統進行分析
  • 易於認證,可在微服務網關上進行認證,然後再將請求轉發到後端的微服務,從而無需在每個微服務中進行認證
  • 減少了客戶端與各個微服務之間的交互次數

4.網關功能解決問題

在這裏插入圖片描述

網關具有身份認證與安全、審查與監控、動態路由、負載均衡、緩存、請求分片與管理、靜態響應
處理等功能。當然最主要的職責還是與“外界聯繫”。

5.環境準備

eureka-server :註冊中心
eureka-server02 :註冊中心
provider-service :商品服務,提供了根據主鍵查詢商品接口
http://localhost:7070/product/{id}
order-server :訂單服務,提供了根據主鍵查詢訂單接口
http://localhost:9090/order/{id}

6.Nginx實現API網關

下載

官網:http://nginx.org/en/download.html 下載穩定版。爲了方便學習,請下載 Windows 版本。

安裝

解壓文件後直接運行根路徑下的 nginx.exe 文件即可。
Nginx 默認端口爲 80,訪問:http://localhost:80/

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nS0EcQUD-1590394355468)(F:\A20200102\高級資源\微服務SpringCloud\img\nginx.png)]

配置路由規則

進入 Nginx 的 conf 目錄,打開 nginx.conf 文件,配置路由規則

server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }
		
		# 路由到商品服務
		location /api-product {
			proxy_pass http://localhost:7070/;
		} 
    #路由到訂單服務
		location /api-order {
			proxy_pass http://localhost:9090/;
		}
    .....
        .....
        
}
訪問

http://localhost/api-product/product/1

在這裏插入圖片描述
http://localhost/api-order/order/1
在這裏插入圖片描述

7.Zuul實現API網關

搭建網關服務
創建zuul-server 項目
添加依賴
<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>zuul-server</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 繼承父依賴 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>zuul-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <properties>
        <!-- 項目打包和編譯使用的編碼字符集以及 jdk 版本 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <!-- 項目依賴 -->
    <dependencies>
        <!-- spring cloud netflix zuul 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!-- netflix eureka client 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- spring cloud netflix hystrix dashboard 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <!-- spring cloud zuul ratelimit 依賴 -->
        <dependency>
            <groupId>com.marcosbarbero.cloud</groupId>
            <artifactId>spring-cloud-zuul-ratelimit</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>
        <!-- spring boot data redis 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- commons-pool2 對象池依賴 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- spring retry 依賴 -->
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
    </dependencies>

</project>

配置文件
spring:
  application:
    name: zuul-server # 應用名稱
  # redis 緩存
  redis:
    timeout: 10000        # 連接超時時間
    host: 192.168.10.101  # Redis服務器地址
    port: 6379            # Redis服務器端口
    password: root        # Redis服務器密碼
    database: 0           # 選擇哪個庫,默認0庫
    lettuce:
      pool:
        max-active: 1024  # 最大連接數,默認 8
        max-wait: 10000   # 最大連接阻塞等待時間,單位毫秒,默認 -1
        max-idle: 200     # 最大空閒連接,默認 8
        min-idle: 5       # 最小空閒連接,默認 0

server:
  port: 9000          # 端口

# 路由規則
zuul:
  # 服務限流
  ratelimit:
    # 開啓限流保護
    enabled: true
    # 限流數據存儲方式
    repository: REDIS
    # default-policy-list 默認配置,全局生效
#    default-policy-list:
#      - limit: 3
#        refresh-interval: 60    # 60s 內請求超過 3 次,服務端就拋出異常,60s 後可以恢復正常請求
#        quota: 30               # 請求時間總和不得超過 30 秒
#        type:
#          - origin
#          - url
#          - user
    # policy-list 自定義配置,局部生效
    policy-list:
      # 指定需要被限流的服務名稱
      order-service:
        - limit: 5
          refresh-interval: 60  # 60s 內請求超過 3 次,服務端就拋出異常,60s 後可以恢復正常請求
          quota: 30             # 請求時間總和不得超過 30 秒
          type:
            - origin
            - url
            - user
  # 禁用 Zuul 默認的異常處理 filter
  SendErrorFilter:
    error:
      disable: true
  #prefix: /api
  #ignored-patterns: /**/order/**  # URL 地址排除,排除所有包含 /order/ 的路徑
  #ignored-services: order-service # 服務名稱排除,多個服務逗號分隔,'*' 排除所有
  #routes:
    #product-service:              # 路由 id 自定義
      #path: /product-service/**   # 配置請求 url 的映射路徑
      #url: http://localhost:7070/ # 映射路徑對應的微服務地址
      #serviceId: product-service  # 根據 serviceId 自動從註冊中心獲取服務地址並轉發請求

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設置服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

# 度量指標監控與健康檢查
management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream

# Hystrix 超時時間設置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000  # 線程池隔離,默認超時時間 1000ms

# Ribbon 超時時間設置:建議設置小於 Hystrix
ribbon:
  ConnectTimeout: 5000                    # 請求連接的超時時間: 默認 5000ms
  ReadTimeout: 5000                       # 請求處理的超時時間: 默認 5000ms
  # 重試次數
  MaxAutoRetries: 1                       # MaxAutoRetries 表示訪問服務集羣下原節點(同路徑訪問)
  MaxAutoRetriesNextServer: 1             # MaxAutoRetriesNextServer表示訪問服務集羣下其餘節點(換臺服務器)
  # Ribbon 開啓重試
  OkToRetryOnAllOperations: true
啓動類
@SpringBootApplication
// 開啓 Zuul 註解
@EnableZuulProxy
public class ZuulServerApplication {

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

}
配置路由規則
URL地址路由
# 路由規則
zuul:
	routes:
		product-service: # 路由 id 自定義
			path: /product-service/** # 配置請求 url 的映射路徑
			url: http://localhost:7070/ # 映射路徑對應的微服務地址

通配符含義:

通配 符 含義 舉例 解釋
? 匹配任意單個字符 /product service/? /product-service/a,/product service/b,…
* 匹配任意數量字符不包括子 路徑 /product service/* /product-service/aa,/product service/bbb,…
** 匹配任意數量字符包括所有 下級路徑 /product service/** /product-service/aa,/product service/aaa/b/ccc

訪問:http://localhost:9000/product-service/product/1

服務名稱路由

​ 微服務一般是由幾十、上百個服務組成,對於 URL 地址路由的方式,如果對每個服務實例手動指定
一個唯一訪問地址,這樣做顯然是不合理的。
​ Zuul 支持與 Eureka 整合開發,根據 serviceId 自動從註冊中心獲取服務地址並轉發請求,這樣做好處不僅可以通過單個端點來訪問應用的所有服務,而且在添加或移除服務實例時不用修改 Zuul 的路
由配置。

添加依賴Eureka Client

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

配置文件

# 路由規則
zuul:
	routes:
		product-service: # 路由 id 自定義
			path: /product-service/** # 配置請求 url 的映射路徑
			serviceId: product-service #  自動從註冊中心獲取服務地址並轉發請求


# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設置服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

啓動類

@SpringBootApplication
// 開啓 Zuul 註解
@EnableZuulProxy
// 開啓 EurekaClient 註解,目前版本如果配置了 Eureka 註冊中心,默認會開啓該註解
//@EnableEurekaClient
public class ZuulServerApplication {

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

訪問:http://localhost:9000/product-service/product/1

簡化路由配置

Zuul 爲了方便大家使用提供了默認路由配置:如果路由 id 和 微服務名稱 一致的話,path 默認對
應 微服務名稱/** ,比如以下配置就沒必要再寫了

# 路由規則
zuul:
	routes:
		product-service: # 路由 id 自定義
			path: /product-service/** # 配置請求 url 的映射路徑
		

訪問:http://localhost:9000/order-service/order/1

路由排除

我們可以通過路由排除設置不允許被訪問的服務。允許被訪問的服務可以通過路由規則進行設置。

URL地址排除
# 路由規則
zuul:
    ignored-patterns: /**/order/** # URL 地址排除,排除所有包含 /order/ 的路徑
	# 不受路由排除影響
	routes:
		product-service: # 路由 id 自定義
			path: /product-service/** # 配置請求 url 的映射路
服務名稱排除
# 路由規則
zuul:
    ignored-services: order-service # 服務名稱排除,多個服務逗號分隔,'*' 排除所有
	# 不受路由排除影響
	routes:
		product-service: # 路由 id 自定義
			path: /product-service/** # 配置請求 url 的映射路
路由前綴
zuul:
	prefix: /api

訪問:http://localhost:9000/api/product-service/product/1

8.網關過濾器

​ Zuul 包含了對請求的路由和過濾兩個核心功能,其中路由功能負責將外部請求轉發到具體的微服務
實例上,是實現外部訪問統一入口的基礎;而過濾器功能則負責對請求的處理過程進行干預,是實現請
求校驗,服務聚合等功能的基礎。然而實際上,路由功能在真正運行時,它的路由映射和請求轉發都是
由幾個不同的過濾器完成的。
​ 路由映射主要通過 pre 類型的過濾器完成,它將請求路徑與配置的路由規則進行匹配,以找到需
要轉發的目標地址;而請求轉發的部分則是由 routing 類型的過濾器來完成,對 pre 類型過濾器
獲得的路由地址進行轉發。所以說,過濾器可以說是 Zuul 實現 API 網關功能最核心的部件,每一個進入
Zuul 的 http 請求都會經過一系列的過濾器處理鏈得到請求響應並返回給客戶端。

關鍵名詞
  • 類型:定義路由流程中應用過濾器的階段。共 pre、routing、post、error 4 個類型。
  • 執行順序:在同類型中,定義過濾器執行的順序。比如多個 pre 類型的執行順序。
  • 條件:執行過濾器所需的條件。true 開啓,false 關閉。
  • 動作:如果符合條件,將執行的動作。具體操作。
過濾器類型

**pre:**請求被路由到源服務器之前執行的過濾器
​ 身份認證
​ 選路由
​ 請求日誌
routing:處理將請求發送到源服務器的過濾器
**post:**響應從源服務器返回時執行的過濾器
​ 對響應增加 HTTP 頭
​ 收集統計和度量指標
​ 將響應以流的方式發送回客戶端
**error:**上述階段中出現錯誤時執行的過濾器

入門案例
創建過濾器

Spring Cloud Netflix Zuul 中實現過濾器必須包含 4 個基本特徵:過濾器類型,執行順序,執行條
件,動作(具體操作)。這些步驟都是 ZuulFilter 接口中定義的 4 個抽象方法:

/**
 * 網關過濾器
 */
@Component
public class CustomFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(CustomFilter.class);

    /**
     * 過濾器類型
     *      pre
     *      routing
     *      post
     *      error
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 過濾器執行順序,數字越小優先級越高
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 過濾器是否啓用,true 開啓 false 關閉
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 過濾器具體行爲
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        System.out.println("pre過濾器被執行...");
        // 獲取請求上下文
        RequestContext rc = RequestContext.getCurrentContext();
        HttpServletRequest request = rc.getRequest();
        logger.info("CustomFilter...method={}, url={}",
                request.getMethod(),
                request.getRequestURL().toString());
        return null;
    }

}

**filterType :**該函數需要返回一個字符串代表過濾器的類型,而這個類型就是在 http 請求過程
中定義的各個階段。在 Zuul 中默認定義了 4 個不同的生命週期過程類型,具體如下:
pre:請求被路由之前調用
routing: 路由請求時被調用
​ **post:**routing 和 error 過濾器之後被調用
​ **error:**處理請求時發生錯誤時被調用
filterOrder :通過 int 值來定義過濾器的執行順序,數值越小優先級越高。
shouldFilter :返回一個 boolean 值來判斷該過濾器是否要執行。
**run :**過濾器的具體邏輯。在該函數中,我們可以實現自定義的過濾邏輯,來確定是否要攔截當
前的請求,不對其進行後續路由,或是在請求路由返回結果之後,對處理結果做一些加工等。

訪問:http://localhost:9000/product-service/product/1

權限驗證案例

接下來我們在網關過濾器中通過 token 判斷用戶是否登錄,完成一個權限驗證案例

創建過濾器
/**
 * 權限驗證過濾器
 */
@Component
public class AccessFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(AccessFilter.class);

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 業務邏輯
     *
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        // Integer.parseInt("zuul");
        // 獲取請求上下文
        RequestContext rc = RequestContext.getCurrentContext();
        HttpServletRequest request = rc.getRequest();
        // 獲取表單中的 token
        String token = request.getParameter("token");
        // 業務邏輯處理
        if (null == token) {
            logger.warn("token is null...");
            // 請求結束,不在繼續向下請求。
            rc.setSendZuulResponse(false);
            // 響應狀態碼,HTTP 401 錯誤代表用戶沒有訪問權限
            rc.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            // 響應類型
            rc.getResponse().setContentType("application/json; charset=utf-8");
            PrintWriter writer = null;
            try {
                writer = rc.getResponse().getWriter();
                // 響應內容
                writer.print("{\"message\":\"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + "\"}");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (null != writer)
                    writer.close();
            }
        } else {
            // 使用 token 進行身份驗證
            logger.info("token is OK!");
        }
        return null;
    }

}

訪問:http://localhost:9000/product-service/product/1

http://localhost:9000/product-service/product/1?token=abc123

9.Zuul請求的生命週期

  1. HTTP 發送請求到 Zuul 網關
  2. Zuul 網關首先經過 pre filter
  3. 驗證通過後進入 routing filter,接着將請求轉發給遠程服務,遠程服務執行完返回結果,如果出
    錯,則執行 error filter
  4. 繼續往下執行 post filter
  5. 最後返回響應給 HTTP 客戶端

10.網關過濾器異常統一處理

創建過濾器
/**
 * 異常過濾器
 */
@Component
public class ErrorFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(ErrorFilter.class);

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 業務邏輯
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext rc = RequestContext.getCurrentContext();
        ZuulException exception = this.findZuulException(rc.getThrowable());
        logger.error("ErrorFilter..." + exception.errorCause, exception);

        HttpStatus httpStatus = null;
        if (429 == exception.nStatusCode)
            httpStatus = HttpStatus.TOO_MANY_REQUESTS;

        if (500 == exception.nStatusCode)
            httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;

        // 響應狀態碼
        rc.setResponseStatusCode(httpStatus.value());
        // 響應類型
        rc.getResponse().setContentType("application/json; charset=utf-8");
        PrintWriter writer = null;
        try {
            writer = rc.getResponse().getWriter();
            // 響應內容
            writer.print("{\"message\":\"" + httpStatus.getReasonPhrase() + "\"}");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != writer)
                writer.close();
        }
        return null;
    }

    private ZuulException findZuulException(Throwable throwable) {
        if (throwable.getCause() instanceof ZuulRuntimeException)
            return (ZuulException) throwable.getCause().getCause();

        if (throwable.getCause() instanceof ZuulException)
            return (ZuulException) throwable.getCause();

        if (throwable instanceof ZuulException)
            return (ZuulException) throwable;
        return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
    }
}
模擬異常

在 pre 過濾器中添加模擬異常代碼。

// 模擬異常
Integer.parseInt("zuul");
配置文件

禁用 Zuul 默認的異常處理 filter: SendErrorFilter

zuul:
	# 禁用 Zuul 默認的異常處理 filter
	SendErrorFilter:
		error:
			disable: true

訪問:http://localhost:9000/product-service/product/1

11.Zuul和Hystrix無縫結合

​ 在 Spring Cloud 中,Zuul 啓動器中包含了 Hystrix 相關依賴,在 Zuul 網關工程中,默認是提供了
Hystrix Dashboard 服務監控數據的(hystrix.stream),但是不會提供監控面板的界面展示。在 Spring Cloud中,Zuul 和 Hystrix 是無縫結合的,我們可以非常方便的實現網關容錯處理。

網關服務監控

​ Zuul 的依賴中包含了 Hystrix 的相關 jar 包,所以我們不需要在項目中額外添加 Hystrix 的依賴。
但是需要開啓數據監控的項目中要添加 dashboard 依賴

<!-- spring cloud netflix hystrix dashboard 依賴 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
配置文件

在配置文件中開啓 hystrix.stream 端點

# 度量指標監控與健康檢查
management:
	endpoints:
		web:
			exposure:
				include: hystrix.stream
啓動類

在需要開啓數據監控的項目啓動類中添加 @EnableHystrixDashboard 註解

@SpringBootApplication
// 開啓 Zuul 註解
@EnableZuulProxy
// 開啓數據監控註解
@EnableHystrixDashboard
public class ZuulServerApplication {

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

訪問:http://localhost:9000/hystrix

http://localhost:9000/actuator/hystrix.stream

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-S96DPMrM-1590394355475)(F:\A20200102\高級資源\微服務SpringCloud\img\hystrix.png)]

網關服務降級

​ 在 Edgware 版本之前,Zuul 提供了接口 ZuulFallbackProvider 用於實現 fallback 處理。從
Edgware 版本開始,Zuul 提供了接口 FallbackProvider 來提供 fallback 處理。
Zuul 的 fallback 容錯處理邏輯,只針對 timeout 異常處理,當請求被 Zuul 路由後,只要服務有返回(包括異常),都不會觸發 Zuul 的 fallback 容錯邏輯。

代碼示例

ProductProviderFallback.java

/**
 * 對商品服務做服務容錯處理
 */
@Component
public class ProductProviderFallback implements FallbackProvider {

    /**
     * return - 返回 fallback 處理哪一個服務。返回的是服務的名稱。
     * 推薦 - 爲指定的服務定義特性化的 fallback 邏輯。
     * 推薦 - 提供一個處理所有服務的 fallback 邏輯。
     * 好處 - 某個服務發生超時,那麼指定的 fallback 邏輯執行。如果有新服務上線,未提供 fallback 邏輯,有一個通用的。
     */
    @Override
    public String getRoute() {
        return "product-service";
    }

    /**
     * 對商品服務做服務容錯處理
     *
     * @param route 容錯服務名稱
     * @param cause 服務異常信息
     * @return
     */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            /**
             * 設置響應的頭信息
             * @return
             */
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders header = new HttpHeaders();
                header.setContentType(new MediaType("application", "json", Charset.forName("utf-8")));
                return header;
            }

            /**
             * 設置響應體
             * Zuul 會將本方法返回的輸入流數據讀取,並通過 HttpServletResponse 的輸出流輸出到客戶端。
             * @return
             */
            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("{\"message\":\"商品服務不可用,請稍後再試。\"}".getBytes());
            }

            /**
             * ClientHttpResponse 的 fallback 的狀態碼 返回 HttpStatus
             * @return
             */
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.INTERNAL_SERVER_ERROR;
            }

            /**
             * ClientHttpResponse 的 fallback 的狀態碼 返回 int
             * @return
             */
            @Override
            public int getRawStatusCode() throws IOException {
                return this.getStatusCode().value();
            }

            /**
             * ClientHttpResponse 的 fallback 的狀態碼 返回 String
             * @return
             */
            @Override
            public String getStatusText() throws IOException {
                return this.getStatusCode().getReasonPhrase();
            }

            /**
             * 回收資源方法
             * 用於回收當前 fallback 邏輯開啓的資源對象。
             */
            @Override
            public void close() {
            }
        };
    }
}

關閉商品服務,訪問:http://localhost:9000/product-service/product/1?token=abc123

網關服務限流

​ Zuul 網關組件也提供了限流保護。當請求並發達到閥值,自動觸發限流保護,返回錯誤結果。只要
提供 error 錯誤處理機制即可。

添加依賴

Zuul 的限流保護需要額外依賴 spring-cloud-zuul-ratelimit 組件,限流數據採用 Redis 存儲所以還要
添加 Redis 組件。

RateLimit 官網文檔:https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit

 <!-- spring cloud zuul ratelimit 依賴 -->
        <dependency>
            <groupId>com.marcosbarbero.cloud</groupId>
            <artifactId>spring-cloud-zuul-ratelimit</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>
        <!-- spring boot data redis 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- commons-pool2 對象池依賴 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
全侷限流配置

使用全侷限流配置,Zuul 會對代理的所有服務提供限流保護。

spring:
  application:
    name: zuul-server # 應用名稱
  # redis 緩存
  redis:
    timeout: 10000        # 連接超時時間
    host: 192.168.10.101  # Redis服務器地址
    port: 6379            # Redis服務器端口
    password: root        # Redis服務器密碼
    database: 0           # 選擇哪個庫,默認0庫
    lettuce:
      pool:
        max-active: 1024  # 最大連接數,默認 8
        max-wait: 10000   # 最大連接阻塞等待時間,單位毫秒,默認 -1
        max-idle: 200     # 最大空閒連接,默認 8
        min-idle: 5       # 最小空閒連接,默認 0

server:
  port: 9000          # 端口

# 路由規則
zuul:
  # 服務限流
  ratelimit:
    # 開啓限流保護
    enabled: true
    # 限流數據存儲方式
    repository: REDIS
    # default-policy-list 默認配置,全局生效
    default-policy-list:
      - limit: 3
        refresh-interval: 60    # 60s 內請求超過 3 次,服務端就拋出異常,60s 後可以恢復正常請求
        quota: 30               # 請求時間總和不得超過 30 秒
        type:
          - origin
          - url
          - user
 
# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設置服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/

Zuul-RateLimiter 基本配置項:

配置項 可選值 說明
enabled true/false 是否啓用限流
repository REDIS:基於 Redis,使用時必須引入 Redis 相關依賴 CONSUL:基於 Consul JPA:基於 SpringDataJPA,需要用到數據 庫 使用 Java 編寫的基於令牌桶算法的限流 庫: BUCKET4J_JCACHE BUCKET4J_HAZELCAST BUCKET4J_IGNITE BUCKET4J_INFINISPAN 限流數據的存儲方式,無默認值 必填項
key-prefix String 限流 key 前綴
default policy-list List of Policy 默認策略
policy-list Map of Lists of Policy 自定義策略
post-filter order - postFilter 過濾順序
pre-filter order - preFilter 過濾順序

Bucket4j 實現需要相關的 bean @Qualifier(“RateLimit”):
JCache - javax.cache.Cache
Hazelcast - com.hazelcast.core.IMap
Ignite - org.apache.ignite.IgniteCache
Infinispan - org.infinispan.functional.ReadWriteMap

Policy 限流策略配置項說明:

說明
limit 單位時間內請求次數限制
quota 單位時間內累計請求時間限制(秒),非必要參數
refresh interval 單位時間(秒),默認 60 秒
type 限流方式: ORIGIN:訪問 IP 限流 URL:訪問 URL 限流 USER:特定用戶或用戶組限流(比如:非會員用戶限制每分鐘只允許下載一個 文件) URL_PATTERN ROLE HTTP_METHOD

訪問:http://localhost:9000/product-service/product/1?token=abc123

局部限流配置

使用局部限流配置,Zuul 僅針對配置的服務提供限流保護。全局配置和局部配置可同時存在,局部
優先級高於全局。

# 路由規則
zuul:
  # 服務限流
  ratelimit:
    # 開啓限流保護
    enabled: true
    # 限流數據存儲方式
    repository: REDIS
    # default-policy-list 默認配置,全局生效
#    default-policy-list:
#      - limit: 3
#        refresh-interval: 60    # 60s 內請求超過 3 次,服務端就拋出異常,60s 後可以恢復正常請求
#        quota: 30               # 請求時間總和不得超過 30 秒
#        type:
#          - origin
#          - url
#          - user
    # policy-list 自定義配置,局部生效
    policy-list:
      # 指定需要被限流的服務名稱
      order-service:
        - limit: 5
          refresh-interval: 60  # 60s 內請求超過 3 次,服務端就拋出異常,60s 後可以恢復正常請求
          quota: 30             # 請求時間總和不得超過 30 秒
          type:
            - origin
            - url
            - user
自定義限流策略
/**
 * 自定義限流策略
 */
@Component
public class RateLimitKeyGenerator extends DefaultRateLimitKeyGenerator {

    public RateLimitKeyGenerator(RateLimitProperties properties, RateLimitUtils rateLimitUtils) {
        super(properties, rateLimitUtils);
    }

    /**
     * 限流邏輯
     *
     * @param request
     * @param route
     * @param policy
     * @return
     */
    @Override
    public String key(HttpServletRequest request, Route route, RateLimitProperties.Policy policy) {
        // 對請求參數中相同的 token 值進行限流
        return super.key(request, route, policy) + ":" + request.getParameter("token");
    }

}

多次訪問:http://localhost:9000/product-service/product/1?token=abc123

錯誤處理

配置 error 類型的網關過濾器進行處理即可。修改之前的 ErrorFilter 讓其變的通用

/**
 * 異常過濾器
 */
@Component
public class ErrorFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(ErrorFilter.class);

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 業務邏輯
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext rc = RequestContext.getCurrentContext();
        ZuulException exception = this.findZuulException(rc.getThrowable());
        logger.error("ErrorFilter..." + exception.errorCause, exception);

        HttpStatus httpStatus = null;
        if (429 == exception.nStatusCode)
            httpStatus = HttpStatus.TOO_MANY_REQUESTS;

        if (500 == exception.nStatusCode)
            httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;

        // 響應狀態碼
        rc.setResponseStatusCode(httpStatus.value());
        // 響應類型
        rc.getResponse().setContentType("application/json; charset=utf-8");
        PrintWriter writer = null;
        try {
            writer = rc.getResponse().getWriter();
            // 響應內容
            writer.print("{\"message\":\"" + httpStatus.getReasonPhrase() + "\"}");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != writer)
                writer.close();
        }
        return null;
    }

    private ZuulException findZuulException(Throwable throwable) {
        if (throwable.getCause() instanceof ZuulRuntimeException)
            return (ZuulException) throwable.getCause().getCause();

        if (throwable.getCause() instanceof ZuulException)
            return (ZuulException) throwable.getCause();

        if (throwable instanceof ZuulException)
            return (ZuulException) throwable;
        return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
    }
}
網關性能調優

​ Zuul 中的 Hystrix 內部使用線程池隔離機制提供請求路由實現,其默認的超時時長爲 1000 毫秒。
Ribbon 底層默認超時時長爲 5000 毫秒。如果 Hystrix 超時,直接返回超時異常。如果 Ribbon 超時,同時 Hystrix 未超時,Ribbon 會自動進行服務集羣輪詢重試,直到 Hystrix 超時爲止。如果 Hystrix 超時時長小於 Ribbon 超時時長,Ribbon 不會進行服務集羣輪詢重試。

配置文件

Zuul 中可配置的超時時長有兩個位置:Hystrix 和 Ribbon

zuul:
	# 開啓 Zuul 網關重試
	retryable: true

# Hystrix 超時時間設置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000  # 線程池隔離,默認超時時間 1000ms

# Ribbon 超時時間設置:建議設置小於 Hystrix
ribbon:
  ConnectTimeout: 5000                    # 請求連接的超時時間: 默認 5000ms
  ReadTimeout: 5000                       # 請求處理的超時時間: 默認 5000ms
  # 重試次數
  MaxAutoRetries: 1                       # MaxAutoRetries 表示訪問服務集羣下原節點(同路徑訪問)
  MaxAutoRetriesNextServer: 1             # MaxAutoRetriesNextServer表示訪問服務集羣下其餘節點(換臺服務器)
  # Ribbon 開啓重試
  OkToRetryOnAllOperations: true
添加依賴

Spring Cloud Netflix Zuul 網關重試機制需要使用 spring-retry 組件。

<!-- spring retry 依賴 -->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
啓動類

啓動類需要開啓 @EnableRetry 重試註解

@SpringBootApplication
// 開啓 EurekaClient 註解,目前版本如果配置了 Eureka 註冊中心,默認會開啓該註解
//@EnableEurekaClient
// 開啓 Zuul 註解
@EnableZuulProxy
// 開啓數據監控註解
@EnableHystrixDashboard
// 開啓重試註解
@EnableRetry
public class ZuulServerApplication {

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

}
模擬超時

商品服務模擬超時

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Product selectProductById(@PathVariable("id") Integer id) {
       try {
           Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return productService.selectProductById(id);
    }
}

配置前訪問:http://localhost:9000/product-service/product/1?token=abc123

12.Zuul和Sentinel整合

網關服務限流和降級
創建zuul-server-sentinel 項目
添加依賴

單獨使用添加 sentinel-zuul-adapter 依賴即可。
若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway 依賴,
同時需要添加 spring-cloud-starter-netflix-zuul 依賴來讓 spring-cloud-alibaba-sentinelgateway 模塊裏的 Zuul 自動化配置類生效。
同時請將 spring.cloud.sentinel.filter.enabled 配置項置爲 false(若在網關流控控制檯
上看到了 URL 資源,就是此配置項沒有置爲 false)

<?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">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>zuul-server-sentinel</artifactId>

    <!-- 繼承父依賴 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>zuul-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <properties>
        <!-- 項目打包和編譯使用的編碼字符集以及 jdk 版本 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <!-- 項目依賴 -->
    <dependencies>
        <!-- spring cloud netflix zuul 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!-- netflix eureka client 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 單獨使用 -->
        <!-- sentinel zuul adapter 依賴 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-zuul-adapter</artifactId>
        </dependency>
        <!-- 和 Sentinel Starter 配合使用 -->
        <!--
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        -->
    </dependencies>

</project>

配置文件
spring:
  application:
    name: zuul-server-sentinel # 應用名稱
  cloud:
    sentinel:
      filter:
        enabled: false

server:
  port: 9001          # 端口

# 配置 Eureka Server 註冊中心
eureka:
  instance:
    prefer-ip-address: true       # 是否使用 ip 地址註冊
    instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
  client:
    service-url:                  # 設置服務註冊中心地址
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
網關服務配置類

配置網關服務過濾器和網關限流規則

/**
 * 網關服務配置類
 */
@Configuration
public class ZuulConfig {

    // 底層繼承了 ZuulFilter
    @Bean
    public ZuulFilter sentinelZuulPreFilter() {
        // We can also provider the filter order in the constructor.
        return new SentinelZuulPreFilter();
    }

    // 底層繼承了 ZuulFilter
    @Bean
    public ZuulFilter sentinelZuulPostFilter() {
        return new SentinelZuulPostFilter();
    }

    // 底層繼承了 ZuulFilter
    @Bean
    public ZuulFilter sentinelZuulErrorFilter() {
        return new SentinelZuulErrorFilter();
    }

    /**
     * Spring 容器初始化的時候執行該方法
     */
    @PostConstruct
    public void doInit() {
        // 註冊 FallbackProvider
        ZuulBlockFallbackManager.registerProvider(new OrderBlockFallbackProvider());
        // 加載網關限流規則
        initGatewayRules();
    }

    /**
     * 網關限流規則
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        /*
            resource:資源名稱,可以是網關中的 route 名稱或者用戶自定義的 API 分組名稱
            count:限流閾值
            intervalSec:統計時間窗口,單位是秒,默認是 1 秒
         */
        rules.add(new GatewayFlowRule("order-service")
                .setCount(3) // 限流閾值
                .setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是 1 秒
        // 加載網關限流規則
        GatewayRuleManager.loadRules(rules);
    }

}

多次訪問:http://localhost:9001/order-service/order/1

自定義限流處理(網關服務降級)

發生限流之後的處理流程 :
發生限流之後可自定義返回參數,通過實現 ZuulBlockFallbackProvider 接口,默認的實現
DefaultBlockFallbackProvider
默認的 fallback route 的規則是 route ID 或自定義的 API 分組名稱。

編寫限流處理類
/**
 * 對訂單服務做服務容錯處理
 */
public class OrderBlockFallbackProvider implements ZuulBlockFallbackProvider {

    private Logger logger = LoggerFactory.getLogger(OrderBlockFallbackProvider.class);

    @Override
    public String getRoute() {
        return "order-service"; // 服務名稱
    }

    @Override
    public BlockResponse fallbackResponse(String route, Throwable cause) {
        logger.error("{} 服務觸發限流", route);
        if (cause instanceof BlockException) {
            return new BlockResponse(429, "服務訪問壓力過大,請稍後再試。", route);
        } else {
            return new BlockResponse(500, "系統錯誤,請聯繫管理員。", route);
        }
    }
}
將限流處理類註冊至 Zuul 容器
 /**
     * Spring 容器初始化的時候執行該方法
     */
    @PostConstruct
    public void doInit() {
        // 註冊 FallbackProvider
        ZuulBlockFallbackManager.registerProvider(new OrderBlockFallbackProvider());
        // 加載網關限流規則
        initGatewayRules();
    }

http://localhost:9001/order-service/order/1

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