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請求的生命週期
- HTTP 發送請求到 Zuul 網關
- Zuul 網關首先經過 pre filter
- 驗證通過後進入 routing filter,接着將請求轉發給遠程服務,遠程服務執行完返回結果,如果出
錯,則執行 error filter- 繼續往下執行 post filter
- 最後返回響應給 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