一、簡介
Spring Cloud Gateway 是Spring Cloud 生態全新項目,基于Spring 5、Spring Boot 2.X、Project Reactor實現的API網關,旨在為微服務提供簡單高效的API路由管理方法。
Spring Cloud Gateway 作為Spring Cloud 生態中的網關,目標是代替Zuul 1.X。Spring Cloud 2.X版本目前仍未對Zuul 2.X高性能版本進行集成,仍使用的是非Reactor的老版本Zuul網關。
- 目前Spring Cloud dependencies 最新版本Hoxton.SR8 仍使用的是Zuul 1.3.1
- Zuul 2.x 高性能Reactor版本本身與18年5月開源,目前最新版本2.1.9
為了提高網關性能,Spring Cloud Gateway基于WebFlux框架實現,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。
1.1 術語
- Route: Gateway的基本構建模塊,由ID、目標URL、謂詞集合和過濾器集合定義。
- Predicate: Java8 Funciton Predicate,輸入類型是 SpringFramework
ServerWebExchange
,可以匹配HTTP請求的所有內容,比如標頭或參數。 - Filter:使用特定工廠構造的Spring Framework
GatewayFilter
實例,可以在發送下游請求之前或之后修改請求或響應。
1.3 特性
- 動態路由:能夠匹配任何請求屬性;
- 可以對路由指定 Predicate(斷言)和 Filter(過濾器);
- 集成Hystrix的斷路器功能;
- 集成 Spring Cloud 服務發現功能;
- 易于編寫的 Predicate(斷言)和 Filter(過濾器);
- 請求限流功能;
- 支持路徑重寫
1.4 Spring Cloud Gateway與Spring Cloud Zuul
Spring Cloud Zuul
Springcloud 2.x 版本到目前為止中所集成的Zuul版本(1.x),采用的是Tomcat容器,使用的是傳統的Servlet IO處理模型。
servlet由servlet container進行生命周期管理。container啟動時構造servlet對象并調用servlet init()進行初始化;container關閉時調用servlet destory()銷毀servlet;container運行時接受請求,并為每個請求分配一個線程(一般從線程池中獲取空閑線程)然后調用service()。
弊端:servlet是一個簡單的網絡IO模型,當請求進入servlet container時,servlet container就會為其綁定一個線程,在并發不高的場景下這種模型是適用的,但是一旦并發上升,線程數量就會上漲,而線程資源代價是昂貴的(上線文切換,內存消耗大)嚴重影響請求的處理時間。在一些簡單的業務場景下,不希望為每個request分配一個線程,只需要1個或幾個線程就能應對極大并發的請求,這種業務場景下servlet模型沒有優勢。
所以Springcloud Zuul 是基于servlet之上的一個阻塞式處理模型,即spring實現了處理所有request請求的一個servlet(DispatcherServlet),并由該servlet阻塞式處理處理。所以Springcloud Zuul無法擺脫servlet模型的弊端。
Webflux模型
Webflux模式替換了舊的Servlet線程模型。用少量的線程處理request和response io操作,這些線程稱為Loop線程,而業務交給響應式編程框架處理,響應式編程是非常靈活的,用戶可以將業務中阻塞的操作提交到響應式框架的work線程中執行,而不阻塞的操作依然可以在Loop線程中進行處理,大大提高了Loop線程的利用率。官方結構圖:
Webflux雖然可以兼容多個底層的通信框架,但是一般情況下,底層使用的還是Netty,畢竟,Netty是目前業界認可的最高性能的通信框架。而Webflux的Loop線程,正好就是著名的Reactor 模式IO處理模型的Reactor線程,如果使用的是高性能的通信框架Netty,這就是Netty的EventLoop線程。
1.5 如何集成Gateway
使用Gateway只需要簡單引入依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
二、工作原理
處理流程:
- Gateway接受客戶端請求;
- 網關處理程序映射確定請求與路由匹配,匹配成功則將其發送到網關Web處理程序;
- Web處理程序處理程序通過特定于請求的過濾器鏈運行請求:
- 請求經過 Filter 過濾器鏈,執行 pre 處理邏輯,如修改請求頭信息等。
- 發出代理請求,請求被轉發至下游服務并返回響應。
- 響應經過 Filter 過濾器鏈,執行 post 處理邏輯。
- 向客戶端響應應答。
注意,在沒有端口的路由中定義的URI,HTTP和HTTPS URI的默認端口值分別為80和443。
- DispatcherHandler:所有請求的調度器,負載請求分發
public class DispatcherHandler implements WebHandler, ApplicationContextAware {
@Nullable
private List<HandlerMapping> handlerMappings;
@Nullable
private List<HandlerAdapter> handlerAdapters;
@Nullable
private List<HandlerResultHandler> resultHandlers;
public DispatcherHandler() {
}
public DispatcherHandler(ApplicationContext applicationContext) {
this.initStrategies(applicationContext);
}
@Nullable
public final List<HandlerMapping> getHandlerMappings() {
return this.handlerMappings;
}
public void setApplicationContext(ApplicationContext applicationContext) {
this.initStrategies(applicationContext);
}
# 初始、校驗HandlerMapping并按order排序
protected void initStrategies(ApplicationContext context) {
Map<String, HandlerMapping> mappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
ArrayList<HandlerMapping> mappings = new ArrayList(mappingBeans.values());
AnnotationAwareOrderComparator.sort(mappings);
this.handlerMappings = Collections.unmodifiableList(mappings);
Map<String, HandlerAdapter> adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
this.handlerAdapters = new ArrayList(adapterBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
Map<String, HandlerResultHandler> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerResultHandler.class, true, false);
this.resultHandlers = new ArrayList(beans.values());
AnnotationAwareOrderComparator.sort(this.resultHandlers);
}
//遍歷handlerMappings ,根據exchange找到對應的handler
// 對于Gateway 會找到對應的RoutePredicateHandlerMapping
public Mono<Void> handle(ServerWebExchange exchange) {
return this.handlerMappings == null ? this.createNotFoundError() : Flux.fromIterable(this.handlerMappings).concatMap((mapping) -> {
return mapping.getHandler(exchange);
}).next().switchIfEmpty(this.createNotFoundError())////如果遍歷不到結果,則切換到錯誤處理
.flatMap((handler) -> {
//通過HandlerAdapter調用handler,
//gateway使用的 SimpleHandlerAdapter
return this.invokeHandler(exchange, handler);
}).flatMap((result) -> {//對響應進行處理
return this.handleResult(exchange, result);
});
}
private <R> Mono<R> createNotFoundError() {
return Mono.defer(() -> {
Exception ex = new ResponseStatusException(HttpStatus.NOT_FOUND, "No matching handler");
return Mono.error(ex);
});
}
private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) {
if (this.handlerAdapters != null) {
Iterator var3 = this.handlerAdapters.iterator();
while(var3.hasNext()) {
HandlerAdapter handlerAdapter = (HandlerAdapter)var3.next();
if (handlerAdapter.supports(handler)) {
//調用handler的handle方法處理請求
return handlerAdapter.handle(exchange, handler);
}
}
}
return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
}
//根據result獲取對應的結果處理handler并處理結果
private Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
return this.getResultHandler(result).handleResult(exchange, result).checkpoint("Handler " + result.getHandler() + " [DispatcherHandler]").onErrorResume((ex) -> {
return result.applyExceptionHandler(ex).flatMap((exResult) -> {
String text = "Exception handler " + exResult.getHandler() + ", error=\"" + ex.getMessage() + "\" [DispatcherHandler]";
return this.getResultHandler(exResult).handleResult(exchange, exResult).checkpoint(text);
});
});
}
private HandlerResultHandler getResultHandler(HandlerResult handlerResult) {
if (this.resultHandlers != null) {
Iterator var2 = this.resultHandlers.iterator();
while(var2.hasNext()) {
HandlerResultHandler resultHandler = (HandlerResultHandler)var2.next();
if (resultHandler.supports(handlerResult)) {
return resultHandler;
}
}
}
throw new IllegalStateException("No HandlerResultHandler for " + handlerResult.getReturnValue());
}
}
- RoutePredicateHandlerMapping:路由謂語匹配器,用于路由的查找,以及找到路由后返回對應的WebHandler,DispatcherHandler會依次遍歷HandlerMapping集合進行處理
public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
private final FilteringWebHandler webHandler;
private final RouteLocator routeLocator;
private final Integer managementPort;
private final RoutePredicateHandlerMapping.ManagementPortType managementPortType;
public RoutePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties, Environment environment) {
this.webHandler = webHandler;
this.routeLocator = routeLocator;
this.managementPort = getPortProperty(environment, "management.server.");
this.managementPortType = this.getManagementPortType(environment);
//設置排序字段1,此處的目的是Spring Cloud Gateway 的 GatewayWebfluxEndpoint 提供 HTTP API ,不需要經過網關
//它通過 RequestMappingHandlerMapping 進行請求匹配處理。RequestMappingHandlerMapping 的 order = 0 ,需要排在 RoutePredicateHandlerMapping 前面。所有,RoutePredicateHandlerMapping 設置 order = 1 。
this.setOrder(1);
this.setCorsConfigurations(globalCorsProperties.getCorsConfigurations());
}
private RoutePredicateHandlerMapping.ManagementPortType getManagementPortType(Environment environment) {
Integer serverPort = getPortProperty(environment, "server.");
if (this.managementPort != null && this.managementPort < 0) {
return RoutePredicateHandlerMapping.ManagementPortType.DISABLED;
} else {
return this.managementPort != null && (serverPort != null || !this.managementPort.equals(8080)) && (this.managementPort == 0 || !this.managementPort.equals(serverPort)) ? RoutePredicateHandlerMapping.ManagementPortType.DIFFERENT : RoutePredicateHandlerMapping.ManagementPortType.SAME;
}
}
private static Integer getPortProperty(Environment environment, String prefix) {
return (Integer)environment.getProperty(prefix + "port", Integer.class);
}
//設置mapping到上下文環境
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
if (this.managementPortType == RoutePredicateHandlerMapping.ManagementPortType.DIFFERENT && this.managementPort != null && exchange.getRequest().getURI().getPort() == this.managementPort) {
return Mono.empty();
} else {
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_HANDLER_MAPPER_ATTR, this.getSimpleName());
// 查找路由
return this.lookupRoute(exchange).flatMap((r) -> {
exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Mapping [" + this.getExchangeDesc(exchange) + "] to " + r);
}
//將查找到的路由設置到上下文環境
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR, r);
//返回mapping對應的WebHandler即FilteringWebHandler
return Mono.just(this.webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
//當前未找到路由時返回空,并移除GATEWAY_PREDICATE_ROUTE_ATTR exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
if (this.logger.isTraceEnabled()) {
this.logger.trace("No RouteDefinition found for [" + this.getExchangeDesc(exchange) + "]");
}
})));
}
}
protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchange exchange) {
return super.getCorsConfiguration(handler, exchange);
}
private String getExchangeDesc(ServerWebExchange exchange) {
StringBuilder out = new StringBuilder();
out.append("Exchange: ");
out.append(exchange.getRequest().getMethod());
out.append(" ");
out.append(exchange.getRequest().getURI());
return out.toString();
}
//通過路由定位器獲取路由信息
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes().concatMap((route) -> {
return Mono.just(route).filterWhen((r) -> {
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return (Publisher)r.getPredicate().apply(exchange);//通過謂詞過濾路由
}).doOnError((e) -> {
this.logger.error("Error applying predicate for route: " + route.getId(), e);
}).onErrorResume((e) -> {
return Mono.empty();
});
}).next().map((route) -> {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Route matched: " + route.getId());
}
this.validateRoute(route, exchange);
return route;
});
}
protected void validateRoute(Route route, ServerWebExchange exchange) {
}
protected String getSimpleName() {
return "RoutePredicateHandlerMapping";
}
public static enum ManagementPortType {
DISABLED,
SAME,
DIFFERENT;
private ManagementPortType() {
}
}
}
- FilteringWebHandler : 使用Filter鏈表處理請求的WebHandler,RoutePredicateHandlerMapping找到路由后返回對應的FilteringWebHandler對請求進行處理,FilteringWebHandler負責組裝Filter鏈表并調用鏈表處理請求。
# 通過過濾器處理web請求的處理器
public class FilteringWebHandler implements WebHandler {
protected static final Log logger = LogFactory.getLog(FilteringWebHandler.class);
# 全局過濾器
private final List<GatewayFilter> globalFilters;
public FilteringWebHandler(List<GlobalFilter> globalFilters) {
this.globalFilters = loadFilters(globalFilters);
}
private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {
return (List)filters.stream().map((filter) -> {
FilteringWebHandler.GatewayFilterAdapter gatewayFilter = new FilteringWebHandler.GatewayFilterAdapter(filter);
if (filter instanceof Ordered) {
int order = ((Ordered)filter).getOrder();
return new OrderedGatewayFilter(gatewayFilter, order);
} else {
return gatewayFilter;
}
}).collect(Collectors.toList());
}
public Mono<Void> handle(ServerWebExchange exchange) {
#獲取請求上下文設置的路由實例
Route route = (Route)exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
# 獲取網關路由定義下的網關過濾器集合
List<GatewayFilter> gatewayFilters = route.getFilters();
# 組合全局的過濾器與路由配置的過濾器,并將路由器定義的過濾器添加集合尾部
List<GatewayFilter> combined = new ArrayList(this.globalFilters);
combined.addAll(gatewayFilters);
AnnotationAwareOrderComparator.sort(combined);
if (logger.isDebugEnabled()) {
logger.debug("Sorted gatewayFilterFactories: " + combined);
}
# 創建過濾器鏈表對其進行鏈式調用
return (new FilteringWebHandler.DefaultGatewayFilterChain(combined)).filter(exchange);
}
private static class GatewayFilterAdapter implements GatewayFilter {
private final GlobalFilter delegate;
GatewayFilterAdapter(GlobalFilter delegate) {
this.delegate = delegate;
}
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return this.delegate.filter(exchange, chain);
}
public String toString() {
StringBuilder sb = new StringBuilder("GatewayFilterAdapter{");
sb.append("delegate=").append(this.delegate);
sb.append('}');
return sb.toString();
}
}
private static class DefaultGatewayFilterChain implements GatewayFilterChain {
private final int index;
private final List<GatewayFilter> filters;
DefaultGatewayFilterChain(List<GatewayFilter> filters) {
this.filters = filters;
this.index = 0;
}
private DefaultGatewayFilterChain(FilteringWebHandler.DefaultGatewayFilterChain parent, int index) {
this.filters = parent.getFilters();
this.index = index;
}
public List<GatewayFilter> getFilters() {
return this.filters;
}
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < this.filters.size()) {
GatewayFilter filter = (GatewayFilter)this.filters.get(this.index);
FilteringWebHandler.DefaultGatewayFilterChain chain = new FilteringWebHandler.DefaultGatewayFilterChain(this, this.index + 1);
return filter.filter(exchange, chain);
} else {
return Mono.empty();
}
});
}
}
}
2.2 Gateway類圖
根據DispatcherHandler入口整理的Gateway類圖
Spring Cloud Gateway的配置由一系列RouteDefinitionLocator實例驅動。以下清單顯示了RouteDefinitionLocator接口的定義:
RouteDefinitionLocator.java
public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}
默認情況下,PropertiesRouteDefinitionLocator使用Spring Boot的@ConfigurationProperties機制來加載屬性。
三、配置路由謂詞工廠和網關過濾工廠
3.1 兩種不同的配置路由方式
Gateway 提供了兩種不同的方式用于配置路由,一種是通過yml文件來配置,另一種是通過Java Bean來配置。
通過yml文件來配置
service-url:
user-service: http://localhost:8201
spring:
cloud:
gateway:
routes:
- id: path_route #路由的ID
uri: ${service-url.user-service}/user/{id} #匹配后路由地址
predicates: # 斷言,路徑相匹配的進行路由
- Path=/user/{id}
通過Java Bean來配置
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route2", r -> r.path("/user/getByUsername")
.uri("http://localhost:8201/user/getByUsername"))
.build();
}
}
3.2 Route Predicate 的使用
Spring Cloud Gateway將路由匹配作為Spring WebFluxHandlerMapping基礎架構的一部分。Spring Cloud Gateway包括許多內置的路由謂詞工廠。所有這些謂詞都與HTTP請求的不同屬性匹配。可以將多個路由謂詞工廠與邏輯and語句結合使用。
Predicate 來源于 Java 8,是 Java 8 中引入的一個函數,Predicate 接受一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(比如:與,或,非)。可以用于接口請求參數校驗、判斷新老數據是否有變化需要進行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性實現了各種路由匹配規則,有通過 Header、請求參數等不同的條件來進行作為條件匹配到對應的路由。
下圖為 Spring Cloud Gateway內置的幾種常見謂詞路由器:
3.2.1 根據datetime 匹配
After Route Predicate
在指定時間之后的請求會匹配該路由。
spring:
cloud:
gateway:
routes:
- id: after_route
uri: ${service-url.user-service}
predicates:
- After=2019-09-24T16:30:00+08:00[Asia/Shanghai]
Before Route Predicate
在指定時間之前的請求會匹配該路由。
spring:
cloud:
gateway:
routes:
- id: before_route
uri: ${service-url.user-service}
predicates:
- Before=2019-09-24T16:30:00+08:00[Asia/Shanghai]
Between Route Predicate
在指定時間區間內的請求會匹配該路由。
spring:
cloud:
gateway:
routes:
- id: before_route
uri: ${service-url.user-service}
predicates:
- Between=2019-09-24T16:30:00+08:00[Asia/Shanghai], 2019-09-25T16:30:00+08:00[Asia/Shanghai]
3.2.2 根據Cookie匹配
帶有指定Cookie的請求會匹配該路由。
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: ${service-url.user-service}
predicates:
- Cookie=username,macro
3.2.3 Header Route Predicate
帶有指定請求頭的請求會匹配該路由。
spring:
cloud:
gateway:
routes:
- id: header_route
uri: ${service-url.user-service}
predicates:
- Header=X-Request-Id, \d+
3.2.4 Host Route Predicate
帶有指定Host的請求會匹配該路由。
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://example.org
predicates:
- Host=**.qt.com
3.2.5 Method Route Predicate
發送指定方法的請求會匹配該路由。
spring:
cloud:
gateway:
routes:
- id: method_route
uri: ${service-url.user-service}
predicates:
- Method=GET
3.2.5 Path Route Predicate
發送指定路徑的請求會匹配該路由。
spring:
cloud:
gateway:
routes:
- id: path_route
uri: ${service-url.user-service}/user/{id}
predicates:
- Path=/user/{id}
3.2.6 Query Route Predicate
帶指定查詢參數的請求可以匹配該路由。
spring:
cloud:
gateway:
routes:
- id: query_route
uri: ${service-url.user-service}/user/getByUsername
predicates:
- Query=username
3.2.7 RemoteAddr Route Predicate
從指定遠程地址發起的請求可以匹配該路由。
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: ${service-url.user-service}
predicates:
- RemoteAddr=192.168.1.1/24
3.3 Route Filter 的使用
根據Gateway工作原理,我們知道Gateway實際是由路由匹配到的一系列Filter過濾鏈來處理請求的,Spring Cloud Gateway包括許多內置的GatewayFilter工廠。具體詳情參考官網:
https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories
3.4 Global Filters 全局過濾器
當請求與路由匹配時,過濾Web處理程序會將的所有實例GlobalFilter和所有特定GatewayFilter于路由的實例添加到過濾器鏈中。該組合的過濾器鏈按org.springframework.core.Ordered接口排序,可以通過實現該getOrder()方法進行設置。
Spring Cloud Gateway區分了執行過濾器邏輯的“前”和“后”階段,因此優先級最高的過濾器是“前”階段的第一個,而“后”階段的最后一個是優先級最低的一個。
例如,下面程序配置了一個過濾器鏈:
@Bean
public GlobalFilter customFilter() {
return new CustomGlobalFilter();
}
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("custom global filter");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1;
}
}
四、 結合注冊中心和配置中心使用
Gateway會根據注冊中心注冊的服務列表,以服務名為路徑創建動態路由。這里主要使用Nacos作為注冊中心和配置中心
4.1 使用動態路由
4.1.1 基本配置
引入依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
啟用DiscoveryClient網關集成
# spring.cloud.gateway.discovery.locator.enabled=true
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #開啟從注冊中心動態創建路由的功能
lower-case-service-id: true #使用小寫服務名,默認是大寫
使用網關訪問服務:
C:\Users\liangbodlz\.ssh>curl 192.168.132.49:1500/nacos-provider/index
Hello!
4.1.2 使用Route Predicate Factory過濾器實現通過指定path訪問服務
在實際生產環境中,我們往往不會通過服務的application-name來訪問服務,而是通過某個固定的url path來訪問,比如xx.xxx/user/login,來訪問用戶服務的接口
通過Spring Cloud Gateway 內置 Path Route Predicate Factory
可以實現該目標:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #開啟從注冊中心動態創建路由的功能
lower-case-service-id: true #使用小寫服務名,默認是大寫
routes:
- id: nacos-provider
uri: lb://nacos-provider
predicates:
- Path=/nprovider/**
filters:
- StripPrefix=1
使用指定path訪問服務
C:\Users\liangbodlz\.ssh>curl 192.168.132.49:1500/nprovider/index
Hello!
C:\Users\liangbodlz\.ssh>
4.1.3 使用Nacos數據源動態加載和刷新路由配置
通常我們將微服務的Route Predicate Path和Gateway應用本身的配置放在一起,但是隨著微服務的擴展,Route Predicate Path會逐漸增加導致Gateway 服務配置會變得臃腫,且Route Predicate Path配置會隨著服務的增減進行變更,而更新的路由配置生效需要重啟Gateway,這都是實際線上環境不可忍受的。因此獨立管理Route Predicate Path配置且支持動態刷新配置變得必要起來。
基于上述需求,我們可以考慮將Gateway 路由配置存儲到內存或者其他介質中。
從源碼分析中可以知道Gateway路由配置信息由RouteDefinitionLocator 接口完成。
RouteDefinitionLocator 是Gateway路由配置讀取的頂級接口,提供從緩存、配置文件、服務注冊中心、組合等不同方式讀取配置,以及提供RouteDefinitionRepository 接口方式對RouteDefinition進行增、刪、查操作。要自定義路由配置實現可以考慮從上述接口著手實現。
這里主要基于Nacos配置中心+RouteDefinitionRepository 自定義路由配置加載,并參考,CachingRouteLocator實現路由配置的動態刷新
核心源碼清單
//自定義路由配置加載核心接口
public interface RouteDefinitionRepository extends RouteDefinitionLocator, RouteDefinitionWriter {
}
//查詢路由
public interface RouteDefinitionLocator {
//返回自定義路由配置加載
Flux<RouteDefinition> getRouteDefinitions();
}
//路由增、刪
public interface RouteDefinitionWriter {
Mono<Void> save(Mono<RouteDefinition> route);
Mono<Void> delete(Mono<String> routeId);
}
//動態路由刷新實現
public class CachingRouteLocator implements Ordered, RouteLocator, ApplicationListener<RefreshRoutesEvent>, ApplicationEventPublisherAware {
....//省略
private ApplicationEventPublisher applicationEventPublisher;
.....//省略
public void onApplicationEvent(RefreshRoutesEvent event) {
try {
this.fetch().collect(Collectors.toList()).subscribe((list) -> {
Flux.fromIterable(list).materialize().collect(Collectors.toList()).subscribe((signals) -> {
this.applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this));
this.cache.put("routes", signals);
}, (throwable) -> {
this.handleRefreshError(throwable);
});
});
} catch (Throwable var3) {
this.handleRefreshError(var3);
}
}
private void handleRefreshError(Throwable throwable) {
if (log.isErrorEnabled()) {
log.error("Refresh routes error !!!", throwable);
}
this.applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this, throwable));
}
代碼實現
//實現RouteDefinitionRepository接口
package com.easy.mall.route;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.api.exception.NacosException;
import com.easy.mall.config.GatewayConfig;
import com.easy.mall.operation.NacosConfigOperation;
import com.easy.mall.operation.NacosSubscribeCallback;
import com.google.common.collect.Lists;
import java.util.List;
import java.util.Optional;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @description: 基于Nacos配置中心實現Gateway 動態路由配置
* @author: liangbo
* @create 2020-12-15 19:29
* @Version 1.0
**/
@Slf4j
@DependsOn(value= {"gatewayConfig","nacosAutoConfiguration"})
@Configuration
@ConditionalOnProperty(prefix = "global.gateway.dynamicRoute", name = "enabled", havingValue = "true")
public class NacosDynamicRouteDefinitionRepository implements RouteDefinitionRepository {
@Autowired
private NacosConfigOperation nacosConfigOperation;
@Autowired
private ApplicationEventPublisher publisher;
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
//從Nacos配置中心讀取路由配置
try {
String dynamicRouteStr = nacosConfigOperation.getConfig(GatewayConfig.NACOS_ROUTE_GROUP, GatewayConfig.NACOS_ROUTE_DATA_ID);
log.info("init dynamicRoute success.:{}", dynamicRouteStr);
List<RouteDefinition> routeDefinitions = Optional.ofNullable(dynamicRouteStr)
.map(str -> JSONObject.parseArray(str, RouteDefinition.class))
.orElse(Lists.newArrayList());
return Flux.fromIterable(routeDefinitions);
} catch (NacosException e) {
log.error("load gateway dynamicRoute config error:{}", e);
}
return Flux.fromIterable(Lists.newArrayList());
}
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return null;
}
@Override
public Mono<Void> delete(Mono<String> routeId) {
return null;
}
/**
* 偵聽nacos config 實時刷新路由配置
*/
@PostConstruct
public void subscribeConfigRefresh() {
try {
nacosConfigOperation.subscribeConfig(GatewayConfig.NACOS_ROUTE_GROUP,
GatewayConfig.NACOS_ROUTE_DATA_ID, null, new NacosSubscribeCallback () {
@Override
public void callback(String config) {
publisher.publishEvent(new RefreshRoutesEvent(this));
}
});
} catch (NacosException e) {
log.error("nacos-addListener-error", e);
}
}
}
動態路由配置清單
[
{
"id": "easy-mall-auth",
"predicates": [{
"name": "Path",
"args": {
"pattern": "/emallauth/**"
}
}],
"uri": "lb://easy-mall-auth",
"filters": [{
"name": "StripPrefix",
"args": {
"parts": "1"
}
}]
}
]
4.2 基于網關+nacos配置中心實現灰度路由
實現思路見Nacos安裝及Spring Cloud 集成 3.4
4.2.1 定義GatewayStrategyAutoConfiguration 網關路由自定義配置入口類
@Configuration
@AutoConfigureBefore(RibbonClientConfiguration.class)
//通過注解@RibbonClient聲明附加配置,此處聲明的配置會覆蓋配置文件中的配置
@RibbonClients(defaultConfiguration = { GatewayStrategyLoadBalanceConfiguration.class })
@ConditionalOnProperty(value = StrategyConstant.SPRING_APPLICATION_STRATEGY_CONTROL_ENABLED, matchIfMissing = true)
public class GatewayStrategyAutoConfiguration {
//省略......
通過入口類,加載自定義全局過濾器、Ribbon自定義負載均衡配置、元數據處理適配器等。
自定義Ribbon 負載均衡實現
自定義Ribbon 負載均衡實現分別對PredicateBasedRule和ZoneAvoidanceRule進行了擴展
//通過注解@RibbonClient聲明附加配置,此處聲明的配置會覆蓋配置文件中的配置
@RibbonClients(defaultConfiguration = { GatewayStrategyLoadBalanceConfiguration.class })
@Bean
public IRule ribbonRule(IClientConfig config) {
if (this.propertiesFactory.isSet(IRule.class, serviceId)) {
return this.propertiesFactory.get(IRule.class, config, serviceId);
}
//開啟和關閉Ribbon默認的ZoneAvoidanceRule負載均衡策略。一旦關閉,則使用RoundRobin簡單輪詢負載均衡策略。缺失則默認為true
boolean zoneAvoidanceRuleEnabled = environment.getProperty(StrategyConstant.SPRING_APPLICATION_STRATEGY_ZONE_AVOIDANCE_RULE_ENABLED, Boolean.class, Boolean.TRUE);
if (zoneAvoidanceRuleEnabled) {
DiscoveryEnabledZoneAvoidanceRule discoveryEnabledRule = new DiscoveryEnabledZoneAvoidanceRule();
discoveryEnabledRule.initWithNiwsConfig(config);
DiscoveryEnabledZoneAvoidancePredicate discoveryEnabledPredicate = discoveryEnabledRule.getDiscoveryEnabledPredicate();
discoveryEnabledPredicate.setPluginAdapter(pluginAdapter);
discoveryEnabledPredicate.setDiscoveryEnabledAdapter(discoveryEnabledAdapter);
return discoveryEnabledRule;
} else {
DiscoveryEnabledBaseRule discoveryEnabledRule = new DiscoveryEnabledBaseRule();
DiscoveryEnabledBasePredicate discoveryEnabledPredicate = discoveryEnabledRule.getDiscoveryEnabledPredicate();
discoveryEnabledPredicate.setPluginAdapter(pluginAdapter);
discoveryEnabledPredicate.setDiscoveryEnabledAdapter(discoveryEnabledAdapter);
return discoveryEnabledRule;
}
}
DiscoveryEnabledZoneAvoidanceRule:
DiscoveryEnabledBaseRule
自定義全局過濾器 實現將網關路由配置以及Http Header加載到請求ServerWebExchange中
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 把ServerWebExchange放入ThreadLocal中
GatewayStrategyContext.getCurrentContext().setExchange(exchange);
// 通過過濾器設置路由Header頭部信息,并全鏈路傳遞到服務端
ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
if (gatewayCoreHeaderTransmissionEnabled) {
// 內置Header預先塞入
Map<String, String> headerMap = strategyWrapper.getHeaderMap();
if (MapUtils.isNotEmpty(headerMap)) {
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
GatewayStrategyFilterResolver.setHeader(requestBuilder, key, value, gatewayHeaderPriority);
}
}
//獲取網關配置的路由規則
String routeVersion = getRouteVersion();
String routeVersionWeight = getRouteVersionWeight();
String routeIdBlacklist = getRouteIdBlacklist();
String routeAddressBlacklist = getRouteAddressBlacklist();
if (StringUtils.isNotEmpty(routeVersion)) {
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_VERSION, routeVersion, gatewayHeaderPriority);
} else {
GatewayStrategyFilterResolver.ignoreHeader(requestBuilder, DiscoveryConstant.N_D_VERSION, gatewayHeaderPriority, gatewayOriginalHeaderIgnored);
}
if (StringUtils.isNotEmpty(routeVersionWeight)) {
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_VERSION_WEIGHT, routeVersionWeight, gatewayHeaderPriority);
} else {
GatewayStrategyFilterResolver.ignoreHeader(requestBuilder, DiscoveryConstant.N_D_VERSION_WEIGHT, gatewayHeaderPriority, gatewayOriginalHeaderIgnored);
}
if (StringUtils.isNotEmpty(routeIdBlacklist)) {
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_ID_BLACKLIST, routeIdBlacklist, gatewayHeaderPriority);
} else {
GatewayStrategyFilterResolver.ignoreHeader(requestBuilder, DiscoveryConstant.N_D_ID_BLACKLIST, gatewayHeaderPriority, gatewayOriginalHeaderIgnored);
}
if (StringUtils.isNotEmpty(routeAddressBlacklist)) {
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_ADDRESS_BLACKLIST, routeAddressBlacklist, gatewayHeaderPriority);
} else {
GatewayStrategyFilterResolver.ignoreHeader(requestBuilder, DiscoveryConstant.N_D_ADDRESS_BLACKLIST, gatewayHeaderPriority, gatewayOriginalHeaderIgnored);
}
} else {
GatewayStrategyFilterResolver.ignoreHeader(requestBuilder, DiscoveryConstant.N_D_VERSION);
GatewayStrategyFilterResolver.ignoreHeader(requestBuilder, DiscoveryConstant.N_D_VERSION_WEIGHT);
GatewayStrategyFilterResolver.ignoreHeader(requestBuilder, DiscoveryConstant.N_D_ID_BLACKLIST);
GatewayStrategyFilterResolver.ignoreHeader(requestBuilder, DiscoveryConstant.N_D_ADDRESS_BLACKLIST);
}
// 對于服務A -> 網關 -> 服務B調用鏈
// 域網關下(zuulHeaderPriority=true),只傳遞網關自身的group,不傳遞服務A的group,起到基于組的網關端服務調用隔離
// 非域網關下(zuulHeaderPriority=false),優先傳遞服務A的group,基于組的網關端服務調用隔離不生效,但可以實現基于相關參數的熔斷限流等功能
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_SERVICE_GROUP, pluginAdapter.getGroup(), gatewayHeaderPriority);
// 網關只負責傳遞服務A的相關參數(例如:serviceId),不傳遞自身的參數,實現基于相關參數的熔斷限流等功能
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_SERVICE_TYPE, pluginAdapter.getServiceType(), false);
String serviceAppId = pluginAdapter.getServiceAppId();
if (StringUtils.isNotEmpty(serviceAppId)) {
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_SERVICE_APP_ID, serviceAppId, false);
}
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_SERVICE_ID, pluginAdapter.getServiceId(), false);
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_SERVICE_VERSION, pluginAdapter.getVersion(), false);
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.N_D_SERVICE_ENVIRONMENT, pluginAdapter.getEnvironment(), false);
ServerHttpRequest newRequest = requestBuilder.build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
ServerWebExchange extensionExchange = extendFilter(newExchange, chain);
ServerWebExchange finalExchange = extensionExchange != null ? extensionExchange : newExchange;
// 把新的ServerWebExchange放入ThreadLocal中
GatewayStrategyContext.getCurrentContext().setExchange(newExchange);
String path = finalExchange.getRequest().getPath().toString();
if (path.contains(DiscoveryConstant.INSPECTOR_ENDPOINT_URL)) {
GatewayStrategyFilterResolver.setHeader(requestBuilder, DiscoveryConstant.INSPECTOR_ENDPOINT_HEADER, pluginAdapter.getPluginInfo(null), true);
}
return chain.filter(finalExchange);
}
自定義DefaultDiscoveryEnabledAdapter封裝實例過濾規則
Ribbon負載均衡器執行默認過濾后會執行該規則
protected boolean apply(Server server) {
if (discoveryEnabledAdapter == null) {
return true;
}
return discoveryEnabledAdapter.apply(server);
}
//自定義過濾規則
@Override
public boolean apply(Server server) {
boolean enabled = applyEnvironment(server);
if (!enabled) {
return false;
}
enabled = applyVersion(server);
if (!enabled) {
return false;
}
enabled = applyIdBlacklist(server);
if (!enabled) {
return false;
}
enabled = applyAddressBlacklist(server);
if (!enabled) {
return false;
}
return applyStrategy(server);
}
4.2.2 網關路由策略發布
基于nacos配置實現網關策略動態發布,根據網關元數據組以及serviceId創建路由策略配置:
配置通過網關的請求都走版本xx
<?xml version="1.0" encoding="UTF-8"?>
<rule>
<strategy>
<version>1.0</version>
</strategy>
</rule>
step1 啟動網關以及2個服務實例
mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=1100 --spring.cloud.nacos.discovery.metadata.version=1.0"
mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=1101 --spring.cloud.nacos.discovery.metadata.version=1.1"
step2 通過網關調用服務,可以驗證到請求始終訪問到version為1.0 的服務實例
192.168.132.49:1500/nacos-provider/index
配置網關路由權重
<?xml version="1.0" encoding="UTF-8"?>
<rule>
<strategy>
<version>1.0;1.1</version>
<version-weight>1.0=90;1.1=10</version-weight>
</strategy>
</rule>
灰度策略信息基于Nacos Client以及異步事件處理,動態更新,無需重啟網關。
通過網關訪問多次服務,請求基本按照9:1的比例命中服務。
配置IP地址和端口屏蔽策略,實現服務流量無損策略下線
服務下線場景中,由于Ribbon負載均衡組件存在著緩存機制,當被調用的服務實例已經下線,而調用的服務實例還暫時緩存著它,直到下個心跳周期才會把已下線的服務實例剔除,在此期間,會造成流量有損
框架提供流量的實時性的絕對無損。采用下線之前,把服務實例添加到屏蔽名單中,負載均衡不會去尋址該服務實例。
代碼清單:
//省略
enabled = applyIdBlacklist(server);
if (!enabled) {
return false;
}
//省略
//過濾黑名單IP,框架會將黑名單中IP從Ribbon負載實例中移除
public boolean applyIdBlacklist(Server server) {
String ids = pluginContextHolder.getContextRouteIdBlacklist();
if (StringUtils.isEmpty(ids)) {
return true;
}
String serviceUUId = pluginAdapter.getServerServiceUUId(server);
List<String> idList = StringUtil.splitToList(ids, DiscoveryConstant.SEPARATE);
if (idList.contains(serviceUUId)) {
return false;
}
return true;
}
配置Ip黑名單
<?xml version="1.0" encoding="UTF-8"?>
<rule>
//此處省略
<strategy-blacklist>
<!-- 單個Address形式。如果多個用“;”分隔,不允許出現空格 -->
<address value="192.168.132.49:1100"/>
</strategy-blacklist>
</rule>
發布配置后,訪問服務可以發現請求屏蔽了端口為1100的服務,確保1100服務下線,請求不會命中到1100服務。
源碼地址