您好,登錄后才能下訂單哦!
這篇文章主要介紹“總結Spring Cloud Gateway相關知識點”,在日常操作中,相信很多人在總結Spring Cloud Gateway相關知識點問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”總結Spring Cloud Gateway相關知識點”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
SpringCloud Gateway 是 Spring Cloud 的一個全新項目,該項目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的網關,它旨在為微服務架構提供一種簡單有效的統一的 API 路由管理方式。
為了提升網關的性能,SpringCloud Gateway是基于WebFlux框架實現的,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway旨在提供一種簡單而有效的途徑來發送API,并為他們提供橫切關注點,例如:安全性,監控/指標和彈性。
Route(路由):這是網關的基本構建塊。它由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。如果斷言為真,則路由匹配,目標URI會被訪問。
Predicate(斷言):這是一個 Java 8 的 Predicate。輸入類型是一個 ServerWebExchange。我們可以使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或參數。
Filter(過濾器):這是org.springframework.cloud.gateway.filter.GatewayFilter的實例,我們可以使用它攔截和修改請求,并且對上文的響應,進行再處理。
基于Spring Framework 5、Project Reactor和Spring Boot 2.0構建
能夠在任意請求屬性上匹配路由
predicates(謂詞) 和 filters(過濾器)是特定于路由的
集成了Hystrix斷路器
集成了Spring Cloud DiscoveryClient
易于編寫謂詞和過濾器
請求速率限制
路徑重寫
客戶端向Gateway發出請求。
然后Gateway Handler Mapping中找到與請求相匹配的路由,將其發送給Gateway Web Handler。
Handler再通過指定的過濾器鏈來將請求發送到我們實際的服務執行業務邏輯,最后返回請求結果。
過濾器之間用虛線分開是因為過濾器可能會在發送代理請求之前或之后執行業務邏輯。
filter在請求之前可以做參數校驗,權限校驗,流量監控,日志輸出,協議轉換等等,在請求之后可以做響應內容、響應頭的修改,日志輸出,流量監控等。
1.基于URI路由配置方式
server: port: 8080 spring: application: name: api-gateway cloud: gateway: routes: -id: url-api-gateway uri: https://www.baidu.com predicates: -Path=/toBaidu
各配置字段含義如下:
id:我們自定義的路由 ID,保持唯一
uri:目標服務地址
predicates:路由條件,Predicate 接受一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(比如:與,或,非)。
上面這段配置的意思是,配置了一個 id 為 url-api-gateway的URI代理規則,路由的規則為:當訪問地址http://localhost:8080/toBaidu時,會路由到上游地址https://www.baidu.com。
2.基于代碼的路由配置
package com.superhero;import com.bstek.ureport.console.UReportServlet;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;import org.springframework.boot.web.servlet.ServletRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ImportResource;import javax.servlet.Servlet;/** * 啟動程序 * @author superhero */@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})@ImportResource("classpath:context.xml")public class SuperHeroApplication { public static void main(String[] args) {// System.setProperty("spring.devtools.restart.enabled", "false"); SpringApplication.run(SuperHeroApplication.class, args); } @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("url-api-gateway", r -> r.path("/toBaidu") .uri("https://www.baidu.com")) .build(); }}
首先我們將剛剛在配置文件中添加的相關路由的配置信息注釋,然后重啟服務,訪問鏈接:http://localhost:8080/ toBaidu, 可以看到和上面一樣的頁面,證明我們測試成功。
上面兩個示例中 uri 都是指向了我的百度的首頁,在實際項目使用中可以將 uri 指向對外提供服務的項目地址,統一對外輸出接口。
上述兩個路由轉發實例是Spring Cloud Gateway最簡單的使用,更多高級特性和核心知識,下面我會一步一步為大家細細講解。
關于什么是跨域在此處就不進行講解,這里我們主要是實現Spring Cloud Gateway跨域訪問。
Spring Cloud Gateway還針對跨域訪問做了設計,可以使用以下配置解決跨域訪問問題。
當服務啟動的時候,跨域配置信息會存儲在GlobalCorsProperties的corsConfigurations映射中,key是 /**,value是CorsConfiguration的對象。上面的配置表示允許來自https://docs.spring.io的Get請求訪問此網關,并且表明服務器允許請求頭中攜帶字段Content-Type。
Spring Cloud Gateway的filter生命周期只有兩個:“pre”和“post”。
pre:在請求被路由之前調用。可以利用這個過濾器實現身份驗證、在集群中選擇請求的微服務、記錄調試的信息。
post:在路由到服務器之后執行。這種過濾器可用來為響應添加HTTP Header、統計信息和指標、響應從微服務發送給客戶端等。
Spring Cloud gateway的filter分為兩種:GatewayFilter和Globalfilter。
GlobalFilter會應用到所有的路由上,而Gatewayfilter將應用到單個路由或者一個分組的路由上。
利用Gatewayfilter可以修改請求的http的請求或者是響應,或者根據請求或者響應做一些特殊的限制。
更多時候我們可以利用Gatewayfilter做一些具體的路由配置。
GatewayFilter 網關過濾器用于攔截并鏈式處理web請求,可以實現橫切的與應用無關的需求,比如:安全、訪問超時的設置等。
我們先來看下GatewayFilter的類圖
從類圖中可以看到,GatewayFilter 有四個實現類,我們介紹其中兩個重要的一個是OrderedGatewayFilter ,它是一個有序的網關過濾器。還有一個GatewayFilterAdapter,它是一個適配器類,是web處理器(FilteringWebHandler)中的內部類。
GatewayFilter 源碼如下
/** * 網關路由過濾器, * Contract for interception-style, chained processing of Web requests that may * be used to implement cross-cutting, application-agnostic requirements such * as security, timeouts, and others. Specific to a Gateway * * Copied from WebFilter * * @author Rossen Stoyanchev * @since 5.0 */public interface GatewayFilter extends ShortcutConfigurable { String NAME_KEY = "name"; String VALUE_KEY = "value";/** * 過濾器執行方法 * Process the Web request and (optionally) delegate to the next * {@code WebFilter} through the given {@link GatewayFilterChain}. * @param exchange the current server exchange * @param chain provides a way to delegate to the next filter * @return {@code Mono<Void>} to indicate when request processing is complete */ Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);}
網關過濾器接口GatewayFilter 源碼中,有且只有一個方法filter,執行當前過濾器,并在此方法中決定過濾器鏈表是否繼續往下執行。
有序的網關過濾器OrderedGatewayFilter
/** * 排序的網關路由過濾器,用于包裝真實的網關過濾器,已達到過濾器可排序 * * @author Spencer Gibb */public class OrderedGatewayFilter implements GatewayFilter, Ordered { //目標過濾器 private final GatewayFilter delegate; //排序字段 private final int order; public OrderedGatewayFilter(GatewayFilter delegate, int order) { this.delegate = delegate; this.order = order; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return this.delegate.filter(exchange, chain); }}
OrderedGatewayFilter實現類是目標過濾器的包裝類,它的主要目的是為了將目標過濾器包裝成可排序的對象類型。
過濾器大多都是有優先級的,因此有序的網關過濾器的使用場景會很多。在實現過濾器接口的同時,有序網關過濾器也實現了 Ordered 接口,構造函數中傳入需要代理的網關過濾器以及優先級就可以構造一個有序的網關過濾器。
具體的過濾功能的實現在被代理的過濾器中實現的,因此在此只需要調用代理的過濾器即可。
適配器類GatewayFilterAdapter
/** * 全局過濾器的包裝類,將全局路由包裝成統一的網關過濾器 */private static class GatewayFilterAdapter implements GatewayFilter { /** * 全局過濾器 */ private final GlobalFilter delegate; public GatewayFilterAdapter(GlobalFilter delegate) { this.delegate = delegate; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return this.delegate.filter(exchange, chain); }}
在網關過濾器鏈 GatewayFilterChain 中會使用 GatewayFilter 過濾請求,GatewayFilterAdapter的作用就是將全局過濾器 GlobalFilter 適配成 網關過濾器 GatewayFilter。
什么是全局過濾器,簡單的一句話來說就是,全局過濾器會作用于全局的路由上。
GlobalGilter 全局過濾器接口與 GatewayFilter 網關過濾器接口具有相同的方法定義。
全局過濾器是一系列特殊的過濾器,會根據條件應用到所有路由中。網關過濾器是更細粒度的過濾器,作用于指定的路由中。
Globalfilter的類圖如下
從類圖中我們可以看到,GlobalGilter有十一個實現類,我們介紹GlobalGilter,就是從這十一個實現類給大家詳解介紹到底什么是GlobalGilter。
首先我們來看一下Globalfilter源碼
public interface GlobalFilter {/*** Process the Web request and (optionally) delegate to the next* {@code WebFilter} through the given {@link GatewayFilterChain}.* @param exchange the current server exchange* @param chain provides a way to delegate to the next filter* @return {@code Mono<Void>} to indicate when request processing is complete*/Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);}
我們看到GlobalGilter接口有一個filter方法,GlobalGilter有十一個實現類都會通過重寫filter方法來實現過濾功能。
因此我們主要了解這十一個類重寫的filter方法的邏輯即可。
接下來我們就開始依次介紹這十一個實現類
1、ForwardRoutingFilter 轉發路由過濾器
ForwardRoutingFilter 在交換屬性 ServerWebExchangeUtils.GATEWAY_ REQUEST_ URL_ ATTR 中 查找 URL, 如果 URL 為轉發模式即 forward:/// localendpoint, 它將使用Spring DispatcherHandler 來處 理請求。未修改的原始 URL 將保存到 GATEWAY_ ORIGINAL_ REQUEST_ URL_ ATTR 屬性的列表中。
public class ForwardRoutingFilter implements GlobalFilter, Ordered { private static final Log log = LogFactory.getLog(ForwardRoutingFilter.class); private final ObjectProvider<DispatcherHandler> dispatcherHandler; public ForwardRoutingFilter(ObjectProvider<DispatcherHandler> dispatcherHandler) { this.dispatcherHandler = dispatcherHandler; } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); //獲取請求URI的請求結構 String scheme = requestUrl.getScheme(); //該路由已經被處理或者URI格式不是forward則繼續其它過濾器 if (isAlreadyRouted(exchange) || !"forward".equals(scheme)) { return chain.filter(exchange); } setAlreadyRouted(exchange); //TODO: translate url? if (log.isTraceEnabled()) { log.trace("Forwarding to URI: "+requestUrl); } // 使用dispatcherHandler進行處理 return this.dispatcherHandler.getIfAvailable().handle(exchange); }}
轉發路由過濾器實現比較簡單,構造函數傳入請求的分發處理器DispatcherHandler。
過濾器執行時,首先獲取請求地址的url前綴,然后判斷該請求是否已被路由處理或者URL的前綴不是forward,則繼續執行過濾器鏈;
否則設置路由處理狀態并交由DispatcherHandler進行處理。
請求路由是否被處理的判斷如下:
// ServerWebExchangeUtils.javapublic static void setAlreadyRouted(ServerWebExchange exchange){ exchange.getAttributes().put(GATEWAY_ALREADY_ROUTED_ATTR,true);}public static boolean isAlreadyRouted(ServerWebExchange exchange){ return exchange.getAttributeOrDefault(GATEWAY_ALREADY_ROUTED_ATTR,false);}
兩個 方法 定義 在 ServerWebExchangeUtils 中, 這 兩個 方法 用于 修改 與 查詢 ServerWebExchange 中的 Map< String, Object> getAttributes(),# getAttributes 方法 返回 當前 exchange 所請 求 屬性 的 可變 映射。
這兩個方法定義在 ServerWebExchangeUtils 中,分別用于修改和查詢 GATEWAY_ALREADY_ROUTED_ATTR 狀態。
2、LoadBalancerClientFilter 負載均衡客戶端過濾器
spring: cloud: gateway: routes: - id: myRoute uri: lb://service predicates: - Path=/service/**
LoadBalancerClientFilter 在交換屬性 GATEWAY_ REQUEST_ URL_ ATTR 中查找URL, 如果URL有一個 lb 前綴 ,即 lb:// myservice,將使用 LoadBalancerClient 將名稱 解析為實際的主機和端口,如示例中的 myservice。
未修改的原始 URL將保存到 GATEWAY_ ORIGINAL_ REQUEST_ URL_ ATTR 屬性的列表中。
過濾器還將查看ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR屬性以查看它是否等于lb,然后應用相同的規則。
@Overridepublic Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain){ URI url=exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); String schemePrefix=exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR); if(url==null||(!"lb".equals(url.getScheme())&&!"lb".equals(schemePrefix))){ return chain.filter(exchange); } //保留原始url addOriginalRequestUrl(exchange,url); log.trace("LoadBalancerClientFilter url before: "+url); //負載均衡到具體服務實例 final ServiceInstance instance=choose(exchange); if(instance==null){ throw new NotFoundException("Unable to find instance for "+url.getHost()); } URI uri=exchange.getRequest().getURI(); //如果沒有提供前綴的話,則會使用默認的'< scheme>',否則使用' lb:< scheme>' 機制。 String overrideScheme=null; if(schemePrefix!=null){ overrideScheme=url.getScheme(); } //根據獲取的服務實例信息,重新組裝請求的 url URI requestUrl=loadBalancer.reconstructURI(new DelegatingServiceInstance(instance,overrideScheme),uri); // Routing 相關 的 GatewayFilter 會 通過 GATEWAY_ REQUEST_ URL_ ATTR 屬性, 發起 請求。 log.trace("LoadBalancerClientFilter url chosen: "+requestUrl); exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR,requestUrl); return chain.filter(exchange);}
從過濾器執行方法中可以看出,負載均衡客戶端過濾器的實現步驟如下:
1、構造函數傳入負載均衡客戶端,依賴中添加 Spring Cloud Netflix Ribbon 即可 注入 該 Bean。
2、獲取請求的 URL 及其前綴,如果 URL 不為空且前綴為lb或者網關請求的前綴是 lb,則保存原始的URL,負載到具體的服務實例并根據獲取的服務實例信息,重新組裝請求的URL。
3、最后,添加請求的URL到GATEWAY_ REQUEST_ URL_ ATTR,并提交到過濾器鏈中繼續執行
在組裝請求的地址時,如果loadbalancer沒有提供前綴的話,則使用默認的,即overrideScheme 為null,否則的話使用 lb:
3、NettyRoutingFilter 和 NettyWriteResponseFilter
如果 ServerWebExchangeUtils.GATEWAY_ REQUEST_ URL_ ATTR 請求屬性中的URL 具有http或https前綴,NettyRoutingFilter 路由過濾器將運行,它使用 Netty HttpClient 代理對下游的請求。
響應信息放在ServerWebExchangeUtils.CLIENT_ RESPONSE_ ATTR 屬性中,在過濾器鏈中進行傳遞。
該過濾器實際處理 和客戶端負載均衡的實現方式類似 ↓
首先獲取請求的URL及前綴,判斷前綴是不是http或者https,如果該請求已經被路由或者前綴不合法,則調用過濾器鏈直接向后傳遞;否則正常對頭部進行過濾操作。
public class NettyRoutingFilter implements GlobalFilter, Ordered { private final HttpClient httpClient; private final ObjectProvider<List<HttpHeadersFilter>> headersFilters; private final HttpClientProperties properties; public NettyRoutingFilter(HttpClient httpClient, ObjectProvider<List<HttpHeadersFilter>> headersFilters, HttpClientProperties properties) { this.httpClient = httpClient; this.headersFilters = headersFilters; this.properties = properties; } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); String scheme = requestUrl.getScheme(); if (isAlreadyRouted(exchange) || (!"http".equals(scheme) && !"https".equals(scheme))) { return chain.filter(exchange); } setAlreadyRouted(exchange); ServerHttpRequest request = exchange.getRequest(); final HttpMethod method = HttpMethod.valueOf(request.getMethod().toString()); final String url = requestUrl.toString(); HttpHeaders filtered = filterRequest(this.headersFilters.getIfAvailable(), exchange); final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders(); filtered.forEach(httpHeaders::set); String transferEncoding = request.getHeaders().getFirst(HttpHeaders.TRANSFER_ENCODING); boolean chunkedTransfer = "chunked".equalsIgnoreCase(transferEncoding); boolean preserveHost = exchange.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false); Mono<HttpClientResponse> responseMono = this.httpClient.request(method, url, req -> { final HttpClientRequest proxyRequest = req.options(NettyPipeline.SendOptions::flushOnEach) .headers(httpHeaders) .chunkedTransfer(chunkedTransfer) .failOnServerError(false) .failOnClientError(false); if (preserveHost) { String host = request.getHeaders().getFirst(HttpHeaders.HOST); proxyRequest.header(HttpHeaders.HOST, host); } if (properties.getResponseTimeout() != null) { proxyRequest.context(ctx -> ctx.addHandlerFirst( new ReadTimeoutHandler(properties.getResponseTimeout().toMillis(), TimeUnit.MILLISECONDS))); } return proxyRequest.sendHeaders() //I shouldn't need this .send(request.getBody().map(dataBuffer -> ((NettyDataBuffer) dataBuffer).getNativeBuffer())); }); return responseMono.doOnNext(res -> { ServerHttpResponse response = exchange.getResponse(); // put headers and status so filters can modify the response HttpHeaders headers = new HttpHeaders(); res.responseHeaders().forEach(entry -> headers.add(entry.getKey(), entry.getValue())); String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE); if (StringUtils.hasLength(contentTypeValue)) { exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR, contentTypeValue); } HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter( this.headersFilters.getIfAvailable(), headers, exchange, Type.RESPONSE); response.getHeaders().putAll(filteredResponseHeaders); HttpStatus status = HttpStatus.resolve(res.status().code()); if (status != null) { response.setStatusCode(status); } else if (response instanceof AbstractServerHttpResponse) { // https://jira.spring.io/browse/SPR-16748 ((AbstractServerHttpResponse) response).setStatusCodeValue(res.status().code()); } else { throw new IllegalStateException("Unable to set status code on response: " + res.status().code() + ", " + response.getClass()); } // Defer committing the response until all route filters have run // Put client response as ServerWebExchange attribute and write response later NettyWriteResponseFilter exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res); }) .onErrorMap(t -> properties.getResponseTimeout() != null && t instanceof ReadTimeoutException, t -> new TimeoutException("Response took longer than timeout: " + properties.getResponseTimeout())) .then(chain.filter(exchange)); }}
NettyRoutingFilter 過濾器的構造函數有三個參數 ↓
HttpClient httpClient : 基于 Netty 實現的 HttpClient,通過該屬性請求后端 的 Http 服務
ObjectProvider<List> headersFilters:ObjectProvider 類型 的 headersFilters,用于頭部過濾
HttpClientProperties properties:Netty HttpClient 的配置屬性
4、NettyRoutingFilter ## HttpHeadersFilter 頭部過濾器接口
filterRequest 用于對請求頭部的信息進行處理,是定義在接口 HttpHeadersFilter 中的默認方法,該接口有三個實現類,請求頭部將會經過這三個頭部過濾器,并最終返回修改之后的頭部。
public interface HttpHeadersFilter { enum Type { REQUEST, RESPONSE } /** * Filters a set of Http Headers * * @param input Http Headers * @param exchange * @return filtered Http Headers */ HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange); static HttpHeaders filterRequest(List<HttpHeadersFilter> filters, ServerWebExchange exchange) { HttpHeaders headers = exchange.getRequest().getHeaders(); return filter(filters, headers, exchange, Type.REQUEST); } static HttpHeaders filter(List<HttpHeadersFilter> filters, HttpHeaders input, ServerWebExchange exchange, Type type) { HttpHeaders response = input; if (filters != null) { HttpHeaders reduce = filters.stream() .filter(headersFilter -> headersFilter.supports(type)) .reduce(input, (headers, filter) -> filter.filter(headers, exchange), (httpHeaders, httpHeaders2) -> { httpHeaders.addAll(httpHeaders2); return httpHeaders; }); return reduce; } return response; } default boolean supports(Type type) { return type.equals(Type.REQUEST); }}
HttpHeadersFilter 接口的三個實現類 ↓
ForwardedHeadersFilter:增加 Forwarded頭部,頭部值為協議類型、host和目標地址
XForwardedHeadersFilter:增加 X- Forwarded- For、 X- Forwarded- Host、 X- Forwarded- Port 和 X- Forwarded- Proto 頭部。代理轉發時,用以自定義的頭部信息向下游傳遞。
RemoveHopByHopHeadersFilter:為了定義緩存和非緩存代理的行為,我們將HTTP頭字段分為兩類:端到端的頭部字段,發送給請求或響應的最終接收人;逐跳頭部字段,對單個傳輸級別連接有意義,并且不被緩存存儲或由代理轉發。
所以該頭部過濾器會移除逐跳頭部字段,包括以下8個字段:
Proxy- Authenticate
Proxy- Authorization
TE
Trailer
Transfer- Encoding
Upgrade
proxy- connection
content- length
5、NettyWriteResponseFilter
NettyWriteResponseFilter 與 NettyRoutingFilter 成對使用。“ 預” 過濾階段沒有任何內容,因為 CLIENT_ RESPONSE_ ATTR 在 WebHandler 運行之前不會被添加。
@Overridepublic Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain){ // NOTICE: nothing in "pre" filter stage as CLIENT_RESPONSE_ATTR is not added // until the WebHandler is run return chain.filter(exchange).then(Mono.defer(()->{ HttpClientResponse clientResponse=exchange.getAttribute(CLIENT_RESPONSE_ATTR); if(clientResponse==null){ return Mono.empty(); } log.trace("NettyWriteResponseFilter start"); ServerHttpResponse response=exchange.getResponse(); NettyDataBufferFactory factory=(NettyDataBufferFactory)response.bufferFactory(); //TODO: what if it's not netty final Flux<NettyDataBuffer> body=clientResponse.receive() .retain() //TODO: needed? .map(factory::wrap); MediaType contentType=null; try{ contentType=response.getHeaders().getContentType(); }catch(Exception e){ log.trace("invalid media type",e); } return(isStreamingMediaType(contentType)?response.writeAndFlushWith(body.map(Flux::just)):response.writeWith(body)); }));}
如果 CLIENT_ RESPONSE_ ATTR 請求 屬性 中 存在 Netty HttpClientResponse, 則 會應用 NettyWriteResponseFilter。
它在其他過濾器完成后運行,并將代理響應寫回 網關客戶端響應。
成對出現的 WebClientHttpRoutingFilter 和 WebClientWriteResponseFilter 過濾器,與基于Nettty 的路由和響應過濾器執行相同 的功能,但不需要使用Netty。
6、RouteToRequestUrlFilter 路由到指定url的過濾器
如果 ServerWebExchangeUtils.GATEWAY_ ROUTE_ ATTR 請求屬性中有Route對象, 則 會運行 RouteToRequestUrlFilter 過濾器。
他會根據請求URI創建一個新的URI。
新的 URI 位于 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 請求屬性中。該過濾器會組裝成發送到代理服務的URL地址,向后傳遞到路由轉發的過濾器。
@Overridepublic Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain){ Route route=exchange.getAttribute(GATEWAY_ROUTE_ATTR); if(route==null){ return chain.filter(exchange); } log.trace("RouteToRequestUrlFilter start"); URI uri=exchange.getRequest().getURI(); boolean encoded=containsEncodedParts(uri); URI routeUri=route.getUri(); if(hasAnotherScheme(routeUri)){ // this is a special url, save scheme to special attribute // replace routeUri with schemeSpecificPart exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR,routeUri.getScheme()); routeUri=URI.create(routeUri.getSchemeSpecificPart()); } URI mergedUrl=UriComponentsBuilder.fromUri(uri) // .uri(routeUri) .scheme(routeUri.getScheme()) .host(routeUri.getHost()) .port(routeUri.getPort()) .build(encoded) .toUri(); exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR,mergedUrl); return chain.filter(exchange);}
1.首先獲取請求中的 Route, 如 果為 空 則 直接 提交 過濾器 鏈;否則 獲取 routeUri, 并 判斷 routeUri 是否 特殊, 如果 是 則需 要 處理 URL, 保存 前綴 到 GATEWAY_SCHEME_PREFIX_ATTR, 并將 routeUri 替換
2.獲取請求中的Route,如果為空則直接提交給過濾器鏈
3.獲取routeUri并判斷是否特殊,如果是則需要處理URL,保存前綴到GATEWAY_SCHEME_PREFIX_ATTR,并將routeUri 替換為schemeSpecificPart
然后拼接requestUrl,將請求的URI轉換為路由定義的routeUri
4.最后,提交到過濾器鏈繼續執行
7、WebsocketRoutingFilter
如果請求中的ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 屬性對應的URL前綴為 ws 或 wss,則啟用Websocket 路由過濾器。它使用Spring Web Socket 作為底層通信組件向下游轉發 WebSocket 請求。
Websocket 可以通過添加前綴 lb來實現負載均衡,如 lb:ws://serviceid。
如果您使用SockJS作為普通http的回調,則應配置正常的HTTP路由以及Websocket路由
spring: cloud: gateway: routes: # SockJS route - id: websocket_sockjs_route uri: http://localhost:3001 predicates: - Path=/websocket/info/** # Normwal Websocket route - id: websocket_route uri: ws://localhost:3001 predicates: - Path=/websocket/**
Websocket 路由過濾器進行處理時,首先獲取請求的URL及其前綴,判斷是否滿足 Websocket 過濾器啟用的條件;
對于未被路由處理且請求前綴為ws或wss的請求,設置路由處理狀態位,構造過濾后的頭部。最后將請求通過代理轉發。
// WebsocketRoutingFilter.java@Overridepublic Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain){ //檢查websocket 是否是 upgrade changeSchemeIfIsWebSocketUpgrade(exchange); URI requestUrl=exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); String scheme=requestUrl.getScheme(); //判斷是否滿足websocket啟用條件 if(isAlreadyRouted(exchange)||(!"ws".equals(scheme)&&!"wss".equals(scheme))){ return chain.filter(exchange); } setAlreadyRouted(exchange); HttpHeaders headers=exchange.getRequest().getHeaders(); HttpHeaders filtered=filterRequest(getHeadersFilters(), exchange); List<String> protocols=headers.get(SEC_WEBSOCKET_PROTOCOL); if(protocols!=null){ protocols=headers.get(SEC_WEBSOCKET_PROTOCOL).stream() .flatMap(header->Arrays.stream(commaDelimitedListToStringArray(header))) .map(String::trim) .collect(Collectors.toList()); } //將請求代理轉發 return this.webSocketService.handleRequest(exchange, new ProxyWebSocketHandler(requestUrl,this.webSocketClient,filtered,protocols));}
ProxyWebSocketHandler 是 WebSocketHandler 的實現類,處理客戶端 WebSocket Session。下面看一下代理 WebSocket 處理器的具體實現:
// WebsocketRoutingFilter.javaprivate static class ProxyWebSocketHandler implements WebSocketHandler { private final WebSocketClient client; private final URI url; private final HttpHeaders headers; private final List<String> subProtocols; public ProxyWebSocketHandler(URI url, WebSocketClient client, HttpHeaders headers, List<String> protocols) { this.client = client; this.url = url; this.headers = headers; if (protocols != null) { this.subProtocols = protocols; } else { this.subProtocols = Collections.emptyList(); } } @Override public List<String> getSubProtocols() { return this.subProtocols; } @Override public Mono<Void> handle(WebSocketSession session) { // pass headers along so custom headers can be sent through return client.execute(url, this.headers, new WebSocketHandler() { @Override public Mono<Void> handle(WebSocketSession proxySession) { // Use retain() for Reactor Netty Mono<Void> proxySessionSend = proxySession .send(session.receive().doOnNext(WebSocketMessage::retain)); // .log("proxySessionSend", Level.FINE); Mono<Void> serverSessionSend = session .send(proxySession.receive().doOnNext(WebSocketMessage::retain)); // .log("sessionSend", Level.FINE); return Mono.zip(proxySessionSend, serverSessionSend).then(); } /** * Copy subProtocols so they are available downstream. * @return */ @Override public List<String> getSubProtocols() { return ProxyWebSocketHandler.this.subProtocols; } }); }}
1.WebSocketClient# execute 方法連接后端被代理的 WebSocket 服務。
2.連接成功后,回調WebSocketHandler實現的內部類的handle( WebSocketSession session)方法
3.WebSocketHandler 實現的內部類實現對消息的轉發:客戶端=> 具體業務服務=> 客戶 端;然后合并代理服務的會話信息 proxySessionSend 和業務服務的會話信息serverSessionSend。
8、其它過濾器
AdaptCachedBodyGlobalFilter用于緩存請求體的過濾器,在全局過濾器中的優先級較高。
ForwardPathFilter請求中的 gatewayRoute 屬性對應 Route 對象,當 Route 中的 URI scheme 為 forward 模式 時, 該過濾器用于設置請求的 URI 路徑為 Route 對象 中的 URI 路徑。
@Component public class IPCheckFilter implements GlobalFilter, Ordered { @Override public int getOrder() { return 0; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { HttpHeaders headers = exchange.getRequest().getHeaders(); InetSocketAddress host = headers.getHost(); // 此處的 IP 地址是寫死的,實際中需要采取配置的方式 String hostName = host.getHostName(); if ("localhost".equals(hostName)) { ServerHttpResponse response = exchange.getResponse(); byte[] datas = "{\"code\": 401,\"message\": \"非法請求\"}".getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(datas); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } return chain.filter(exchange); } }
在學習Spring Cloud Gateway 路由轉發規則之前,我們需要先了解下Spring Cloud Gateway內部提供的所有predicates(謂語、斷言)。
predicates是路由轉發的判斷條件,目前SpringCloud Gateway支持多種方式,具體如下圖所示
每一個Predicate的使用,你可以理解為:當滿足這種條件后才會被轉發,如果是多個,那就是都滿足的情況下被轉發。
其實在上文中我們在介紹路由配置方式的時候已經介紹了Path方式匹配轉發,接下來我們就挑幾個在日常開發中經常使用的幾種方式進行介紹。
Predicate 支持設置一個時間,在請求進行轉發的時候,可以通過判斷在這個時間之前或者之后進行轉發。比如我們現在設置只有在2019年1月1日才會轉發到我的網站,在這之前不進行轉發,我就可以這樣配置:
spring: cloud: gateway: routes: - id: time_route uri: http://ityouknow.com predicates: - After=2018-01-20T06:06:06+08:00[Asia/Shanghai]
Spring 是通過 ZonedDateTime 來對時間進行的對比,ZonedDateTime 是 Java 8 中日期時間功能里,用于表示帶時區的日期與時間信息的類,ZonedDateTime 支持通過時區來設置時間,中國的時區是:Asia/Shanghai
。
After Route Predicate 是指在這個時間之后的請求都轉發到目標地址。上面的示例是指,請求時間在 2018年1月20日6點6分6秒之后的所有請求都轉發到地址http://ityouknow.com
。+08:00
是指時間和UTC時間相差八個小時,時間地區為Asia/Shanghai
。
添加完路由規則之后,訪問地址http://localhost:8080
會自動轉發到http://ityouknow.com
。
Before Route Predicate 剛好相反,在某個時間之前的請求的請求都進行轉發。我們把上面路由規則中的 After 改為 Before,如下:
spring: cloud: gateway: routes: - id: after_route uri: http://ityouknow.com predicates: - Before=2018-01-20T06:06:06+08:00[Asia/Shanghai]
就表示在這個時間之前可以進行路由,在這時間之后停止路由,修改完之后重啟項目再次訪問地址http://localhost:8080
,頁面會報 404 沒有找到地址。
除過在時間之前或者之后外,Gateway 還支持限制路由請求在某一個時間段范圍內,可以使用 Between Route Predicate 來實現。
spring: cloud: gateway: routes: - id: after_route uri: http://ityouknow.com predicates: - Between=2018-01-20T06:06:06+08:00[Asia/Shanghai], 2019-01-20T06:06:06+08:00[Asia/Shanghai]
這樣設置就意味著在這個時間段內可以匹配到此路由,超過這個時間段范圍則不會進行匹配。通過時間匹配路由的功能很酷,可以用在限時搶購的一些場景中。
Cookie Route Predicate 可以接收兩個參數,一個是 Cookie name ,一個是正則表達式,路由規則會通過獲取對應的 Cookie name 值和正則表達式去匹配,如果匹配上就會執行路由,如果沒有匹配上則不執行。
spring: cloud: gateway: routes: - id: cookie_route uri: http://ityouknow.com predicates: - Cookie=ityouknow, kee.e
使用 curl 測試,命令行輸入:
curl http://localhost:8080 --cookie "ityouknow=kee.e"
則會返回頁面代碼,如果去掉--cookie "ityouknow=kee.e"
,后臺匯報 404 錯誤。
Header Route Predicate 和 Cookie Route Predicate 一樣,也是接收 2 個參數,一個 header 中屬性名稱和一個正則表達式,這個屬性值和正則表達式匹配則執行。
spring: cloud: gateway: routes: - id: header_route uri: http://ityouknow.com predicates: - Header=X-Request-Id, \d+
使用 curl 測試,命令行輸入:
curl http://localhost:8080 -H "X-Request-Id:666666"
則返回頁面代碼證明匹配成功。將參數-H "X-Request-Id:666666"
改為-H "X-Request-Id:neo"
再次執行時返回404證明沒有匹配。
Host Route Predicate 接收一組參數,一組匹配的域名列表,這個模板是一個 ant 分隔的模板,用.
號作為分隔符。它通過參數中的主機地址作為匹配規則。
spring: cloud: gateway: routes: - id: host_route uri: http://ityouknow.com predicates: - Host=**.ityouknow.com
使用 curl 測試,命令行輸入:
curl http://localhost:8080 -H "Host: www.ityouknow.com" curl http://localhost:8080 -H "Host: md.ityouknow.com"
經測試以上兩種 host 均可匹配到 host_route 路由,去掉 host 參數則會報 404 錯誤。
可以通過是 POST、GET、PUT、DELETE 等不同的請求方式來進行路由。
spring: cloud: gateway: routes: - id: method_route uri: http://ityouknow.com predicates: - Method=GET
使用 curl 測試,命令行輸入:
# curl 默認是以 GET 的方式去請求 curl http://localhost:8080
測試返回頁面代碼,證明匹配到路由,我們再以 POST 的方式請求測試。
# curl 默認是以 GET 的方式去請求 curl -X POST http://localhost:8080
返回 404 沒有找到,證明沒有匹配上路由
Path Route Predicate 接收一個匹配路徑的參數來判斷是否走路由。
spring: cloud: gateway: routes: - id: host_route uri: http://ityouknow.com predicates: - Path=/foo/{segment}
如果請求路徑符合要求,則此路由將匹配,例如:/foo/1 或者 /foo/bar。
使用 curl 測試,命令行輸入:
curl http://localhost:8080/foo/1 curl http://localhost:8080/foo/xx curl http://localhost:8080/boo/xx
經過測試第一和第二條命令可以正常獲取到頁面返回值,最后一個命令報404,證明路由是通過指定路由來匹配。
Query Route Predicate 支持傳入兩個參數,一個是屬性名一個為屬性值,屬性值可以是正則表達式。
spring: cloud: gateway: routes: - id: query_route uri: http://ityouknow.com predicates: - Query=smile
這樣配置,只要請求中包含 smile 屬性的參數即可匹配路由。
使用 curl 測試,命令行輸入:
curl localhost:8080?smile=x&id=2
經過測試發現只要請求匯總帶有 smile 參數即會匹配路由,不帶 smile 參數則不會匹配。
還可以將 Query 的值以鍵值對的方式進行配置,這樣在請求過來時會對屬性值和正則進行匹配,匹配上才會走路由。
spring: cloud: gateway: routes: - id: query_route uri: http://ityouknow.com predicates: - Query=keep, pu.
這樣只要當請求中包含 keep 屬性并且參數值是以 pu 開頭的長度為三位的字符串才會進行匹配和路由。
使用 curl 測試,命令行輸入:
curl localhost:8080?keep=pub
測試可以返回頁面代碼,將 keep 的屬性值改為 pubx 再次訪問就會報 404,證明路由需要匹配正則表達式才會進行路由。
Predicate 也支持通過設置某個 ip 區間號段的請求才會路由,RemoteAddr Route Predicate 接受 cidr 符號(IPv4 或 IPv6 )字符串的列表(最小大小為1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子網掩碼)。
spring: cloud: gateway: routes: - id: remoteaddr_route uri: http://ityouknow.com predicates: - RemoteAddr=192.168.1.1/24
可以將此地址設置為本機的 ip 地址進行測試。
curl localhost:8080
果請求的遠程地址是 192.168.1.10,則此路由將匹配。
上面為了演示各個 Predicate 的使用,我們是單個單個進行配置測試,其實可以將各種 Predicate 組合起來一起使用。
例如:
spring: cloud: gateway: routes: - id: host_foo_path_headers_to_httpbin uri: http://ityouknow.com predicates: - Host=**.foo.org - Path=/headers - Method=GET - Header=X-Request-Id, \d+ - Query=foo, ba. - Query=baz - Cookie=chocolate, ch.p - After=2018-01-20T06:06:06+08:00[Asia/Shanghai]
各種 Predicates 同時存在于同一個路由時,請求必須同時滿足所有的條件才被這個路由匹配。
一個請求滿足多個路由的謂詞條件時,請求只會被首個成功匹配的路由轉發
在之前的 Spring Cloud 系列文章中,在 什么是Hystrix,阿里技術最終面,遺憾的倒在Hystrix面前! 中 我們就對熔斷做了詳細的介紹。
Spring Cloud Gateway 也可以利用 Hystrix 的熔斷特性,在流量過大時進行服務降級,同樣我們還是首先給項目添加上依賴。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
配置示例
spring: cloud: gateway: routes: - id: hystrix_route uri: http://example.org filters: - Hystrix=myCommandName
配置后,gateway 將使用 myCommandName 作為名稱生成 HystrixCommand 對象來進行熔斷管理。如果想添加熔斷后的回調內容,需要在添加一些配置。
spring: cloud: gateway: routes: - id: hystrix_route uri: lb://spring-cloud-producer predicates: - Path=/consumingserviceendpoint filters: - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/incaseoffailureusethis
fallbackUri: forward:/incaseoffailureusethis
配置了 fallback 時要會調的路徑,當調用 Hystrix 的 fallback 被調用時,請求將轉發到/incaseoffailureuset
這個 URI。
首先,我們要知道我們為什么要使用重試機制,通常我們在調用服務的時候,總是會不可避免的遇到像網絡波動或是別的某種原因導致服務調用失敗。
這個時候我們就會想要重新訪問服務,這里我們就用到了重試機制。
但是我們也不能濫用重試機制,比如如果我們寫數據的時候使用重試機制就要分外小心了,必須做好接口的冪等性,防止數據重復寫入庫中。
而且大量的重試機制勢必會導致請求量增加,給系統的壓力增大,所以我們設置合理的重試次數也是至關重要的。
下面我們來講講Spring Cloud Gateway中的重試機制和使用。
我們來看下GatewayAutoConfiguration,根據類名我們知道他是Gateway的自動裝配類
源碼如下:
@Configuration@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)@EnableConfigurationProperties@AutoConfigureBefore(HttpHandlerAutoConfiguration.class)@AutoConfigureAfter({GatewayLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class})@ConditionalOnClass(DispatcherHandler.class)public class GatewayAutoConfiguration { //...... @Bean public RetryGatewayFilterFactory retryGatewayFilterFactory() { return new RetryGatewayFilterFactory(); } //......}
我們發現程序默認啟用了RetryGatewayFilterFactory,我們來看看RetryGatewayFilterFactory的源碼
org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory
public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory<RetryGatewayFilterFactory.RetryConfig> { private static final Log log = LogFactory.getLog(RetryGatewayFilterFactory.class); public RetryGatewayFilterFactory() { super(RetryConfig.class); } @Override public GatewayFilter apply(RetryConfig retryConfig) { // 驗證重試配置格式是否正確 retryConfig.validate(); Repeat<ServerWebExchange> statusCodeRepeat = null; if (!retryConfig.getStatuses().isEmpty() || !retryConfig.getSeries().isEmpty()) { Predicate<RepeatContext<ServerWebExchange>> repeatPredicate = context -> { ServerWebExchange exchange = context.applicationContext(); // 判斷重試次數是否已經達到了配置的最大值 if (exceedsMaxIterations(exchange, retryConfig)) { return false; } // 獲取響應的狀態碼 HttpStatus statusCode = exchange.getResponse().getStatusCode(); // 獲取請求方法類型 HttpMethod httpMethod = exchange.getRequest().getMethod(); // 判斷響應狀態碼是否在配置中存在 boolean retryableStatusCode = retryConfig.getStatuses().contains(statusCode); if (!retryableStatusCode && statusCode != null) { // null status code might mean a network exception? // try the series retryableStatusCode = retryConfig.getSeries().stream() .anyMatch(series -> statusCode.series().equals(series)); } // 判斷方法是否包含在配置中 boolean retryableMethod = retryConfig.getMethods().contains(httpMethod); // 決定是否要進行重試 return retryableMethod && retryableStatusCode; }; statusCodeRepeat = Repeat.onlyIf(repeatPredicate) .doOnRepeat(context -> reset(context.applicationContext())); } //TODO: support timeout, backoff, jitter, etc... in Builder Retry<ServerWebExchange> exceptionRetry = null; if (!retryConfig.getExceptions().isEmpty()) { Predicate<RetryContext<ServerWebExchange>> retryContextPredicate = context -> { if (exceedsMaxIterations(context.applicationContext(), retryConfig)) { return false; } // 異常判斷 for (Class<? extends Throwable> clazz : retryConfig.getExceptions()) { if (clazz.isInstance(context.exception())) { return true; } } return false; }; // 使用reactor extra的retry組件 exceptionRetry = Retry.onlyIf(retryContextPredicate) .doOnRetry(context -> reset(context.applicationContext())) .retryMax(retryConfig.getRetries()); } return apply(statusCodeRepeat, exceptionRetry); } public boolean exceedsMaxIterations(ServerWebExchange exchange, RetryConfig retryConfig) { Integer iteration = exchange.getAttribute(RETRY_ITERATION_KEY); //TODO: deal with null iteration return iteration != null && iteration >= retryConfig.getRetries(); } public void reset(ServerWebExchange exchange) { //TODO: what else to do to reset SWE? exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_ALREADY_ROUTED_ATTR); } public GatewayFilter apply(Repeat<ServerWebExchange> repeat, Retry<ServerWebExchange> retry) { return (exchange, chain) -> { if (log.isTraceEnabled()) { log.trace("Entering retry-filter"); } // chain.filter returns a Mono<Void> Publisher<Void> publisher = chain.filter(exchange) //.log("retry-filter", Level.INFO) .doOnSuccessOrError((aVoid, throwable) -> { // 獲取已經重試的次數,默認值為-1 int iteration = exchange.getAttributeOrDefault(RETRY_ITERATION_KEY, -1); // 增加重試次數 exchange.getAttributes().put(RETRY_ITERATION_KEY, iteration + 1); }); if (retry != null) { // retryWhen returns a Mono<Void> // retry needs to go before repeat publisher = ((Mono<Void>)publisher).retryWhen(retry.withApplicationContext(exchange)); } if (repeat != null) { // repeatWhen returns a Flux<Void> // so this needs to be last and the variable a Publisher<Void> publisher = ((Mono<Void>)publisher).repeatWhen(repeat.withApplicationContext(exchange)); } return Mono.fromDirect(publisher); }; }}
可以看到這個filter使用了reactor的Retry組件,同時往exchange的attribues添加retry_iteration,用來記錄重試次數,該值默認從-1開始,第一次執行的時候,retry_iteration+1為0。之后每重試一次,就添加1。
filter的apply接收兩個參數,一個是Repeat<ServerWebExchange>,一個是Retry<ServerWebExchange>。
repeat與retry的區別是repeat是在onCompleted的時候會重試,而retry是在onError的時候會重試。這里由于不一定是異常的時候才可能重試,所以加了repeat。
我們在來看看核心配置類RetryConfig
public static class RetryConfig { private int retries = 3; private List<Series> series = toList(Series.SERVER_ERROR); private List<HttpStatus> statuses = new ArrayList<>(); private List<HttpMethod> methods = toList(HttpMethod.GET); private List<Class<? extends Throwable>> exceptions = toList(IOException.class); //...... public void validate() { Assert.isTrue(this.retries > 0, "retries must be greater than 0"); Assert.isTrue(!this.series.isEmpty() || !this.statuses.isEmpty(), "series and status may not both be empty"); Assert.notEmpty(this.methods, "methods may not be empty"); } //......}
我們可以看到配置文件有5個屬性,詳解如下:
retries:重試次數,默認值是3次
series:狀態碼配置(分段),符合的某段狀態碼才會進行重試邏輯,默認值是
SERVER_ERROR,值是5,也就是5XX(5開頭的狀態碼),共有5個值:
public enum Series { INFORMATIONAL(1), SUCCESSFUL(2), REDIRECTION(3), CLIENT_ERROR(4), SERVER_ERROR(5);}
statuses:狀態碼配置,和series不同的是這邊是具體狀態碼的配置,取值請參考:
org.springframework.http.HttpStatus
methods:指定哪些方法的請求需要進行重試邏輯,默認值是GET方法,取值如下:
public enum HttpMethod { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;}
exceptions:指定哪些異常需要進行重試邏輯,默認值是java.io.IOException
限流的目的是通過對并發訪問/請求進行限速,或對一個時間窗口內的請求進行限速來保護系統。一旦達到限制速率則可以拒絕服務、排隊或等待、降級。
一般開發高并發系統常見的限流有:限制總并發數、限制瞬時并發數、限制時間窗口內的平均速率、限制遠程接口的調用速率、限制MQ的消費速率,或根據網絡連接數、網絡流量、CPU或內存負載等來限流。
本文主要就分布式限流方法,對Spring Cloud Gateway的限流原理進行分析。
分布式限流最關鍵的是要將限流服務做成原子化,常見的限流算法有:令牌桶、漏桶等,Spring Cloud Gateway使用Redis+Lua技術實現高并發和高性能的限流方案。
令牌桶算法
令牌桶算法是一個存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
1、假如用戶配置的平均速率為r,則每隔1/r秒一個令牌被加入到桶中;
2、假設桶最多可以存發b個令牌。如果令牌到達時令牌桶已經滿了,那么這個令牌會被丟棄;
3、當一個n個字節大小的數據包到達,將從桶中刪除n個令牌,接著數據包被發送到網絡上;
4、如果令牌桶中少于n個令牌,那么不會刪除令牌,并且認為這個數據包在流量限制之外;
算法允許最長b個字節的突發,但從長期運行結果看,數據包的速率被限制成常量r。對于在流量限制外的數據包可以以不同的方式處理:
1、它們可以被丟棄;
2、它們可以排放在隊列中以便當令牌桶中累積了足夠多的令牌時再傳輸;
3、它們可以繼續發送,但需要做特殊標記,網絡過載的時候將這些特殊標記的包丟棄。
漏桶算法
漏桶作為計量工具(The Leaky Bucket Algorithm as a Meter)時,可以用于流量整形(Traffic Shaping)和流量控制(Traffic Policing),漏桶算法的描述如下:
1、一個固定容量的漏桶,按照常量固定速率流出水滴;
2、如果桶是空的,則不需流出水滴;
3、可以以任意速率流入水滴到漏桶;
4、如果流入水滴超出了桶的容量,則流入的水滴溢出了(被丟棄),而漏桶容量是不變的。
本文介紹一種簡單的限流方式,通過配置的方式進行限流。
我們配置gateway的application.yml文件,如下:
server: port: 8099spring: application: name: gateway-frame cloud: gateway: discovery: locator: enabled: true # 服務名小寫 lower-case-service-id: true routes: - id: gateway-service # lb代表從注冊中心獲取服務,且已負載均衡方式轉發 uri: lb://gateway-service predicates: - Path=/service/** # 限流filter配置 filters: - name: RequestRateLimiter args: key-resolver: '#{@uriKeyResolver}' redis-rate-limiter.replenishRate: 300 redis-rate-limiter.burstCapacity: 300 redis: host: xxxxxxx port: 6379 password: xxxxxx database: 0# 注冊中心eureka: instance: prefer-ip-address: true client: service-url: defaultZone: http://xxxxxxx:8761/eureka/
配置文件解讀:
1、引入filte的配置-name: RequestRateLimiter,這個是使用已經實現好的RequestRateLimiterGatewayFilterFactory類進行限流;
2、key-resolver:用于獲取限流維度的實現類,可以根據ip、uri、設備號、用戶id等進行限流,這里使用的uriKeyResolver對應實現使用uri限流的類;
3、redis-rate-limiter.burstCapacity:令牌桶容量,就是沒秒能夠同時有多少個訪問請求;
4、redis-rate-limiter.replenishRate:令牌桶每秒的填充量;
5、因為限流類依賴于redis進行統計數據的存儲,所以這里要加上redis的連接配置;
不同的tps,同樣的請求時間(50s),對兩種網關產品進行壓力測試,結果如下:
并發較低的場景下,兩種網關的表現差不多
配置同樣的線程數(2000),同樣的請求時間(5分鐘),后端服務在不同的響應時間(休眠時間),對兩種網關產品進行壓力測試,結果如下:
Zuul網關的tomcat最大線程數為400,hystrix超時時間為100000。
Gateway在高并發和后端服務響應慢的場景下比Zuul1的表現要好。
Spring Cloud Gateway的開發者提供了benchmark項目用來對比Gateway和Zuul1的性能,官方提供的性能對比結果如下:
測試工具為wrk,測試時間30秒,線程數為10,連接數為200。
從官方的對比結果來看,Gateway的RPS是Zuul1的1.55倍,平均延遲是Zuul1的一半。
到此,關于“總結Spring Cloud Gateway相關知識點”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。