九、服務網(wǎng)關:Gateway
9.1、網(wǎng)關簡介
大家都都知道在微服務架構中,一個系統(tǒng)會被拆分為很多個微服務。那么作為客戶端要如何去調(diào)用這么多的微服務呢?如果沒有網(wǎng)關的存在,我們只能在客戶端記錄每個微服務的地址,然后分別去調(diào)用。
這樣的架構會存在許多的問題:
客戶端多次請求不同的微服務,增加客戶端代碼或配置編寫的復雜性。
認證復雜,每個服務都需要獨立認證。
-
存在跨域請求,在一定場景下處理相對復雜。
網(wǎng)關就是為了解決這些問題而生的。所謂的API網(wǎng)關,就是指系統(tǒng)的統(tǒng)一入口,它封裝了應用程序的內(nèi)部結構,為客戶端提供統(tǒng)一服務,一些與業(yè)務本身功能無關的公共邏輯可以在這里實現(xiàn),諸如認證、鑒權、監(jiān)控、路由轉發(fā)等等。
9.2、常用的網(wǎng)關
9.2.1、Ngnix+lua
使用nginx的反向代理和負載均衡可實現(xiàn)對api服務器的負載均衡及高可用。
lua是一種腳本語言,可以來編寫一些簡單的邏輯, nginx支持lua腳本
9.2.2、Kong
基于Nginx+Lua開發(fā),性能高,穩(wěn)定,有多個可用的插件(限流、鑒權等等)可以開箱即用。
他的缺點:
- 只支持Http協(xié)議。
- 二次開發(fā),自由擴展困難。
- 提供管理API,缺乏更易用的管控、配置方式。
9.2.3、Zuul
Netflix開源的網(wǎng)關,功能豐富,使用JAVA開發(fā),易于二次開發(fā)。
他的缺點:
- 缺乏管控,無法動態(tài)配置。
- 依賴組件較多。
- 處理Http請求依賴的是Web容器,性能不如Nginx。
9.2.4、Spring Cloud Gateway
Spring公司為了替換Zuul而開發(fā)的網(wǎng)關服務,SpringCloud alibaba技術棧中并沒有提供自己的網(wǎng)關,我們可以采用Spring Cloud Gateway來做網(wǎng)關
9.3、Gateway簡介
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發(fā)的網(wǎng)關,它旨在為微服務架構提供一種簡單有效的統(tǒng)一的 API 路由管理方式。它的目標是替代Netflix Zuul,其不僅提供統(tǒng)一的路由方式,并且基于 Filter 鏈的方式提供了網(wǎng)關基本的功能,例如:安全,監(jiān)控和限流。
他的主要功能是:
- 進行轉發(fā)重定向。
- 在開始的時候,所有類都需要做的初始化操作。
- 進行網(wǎng)絡隔離。
9.4、快速入門
需求:通過瀏覽器訪問api網(wǎng)關,然后通過網(wǎng)關將請求轉發(fā)到商品微服務。
9.4.1、基礎版
創(chuàng)建一個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網(wǎng)關-->
<dependency>
<groupId>org.springframework.cloud</groupId>
spring-cloud-starter-gateway
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
lombok
</dependency>
</dependencies>
</project>
復制代碼
編寫配置文件
server:
port: 9000 # 指定網(wǎng)關服務的端口
spring:
application:
name: api-gateway
cloud:
gateway:
routes: # 路由數(shù)組[路由 就是指定當請求滿足什么條件的時候轉到哪個微服務]
- id: product_route # 當前路由的標識, 要求唯一
uri: http://localhost:8081 # 請求要轉發(fā)到的地址
order: 1 # 路由的優(yōu)先級,數(shù)字越小級別越高
predicates: # 斷言(就是路由轉發(fā)要滿足的條件)
- Path=/product-serv/** # 當請求路徑滿足Path指定的規(guī)則時,才進行路由轉發(fā)
filters: # 過濾器,請求在傳遞過程中可以通過過濾器對其進行一定的修改
- StripPrefix=1 # 轉發(fā)之前去掉1層路徑
復制代碼
測試
9.4.2、升級版
我們發(fā)現(xiàn)升級版有一個很大的問題,那就是在配置文件中寫死了轉發(fā)路徑的地址,我們需要在注冊中心來獲取地址。
加入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網(wǎng)關-->
<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可以發(fā)現(xiàn)nacos中的微服務
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是從nacos中按照名稱獲取微服務,并遵循負載均衡策略
predicates:
- Path=/product-serv/** # 符合這個規(guī)定的才進行1轉發(fā)
filters:
- StripPrefix=1 # 將第一層去掉
復制代碼
我們還可以自定義多個路由規(guī)則。
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那么復雜就可以實現(xiàn)功能,有一個簡寫版。
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true # 讓gateway可以發(fā)現(xiàn)nacos中的微服務
復制代碼
我們發(fā)現(xiàn),就發(fā)現(xiàn)只要按照網(wǎng)關地址/微服務名稱/接口的格式去訪問,就可以得到成功響應。
9.5、Gateway核心架構
9.5.1、基本概念
路由(Route) 是 gateway 中最基本的組件之一,表示一個具體的路由信息載體。主要定義了下面的幾個信息:
- id:路由標識符,區(qū)別于其他 Route。
- uri:路由指向的目的地 uri,即客戶端請求最終被轉發(fā)到的微服務。
- order:用于多個 Route 之間的排序,數(shù)值越小排序越靠前,匹配優(yōu)先級越高。
- predicate:斷言的作用是進行條件判斷,只有斷言都返回真,才會真正的執(zhí)行路由。
- filter:過濾器用于修改請求和響應信息。
- predicate:斷言,用于進行條件判斷,只有斷言都返回真,才會真正的執(zhí)行路由。
9.5.2、執(zhí)行原理
- 接收用戶的請求,請求處理器交給處理器映射器,返回執(zhí)行鏈。
- 請求處理器去調(diào)用web處理器,在web處理器里面對我們的路徑1進行處理。假設1我們的路徑1是:http://localhost:9000/product-serv/get?id=1 ,根據(jù)配置的路由規(guī)則,上本地找對應的服務信息:product-service對應的主機ip是192.168.10.130。
- 根據(jù)1ribbon的負載均衡策略去選擇一個節(jié)點,然后拼接好,將路徑中的product-serv替換成192.168.10.130:8081,如果你配置了filter,那么他還會走filter。
- 如果你沒有自定義路由的話,默認Gateway會幫你把第一層去掉。網(wǎng)關端口從此一個
/開始到第二個/開始算第一層。
9.6、過濾器
Gateway的過濾器的作用是:是在請求的傳遞過程中,對請求和響應做一些手腳。
Gateway的過濾器的生命周期:
PRE:這種過濾器在請求被路由之前調(diào)用。我們可利用這種過濾器實現(xiàn)身份驗證、在集群中選擇 請求的微服務、記錄調(diào)試信息等。
-
POST:這種過濾器在路由到微服務以后執(zhí)行。這種過濾器可用來為響應添加標準的HTTP Header、收集統(tǒng)計信息和指標、將響應從微服務發(fā)送給客戶端等。
Gateway 的Filter從作用范圍可分為兩種: GatewayFilter與GlobalFilter:
GatewayFilter:應用到單個路由或者一個分組的路由上。
GlobalFilter:應用到所有的路由上。
9.6.1、局部過濾器
局部過濾器是針對單個路由的過濾器。他分為內(nèi)置過濾器和自定義過濾器。
9.6.1.1、內(nèi)置過濾器
在SpringCloud Gateway中內(nèi)置了很多不同類型的網(wǎng)關路由過濾器。
9.6.1.1.1、局部過濾器內(nèi)容
| 過濾器工廠 | 作用 | 參數(shù) |
|---|---|---|
| AddRequestHeader | 為原始請求添加Header | Header的名稱及值 |
| AddRequestParameter | 為原始請求添加請求參數(shù) | 參數(shù)名稱及值 |
| AddResponseHeader | 為原始響應添加Header | Header的名稱及值 |
| DedupeResponseHeader | 剔除響應頭中重復的值 | 需要去重的Header名稱及去重策略 |
| Hystrix | 為路由引入Hystrix的斷路器保護 | HystrixCommand 的名稱 |
| FallbackHeaders | 為fallbackUri的請求頭中添加具體的異常信息 | Header的名稱 |
| PrefixPath | 為原始請求路徑添加前綴 | 前綴路徑 |
| PreserveHostHeader | 為請求添加一個preserveHostHeader=true的屬性,路由過濾器會檢查該屬性以決定是否要發(fā)送原始的Host | 無 |
| RequestRateLimiter | 用于對請求限流,限流算法為令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
| RedirectTo | 將原始請求重定向到指定的URL | http狀態(tài)碼及重定向的url |
| RemoveHopByHopHeadersFilter | 為原始請求刪除IETF組織規(guī)定的一系列Header | 默認就會啟用,可以通過配置指定僅刪除哪些Header |
| RemoveRequestHeader | 為原始請求刪除某個Header | Header名稱 |
| RemoveResponseHeader | 為原始響應刪除某個Header | Header名稱 |
| RewritePath | 重寫原始的請求路徑 | 原始路徑正則表達式以及重寫后路徑的正則表達式 |
| RewriteResponseHeader | 重寫原始響應中的某個Header | Header名稱,值的正則表達式,重寫后的值 |
| SaveSession | 在轉發(fā)請求之前,強制執(zhí)行WebSession::save操作 |
無 |
| secureHeaders | 為原始響應添加一系列起安全作用的響應頭 | 無,支持修改這些安全響應頭的值 |
| SetPath | 修改原始的請求路徑 | 修改后的路徑 |
| SetResponseHeader | 修改原始響應中某個Header的值 | Header名稱,修改后的值 |
| SetStatus | 修改原始響應的狀態(tài)碼 | HTTP 狀態(tài)碼,可以是數(shù)字,也可以是字符串 |
| StripPrefix | 用于截斷原始請求的路徑 | 使用數(shù)字表示要截斷的路徑的數(shù)量 |
| Retry | 針對不同的響應進行重試 | retries、statuses、methods、series |
| RequestSize | 設置允許接收最大請求包的大小。如果請求包大小超過設置的值,則返回 413 Payload Too Large | 請求包大小,單位為字節(jié),默認值為5M |
| ModifyRequestBody | 在轉發(fā)請求之前修改原始請求體內(nèi)容 | 修改后的請求體內(nèi)容 |
| ModifyResponseBody | 修改原始響應體的內(nèi)容 | 修改后的響應體內(nèi)容 |
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可以發(fā)現(xiàn)nacos中的微服務
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是從nacos中按照名稱獲取微服務,并遵循負載均衡策略
predicates:
- Path=/product-serv/** # 符合這個規(guī)定的才進行1轉發(fā)
filters:
- StripPrefix=1 # 將第一層去掉
- SetStatus=2000 # 這里使用內(nèi)置的過濾器,修改返回狀態(tài)
復制代碼
9.6.1.2、自定義局部過濾器
很多的時候,內(nèi)置過濾器沒辦法滿足我們的需求,這個時候就必須自定義局部過濾器。我們假定一個需求是:統(tǒng)計訂單服務調(diào)用耗時。
編寫一個類,用于實現(xiàn)邏輯
**名稱是有固定格式xxxGatewayFilterFactory**
@Component
public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {
private static final String BEGIN_TIME = "beginTime";
//構造函數(shù)
public TimeGatewayFilterFactory() {
super(TimeGatewayFilterFactory.Config.class);
}
//讀取配置文件中的參數(shù) 賦值到 配置類中
@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可以發(fā)現(xiàn)nacos中的微服務
routes:
- id: product_route # 路由的名字
uri: lb://product-service # lb指的是從nacos中按照名稱獲取微服務,并遵循負載均衡策略
predicates:
- Path=/product-serv/** # 符合這個規(guī)定的才進行1轉發(fā)
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、全局過濾器
全局過濾器作用于所有路由, 無需配置。通過全局過濾器可以實現(xiàn)對權限的統(tǒng)一校驗,安全性驗證等功能。SpringCloud Gateway內(nèi)部也是通過一系列的內(nèi)置全局過濾器對整個路由轉發(fā)進行處理。
開發(fā)中的鑒權邏輯:
- 當客戶端第一次請求服務時,服務端對用戶進行信息認證(登錄)。
- 認證通過,將用戶信息進行加密形成token,返回給客戶端,作為登錄憑證。
- 以后每次請求,客戶端都攜帶認證的token。
- 服務端對token進行解密,判斷是否有效。
我們來模擬一個需求:實現(xiàn)統(tǒng)一鑒權的功能,我們需要在網(wǎng)關判斷請求中是否包含token且,如果沒有則不轉發(fā)路由,有則執(zhí)行正常邏輯。
編寫全局過濾器
@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、網(wǎng)關限流
網(wǎng)關是所有請求的公共入口,所以可以在網(wǎng)關進行限流,而且限流的方式也很多,我們本次采用前面學過的Sentinel組件來實現(xiàn)網(wǎng)關的限流。Sentinel支持對SpringCloud Gateway、Zuul等主流網(wǎng)關進行限流。
從1.6.0版本開始,Sentinel提供了SpringCloud Gateway的適配模塊,可以提供兩種資源維度的限流:
- route維度:即在Spring配置文件中配置的路由條目,資源名為對應的routeId
- 自定義API維度:用戶可以利用Sentinel提供的API來自定義一些API分組
9.6.3.1、網(wǎng)關集成Sentinel
添加依賴
<dependency>
<groupId>com.alibaba.csp</groupId>
sentinel-spring-cloud-gateway-adapter
</dependency>
復制代碼
編寫配置類進行限流
配置類的本質(zhì)是用代碼替代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分組
我們可以發(fā)現(xiàn),上面的這種定義,對整個服務進行了限流,粒度不夠細。自定義API分組是一種更細粒度的限流規(guī)則定義,它可以實現(xiàn)某個方法的細粒度限流。
在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](https://link.juejin.cn?target=http%3A%2F%2Flocalhost%3A8082%2Fapi%2Fhello "http://localhost:8082/api/hello") 是不會發(fā)生限流的,訪問[http://localhost:9000/order-serv/api/hello](https://link.juejin.cn?target=http%3A%2F%2Flocalhost%3A9000%2Forder-serv%2Fapi%2Fhello "http://localhost:9000/order-serv/api/hello") 就會出現(xiàn)限流了。
作者:XiaoLin_Java
鏈接:https://juejin.cn/post/7001816849826447397
來源:稀土掘金
著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。
微信公眾號【程序員黃小斜】作者是前螞蟻金服Java工程師,專注分享Java技術干貨和求職成長心得,不限于BAT面試,算法、計算機基礎、數(shù)據(jù)庫、分布式、spring全家桶、微服務、高并發(fā)、JVM、Docker容器,ELK、大數(shù)據(jù)等。關注后回復【book】領取精選20本Java面試必備精品電子書。