微服務閘道器之SpringCloudGateway
一、SpringCloudGateway
1.1 簡介
SpringCloud Gateway 是 Spring Cloud 的一個全新專案,該專案是基於 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的閘道器,它旨在為微服務架構提供一種簡單有效的統一的 API 路由管理方式。
SpringCloud Gateway 作為 Spring Cloud 生態系統中的閘道器,目標是替代 Zuul,在 Spring Cloud 2.0 以上版本中,沒有對新版本的 Zuul 2.0 以上最新高效能版本進行整合,仍然還是使用的 Zuul 2.0 之前的 非Reactor 模式
Spring Cloud Gateway 不僅提供統一的路由方式,並且基於 Filter 鏈 的方式提供了閘道器基本的功能,例如:安全,監控/指標,和限流等。
1.2 名詞解釋
-
Filter(過濾器):
和 Zuul 的過濾器類似,可以使用它攔截和修改請求,並且對上游的響應,進行二次處理。過濾器為 org.springframework.cloud.gateway.filter.GatewayFilter 類的例項。
-
Route(路由):
閘道器配置的基本組成模組,和 Zuul 的路由配置模組類似。一個 Route 模組 由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。如果斷言為真,則路由匹配,目標URI會被訪問。
-
Predicate(斷言):
這是一個 Java 8 的 Predicate,可以使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或引數。斷言的 輸入型別是一個 ServerWebExchange。
1.3 Gateway 處理流程
客戶端向 Spring Cloud Gateway 發出請求,在 Gateway Handler Mapping 中找到與請求相匹配的路由,將其傳送到 Gateway Web Handler
二、準備工作
2.1 建立 OrderService 模組
- 新增依賴
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.3.0.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 編寫配置檔案
spring:
application:
# 服務名稱
name: order-service
cloud:
nacos:
discovery:
# 註冊服務中心地址
server-addr: 192.168.205.10:8848
server:
# 設定服務埠
port: 8881
- 建立 OrderController 類
@RestController
@RequestMapping("/order")
public class OrderController {
@RequestMapping("/create")
public String create() {
return "訂單建立成功";
}
}
4) 啟動應用,訪問 http://localhost:8881/order/create ,返回:
訂單建立成功
2.2 建立閘道器模組
- 新增依賴
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring-boot.version>2.3.0.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
三、路由配置方式
3.1 基於配置檔案指定URI路由配置方式
spring:
application:
# 配置應用名稱
name: gateway
cloud:
gateway:
routes:
- id: order-service
uri: http://localhost:8881
predicates:
- Path=/order/**
server:
# 配置應用埠
port: 8080
配置說明:
* routes : 表示路由配置,可以存在多個
* id:自定義的路由 ID,保持唯一
* uri:目標服務地址
* predicates::路由條件,Predicate 接受一個輸入引數,返回一個布林值結果。該介面包含多種預設方法來將 Predicate 組合成其他複雜的邏輯(比如:與,或,非)。
*- Path:基於路徑路由
上面的配置用文字表達如下:
配置一個 Id 為 order-service 的路由規則,當訪問地址以 /order 開頭時,將會把請求轉發到 http://localhost:8881 上去
3.2 基於程式碼的路由配置方式
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator orderRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/order/**")
.uri("http://localhost:8881"))
.build();
}
}
3.3 與註冊中心相結合的路由配置方式
- 新增依賴
<dependencyManagement>
<dependencies>
......
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
- 調整配置
spring:
application:
# 配置應用名稱
name: gateway
cloud:
nacos:
discovery:
# 註冊服務中心地址
server-addr: 192.168.205.10:8848
gateway:
routes:
- id: order-service
# 與單個URL的區別僅僅在於URI的schema協議不同
uri: lb://order-service
predicates:
- Path=/order/**
server:
# 配置應用埠
port: 8080
四、匹配規則
Spring Cloud Gateway 是通過 Spring WebFlux 的 HandlerMapping 做為底層支援來匹配到轉發路由,它內建了很多 Predicates 工廠,這些 Predicates 工廠通過不同的 HTTP 請求引數來匹配,多個 Predicates 工廠可以組合使用。
4.1 Predicate 斷言條件介紹
Predicate 來源於 Java 8,是 Java 8 中引入的一個函式,Predicate 接受一個輸入引數,返回一個布林值結果。該介面包含多種預設方法來將 Predicate 組合成其他複雜的邏輯(比如:與,或,非)。可以用於介面請求引數校驗、判斷新老資料是否有變化需要進行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性實現了各種路由匹配規則,有通過 Header、請求引數等不同的條件來進行作為條件匹配到對應的路由。下圖總結了 Spring Cloud 內建的幾種 Predicate 的實現:
4.2 匹配方式
方式名稱 | 引數名 | 例項 |
---|---|---|
通過請求引數匹配 | Query | - Query=name,aaa |
通過 Header 屬性匹配 | Header | - Header=X-Request-Id, \d+ |
通過Cookie匹配 | Cookie | - Cookie=sessionId,1001 |
通過 Host 匹配 | Host | - Host=..com |
通過請求方式匹配 | Method | - Method=GET |
通過請求路徑匹配 | Path | - Path=/order/** |
通過請求 ip 地址進行匹配 | RemoteAddr | - RemoteAddr=192.168.1.1/24 |
4.3 通過請求引數匹配例項
Query Route Predicate 支援傳入兩個引數,一個是屬性名一個為屬性值,屬性值可以是正則表示式。
spring:
application:
# 配置應用名稱
name: gateway
cloud:
nacos:
discovery:
# 註冊服務中心地址
server-addr: 192.168.205.10:8848
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Query=name
只有包含屬性名 name 才會轉發,如 http://localhost:8080/order/create?name=aaa。
Query 值可以以鍵值對的方式進行配置:
spring:
application:
# 配置應用名稱
name: gateway
cloud:
nacos:
discovery:
# 註冊服務中心地址
server-addr: 192.168.205.10:8848
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Query=name,aaa
只有當請求中包含屬性名 name 並且值為 aaa 才會轉發 ,http://localhost:8080/order/create?name=aaa。
4.3 通過 Header 屬性匹配例項
spring:
application:
# 配置應用名稱
name: gateway
cloud:
nacos:
discovery:
# 註冊服務中心地址
server-addr: 192.168.205.10:8848
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
# \d 表示匹配數字,+ 表示1 次或多次匹配。
- Header=X-Request-Id, \d+
通過 postman 發起請求,在 header 頭上增加屬性 X-Request-Id ,配置值為任意數值,如 99,即可訪問成功。
五、熔斷降級
5.1 為什麼要實現熔斷降級?
在分散式系統中,閘道器作為流量的入口,因此會有大量的請求進入閘道器,向其他服務發起呼叫,其他服務不可避免的會出現呼叫失敗(超時、異常),失敗時不能讓請求堆積在閘道器上,需要快速失敗並返回給客戶端,想要實現這個要求,就必須在閘道器上做熔斷、降級操作。
5.2 基於 hystrix 熔斷降級
- 新增依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 配置
server:
# 配置應用埠
port: 8080
spring:
application:
# 配置應用名稱
name: gateway
cloud:
nacos:
discovery:
# 註冊服務中心地址
server-addr: 192.168.205.10:8848
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
filters:
# 配置 Hystrix
- name: Hystrix
args:
name: fallbackCmdA
# 降級呼叫 URI
fallbackUri: forward:/fallbackA
# 設定超時時間,單位:ms
hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000
- 建立降級回撥方法
@RestController
public class FallbackController {
@GetMapping("/fallbackA")
public String fallbackA() {
return "服務暫時不可用";
}
}
- 啟動 OrderService 和 gateway 服務,並訪問 http://localhost:8080/order/create 返回:
訂單建立成功
- 關閉OrderService 訪問 http://localhost:8080/order/create 返回:
服務暫時不可用
證明熔斷降級已生效。
六、限流
6.1 為什麼需要限流?
- 防止大量的請求使伺服器過載,導致服務不可用
- 防止網路攻擊
6.2 常見的限流演算法
計數器演算法
在指定時間內對請求數做累計,當數量大於設定的值時,後續的請求都將被拒絕。當指定時間過去後,將計數重置為0,重新開始計數。
弊端:如果在單位時間1s內只能允許100個請求訪問,在前10ms已經通過了100個請求,那後面的990ms所有的請求都會被拒絕,這種現象稱為“突刺現象”。
漏桶演算法
漏桶演算法可以消除"突刺現象",漏桶演算法內部有一個容器,類似生活用到的漏斗,當請求進來時,相當於水倒入漏斗,然後從下端小口慢慢勻速的流出。不管上面流量多大,下面流出的速度始終保持不變。不管服務呼叫方多麼不穩定,通過漏桶演算法進行限流,每10毫秒處理一次請求。因為處理的速度是固定的,請求進來的速度是未知的,可能突然進來很多請求,沒來得及處理的請求就先放在桶裡,既然是個桶,肯定是有容量上限,如果桶滿了,那麼新進來的請求就丟棄
弊端:無法應對短時間的突發流量。
令牌桶演算法
在令牌桶演算法中,存在一個桶,用來存放固定數量的令牌。演算法中存在一種機制,以一定的速率往桶中放令牌。每次請求呼叫需要先獲取令牌,只有拿到令牌,才有機會繼續執行,否則選擇選擇等待可用的令牌、或者直接拒絕。放令牌這個動作是持續不斷的進行,如果桶中令牌數達到上限,就丟棄令牌,所以就存在這種情況,桶中一直有大量的可用令牌,這時進來的請求就可以直接拿到令牌執行,比如設定qps為100,那麼限流器初始化完成一秒後,桶中就已經有100個令牌了,這時服務還沒完全啟動好,等啟動完成對外提供服務時,該限流器可以抵擋瞬時的100個請求。所以,只有桶中沒有令牌時,請求才會進行等待,最後相當於以一定的速率執行。
6.3 Gateway 限流支援
在 Spring Cloud Gateway 中,有 Filter 過濾器,因此可以在 pre 型別的 Filter 中自行實現上述三種過濾器。但是限流作為閘道器最基本的功能,Spring Cloud Gateway 官方就提供了 RequestRateLimiterGatewayFilterFactory 這個類,適用在 Redis 內的通過執行 Lua 指令碼實現了令牌桶的方式。具體實現邏輯在 RequestRateLimiterGatewayFilterFactory 類中,lua 指令碼在如下圖所示的資料夾中:
6.4 例項
- 新增 redis 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
- 在配置檔案中配置
server:
# 配置應用埠
port: 8080
spring:
application:
# 配置應用名稱
name: gateway
cloud:
nacos:
discovery:
# 註冊服務中心地址
server-addr: 192.168.205.10:8848
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- name: RequestRateLimiter
args:
# 用於限流的鍵的解析器的 Bean 物件的名字,通過 SpEL 表示式從 Spring 容器中獲取
key-resolver: '#{@hostAddrKeyResolver}'
# 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的上限
redis-rate-limiter.burstCapacity: 3
redis:
host: localhost
port: 6379
database: 0
- 自定義限流策略,通過實現 KeyResolver 介面
基於 hostAddress 限流:
public class HostAddrKeyResolver implements KeyResolver {
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
基於 URI 限流:
public class UriKeyResolver implements KeyResolver {
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getURI().getPath());
}
}
基於使用者 限流:
public class UserKeyResolver implements KeyResolver {
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
}
只允許一個策略生效,這裡我們採用 HostAddrKeyResolver :
@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
return new HostAddrKeyResolver();
}
- 啟動 OrderService 和 gateway 服務,通過 jmeter 併發訪問
可以看到請求部分成功,部分失敗。