1. 程式人生 > 實用技巧 >SpringCloud 之 Netflix Zuul 服務閘道器

SpringCloud 之 Netflix Zuul 服務閘道器

本文較大篇幅引用https://www.mrhelloworld.com/zuul/,相關內容版權歸該文章作者所有

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

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

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

API Gateway(APIGW / API 閘道器),顧名思義,是出現在系統邊界上的一個面向 API 的、序列集中式的強管控服務,這裡的邊界是企業 IT 系統的邊界,可以理解為企業級應用防火牆

,主要起到隔離外部訪問與內部系統的作用。在微服務概念的流行之前,API 閘道器就已經誕生了,例如銀行、證券等領域常見的前置機系統,它也是解決訪問認證、報文轉換、訪問統計等問題的。

  API 閘道器的流行,源於近幾年來移動應用與企業間互聯需求的興起。移動應用、企業互聯,使得後臺服務支援的物件,從以前單一的 Web 應用,擴充套件到多種使用場景,且每種使用場景對後臺服務的要求都不盡相同。這不僅增加了後臺服務的響應量,還增加了後臺服務的複雜性。隨著微服務架構概念的提出,API 閘道器成為了微服務架構的一個標配元件

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

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

  • 聚合介面使得服務對呼叫者透明,客戶端與後端的耦合度降低
  • 聚合後臺服務,節省流量,提高效能,提升使用者體驗
  • 提供安全、流控、過濾、快取、計費、監控等 API 管理功能
  • 單體應用:瀏覽器發起請求到單體應用所在的機器,應用從資料庫查詢資料原路返回給瀏覽器,對於單體應用來說是不需要閘道器的。
  • 微服務:微服務的應用可能部署在不同機房,不同地區,不同域名下。此時客戶端(瀏覽器/手機/軟體工具)想要請求對應的服務,都需要知道機器的具體 IP 或者域名 URL,當微服務例項眾多時,這是非常難以記憶的,對於客戶端來說也太複雜難以維護。此時就有了閘道器,客戶端相關的請求直接傳送到閘道器,由閘道器根據請求標識解析判斷出具體的微服務地址,再把請求轉發到微服務例項。這其中的記憶功能就全部交由閘道器來操作了。

總結

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

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

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

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

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

  總結一下,閘道器應當具備以下功能:

  • 效能:API 高可用,負載均衡,容錯機制。
  • 安全:許可權身份認證、脫敏,流量清洗,後端簽名(保證全鏈路可信呼叫),黑名單(非法呼叫的限制)。
  • 日誌:日誌記錄,一旦涉及分散式,全鏈路跟蹤必不可少。
  • 快取:資料快取。
  • 監控:記錄請求響應資料,API 耗時分析,效能監控。
  • 限流:流量控制,錯峰流控,可以定義多種限流規則。
  • 灰度:線上灰度部署,可以減小風險。
  • 路由:動態路由規則。

Zuul 是 Netflix 公司開源的一個 API 閘道器元件,Spring Cloud 對其進行二次基於 Spring Boot 的註解式封裝做到開箱即用。

目前來說,結合 Sring Cloud 提供的服務治理體系,可以做到請求轉發,根據配置或者預設的路由規則進行路由和 Load Balance,無縫整合 Hystrix。

雖然可以通過自定義 Filter 實現我們想要的功能,但是由於 Zuul 本身的設計是基於單執行緒的接收請求和轉發處理,是阻塞 IO,不支援長連線。

目前來看 Zuul 就顯得很雞肋,隨著 Zuul 2.x 一直跳票(2019 年 5 月釋出了 Zuul 2.0 版本),Spring Cloud 推出自己的 Spring Cloud Gateway。

大意就是:Zuul 已死,Spring Cloud Gateway 永生(手動狗頭)。但是我們這裡還是先學一下

zuul-demo聚合工程。SpringBoot 2.2.4.RELEASESpring Cloud Hoxton.SR1

  • eureka-server:註冊中心
  • eureka-server02:註冊中心
  • product-service:商品服務,提供了根據主鍵查詢商品介面http://localhost:7070/product/{id}
  • order-service:訂單服務,提供了根據主鍵查詢訂單介面http://localhost:9090/order/{id}且訂單服務呼叫商品服務。

建立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>

    <!-- 專案依賴 -->
    <dependencies>
        <!-- spring cloud netflix zuul 依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

</project>
server:
  port: 9000 # 埠

spring:
  application:
    name: zuul-server # 應用名稱

啟動類需要開啟@EnableZuulProxy註解。

package com.example;

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

@SpringBootApplication
// 開啟 Zuul 註解
@EnableZuulProxy
public class ZuulServerApplication {

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

}

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

萬用字元含義:

訪問:http://localhost:9000/product-service/product/1 結果如下:

相當於訪問http://localhost:7070/product/1

服務名稱路由

 微服務一般是由幾十、上百個服務組成,對於 URL 地址路由的方式,如果對每個服務例項手動指定一個唯一訪問地址,這樣做顯然是不合理的。

 Zuul 支援與 Eureka 整合開發,根據 serviceId 自動從註冊中心獲取服務地址並轉發請求,這樣做的好處不僅可以通過單個端點來訪問應用的所有服務,而且在新增或移除服務例項時不用修改 Zuul 的路由配置。

1.新增 Eureka Client 依賴

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

2.配置註冊中心和路由規則

# 路由規則
zuul:
  routes:
    product-service:              # 路由 id 自定義
      path: /product-service/**   # 配置請求 url 的對映路徑
      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/

3.啟動類

package com.example;

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

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

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

}

4.訪問

訪問:http://localhost:9000/product-service/product/1 結果如下:

Zuul 為了方便大家使用,提供了預設路由配置:路由 id 和微服務名稱一致,path 預設對應/微服務名稱/**,所以以下配置就沒必要再寫了。

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

訪問

  此時我們並沒有配置任何訂單服務的路由規則,訪問:http://localhost:9000/order-service/order/1 結果如下:

我們可以通過路由排除設定不允許被訪問的資源。允許被訪問的資源可以通過路由規則進行設定。

# 路由規則
zuul:
  ignored-patterns: /**/order/**  # URL 地址排除,排除所有包含 /order/ 的路徑
  # 不受路由排除影響
  routes:
    product-service:              # 路由 id 自定義
      path: /product-service/**   # 配置請求 url 的對映路徑
      serviceId: product-service  # 根據 serviceId 自動從註冊中心獲取服務地址並轉發請求
# 路由規則
zuul:
  ignored-services: order-service # 服務名稱排除,多個服務逗號分隔,'*' 排除所有
  # 不受路由排除影響
  routes:
    product-service:              # 路由 id 自定義
      path: /product-service/**   # 配置請求 url 的對映路徑
      serviceId: product-service  # 根據 serviceId 自動從註冊中心獲取服務地址並轉發請求
zuul:
  prefix: /api

訪問

  訪問:http://localhost:9000/api/product-service/product/1 結果如下:

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 個抽象方法:

package com.example.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 閘道器過濾器
 */
@Component
public class CustomFilter extends ZuulFilter {

    private static final 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 {
        // 獲取請求上下文
        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 控制檯輸出如下:

CustomFilter...method=GET, url=http://localhost:9000/product-service/product/1

接下來我們在閘道器過濾器中通過 token 判斷使用者是否登入,完成一個統一鑑權案例。

package com.example.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 許可權驗證過濾器
 */
@Component
public class AccessFilter extends ZuulFilter {

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

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

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

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

    @Override
    public Object run() throws ZuulException {
        // 獲取請求上下文
        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 結果如下:

  • HTTP 傳送請求到 Zuul 閘道器
  • Zuul 閘道器首先經過 pre filter
  • 驗證通過後進入 routing filter,接著將請求轉發給遠端服務,遠端服務執行完返回結果,如果出錯,則執行 error filter
  • 繼續往下執行 post filter
  • 最後返回響應給 HTTP 客戶端
package com.example.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;

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

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

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

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

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

    @Override
    public Object run() throws ZuulException {
        RequestContext rc = RequestContext.getCurrentContext();
        Throwable throwable = rc.getThrowable();
        logger.error("ErrorFilter..." + throwable.getCause().getMessage(), throwable);
        // 響應狀態碼,HTTP 500 伺服器錯誤
        rc.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        // 響應型別
        rc.getResponse().setContentType("application/json; charset=utf-8");
        PrintWriter writer = null;
        try {
            writer = rc.getResponse().getWriter();
            // 響應內容
            writer.print("{\"message\":\"" + HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + "\"}");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != writer)
                writer.close();
        }
        return null;
    }

}

在 pre 過濾器中新增模擬異常程式碼。

// 模擬異常
Integer.parseInt("zuul");

禁用 Zuul 預設的異常處理 filter:SendErrorFilter

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

訪問:http://localhost:9000/product-service/product/1 結果如下:

在 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註解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@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/product-service/product/1?token=abc123 結果如下:

在 Edgware 版本之前,Zuul 提供了介面ZuulFallbackProvider用於實現 fallback 處理。從 Edgware 版本開始,Zuul 提供了介面FallbackProvider來提供 fallback 處理。

  Zuul 的 fallback 容錯處理邏輯,只針對 timeout 異常處理,當請求被 Zuul 路由後,只要服務有返回(包括異常),都不會觸發 Zuul 的 fallback 容錯邏輯。

因為對於Zuul閘道器來說,做請求路由分發的時候,結果由遠端服務運算。遠端服務反饋了異常資訊,Zuul 閘道器不會處理異常,因為無法確定這個錯誤是否是應用程式真實想要反饋給客戶端的。

ProductProviderFallback.java

package com.example.fallback;

import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

/**
 * 對商品服務做服務容錯處理
 */
@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 結果如下:

顧名思義,限流就是限制流量,就像你寬頻包有 1 個 G 的流量,用完了就沒了。

通過限流,我們可以很好地控制系統的 QPS,從而達到保護系統的目的。Zuul 閘道器元件也提供了限流保護。當請求併發達到閥值,自動觸發限流保護,返回錯誤結果。只要提供 error 錯誤處理機制即可。

比如 Web 服務、對外 API,這種型別的服務有以下幾種可能導致機器被拖垮:

  • 使用者增長過快(好事)
  • 因為某個熱點事件(微博熱搜)
  • 競爭物件爬蟲
  • 惡意的請求

  這些情況都是無法預知的,不知道什麼時候會有 10 倍甚至 20 倍的流量打進來,如果真碰上這種情況,擴容是根本來不及的

從上圖可以看出,對內而言:上游的 A、B 服務直接依賴了下游的基礎服務 C,對於 A,B 服務都依賴的基礎服務 C 這種場景,

服務 A 和 B 其實處於某種競爭關係,如果服務 A 的併發閾值設定過大,當流量高峰期來臨,有可能直接拖垮基礎服務 C 並影響服務 B,即雪崩效應。

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 會對代理的所有服務提供限流保護。

server:
  port: 9000 # 埠

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

# 配置 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:
  # 服務限流
  ratelimit:
    # 開啟限流保護
    enabled: true
    # 限流資料儲存方式
    repository: REDIS
    # default-policy-list 預設配置,全域性生效
    default-policy-list:
      - limit: 3
        refresh-interval: 60    # 60s 內請求超過 3 次,服務端就丟擲異常,60s 後可以恢復正常請求
        type:
          - origin
          - url
          - user

Zuul-RateLimiter 基本配置項:

Bucket4j 實現需要相關的 bean @Qualifier(“RateLimit”):

  • JCache - javax.cache.Cache
  • Hazelcast - com.hazelcast.core.IMap
  • Ignite - org.apache.ignite.IgniteCache
  • Infinispan - org.infinispan.functional.ReadWriteMap

Policy 限流策略配置項說明:

訪問

  訪問:http://localhost:9000/product-service/product/1?token=abc123 控制檯結果如下:

ErrorFilter...com.netflix.zuul.exception.ZuulException: 429 TOO_MANY_REQUESTS

使用區域性限流配置,Zuul 僅針對配置的服務提供限流保護。

zuul:
  # 服務限流
  ratelimit:
    # 開啟限流保護
    enabled: true
    # 限流資料儲存方式
    repository: REDIS
    # policy-list 自定義配置,區域性生效
    policy-list:
      # 指定需要被限流的服務名稱
      order-service:
        - limit: 5
          refresh-interval: 60  # 60s 內請求超過 5 次,服務端就丟擲異常,60s 後可以恢復正常請求
          type:
            - origin
            - url
            - user

訪問:http://localhost:9000/order-service/order/1?token=abc123 控制檯結果如下:

ErrorFilter...com.netflix.zuul.exception.ZuulException: 429 TOO_MANY_REQUESTS

 如果希望自己控制限流策略,可以通過自定義RateLimitKeyGenerator的實現來增加自己的策略邏輯。

  修改商品服務控制層程式碼如下,新增/product/single

package com.example.controller;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    @Autowired
    private ProductService productService;

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Product selectProductById(@PathVariable("id") Integer id) {
        return productService.selectProductById(id);
    }

    /**
     * 根據主鍵查詢商品
     *
     * @param id
     * @return
     */
    @GetMapping("/single")
    public Product selectProductSingle(Integer id) {
        return productService.selectProductById(id);
    }

}

自定義限流策略類。

package com.example.ratelimit;

import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties;
import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitKeyGenerator;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 自定義限流策略
 */
@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) {
        // 對請求引數中相同的 id 值進行限流
        return super.key(request, route, policy) + ":" + request.getParameter("id");
    }

}

多次訪問:http://localhost:9000/api/product-service/product/single?token=abc123&id=1 被限流後,馬上更換id=2重新訪問發現服務任然可用,再繼續多次訪問,發現更換過的id=2也被限流了。Redis 資訊如下:

127.0.0.1:6379> keys *
1) "zuul-server:product-service:0:0:0:0:0:0:0:1:/product/single:anonymous:1"
2) "zuul-server:product-service:0:0:0:0:0:0:0:1:/product/single:anonymous:2"

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

使用 Zuul 的 Spring Cloud 微服務結構圖:

從上圖中可以看出。整體請求邏輯還是比較複雜的,在沒有 Zuul 閘道器的情況下,client 請求 service 的時候,也有請求超時的可能。那麼當增加了 Zuul 閘道器的時候,請求超時的可能就更明顯了。

  當請求通過 Zuul 閘道器路由到服務,並等待服務返回響應,這個過程中 Zuul 也有超時控制。Zuul 的底層使用的是 Hystrix + Ribbon 來實現請求路由。

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                    # 請求連線的超時時間: 預設超時時間 1000ms
  ReadTimeout: 5000                       # 請求處理的超時時間: 預設超時時間 1000ms
  # 重試次數
  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重試註解。

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.retry.annotation.EnableRetry;

@SpringBootApplication
// 開啟 Zuul 註解
@EnableZuulProxy
// 開啟 EurekaClient 註解,目前版本如果配置了 Eureka 註冊中心,預設會開啟該註解
//@EnableEurekaClient
// 開啟資料監控註解
@EnableHystrixDashboard
// 開啟重試註解
@EnableRetry
public class ZuulServerApplication {

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

}

商品服務模擬超時。

package com.example.controller;

import com.example.pojo.Product;
import com.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@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 結果如下(觸發了閘道器服務降級):

配置後訪問:http://localhost:9000/product-service/product/1?token=abc123 結果如下:

至此 Zuul 服務閘道器所有的知識點就講解結束了。