九、服務網關:Gateway
9.1、網關簡介
大家都都知道在微服務架構中,一個系統會被拆分為很多個微服務。那么作為客戶端要如何去調用這么多的微服務呢?如果沒有網關的存在,我們只能在客戶端記錄每個微服務的地址,然后分別去調用。
這樣的架構會存在許多的問題:
- 客戶端多次請求不同的微服務,增加客戶端代碼或配置編寫的復雜性。
- 認證復雜,每個服務都需要獨立認證。
- 存在跨域請求,在一定場景下處理相對復雜。
網關就是為了解決這些問題而生的。所謂的API網關,就是指系統的統一入口,它封裝了應用程序的內部結構,為客戶端提供統一服務,一些與業務本身功能無關的公共邏輯可以在這里實現,諸如認證、鑒權、監控、路由轉發等等。
9.2、常用的網關
9.2.1、Ngnix+lua
使用nginx的反向代理和負載均衡可實現對api服務器的負載均衡及高可用。
lua是一種腳本語言,可以來編寫一些簡單的邏輯, nginx支持lua腳本
9.2.2、Kong
基於Nginx+Lua開發,性能高,穩定,有多個可用的插件(限流、鑒權等等)可以開箱即用。
他的缺點:
- 只支持Http協議。
- 二次開發,自由擴展困難。
- 提供管理API,缺乏更易用的管控、配置方式。
9.2.3、Zuul
Netflix開源的網關,功能豐富,使用JAVA開發,易於二次開發。
他的缺點:
- 缺乏管控,無法動態配置。
- 依賴組件較多。
- 處理Http請求依賴的是Web容器,性能不如Nginx。
9.2.4、Spring Cloud Gateway
Spring公司為了替換Zuul而開發的網關服務,SpringCloud alibaba技術棧中並沒有提供自己的網關,我們可以采用Spring Cloud Gateway來做網關
9.3、Gateway簡介
Spring Cloud Gateway是Spring公司基於Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的網關,它旨在為微服務架構提供一種簡單有效的統一的 API 路由管理方式。它的目標是替代Netflix Zuul,其不僅提供統一的路由方式,並且基於 Filter 鏈的方式提供了網關基本的功能,例如:安全,監控和限流。
他的主要功能是:
- 進行轉發重定向。
- 在開始的時候,所有類都需要做的初始化操作。
- 進行網絡隔離。
9.4、快速入門
需求:通過瀏覽器訪問api網關,然后通過網關將請求轉發到商品微服務。
9.4.1、基礎版
創建一個api-gateway 模塊,並且導入下面的依賴。
<?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">
<parent>
Shop-parent
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
api-gateway
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!--gateway網關-->
<dependency>
<groupId>org.springframework.cloud</groupId>
spring-cloud-starter-gateway
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
lombok
</dependency>
</dependencies>
</project>
復制代碼
編寫配置文件
server:
port: 9000 # 指定網關服務的端口
spring:
application:
name: api-gateway
cloud:
gateway:
routes: # 路由數組[路由 就是指定當請求滿足什么條件的時候轉到哪個微服務]
- id: product_route # 當前路由的標識, 要求唯一
uri: http://localhost:8081 # 請求要轉發到的地址
order: 1 # 路由的優先級,數字越小級別越高
predicates: # 斷言(就是路由轉發要滿足的條件)
- Path=/product-serv/** # 當請求路徑滿足Path指定的規則時,才進行路由轉發
filters: # 過濾器,請求在傳遞過程中可以通過過濾器對其進行一定的修改
- StripPrefix=1 # 轉發之前去掉1層路徑
復制代碼
測試
9.4.2、升級版
我們發現升級版有一個很大的問題,那就是在配置文件中寫死了轉發路徑的地址,我們需要在注冊中心來獲取地址。
加入nacos依賴
<?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">
<parent>
Shop-parent
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
api-gateway
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!--gateway網關-->
<dependency>
<groupId>org.springframework.cloud</groupId>
spring-cloud-starter-gateway
</dependency>
<!--nacos客戶端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
spring-cloud-starter-alibaba-nacos-discovery
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
lombok
</dependency>
</dependencies>
</project>
復制代碼
在主類上添加注解
@SpringBootApplication
@EnableDiscoveryClient
public class GateWayServerApp {
public static void main(String[] args) {
SpringApplication.run(GateWayServerApp.class,args);
}
}
復制代碼
修改配置文件
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 讓gateway可以發現nacos中的微服務
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是從nacos中按照名稱獲取微服務,並遵循負載均衡策略
predicates:
- Path=/product-serv/** # 符合這個規定的才進行1轉發
filters:
- StripPrefix=1 # 將第一層去掉
復制代碼
我們還可以自定義多個路由規則。
spring:
application:
gateway:
routes:
- id: product_route
uri: lb://product-service
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
- id: order_route
uri: lb://order-service
predicates:
- Path=/order-serv/**
filters:
- StripPrefix=1
復制代碼
9.4.3、簡寫版
我們的配置文件無需寫的1那么復雜就可以實現功能,有一個簡寫版。
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true # 讓gateway可以發現nacos中的微服務
復制代碼
我們發現,就發現只要按照網關地址/微服務名稱/接口的格式去訪問,就可以得到成功響應。
9.5、Gateway核心架構
9.5.1、基本概念
路由(Route) 是 gateway 中最基本的組件之一,表示一個具體的路由信息載體。主要定義了下面的幾個信息:
- id:路由標識符,區別於其他 Route。
- uri:路由指向的目的地 uri,即客戶端請求最終被轉發到的微服務。
- order:用於多個 Route 之間的排序,數值越小排序越靠前,匹配優先級越高。
- predicate:斷言的作用是進行條件判斷,只有斷言都返回真,才會真正的執行路由。
- filter:過濾器用於修改請求和響應信息。
- predicate:斷言,用於進行條件判斷,只有斷言都返回真,才會真正的執行路由。
9.5.2、執行原理
- 接收用戶的請求,請求處理器交給處理器映射器,返回執行鏈。
- 請求處理器去調用web處理器,在web處理器里面對我們的路徑1進行處理。假設1我們的路徑1是:http://localhost:9000/product-serv/get?id=1 ,根據配置的路由規則,上本地找對應的服務信息:product-service對應的主機ip是192.168.10.130。
- 根據1ribbon的負載均衡策略去選擇一個節點,然后拼接好,將路徑中的product-serv替換成192.168.10.130:8081,如果你配置了filter,那么他還會走filter。
- 如果你沒有自定義路由的話,默認Gateway會幫你把第一層去掉。網關端口從此一個
/
開始到第二個/
開始算第一層。
9.6、過濾器
Gateway的過濾器的作用是:是在請求的傳遞過程中,對請求和響應做一些手腳。
Gateway的過濾器的生命周期:
- PRE:這種過濾器在請求被路由之前調用。我們可利用這種過濾器實現身份驗證、在集群中選擇 請求的微服務、記錄調試信息等。
- POST:這種過濾器在路由到微服務以后執行。這種過濾器可用來為響應添加標准的HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。
Gateway 的Filter從作用范圍可分為兩種: GatewayFilter與GlobalFilter:
- GatewayFilter:應用到單個路由或者一個分組的路由上。
- GlobalFilter:應用到所有的路由上。
9.6.1、局部過濾器
局部過濾器是針對單個路由的過濾器。他分為內置過濾器和自定義過濾器。
9.6.1.1、內置過濾器
在SpringCloud Gateway中內置了很多不同類型的網關路由過濾器。
9.6.1.1.1、局部過濾器內容
過濾器工廠 | 作用 | 參數 |
---|---|---|
AddRequestHeader | 為原始請求添加Header | Header的名稱及值 |
AddRequestParameter | 為原始請求添加請求參數 | 參數名稱及值 |
AddResponseHeader | 為原始響應添加Header | Header的名稱及值 |
DedupeResponseHeader | 剔除響應頭中重復的值 | 需要去重的Header名稱及去重策略 |
Hystrix | 為路由引入Hystrix的斷路器保護 | HystrixCommand 的名稱 |
FallbackHeaders | 為fallbackUri的請求頭中添加具體的異常信息 | Header的名稱 |
PrefixPath | 為原始請求路徑添加前綴 | 前綴路徑 |
PreserveHostHeader | 為請求添加一個preserveHostHeader=true的屬性,路由過濾器會檢查該屬性以決定是否要發送原始的Host | 無 |
RequestRateLimiter | 用於對請求限流,限流算法為令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
RedirectTo | 將原始請求重定向到指定的URL | http狀態碼及重定向的url |
RemoveHopByHopHeadersFilter | 為原始請求刪除IETF組織規定的一系列Header | 默認就會啟用,可以通過配置指定僅刪除哪些Header |
RemoveRequestHeader | 為原始請求刪除某個Header | Header名稱 |
RemoveResponseHeader | 為原始響應刪除某個Header | Header名稱 |
RewritePath | 重寫原始的請求路徑 | 原始路徑正則表達式以及重寫后路徑的正則表達式 |
RewriteResponseHeader | 重寫原始響應中的某個Header | Header名稱,值的正則表達式,重寫后的值 |
SaveSession | 在轉發請求之前,強制執行WebSession::save 操作 |
無 |
secureHeaders | 為原始響應添加一系列起安全作用的響應頭 | 無,支持修改這些安全響應頭的值 |
SetPath | 修改原始的請求路徑 | 修改后的路徑 |
SetResponseHeader | 修改原始響應中某個Header的值 | Header名稱,修改后的值 |
SetStatus | 修改原始響應的狀態碼 | HTTP 狀態碼,可以是數字,也可以是字符串 |
StripPrefix | 用於截斷原始請求的路徑 | 使用數字表示要截斷的路徑的數量 |
Retry | 針對不同的響應進行重試 | retries、statuses、methods、series |
RequestSize | 設置允許接收最大請求包的大小。如果請求包大小超過設置的值,則返回 413 Payload Too Large | 請求包大小,單位為字節,默認值為5M |
ModifyRequestBody | 在轉發請求之前修改原始請求體內容 | 修改后的請求體內容 |
ModifyResponseBody | 修改原始響應體的內容 | 修改后的響應體內容 |
9.6.1.1.2、局部過濾器的使用
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 讓gateway可以發現nacos中的微服務
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是從nacos中按照名稱獲取微服務,並遵循負載均衡策略
predicates:
- Path=/product-serv/** # 符合這個規定的才進行1轉發
filters:
- StripPrefix=1 # 將第一層去掉
- SetStatus=2000 # 這里使用內置的過濾器,修改返回狀態
復制代碼
9.6.1.2、自定義局部過濾器
很多的時候,內置過濾器沒辦法滿足我們的需求,這個時候就必須自定義局部過濾器。我們假定一個需求是:統計訂單服務調用耗時。
編寫一個類,用於實現邏輯
名稱是有固定格式xxxGatewayFilterFactory
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {
private static final String BEGIN_TIME = "beginTime";
//構造函數
public TimeGatewayFilterFactory() {
super(TimeGatewayFilterFactory.Config.class);
}
//讀取配置文件中的參數 賦值到 配置類中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("show");
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!config.show){
// 如果配置類中的show為false,表示放行
return chain.filter(exchange);
}
exchange.getAttributes().put(BEGIN_TIME, System.currentTimeMillis());
/**
* pre的邏輯
* chain.filter().then(Mono.fromRunable(()->{
* post的邏輯
* }))
*/
return chain.filter(exchange).then(Mono.fromRunnable(()->{
Long startTime = exchange.getAttribute(BEGIN_TIME);
if (startTime != null) {
System.out.println(exchange.getRequest().getURI() + "請求耗時: " + (System.currentTimeMillis() - startTime) + "ms");
}
}));
}
};
}
@Setter
@Getter
static class Config{
private boolean show;
}
}
復制代碼
編寫application.xml
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true # 讓gateway可以發現nacos中的微服務
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是從nacos中按照名稱獲取微服務,並遵循負載均衡策略
predicates:
- Path=/product-serv/** # 符合這個規定的才進行1轉發
filters:
- StripPrefix=1 # 將第一層去掉
- id: order_route
uri: lb://order-service
predicates:
- Path=/order-serv/**
filters:
- StripPrefix=1
- Time=true
復制代碼
訪問路徑:http://localhost:9000/order-serv/getById?o=1&pid=1
9.6.2、全局過濾器
全局過濾器作用於所有路由, 無需配置。通過全局過濾器可以實現對權限的統一校驗,安全性驗證等功能。SpringCloud Gateway內部也是通過一系列的內置全局過濾器對整個路由轉發進行處理。
開發中的鑒權邏輯:
- 當客戶端第一次請求服務時,服務端對用戶進行信息認證(登錄)。
- 認證通過,將用戶信息進行加密形成token,返回給客戶端,作為登錄憑證。
- 以后每次請求,客戶端都攜帶認證的token。
- 服務端對token進行解密,判斷是否有效。
我們來模擬一個需求:實現統一鑒權的功能,我們需要在網關判斷請求中是否包含token且,如果沒有則不轉發路由,有則執行正常邏輯。
編寫全局過濾器
@Component
public class AuthGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token)) {
System.out.println("鑒權失敗");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
}
復制代碼
9.6.3、網關限流
網關是所有請求的公共入口,所以可以在網關進行限流,而且限流的方式也很多,我們本次采用前面學過的Sentinel組件來實現網關的限流。Sentinel支持對SpringCloud Gateway、Zuul等主流網關進行限流。
從1.6.0版本開始,Sentinel提供了SpringCloud Gateway的適配模塊,可以提供兩種資源維度的限流:
- route維度:即在Spring配置文件中配置的路由條目,資源名為對應的routeId
- 自定義API維度:用戶可以利用Sentinel提供的API來自定義一些API分組
9.6.3.1、網關集成Sentinel
添加依賴
<dependency>
<groupId>com.alibaba.csp</groupId>
sentinel-spring-cloud-gateway-adapter
</dependency>
復制代碼
編寫配置類進行限流
配置類的本質是用代碼替代nacos圖形化界面限流。
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 配置限流的異常處理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 初始化一個限流的過濾器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
//增加對商品微服務的限流
@PostConstruct
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_route")
.setCount(3) // 三次
.setIntervalSec(1) // 一秒,表示一秒鍾1超過了三次就會限流
);
GatewayRuleManager.loadRules(rules);
}
}
復制代碼
修改限流默認返回格式
如果我們不想在限流的時候返回默認的錯誤,那么就需要自定義錯誤,指定自定義的返回格式。我們只需在類中添加一段配置即可。
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON).
body(BodyInserters.fromValue(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
復制代碼
測試
9.6.3.2、自定義API分組
我們可以發現,上面的這種定義,對整個服務進行了限流,粒度不夠細。自定義API分組是一種更細粒度的限流規則定義,它可以實現某個方法的細粒度限流。
在Shop-order-server項目中添加ApiController
@RestController
@RequestMapping("/api")
public class ApiController {
@RequestMapping("/hello")
public String api1(){
return "api";
}
}
復制代碼
在GatewayConfiguration中添加配置
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/order-serv/api/**"). setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
@PostConstruct
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product_route")
.setCount(3)
.setIntervalSec(1)
);
rules.add(new GatewayFlowRule("order_api").
setCount(1).
setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
復制代碼
測試
直接訪問http://localhost:8082/api/hello 是不會發生限流的,訪問http://localhost:9000/order-serv/api/hello 就會出現限流了。
作者:XiaoLin_Java
鏈接:https://juejin.cn/post/7001816849826447397
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
微信公眾號【程序員黃小斜】作者是前螞蟻金服Java工程師,專注分享Java技術干貨和求職成長心得,不限於BAT面試,算法、計算機基礎、數據庫、分布式、spring全家桶、微服務、高並發、JVM、Docker容器,ELK、大數據等。關注后回復【book】領取精選20本Java面試必備精品電子書。