SpringCloud 之 Netflix Zuul 服務閘道器
本文較大篇幅引用https://www.mrhelloworld.com/zuul/,相關內容版權歸該文章作者所有
什麼是 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.RELEASE
、Spring Cloud Hoxton.SR1
。
eureka-server
:註冊中心eureka-server02
:註冊中心product-service
:商品服務,提供了根據主鍵查詢商品介面http://localhost:7070/product/{id}
order-service
:訂單服務,提供了根據主鍵查詢訂單介面http://localhost:9090/order/{id}
且訂單服務呼叫商品服務。
Zuul 實現 API 閘道器
搭建閘道器服務
1.建立專案
建立zuul-server
專案。
2.新增依賴
<?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>
3.配置檔案
server: port: 9000 # 埠 spring: application: name: zuul-server # 應用名稱
4.啟動類
啟動類需要開啟@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); } }
配置路由規則
URL 地址路由
# 路由規則 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 結果如下:
路由排除
我們可以通過路由排除設定不允許被訪問的資源。允許被訪問的資源可以通過路由規則進行設定。
1.URL 地址排除
# 路由規則 zuul: ignored-patterns: /**/order/** # URL 地址排除,排除所有包含 /order/ 的路徑 # 不受路由排除影響 routes: product-service: # 路由 id 自定義 path: /product-service/** # 配置請求 url 的對映路徑 serviceId: product-service # 根據 serviceId 自動從註冊中心獲取服務地址並轉發請求
2.服務名稱排除
# 路由規則 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:上述階段中出現錯誤時執行的過濾器
入門案例
1.建立過濾器
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 結果如下:
Zuul 請求的生命週期
- 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 結果如下:
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
註解。
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 錯誤處理機制即可。
1.,為什麼需要限流
比如 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 服務閘道器所有的知識點就講解結束了。