學習視頻:https://www.bilibili.com/video/BV1io4y1m72G?p=1
1. 什么是Spring Cloud Gateway
Spring Cloud Gateway作為Spring Cloud生態系統中的網關,目標是替代 Netfix Zuul,Zuul並不僅提供統一的路由方式,並且還基於Filter 鏈的方式提供了網關的基本功能。目前最新版 Spring Cloud 中引用的還是Zuul 1.x版本,而這個版本是基於過濾器的,是阻塞IO,並不支持長連接。
Spring Cloud Gateway 是基於Spring 生態系統之上狗叫的API網關,包括Spring5,Spring Boot 2 和 Project Reactor。Spring Cloud Gateway 旨在提供一種簡單而有效的方法來路由到API,並為它們提供跨領域的關注點,例如:安全性,監視/指標,限流等。 由於Spring 5.0 支持Netty,Http2,而Spring Boot 2.0 支持 Spring 5.0,因此Spring Cloud Gateway 支持Netty 和 Http2 順利成章。
2. 什么是服務網關
API Gateway(APIGW / API網關),顧名思義,是出現在系統邊界上的一個面向API的、串行集中式的強管控服務,這里的邊界是企業IT系統的邊界,可以理解為 企業級應用防火牆,主要起到 隔離外部訪問與內部系統的作用。在微服務概念的流行之前,API網關就已經誕生了,例如銀行、證券等領悅常見的前置機系統,它也是解決訪問認證、報文轉換、訪問統計等問題的。
API 網關的流行,源於近幾年來移動應用與企業間互聯需求的興起。移動應用、企業互聯,使得后台服務支持的對象,從以前單一個Web應用,擴展到多種使用場景,且每種引用場景對后台服務的要求都不盡相同。這不僅增加了后台服務的響應量,還增加了后台服務的復雜性。隨着微服務架構概念的提出,API 網關成了微服務架構的一個標配組件。
API 網關是一個服務器,是系統對外的唯一入口。API網關封裝了系統內部架構,為每個客戶端提供定制的API。所有的客戶端和消費端都通過統一的網關接入微服務,在網關層處理所有非業務功能。
API網關並不是微服務場景中必須的組件。但對於服務數量眾多、復雜度比較高、規模比較大的業務來說,引入API網關也有一系列的好處:
(1)聚合接口,使服務對調用者透明,客戶端和后端的耦合度降低;
(2)聚合后台服務,節省流量,提高性能,提升用戶體驗;
(3)提供安全、流控、過濾、緩存、計費、監控等API管理功能;
3. 為什么要使用網關
單體應用:瀏覽器發起請求到單體應用所在的機器,應用從數據庫查詢數據原路返回給瀏覽器,對於單體應用來說不需要網關的。
微服務:微服務的應用可能部署在不同機房、不同地區、不同域名下。此時客戶端(瀏覽器/手機/軟件工具)想要請求對應的服務,都需要知道機器的具體IP或者域名URL,當微服務實例眾多時,這是非常難以記憶的,對於客戶端來說也太復雜難以維護。此時就有了網關,客戶端相關的請求直接發送到網關,由網關根據請求標識解析判斷出具體的微服務地址,再把騎牛轉發到微服務實例。這其中的記憶功能就全部交由網關來操作了。
總結:
如果讓客戶端直接與各個微服務交互:
(1)客戶端會多次請求不通的微服務,增加了客戶端的復雜性
(2)存在跨域請求,在一定場景下處理相對復雜;
(3)身份認證問題,每個微服務需要獨立身份認證;
(4)難以重構,隨着項目的迭代,可能需要重新划分微服務;
(5)某些微服務可能使用了防火牆/瀏覽器不友好的協議,直接訪問會有一定的困難;
因此,我們需要網關介於客戶端與服務器之間的中間層,所有外部請求率先經過微服務網關,客戶端只需要與網關教育,只需要知道網關地址即可。這樣便簡化了開發且有一些優點:
(1)易於監控,可在微服務網關手機監控數據並將其推送到外部系統進行分析;
(2)易於認證,可在微服務網關上進行認證,然后再將請求轉發到后端的微服務,從而無需在每個微服務中進行認證;
(3)減少了客戶端與各個微服務之間的交互次數;
網關具有身份認證與安全、審查與監控、動態路由、緩存、請求分片與管理、靜態響應處理等功能。當然最主要的職責還是與“外界聯系”。總結一下,網關應當具備以下功能:
性能:API高可用,負載均衡,容錯機制
安全:權限身份認證、脫敏、流量清洗、后端簽名(保證全鏈路可信調用),黑名單(非法調用的限制)
日志:日志記錄,一旦涉及分布式,全鏈路跟蹤必不可少
緩存:數據緩存。
監控:記錄請求響應數據,API耗時分析,性能監控。
限流:流向控制,錯峰流控,可以自定義多種限流規則。
灰度:線上灰度部署,可以減小風險。
路由:動態路由規則。
4. 環境准備
在開發之前我們要准備以下幾個項目
注冊中心:eureka-server 、eureka-server02
商品服務:product-service 提供了服務主鍵查詢商品接口 http://localhost:7070/product/{id}
訂單服務:order-service 提供了根據主鍵查詢訂單接口 http://localhost:9000/order/{id} 且訂單服務調用商品服務。
具體代碼就不提供了,大家可以按照自己的業務邏輯去模擬。
5. Gateway 實現API網關
Gateway官網:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/
5.1 核心概念
路由(Route):路由是網關最基礎的部分,路由信息由ID、目標URI、一組斷言和一個過濾器組成。如果斷言路由為真,則說明請求的URI和配置匹配。
斷言(Predicate):Java8中的斷言函數。Spring Cloud Gateway中的斷言函數輸入類型是Spring5.0框架中的 ServerWebExchange。Spring Cloud Gateway 中的斷言函數允許開發者去自定義匹配來自於 Http Request 中的任何信息,比如請求頭和參數等。
過濾器(Filter):一個標准的Spring Web Filter。Spring Cloud Gateway 中的Filter 分為兩種類型,分別是Gateway Filter 和Global Filter。過濾器將會對請求和影響進行處理。
5.2 工作原理
這是Spring Cloud Gateway官網上提供的工作原理圖。如上圖所示,客戶端向Spring Cloud Gateway 發出請求。再由網關處理程序 Gateway Handler Mapping 映射確定與請求向匹配的路由,將其發送到網關Web處理程序 Gateway Web Handler。該處理器程序通過指定的過濾器鏈將請求發送到我們實際的服務執行業務邏輯,然后返回。過濾器由虛線分隔的原因是,過濾器可以在發送帶來請求之前和之后運行邏輯。所有pre 過濾器邏輯均被執行,然后發出代理請求。發出代理請求后,將運行post 過濾器邏輯。
5.3 搭建Gateway服務
5.3.1 創建項目
我們在之前的項目包里創建一個 gateway-server 的項目
5.3.2 添加依賴
在pom.xml文件中添加Spring Cloud Gateway 的依賴。
<!-- spring cloud gateway 依賴 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
5.3.3 配置文件
在application.yml 文件中添加基本的配置信息
server: port: 9000 spring: application: name: gateway-server # 應用名稱
5.3.4 啟動類
@SpringBootApplication public class GatewayServerApplication { public static void main(String[] args) { SpringApplication.run(GatewayServerApplication.class, args); } }
5.4 配置路由規則
下面我們在yml文件中配置一段最基本的路由規則
spring: application: name: gateway-server # 應用名稱 cloud: gateway: # 路由規則 routes: - id: product-service # 路由ID,唯一 uri: http://localhost:7070/ # 目標URI,路由到微服務的地址 predicates: # 斷言(判斷條件) - Path=/product/** # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后
請求接口: http://localhost:9000/product/1 將會路由到 http://localhost:7070/product/1
6 路由規則
Spring Cloud Gateway 創建 Route 對象時,使用 RoutePredicateFactory 創建 Predicate 對象,Predicate對象可以賦值給 Route。
(1)Spring Cloud Gateway 包含需要內置的 Route Predicate Factories。
(2)所有這些斷言都匹配 HTTP 請求的不同屬性。
(3)多個 Route Predicate Factories 可以通過邏輯與(and)結合起來一起使用。
路由斷言工廠 RoutePredicateFactory 包含的主要實現類如圖所示,包含 Datetime、請求的遠端地址、路由權重、請求頭、Host地址、請求方法、請求路徑和請求參數等類型的路由斷言。
6.1 Path 路由
spring: application: name: gateway-server # 應用名稱 cloud: gateway: # 路由規則 routes: - id: product-service # 路由ID,唯一 uri: http://localhost:7070/ # 目標URI,路由到微服務的地址 predicates: # 斷言(判斷條件) - Path=/product/** # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后
請求接口: http://localhost:9000/product/1 將會路由到 http://localhost:7070/product/1
10.2 Query 路由
spring: application: name: gateway-server # 應用名稱 cloud: gateway: # 路由規則 routes: - id : product-service # 路由ID,唯一 uri: http://localhost:7070/ # 目標URI,路由到微服務的地址 predicates: # 斷言(判斷條件) # - Query=token # 匹配請求參數中包含 token 的請求 - Query=token, abc. # 匹配請求參數中包含 token 並且參數值滿足正則表達式是 abc. 的請求
Query=token 匹配請求,比如:http://localhost:9000/product/1?token=123
Query=token, abc. 匹配請求,比如:http://localhost:9000/product/1?token=abc1
5.3 Method 路由
spring: application: name: gateway-server cloud: gateway: # 路由規則 routes: - id : product-service # 路由ID,唯一 uri: http://localhost:7070/ # 目標URI,路由到微服務的地址 predicates: # 斷言(判斷條件) - Method=GET # 匹配任何GET請求
5.4 Datetime 路由
spring: application: name: gateway-server cloud: gateway: # 路由規則 routes: - id : product-service # 路由ID,唯一 uri: http://localhost:7070/ # 目標URI,路由到微服務的地址 predicates: # 斷言(判斷條件) - After=2020-02-02T20:20:20.000+08:00[Asia/Shanghai] # 匹配中國上海時間 2020-02-02 20:20:20 之后的請求
5.5 RemoteAddr 路由
spring: application: name: gateway-server cloud: gateway: # 路由規則 routes: - id : product-service # 路由ID,唯一 uri: http://localhost:7070/ # 目標URI,路由到微服務的地址 predicates: # 斷言(判斷條件) - RemoteAddr=192.168.10.1/0 # 匹配遠程地址請求的是 RemoteAddr的請求,O表示子網掩碼
RemoteAddr=192.168.10.1/0 匹配情況,比如:http://192.168.10.1:9000/product/1
5.6 Header 路由
spring: application: name: gateway-server # 應用名稱 cloud: gateway: # 路由規則 routes: - id : product-service # 路由ID,唯一 uri: http://localhost:7070/ # 目標URI,路由到微服務的地址 predicates: # 斷言(判斷條件) - Header=X-Request-Id, \d+ # 匹配請求頭包含X-Request-Id 並且其值匹配正則表達式 \d+ 的請求
6 動態路由
動態路由其實就是面向服務的路由,Spring Cloud Gateway 支持與 Eureka / Nacos 整合開發,根據 serviceId 自動從注冊中心獲取服務地址並且轉發請求,這樣做的好處不僅可以通過單個端點來訪問應用的所有服務,而且在添加或移除服務實例時不用修改 Gateway 的路由配置。
6.1 添加依賴
我們需要添加注冊中心的jar包,按照自己要求添加eureka 或者 nacos 的client 的jar。
修改gateway-server 的yml文件,添加注冊中心的相關配置。
6.2 動態獲取URI
我們主要修改uri 的相關配置,把原來的 IP 換成服務名稱
routes: - id : product-service # 路由ID,唯一 uri: lb://product-service # lb:// 根據服務名稱從注冊中心獲取服務請求地址 predicates: # 斷言(判斷條件) - Path=/product/** # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后
6.3 服務名稱轉發
如果我們要請求Order服務,我們就需要在配置文件上配置一個order-servcie的路由。
如果我們也很多個微服務,按照上面的原則,每個微服務都需要在這里配置他的微服務名。
但是,SpringCloud 提供了一個“約定大於配置”,根據服務名稱轉發的功能,配置后自動讀取注冊中心的服務名,用於自動轉發。
spring: cloud: gateway:
# 去掉之前的路由配置 discovery: locator: # 是否與服務發生組件進行結合,通過 serviceId 轉發到具體的服務實例。 enabled: true # 是否開啟基於服務發現的路由規則 lower-case-service-id: true # 是否將服務名稱轉小寫
這是請求的URL就是:http://localhost:9000/order-service/order/1
URL在IP和端口后面,緊跟這的是服務名稱,后面才是接口地址。
7. 過濾器
Spring Cloud Gateway 根據作用范圍划分為 GatewayFilter 和GlobalFilter,二者區別如下:
GatewayFilter:網關過濾器,需要通過 spring.cloud.routes.filters 配置在具體路由上,只作用在當前路由上或通過 spring.cloud.default.filters 配置在全局,作用在所有路由上。
GlobalFilter:全局過濾器,不需要在配置文件上配置,作用在所有的路由上,最終通過 GatewayFilterAdapter 包裝成 GatewayFilterChain 可識別的過濾器,它為請求業務以及路由的 URI 轉換為真實業務服務請求地址的核心過濾器,不需要配置系統初始化時加載,並作用在每個路由上。
7.1 網關過濾器 GatewayFilter
網關過濾器用於攔截並鏈式處理Web請求,可以實現橫切與應用無關的需求,比如:安全、訪問超時的設置等。修改傳入的HTTP請求或傳出HTTP響應。Spring Cloud Gateway 包含許多內置的網關過濾器工廠,一共22個。包括頭部過濾器、路徑過濾器、Hystrix過濾器和重寫請求URL的過濾器,還有參數和狀態碼等其他類型的過濾器。根據過濾器工程的用途來划分,可以分為以下幾種:Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter 和 Hystrix。
7.1.1 Path 路徑過濾器
Path 路徑過濾器可以實現URL 重寫,通過重寫URL可以實現隱藏實際路徑提高安全性,易於用戶記憶和鍵入,易於被搜索引擎收錄等優點。實現方式如下:
7.1.1.1 RewritePathGatewayFilterFactory
RewritePath 網關過濾器工廠采集路徑正則表達式參數和替換參數,使用Java正則表達式來靈活地重寫請求地址。
spring: application: name: gateway-server cloud: gateway: # 路由規則 routes: - id : product-service # 路由ID,唯一 uri: lb://product-service # lb:// 根據服務名稱從注冊中心獲取服務請求地址 predicates: # 斷言(判斷條件) # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后 - Path=/product/**, /api-gateway/** filters: # 將 /api-gateway/product/1 重寫為 /product/1 - RewritePath=/api-gateway(?<segment>/?.*), $\{segment}
訪問:http://localhost:9000/api-gateway/product/1 結果與 http://localhost:9000/product/1 相同
7.1.1.2 PrefixPathGatewayFilterFactory
PrefixPath 網關過濾器工廠為匹配的URI 添加指定前綴。
spring: application: name: gateway-server cloud: gateway: routes: - id : product-service # 路由ID,唯一 uri: lb://product-service # lb:// 根據服務名稱從注冊中心獲取服務請求地址 predicates: # 斷言(判斷條件) # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后 - Path=/** filters: # 將 /1 重寫為 /product/1 - PrefixPath=/product
訪問地址:http://localhost:9000/1 結果與 http://localhost:9000/product/1 相同
7.1.1.3 StripPrefixGatewayFilterGateway
StripPrefix 網關過濾器工廠采用一個參數 StripPrefix,該參數表示在將請求發送到下游之前,從請求中剝離的路徑個數。
spring: application: name: gateway-server cloud: gateway: routes: - id : product-service # 路由ID,唯一 uri: lb://product-service # lb:// 根據服務名稱從注冊中心獲取服務請求地址 predicates: # 斷言(判斷條件) # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后 - Path=/** filters: # 將 /api/123/product/1 重寫為 /product/1 - StripPrefix=2
訪問地址:http://localhost:9000/api/123/product/1 結果與 http://localhost:9000/product/1 相同
7.1.1.4 SetPathGatewayFilterGateway
Setpath網關過濾器工廠采用路徑模板參數。它提供了一種通過允許模板路徑段來操作請求路徑的簡單方法,使用Spring Framework 中的 uri 模板,允許多個匹配段。spring: application: name: gateway-server cloud: gateway: routes: - id : product-service # 路由ID,唯一 uri: lb://product-service # lb:// 根據服務名稱從注冊中心獲取服務請求地址 predicates: # 斷言(判斷條件) # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后 - Path=/api/product/{segment} filters: # 將 /api//product/1 重寫為 /product/1 - SetPath=/product/{segment}
訪問地址:http://localhost:9000/api/product/1
7.1.2 Parameter 參數過濾器
AddRequestParameter 網關過濾器工廠會將制定參數添加至匹配到下游請求中。
spring: application: name: gateway-server cloud: gateway: routes: - id : product-service # 路由ID,唯一 uri: lb://product-service # lb:// 根據服務名稱從注冊中心獲取服務請求地址 predicates: # 斷言(判斷條件) # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后 - Path=/api-gateway/** filters: # 將 /api-gateway/product/1 重寫為 /product/1 - RewritePath=/api-gateway/(?<segment>/?.*),$\{segment} # 在下游請求中添加 flag=1 - AddRequestParameter=flag,1
注意:filter 是可以組合使用的,這里我們同時使用了 RewritePath 和 AddReequestParameter ,訪問地址:http://localhost:9000/api-gateway/product/1
我們可以修改這個接口:/product/1 多一個Integer flag 參數,打印日志后看是否可以收到此參數。
7.1.3 Status 狀態過濾器
spring: application: name: gateway-server cloud: gateway: routes: - id : product-service # 路由ID,唯一 uri: lb://product-service # lb:// 根據服務名稱從注冊中心獲取服務請求地址 predicates: # 斷言(判斷條件) # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后 - Path=/api-gateway/** filters: # 將 /api-gateway/product/1 重寫為 /product/1 - RewritePath=/api-gateway/(?<segment>/?.*),$\{segment} # 任何情況下,響應的 HTTP 狀態都將設置為 404 - SetStatus=404
訪問地址:http://localhost:9000/api-gateway/product/1 發現雖然后返回結果,但Http的狀態碼是404
7.2 全局過濾器

7.3 自定義過濾器
即使 Spring Cloud Gateway 自帶許多實用的 GatewayFilter Factory、Gateway Filter、GlobalFilter,但是在很多情景下我們仍然系統可以自定義自己的過濾器,實現一些自定義操作。
7.3.1 自定義網關過濾器
自定義網關過濾器需要實現以下兩個接口:GatewayFilter,Ordered
7.3.1.1 創建過濾器
/** * 自定義網關過濾器 */ public class CustomGatewayFilter implements GatewayFilter, Ordered { /** * 過濾器,業務邏輯 */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("自定義網關過濾器被執行"); // 繼續往下執行 return chain.filter(exchange); } /** * 過濾器執行順序,數值越小,優先級越高 */ @Override public int getOrder() { return 0; } }
7.3.1.2 注冊過濾器
自定義的網關過濾器是需要顯示配置之后,才能生效的。基本配置內容如下:
/** * 自定義網關過濾器,配置類 */ @Configuration public class GatewayRoutesConfiguration { @Bean public RouteLocator routeLocator(RouteLocatorBuilder builder) { return builder.routes().route(r -> r // 斷言(判斷條件) .path("/product/**") // 目標URI,路由到微服務的地址 .uri("lb://product-service") // 注冊自定義網關過濾器 .filters(new CustomGatewayFilter()) // 路由ID,唯一 .id("product-service")) .build(); } }
7.3.2 自定義全局過濾器
自定義全局過濾器需要實現以下兩個即可歐:GlobalFilter,Ordered,通過全局過濾器可以實現權限校驗,安全性驗證等功能。
7.3.2.1 創建過濾器
實現指定接口,添加 @Component 注解即可。
/** * 自定義全局過濾器 */ @Component public class CustomGlobalFilter implements GlobalFilter, Ordered { /** * 過濾器邏輯 */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("自定義全局過濾器被執行"); // 繼續往下執行 return chain.filter(exchange); } /** * 過濾器執行順序,數值越小,優先級越高 */ @Override public int getOrder() { return 0; } }
7.3.3 統一鑒權
接下來我們在網關過濾器中通過 token 判斷用戶是否登錄,完成一個統一鑒權案例。
7.3.3.1 創建過濾器
/** * 鑒權過濾器 */ @Component public class AccessFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = ""; // 從Header中獲取Token List<String> headerTokenList = exchange.getRequest().getHeaders().get("token"); if(headerTokenList != null && headerTokenList.size() > 0){ token = headerTokenList.get(0); } else { // 從請求參數,獲取Token token = exchange.getRequest().getQueryParams().getFirst("token"); } if(null == token || token.equals("")){ System.out.println("warn token is null ..."); ServerHttpResponse response = exchange.getResponse(); // 響應類型 response.getHeaders().add("Content-Type", "application/json; charset=UTF-8"); // 響應狀態碼,HTTP 401 錯誤代表用戶沒有訪問權限 response.setStatusCode(HttpStatus.UNAUTHORIZED); // 響應內容 String message = "{\"message\":\"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + "\"}"; DataBuffer buffer = response.bufferFactory().wrap(message.getBytes()); // 請求結束,不在繼續往下請求 return response.writeWith(Mono.just(buffer)); } // TODO 接下來應該是驗證Token是否有效的邏輯 System.out.println("token is OK!"); return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
8 網關限流
顧名思義,限流就是限制流量,就像你帶寬包有1個G的流量,用完了就沒了。通過限流,我們可以很好地控制系統的QPS,從而達到保護系統的目的。
8.1 為什么需要限流
比如Web服務、對外API,這種類型的服務有以下幾種可能導致機器被拖垮:
(1)用戶增長過快(好事)
(2)因為某個熱點事件(微博熱搜,秒殺)
(3)競爭對手爬蟲
(4)惡意的請求
這些情況是無法預知的,不知道什么時候回有10倍甚至20倍的流量打引來,如果真碰上這種情況,擴容是根本來不及的。
8.2 限流算法
常見的限流算法有:計數器算法、漏桶(Leaky Bucket)算法、令牌桶(Token Bucket) 算法
8.2.1 計數器算法
計數器算法是限流算法里最簡單也是最容器實現的一種算法。比如我們規定,對於A接口來說,我們1分鍾的訪問次數不能超過100個。那么我們可以這么做:在一開始的時候,我們可以設置一個計數器counter,每當一個請求過來的時候,counter 就加1, 如果 counter 的值大於100,並且該請求與第一個請求的間隔時間還在1分鍾之內,觸發限流;如果該請求與第一個請求的間隔時間大於1分鍾,重置counter重新計數,具體算法的示意圖如下:
這個算法雖然簡單,但有一個十分致命的問題,那就是臨界問題。
假設有一個惡意用戶,他在0:59時,瞬間發送了100個請求,並且1:00又瞬間發送了100個請求,那么其實這個用戶在1秒里面,瞬間發送了200個請求,可以瞬間超過我們的速率限制。用戶有可能通過算法的這個漏洞,瞬間壓垮我們的應用。
還有資源浪費的問題存在,我們的預期想法是系統100個請求可以均勻分散到這1分鍾內,假設30s以內我們就請求上限了,name剩余的半分鍾服務器就會處於閑置狀態。
8.2.2 漏桶算法
漏桶算法其實也很簡單,可以粗略的認為就是注水漏水的過程,往桶中以任意速率注入水,以一定速率流出水,當水超過桶流量則丟棄,因為桶容量是不變的,保證了整體的速率。
漏桶算法的示意圖在網上有很多,大家可以自己找來看看。
漏桶算法是使用隊列機制實現的。
8.2.3 令牌桶算法
令牌桶算法是對漏桶算法的一種改進,漏桶算法能夠限制請求調用的速率,而令牌桶算法能夠在限制調用的平均速率的同時還允許一定程度的突發調用。在令牌桶算法中,存在一個桶,用來存放固定數量的令牌。算法中存在一種機制,以一定的速率往桶中放令牌。每次請求調用需要先獲取令牌,只有拿到令牌,才有機會繼續執行,否則選擇等待可以用令牌、或者直接拒絕。放令牌這個動作是持續不斷的進行,如果桶中令牌數量達到上線,就丟棄令牌。
場景大概是這樣的:桶中一直有大量的可用令牌,這是進來的請求可以直接拿到令牌執行,比如設置QPS為100/s,那么限流器初始化完成一秒后,桶中就已經有100個令牌了,等服務啟動完成對外提供服務時,該限流器可以抵擋瞬時的100個請求。當桶中沒有令牌時,請求會進行等待,最后相當於以一定的速率執行。
Spring Cloud Gateway 內部使用的就是該算法,大概描述如下:
(1)所有的請求在處理之前都需要拿到一個可用的令牌才會被處理;
(2)根據限流大小,設置按照一定的速率往桶里添加令牌;
(3)桶設置最大的放置令牌限制,當桶滿時、新添加的令牌就被丟棄或者拒絕;
(4)請求到達后首先要獲取令牌桶中的令牌,拿到令牌才可以進行其他的業務邏輯,處理完業務邏輯之后,將令牌直接刪除;
(5)令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之后就不會刪除令牌,以此保證足夠的限流。
漏桶算法主要用途在於保護它人,而令牌桶算法主要目的在於保護自己,將請求壓力交由目標服務處理。假設突然進來很多請求,只要拿到令牌這些請求會瞬時被處理調用目標服務。
8.3 Gateway 限流
Spring Cloud Gateway 官方提供了 RequestRateLimiterGatewayFilterFactory 過濾器工廠,使用 Redis 和 Lua 腳本實現了令牌桶的方式。
官方文檔:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#the-redis-ratelimiter
具體實現邏輯在 RequestRateLimiterGatewayFilterFactory 類中,Lua 腳本在如下圖所以所示的源碼文件夾中:
8.3.1 添加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
8.3.2 限流規則
8.3.2.1 URI 限流
配置限流過濾器和限流過濾器引用的Bean對象。
@Configuration public class KeyResolverConfiguration { /** * 限流規則 */ @Bean public KeyResolver pathKeyResolver(){ // 匿名內部類的寫法 // return new KeyResolver() { // @Override // public Mono<String> resolve(ServerWebExchange exchange) { // return Mono.just(exchange.getRequest().getPath().toString()); // } // }; // JDK1.8 lambda寫法 return exchange -> Mono.just(exchange.getRequest().getURI().getPath()); } }
配置文件內容:
server: port: 9000 spring: application: name: gateway-server cloud: # 注冊中心 nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id : product-service # 路由ID,唯一 uri: lb://product-service # lb:// 根據服務名稱從注冊中心獲取服務請求地址 predicates: # 斷言(判斷條件) # 匹配對應URL的請求,將匹配到的請求追加在目標URI之后 - Path=/product/** filters: # 限流過濾器 - name: RequestRateLimiter args: redis-rate-limiter: replenishRate: 1 # 令牌桶每秒填充速率 burstCapacity: 2 # 令牌桶總容量 key-resolver: "#{@pathKeyResolver}" # 使用SpEL 表達式按名稱引用bean # redis 配置 redis: host: 127.0.0.1 port: 6379 password: database: 0 timeout: 10000 # 連接超時時間 lettuce: pool: max-active: 1024 # 最大連接數,默認 8 max-wait: 10000 # 最大連接阻塞等待時間,默認毫秒,默認 -1 max-idle: 200 # 最大空閑連接,默認 8 min-idle: 5 # 最小空閑連接,默認 0
注意:為了測試限流,我們需要把之前的自定義網關配置,注釋掉。GatewayRoutesConfiguration
請求地址:http://localhost:9000/product/1?token=123
當我們每秒請求1次的時候,都可以正常訪問。如果在1秒內多次訪問地址,就會出現限流提示:
此時我們打開Redis的可視化工具,就可以看到Gateway限流功能,自動生成了以下2個key
8.3.2.2 參數限流
注意:KeyResolverConfiguration配置類里面,同時只能出現一個@Bean 對象。所以要把前一個刪掉。否則Gateway啟動會報錯。
@Configuration public class KeyResolverConfiguration { /** * 根據參數限流 */ @Bean public KeyResolver parameterKeyResolver(){ return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId")); } }
配置文件只需要修改紅色部分
filters: # 限流過濾器 - name: RequestRateLimiter args: redis-rate-limiter: replenishRate: 1 # 令牌桶每秒填充速率 burstCapacity: 2 # 令牌桶總容量 key-resolver: "#{@parameterKeyResolver}" # 使用SpEL 表達式按名稱引用bean
請求地址:http://localhost:9000/product/1?token=123&userId=1
因為我們配置的參數限流的參數是userId,所有在請求地址里必須傳入userId,否則在 parameterKeyResolver 方法里會報空指針異常。所以正常的邏輯在 parameterKeyResolver 還需要判空的。
當多次請求后也會出現上面的限流提示。
Redis中的Key如下:
8.3.2.3 IP限流
加入IP限流的Bean對象,並且注銷之前的Bean
/** * 根據IP限流 */ @Bean public KeyResolver ipKeyResolver(){ return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); }
修改配置類的 紅色部分
filters: # 限流過濾器 - name: RequestRateLimiter args: redis-rate-limiter: replenishRate: 1 # 令牌桶每秒填充速率 burstCapacity: 2 # 令牌桶總容量 key-resolver: "#{@ipKeyResolver}" # 使用SpEL 表達式按名稱引用bean
請求地址:http://localhost:9000/product/1?token=123&userId=1
這時的請求就與參數無關了,Gateway會抓取請求的IP地址,Redis的Key如下:
8.4 Sentinel 限流
Sentinel 支持對 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 進行限流。
Sentinel 網關限流公共模塊(API Gateway Adapter Common)支持的功能包括:請求屬性解析、網關規則管理、網關規則檢查、調用參數組裝、自定義API分組管理、API路徑匹配
官網文檔:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81
8.4.1 創建項目
創建一個 gateway-server-sentinel 的項目。
8.4.2 添加依賴
單獨使用添加 sentinel gateway adapter 依賴即可。
若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway
依賴,同時需要添加 spring-cloud-starter-gateway
依賴來讓 spring-cloud-alibaba-sentinel-gateway
模塊里的 Spring Cloud Gateway 自動化配置類生效。
同時請將 spring.cloud.sentinel.filter.enabled
配置項置為 false(若在網關流控控制台上看到了 URL 資源,就是此配置項沒有置為 false)。Sentinel 網關流控默認的粒度是 route 維度以及自定義 API 分組維度,默認不支持 URL 粒度。如需細化到 URL 粒度,請參考 網關流控文檔 自定義 API 分組。
注意:網關流控規則數據源類型是 gw-flow
,若將網關流控規則數據源指定為 flow 則不生效。
<dependencies> <!-- 注冊中心 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.1.1.RELEASE</version> </dependency> <!-- spring cloud gateway 依賴 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <version>2.2.1.RELEASE</version> </dependency> <!-- Gateway 和 Sentinel Starter 配合使用 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2.1.0.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> <version>2.1.0.RELEASE</version> </dependency> </dependencies>
8.4.3 yml配置文件
server: port: 9001 spring: application: name: gateway-server-sentinel cloud: # 注冊中心 nacos: discovery: server-addr: 127.0.0.1:8848 # 禁止網關流控控制台上看到 URL 資源 sentinel: filter: enabled: false gateway: discovery: locator: # 是否與服務發現組件進行結合,通過 serviceId 轉發到具體服務實例 enabled: true # 是否開啟基於服務發現的路由規則 lower-case-service-id: true # 是否將服務名轉為小寫 # 路由規則 routes: - id: order-service uri: lb://order-service predicates: # 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之后 - Path=/order/**
8.4.4 限流規則配置類
使用時只需注入對應的 SentinelGatewayFilter 實例以及 SentinelGatewayBlockExceptionHandler 實例即可。
/** * 限流規則配置類 * @author he.zhang * @date 2022/4/21 14:36 */ @Configuration public class GatewayConfiguration { private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; /** * 構造器 * @param viewResolverProvider * @param serverCodecConfigurer */ public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolverProvider, ServerCodecConfigurer serverCodecConfigurer){ this.viewResolvers = viewResolverProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } /** * 限流異常處理器 */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler(){ return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } // /** // * 限流過濾器,使用Sentinel Starter 方式集成,會自動繼承,不需要配置 // */ // @Bean // @Order(Ordered.HIGHEST_PRECEDENCE) // public GlobalFilter sentinelGatewayFilter(){ // return new SentinelGatewayFilter(); // } /** * Spring 容器初始化的時候執行該方法 */ @PostConstruct public void doInit(){ // 加載網關限流規則 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); } }
8.4.5 啟動類
@SpringBootApplication public class GatewayServerSentinelApplication { public static void main(String[] args) { SpringApplication.run(GatewayServerSentinelApplication.class, args); } }
8.4.6 訪問
多次訪問:http://localhost:9001/order/1 結果如下:Blocked by Sentinel: ParamFlowException
接口 BlockReequestHandler 的默認實現了 DefaultBlockRequestHandler,當觸發限流時會返回默認的錯誤信息:Blocked by Sentinel: FlowException。我們可以通過 GatewayCallbackManager 定制異常提示信息。
8.4.7 自定義異常提示
GatewayCallbackManager 的 setBlockHandler 注冊函數用於實現自定義的邏輯,處理被限流的請求。
接下來我們在 GatewayConfiguration 這個配置里改造,新建一個 initBlockHandler 類,並在doInit 初始化方法里調用。
/** * Spring 容器初始化的時候執行該方法 */ @PostConstruct public void doInit(){ // 加載網關限流規則 initGatewayRules(); initBlockHandler(); }/** * 自定義限流異常處理器 */ private void initBlockHandler(){ BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @Override public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map<String, String> result = new HashMap<>(); result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value())); result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase()); result.put("route", "order-service"); return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(result)); } }; // 加載自定義限流異常處理器 GatewayCallbackManager.setBlockHandler(blockRequestHandler); }
我重啟 gateway-server-sentinel 網關之后,再次頻繁請求 http://localhost:9001/order/1 接口,返回信息如下:{"code":"429","route":"order-service","message":"Too Many Requests"}
8.4.8 分組限流
改造 GatewayConfiguration 類的 initGatewayRules 方法,並新寫一個 initCustomizedApis 方法。
/** * 網關限流規則 */ private void initGatewayRules(){ Set<GatewayFlowRule> rules = new HashSet<>(); /* * resource:資源名稱,可以是網關中的 route 名稱或者用戶自定義的 API 分組名稱 * count:限流閾值 * intervalSec:統計時間窗口,單位是秒,默認是 1 秒 */ // ------------ 限流分組 --------------- rules.add(new GatewayFlowRule("product-api") .setCount(3) // 限流閾值 .setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是1秒 rules.add(new GatewayFlowRule("order-api") .setCount(3) // 限流閾值 .setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是1秒 // 加載網關限流規則 GatewayRuleManager.loadRules(rules); // 加載限流分組 initCustomizedApis(); } /** * 限流分組 */ public void initCustomizedApis(){ Set<ApiDefinition> definitions = new HashSet<>(); // product-api 組 ApiDefinition api1 = new ApiDefinition("product-api") .setPredicateItems(new HashSet<ApiPredicateItem>() {{ // 匹配 /product-service/product 以及其子路徑的所有請求 add(new ApiPathPredicateItem().setPattern("/product-service/product/**") .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); // order-api 組 ApiDefinition api2 = new ApiDefinition("order-api") .setPredicateItems(new HashSet<ApiPredicateItem>() {{ // 只匹配 /order-service/order/index add(new ApiPathPredicateItem().setPattern("/order-service/order/index")); }}); definitions.add(api1); definitions.add(api2); // 加載限流分組 GatewayApiDefinitionManager.loadApiDefinitions(definitions); }
訪問測試:
http://localhost:9001/order-service/order/1 多次訪問不會被限制
http://localhost:9001/order-service/order/index 開始可以訪問通,多次訪問后被限制
http://localhost:9001/product-service/product/** 下面的所有接口,訪問多次都會被限制
9 高可用網關
業內通常用多少 9 來衡量網站的可用性,例如QQ的可用性是4個9,就是說 QQ 能夠保證在一年里,服務在 99.99% 的時間是可用的,只有 0.01% 的時間不可用,大約最多53 分鍾。
對於大多數的網站,2 個 9 是基本可用; 3 個 9 是叫高可用; 4 個 9 是擁有自動回復能力的高可用。
實現高可用的主要手段是 數據的冗余備份 和 服務的失效轉移,這兩種手段具體可以怎么做呢?在網關里如何體現?主要有以下幾個方向: 集群部署、負載均衡、健康檢查、節點自動重啟、熔斷、服務降級、接口重試
9.1 Nginx + 網關集群實現高可用網關
通過Nginx攔截請求,再把請求輪詢轉到網關集群上每一個實例。