Spring Cloud 系列之 Gateway 服務網關(四)
本篇文章為系列文章,未讀第一集的同學請猛戳這裡:
本篇文章講解 Gateway 網關如何實現限流、整合 Sentinel 實現限流以及高可用網關環境搭建。
網關限流
顧名思義,限流就是限制流量,就像你寬頻包有 1 個 G 的流量,用完了就沒了。通過限流,我們可以很好地控制系統的 QPS,從而達到保護系統的目的。
為什麼需要限流
比如 Web 服務、對外 API,這種類型的服務有以下幾種可能導致機器被拖垮:
- 用戶增長過快(好事)
- 因為某個熱點事件(微博熱搜)
- 競爭對象爬蟲
- 惡意的請求
這些情況都是無法預知的,不知道什麼時候會有 10 倍甚至 20 倍的流量打進來,如果真碰上這種情況,擴容是根本來不及的。
從上圖可以看出,對內而言:上游的 A、B 服務直接依賴了下游的基礎服務 C,對於 A,B 服務都依賴的基礎服務 C 這種場景,服務 A 和 B 其實處於某種競爭關係,如果服務 A 的並發閾值設置過大,當流量高峰期來臨,有可能直接拖垮基礎服務 C 並影響服務 B,即雪崩效應。
限流演算法
點擊鏈接觀看:限流演算法影片(獲取更多請關注公眾號「哈嘍沃德先生」)
常見的限流演算法有:
- 計數器演算法
- 漏桶(Leaky Bucket)演算法
- 令牌桶(Token Bucket)演算法
計數器演算法
計數器演算法是限流演算法里最簡單也是最容易實現的一種演算法。比如我們規定,對於 A 介面來說,我們 1 分鐘的訪問次數不能超過 100 個。那麼我們可以這麼做:在一開始的時候,我們可以設置一個計數器 counter,每當一個請求過來的時候,counter 就加 1,如果 counter 的值大於 100 並且該請求與第一個請求的間隔時間還在 1 分鐘之內,觸發限流;如果該請求與第一個請求的間隔時間大於 1 分鐘,重置 counter 重新計數,具體演算法的示意圖如下:
這個演算法雖然簡單,但是有一個十分致命的問題,那就是臨界問題,我們看下圖:
從上圖中我們可以看到,假設有一個惡意用戶,他在 0:59 時,瞬間發送了 100 個請求,並且 1:00 又瞬間發送了 100 個請求,那麼其實這個用戶在 1 秒裡面,瞬間發送了 200 個請求。我們剛才規定的是 1 分鐘最多 100 個請求,也就是每秒鐘最多 1.7 個請求,用戶通過在時間窗口的重置節點處突發請求, 可以瞬間超過我們的速率限制。用戶有可能通過演算法的這個漏洞,瞬間壓垮我們的應用。
還有資料浪費的問題存在,我們的預期想法是希望 100 個請求可以均勻分散在這一分鐘內,假設 30s 以內我們就請求上限了,那麼剩餘的半分鐘伺服器就會處於閑置狀態,比如下圖:
漏桶演算法
漏桶演算法其實也很簡單,可以粗略的認為就是注水漏水的過程,往桶中以任意速率流入水,以一定速率流出水,當水超過桶流量則丟棄,因為桶容量是不變的,保證了整體的速率。
漏桶演算法是使用隊列機制實現的。
漏桶演算法主要用途在於保護它人(服務),假設入水量很大,而出水量較慢,則會造成網關的資源堆積可能導致網關癱瘓。而目標服務可能是可以處理大量請求的,但是漏桶演算法出水量緩慢反而造成服務那邊的資源浪費。
漏桶演算法無法應對突發調用。不管上面流量多大,下面流出的速度始終保持不變。因為處理的速度是固定的,請求進來的速度是未知的,可能突然進來很多請求,沒來得及處理的請求就先放在桶里,既然是個桶,肯定是有容量上限,如果桶滿了,那麼新進來的請求就會丟棄。
令牌桶演算法
令牌桶演算法是對漏桶演算法的一種改進,漏桶演算法能夠限制請求調用的速率,而令牌桶演算法能夠在限制調用的平均速率的同時還允許一定程度的突發調用。在令牌桶演算法中,存在一個桶,用來存放固定數量的令牌。演算法中存在一種機制,以一定的速率往桶中放令牌。每次請求調用需要先獲取令牌,只有拿到令牌,才有機會繼續執行,否則選擇選擇等待可用的令牌、或者直接拒絕。放令牌這個動作是持續不斷的進行,如果桶中令牌數達到上限,就丟棄令牌。
場景大概是這樣的:桶中一直有大量的可用令牌,這時進來的請求可以直接拿到令牌執行,比如設置 QPS 為 100/s,那麼限流器初始化完成一秒後,桶中就已經有 100 個令牌了,等服務啟動完成對外提供服務時,該限流器可以抵擋瞬時的 100 個請求。當桶中沒有令牌時,請求會進行等待,最後相當於以一定的速率執行。
Spring Cloud Gateway 內部使用的就是該演算法,大概描述如下:
- 所有的請求在處理之前都需要拿到一個可用的令牌才會被處理;
- 根據限流大小,設置按照一定的速率往桶里添加令牌;
- 桶設置最大的放置令牌限制,當桶滿時、新添加的令牌就被丟棄或者拒絕;
- 請求到達後首先要獲取令牌桶中的令牌,拿著令牌才可以進行其他的業務邏輯,處理完業務邏輯之後,將令牌直接刪除;
- 令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之後將不會刪除令牌,以此保證足夠的限流。
漏桶演算法主要用途在於保護它人,而令牌桶演算法主要目的在於保護自己,將請求壓力交由目標服務處理。假設突然進來很多請求,只要拿到令牌這些請求會瞬時被處理調用目標服務。
Gateway 限流
點擊鏈接觀看:Gateway 服務限流影片(獲取更多請關注公眾號「哈嘍沃德先生」)
Spring Cloud Gateway 官方提供了 RequestRateLimiterGatewayFilterFactory
過濾器工廠,使用 Redis
和 Lua
腳本實現了令牌桶的方式。
官網文檔://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#the-redis-ratelimiter 具體實現邏輯在 RequestRateLimiterGatewayFilterFactory
類中,Lua
腳本在如下圖所示的源碼文件夾中:
添加依賴
<!-- spring data redis reactive 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- commons-pool2 對象池依賴 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
限流規則
URI 限流
配置限流過濾器和限流過濾器引用的 bean 對象。
spring:
application:
name: gateway-server # 應用名稱
cloud:
gateway:
# 路由規則
routes:
- id: product-service # 路由 ID,唯一
uri: lb://product-service # lb:// 根據服務名稱從註冊中心獲取服務請求地址
predicates: # 斷言(判斷條件)
# 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之後
- Path=/product/**
filters: # 網關過濾器
# 限流過濾器
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶總容量
key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表達式按名稱引用 bean
# 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
編寫限流規則配置類。
package com.example.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* 限流規則配置類
*/
@Configuration
public class KeyResolverConfiguration {
/**
* 限流規則
*
* @return
*/
@Bean
public KeyResolver pathKeyResolver() {
/*
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getPath().toString());
}
};
*/
// JDK 1.8
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
}
多次訪問://localhost:9000/product/1 結果如下:
Redis 結果如下:
參數限流
配置限流過濾器和限流過濾器引用的 bean 對象。
spring:
application:
name: gateway-server # 應用名稱
cloud:
gateway:
# 路由規則
routes:
- id: product-service # 路由 ID,唯一
uri: lb://product-service # lb:// 根據服務名稱從註冊中心獲取服務請求地址
predicates: # 斷言(判斷條件)
# 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之後
- Path=/product/**
filters: # 網關過濾器
# 限流過濾器
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶總容量
key-resolver: "#{@parameterKeyResolver}" # 使用 SpEL 表達式按名稱引用 bean
# 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
編寫限流規則配置類。
package com.example.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* 限流規則配置類
*/
@Configuration
public class KeyResolverConfiguration {
/**
* 根據參數限流
*
* @return
*/
@Bean
public KeyResolver parameterKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
}
多次訪問://localhost:9000/product/1?userId=123 結果如下:
Redis 結果如下:
IP 限流
配置限流過濾器和限流過濾器引用的 bean 對象。
spring:
application:
name: gateway-server # 應用名稱
cloud:
gateway:
# 路由規則
routes:
- id: product-service # 路由 ID,唯一
uri: lb://product-service # lb:// 根據服務名稱從註冊中心獲取服務請求地址
predicates: # 斷言(判斷條件)
# 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之後
- Path=/product/**
filters: # 網關過濾器
# 限流過濾器
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶總容量
key-resolver: "#{@ipKeyResolver}" # 使用 SpEL 表達式按名稱引用 bean
# 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
編寫限流規則配置類。
package com.example.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* 限流規則配置類
*/
@Configuration
public class KeyResolverConfiguration {
/**
* 根據 IP 限流
*
* @return
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
多次訪問://localhost:9000/product/1 結果如下:
Redis 結果如下:
Sentinel 限流
點擊鏈接觀看:Sentinel 服務限流影片(獲取更多請關注公眾號「哈嘍沃德先生」)
Sentinel 支援對 Spring Cloud Gateway、Netflix Zuul 等主流的 API Gateway 進行限流。
官網文檔:
- //github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
- //github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81#spring-cloud-gateway
創建項目
創建 gateway-server-sentinel
項目。
添加依賴
單獨使用添加 sentinel gateway adapter
依賴即可。
若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway
依賴來讓 spring-cloud-alibaba-sentinel-gateway
模組里的 Spring Cloud Gateway 自動化配置類生效。
同時請將 spring.cloud.sentinel.filter.enabled
配置項置為 false(若在網關流控控制台上看到了 URL 資源,就是此配置項沒有置為 false)。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>gateway-server-sentinel</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 繼承父依賴 -->
<parent>
<groupId>com.example</groupId>
<artifactId>gateway-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 項目依賴 -->
<dependencies>
<!-- spring cloud gateway 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- netflix eureka client 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 單獨使用 -->
<!-- sentinel gateway adapter 依賴 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-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>
配置文件
server:
port: 9001 # 埠
spring:
application:
name: gateway-server-sentinel # 應用名稱
cloud:
sentinel:
filter:
enabled: false
gateway:
discovery:
locator:
# 是否與服務發現組件進行結合,通過 serviceId 轉發到具體服務實例。
enabled: true # 是否開啟基於服務發現的路由規則
lower-case-service-id: true # 是否將服務名稱轉小寫
# 路由規則
routes:
- id: order-service # 路由 ID,唯一
uri: lb://order-service # 目標 URI,lb:// 根據服務名稱從註冊中心獲取服務請求地址
predicates: # 斷言(判斷條件)
# 匹配對應 URI 的請求,將匹配到的請求追加在目標 URI 之後
- Path=/order/**
# 配置 Eureka Server 註冊中心
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址註冊
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
service-url: # 設置服務註冊中心地址
defaultZone: //localhost:8761/eureka/,//localhost:8762/eureka/
限流規則配置類
使用時只需注入對應的 SentinelGatewayFilter
實例以及 SentinelGatewayBlockExceptionHandler
實例即可。
GatewayConfiguration.java
package com.example.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 限流規則配置類
*/
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
/**
* 構造器
*
* @param viewResolversProvider
* @param serverCodecConfigurer
*/
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 限流異常處理器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 限流過濾器
*
* @return
*/
@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);
}
}
啟動類
GatewayServerSentinelApplication.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// 開啟 EurekaClient 註解,目前版本如果配置了 Eureka 註冊中心,默認會開啟該註解
//@EnableEurekaClient
@SpringBootApplication
public class GatewayServerSentinelApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerSentinelApplication.class, args);
}
}
訪問
多次訪問://localhost:9001/order/1 結果如下:
介面 BlockRequestHandler
的默認實現為 DefaultBlockRequestHandler
,當觸發限流時會返回默認的錯誤資訊:Blocked by Sentinel: FlowException
。我們可以通過 GatewayCallbackManager
訂製異常提示資訊。
自定義異常提示
GatewayCallbackManager
的 setBlockHandler
註冊函數用於實現自定義的邏輯,處理被限流的請求。
package com.example.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* 限流規則配置類
*/
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
/**
* 構造器
*
* @param viewResolversProvider
* @param serverCodecConfigurer
*/
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 限流異常處理器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 限流過濾器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* Spring 容器初始化的時候執行該方法
*/
@PostConstruct
public void doInit() {
// 載入網關限流規則
initGatewayRules();
// 載入自定義限流異常處理器
initBlockHandler();
}
/**
* 網關限流規則
*/
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);
}
/**
* 自定義限流異常處理器
*/
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);
}
}
訪問
多次訪問://localhost:9001/order/1 結果如下:
分組限流
package com.example.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* 限流規則配置類
*/
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
/**
* 構造器
*
* @param viewResolversProvider
* @param serverCodecConfigurer
*/
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 限流異常處理器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 限流過濾器
*
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* Spring 容器初始化的時候執行該方法
*/
@PostConstruct
public void doInit() {
// 載入網關限流規則
initGatewayRules();
// 載入自定義限流異常處理器
initBlockHandler();
}
/**
* 網關限流規則
*/
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 秒
// --------------------限流分組----------start----------
rules.add(new GatewayFlowRule("product-api")
.setCount(3) // 限流閾值
.setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是 1 秒
rules.add(new GatewayFlowRule("order-api")
.setCount(5) // 限流閾值
.setIntervalSec(60)); // 統計時間窗口,單位是秒,默認是 1 秒
// --------------------限流分組-----------end-----------
// 載入網關限流規則
GatewayRuleManager.loadRules(rules);
// 載入限流分組
initCustomizedApis();
}
/**
* 自定義限流異常處理器
*/
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);
}
/**
* 限流分組
*/
private 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);
}
}
訪問
訪問://localhost:9001/product-service/product/1 觸發限流
訪問://localhost:9001/order-service/order/index 觸發限流
訪問://localhost:9001/order-service/order/1 不會觸發限流
高可用網關
業內通常用多少 9 來衡量網站的可用性,例如 QQ 的可用性是 4 個 9,就是說 QQ 能夠保證在一年裡,服務在 99.99% 的時間是可用的,只有 0.01% 的時間不可用,大約最多 53 分鐘。
對於大多數網站,2 個 9 是基本可用;3 個 9 是叫高可用;4 個 9 是擁有自動恢復能力的高可用。
實現高可用的主要手段是數據的冗餘備份和服務的失效轉移,這兩種手段具體可以怎麼做呢,在網關里如何體現?主要有以下幾個方向:
- 集群部署
- 負載均衡
- 健康檢查
- 節點自動重啟
- 熔斷
- 服務降級
- 介面重試
Nginx + 網關集群實現高可用網關
下載
官網://nginx.org/en/download.html 下載穩定版。為了方便學習,請下載 Windows 版本。
安裝
解壓文件後直接運行根路徑下的 nginx.exe
文件即可。
Nginx 默認埠為 80,訪問://localhost:80/ 看到下圖說明安裝成功。
配置網關集群
進入 Nginx 的 conf
目錄,打開 nginx.conf
文件,配置網關集群:
http {
...
# 網關集群
upstream gateway {
server 127.0.0.1:9000;
server 127.0.0.1:9001;
}
server {
listen 80;
server_name localhost;
...
# 代理網關集群,負載均衡調用
location / {
proxy_pass //gateway;
}
...
}
...
}
訪問
啟動兩台網關伺服器 //localhost:9000/
,//localhost:9001/
和相關服務。
訪問://localhost/product-service/product/1 實現高可用網關。
總結
一個請求過來,首先經過 Nginx 的一層負載,到達網關,然後由網關負載到真實後端,若後端有問題,網關會進行重試訪問,多次訪問後仍返回失敗,可以通過熔斷或服務降級立即返回結果。而且,由於是負載均衡,網關重試時不一定會訪問到出錯的後端。
至此 Gateway 服務網關所有的知識點就講解結束了。
本文採用 知識共享「署名-非商業性使用-禁止演繹 4.0 國際」許可協議
。
大家可以通過 分類
查看更多關於 Spring Cloud
的文章。
🤗 您的點贊
和轉發
是對我最大的支援。
📢 掃碼關注 哈嘍沃德先生
「文檔 + 影片」每篇文章都配有專門影片講解,學習更輕鬆噢 ~