全網最全講解 Spring Cloud Gateway,認真看完這一篇就夠了!

原文首發地址全網最全講解Spring Cloud Gateway,認真看完這一篇就夠了!java

前言

上文咱們介紹過Zuul,據說SpringCloud不許備要我了,但是爲何面試還要每天問我?,雖然如今Spring Cloud已經放棄Netflix Zuul 2.x了,可是不少面試官仍是會挑Zuul相關的面試題進行考察,目的就是爲了瞭解面試者對java架構知識的全面性。react

如今Spring Cloud中引用的仍是Zuul 1.x版本,1.x版本基於過濾器的,是阻塞IO,不支持長鏈接。web

雖然Zuul 2.x版本跟1.x的架構不同,性能也有所提高。可是在Spring Cloud已經再也不集成Zuul 2.x了,因此說Spring Cloud 二代架構已經使用Spring Cloud Gateway來替換Zuul。面試

仍是得強調下,不要由於Zuul已經不使用了,就忽略它,基本的知識點仍是要清楚明白的,畢竟有些時候知識學來在工做中並不必定能用到,可是面試中掌握全面的知識,從容的應付面試,纔是你最後談薪資的資本。正則表達式

什麼是Spring Cloud Gateway?

SpringCloud Gateway 是 Spring Cloud 的一個全新項目,該項目是基於 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的網關,它旨在爲微服務架構提供一種簡單有效的統一的 API 路由管理方式。redis

爲了提高網關的性能,SpringCloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層則使用了高性能的Reactor模式通訊框架Netty。算法

Spring Cloud Gateway旨在提供一種簡單而有效的途徑來發送API,併爲他們提供橫切關注點,例如:安全性,監控/指標和彈性。spring

Spring Cloud Gateway核心概念

Route(路由):這是網關的基本構建塊。它由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。若是斷言爲真,則路由匹配,目標URI會被訪問json

Predicate(斷言):這是一個 Java 8 的 Predicate。輸入類型是一個 ServerWebExchange。咱們可使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或參數。後端

Filter(過濾器):這是org.springframework.cloud.gateway.filter.GatewayFilter的實例,咱們可使用它攔截和修改請求,而且對上文的響應,進行再處理

Spring Cloud Gateway 特性

  • 基於Spring Framework 五、Project Reactor和Spring Boot 2.0構建

  • 可以在任意請求屬性上匹配路由

  • predicates(謂詞) 和 filters(過濾器)是特定於路由的

  • 集成了Hystrix斷路器

  • 集成了Spring Cloud DiscoveryClient

  • 易於編寫謂詞和過濾器

  • 請求速率限制

  • 路徑重寫

Spring Cloud Gateway的工做流程

客戶端向Gateway發出請求。

而後Gateway Handler Mapping中找到與請求相匹配的路由,將其發送給Gateway Web Handler。

Handler再經過指定的過濾器鏈來將請求發送到咱們實際的服務執行業務邏輯,最後返回請求結果。

過濾器之間用虛線分開是由於過濾器可能會在發送代理請求以前或以後執行業務邏輯。

filter在請求以前能夠作參數校驗,權限校驗,流量監控,日誌輸出,協議轉換等等,在請求以後能夠作響應內容、響應頭的修改,日誌輸出,流量監控等。

Spring Cloud Gateway核心知識點解讀

Spring Cloud Gateway路由配置

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跨域訪問。

Spring Cloud Gateway還針對跨域訪問作了設計,可使用如下配置解決跨域訪問問題。

當服務啓動的時候,跨域配置信息會存儲在GlobalCorsProperties的corsConfigurations映射中,key是 /**,value是CorsConfiguration的對象。上面的配置表示容許來自https://docs.spring.io的Get請求訪問此網關,而且代表服務器容許請求頭中攜帶字段Content-Type。

Spring Cloud Gateway過濾器詳解

Spring Cloud Gateway的filter生命週期只有兩個:「pre」和「post」。

pre:在請求被路由以前調用。能夠利用這個過濾器實現身份驗證、在集羣中選擇請求的微服務、記錄調試的信息。

post:在路由到服務器以後執行。這種過濾器可用來爲響應添加HTTP Header、統計信息和指標、響應從微服務發送給客戶端等。

Spring Cloud gateway的filter分爲兩種:GatewayFilterGlobalfilter

GlobalFilter會應用到全部的路由上,而Gatewayfilter將應用到單個路由或者一個分組的路由上。

利用Gatewayfilter能夠修改請求的http的請求或者是響應,或者根據請求或者響應作一些特殊的限制。

更多時候咱們能夠利用Gatewayfilter作一些具體的路由配置。

網關過濾器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。

全局過濾器Globalfilter

什麼是全局過濾器,簡單的一句話來講就是,全局過濾器會做用於全局的路由上。

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方法的邏輯便可。

接下來咱們就開始依次介紹這十一個實現類

一、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 狀態。

二、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);}

從過濾器執行方法中能夠看出,負載均衡客戶端過濾器的實現步驟以下:

一、構造函數傳入負載均衡客戶端,依賴中添加 Spring Cloud Netflix Ribbon 便可 注入 該 Bean。

二、獲取請求的 URL 及其前綴,若是 URL 不爲空且前綴爲lb或者網關請求的前綴是 lb,則保存原始的URL,負載到具體的服務實例並根據獲取的服務實例信息,從新組裝請求的URL。

三、最後,添加請求的URL到GATEWAY_ REQUEST_ URL_ ATTR,並提交到過濾器鏈中繼續執行

在組裝請求的地址時,若是loadbalancer沒有提供前綴的話,則使用默認的,即overrideScheme 爲null,不然的話使用 lb:

三、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 的配置屬性

四、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個字段:

  1. Proxy- Authenticate

  2. Proxy- Authorization

  3. TE

  4. Trailer

  5. Transfer- Encoding

  6. Upgrade

  7. proxy- connection

  8. content- length

五、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。

六、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.最後,提交到過濾器鏈繼續執行

七、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。

八、其它過濾器

AdaptCachedBodyGlobalFilter用於緩存請求體的過濾器,在全局過濾器中的優先級較高。

ForwardPathFilter請求中的 gatewayRoute 屬性對應 Route 對象,當 Route 中的 URI scheme 爲 forward 模式 時, 該過濾器用於設置請求的 URI 路徑爲 Route 對象 中的 URI 路徑。

實戰總結,自定義一個 GlobalFilter,實現對 IP 地址的限制。

 


@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 路由轉發規則以前,咱們須要先了解下Spring Cloud Gateway內部提供的全部predicates(謂語、斷言)。

predicates是路由轉發的判斷條件,目前SpringCloud Gateway支持多種方式,具體以下圖所示

每個Predicate的使用,你能夠理解爲:當知足這種條件後纔會被轉發,若是是多個,那就是都知足的狀況下被轉發。

其實在上文中咱們在介紹路由配置方式的時候已經介紹了Path方式匹配轉發,接下來咱們就挑幾個在平常開發中常用的幾種方式進行介紹。

1.經過時間匹配

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] 

這樣設置就意味着在這個時間段內能夠匹配到此路由,超過這個時間段範圍則不會進行匹配。經過時間匹配路由的功能很酷,能夠用在限時搶購的一些場景中。

2.經過 Cookie 匹配

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 錯誤。

3.經過 Header 屬性匹配

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證實沒有匹配。

4.經過 Host 匹配

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 錯誤。

5.經過請求方式匹配

能夠經過是 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 沒有找到,證實沒有匹配上路由

6.經過請求路徑匹配

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,證實路由是經過指定路由來匹配。

7.經過請求參數匹配

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,證實路由須要匹配正則表達式纔會進行路由。

8.經過請求 ip 地址進行匹配

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,則此路由將匹配。

9.組合使用

上面爲了演示各個 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 Gateway熔斷機制

在以前的 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重試機制

首先,咱們要知道咱們爲何要使用重試機制,一般咱們在調用服務的時候,老是會不可避免的遇到像網絡波動或是別的某種緣由致使服務調用失敗。

這個時候咱們就會想要從新訪問服務,這裏咱們就用到了重試機制。

可是咱們也不能濫用重試機制,好比若是咱們寫數據的時候使用重試機制就要分外當心了,必須作好接口的冪等性,防止數據重複寫入庫中。

並且大量的重試機制勢必會致使請求量增長,給系統的壓力增大,因此咱們設置合理的重試次數也是相當重要的。

下面咱們來說講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

Spring Cloud Gateway限流機制

限流的目的是經過對併發訪問/請求進行限速,或對一個時間窗口內的請求進行限速來保護系統。一旦達到限制速率則能夠拒絕服務、排隊或等待、降級。 


通常開發高併發系統常見的限流有:限制總併發數、限制瞬時併發數、限制時間窗口內的平均速率、限制遠程接口的調用速率、限制MQ的消費速率,或根據網絡鏈接數、網絡流量、CPU或內存負載等來限流。 


本文主要就分佈式限流方法,對Spring Cloud Gateway的限流原理進行分析。 


分佈式限流最關鍵的是要將限流服務作成原子化,常見的限流算法有:令牌桶、漏桶等,Spring Cloud Gateway使用Redis+Lua技術實現高併發和高性能的限流方案。

 

令牌桶算法

令牌桶算法是一個存放固定容量令牌的桶,按照固定速率往桶裏添加令牌。令牌桶算法的描述以下: 


一、假如用戶配置的平均速率爲r,則每隔1/r秒一個令牌被加入到桶中;

二、假設桶最多能夠存發b個令牌。若是令牌到達時令牌桶已經滿了,那麼這個令牌會被丟棄; 

三、當一個n個字節大小的數據包到達,將從桶中刪除n個令牌,接着數據包被髮送到網絡上; 

四、若是令牌桶中少於n個令牌,那麼不會刪除令牌,而且認爲這個數據包在流量限制以外; 

算法容許最長b個字節的突發,但從長期運行結果看,數據包的速率被限制成常量r。對於在流量限制外的數據包能夠以不一樣的方式處理: 
一、它們能夠被丟棄; 

二、它們能夠排放在隊列中以便當令牌桶中累積了足夠多的令牌時再傳輸; 

三、它們能夠繼續發送,但須要作特殊標記,網絡過載的時候將這些特殊標記的包丟棄。 

漏桶算法

 

​​​​​​​

漏桶做爲計量工具(The Leaky Bucket Algorithm as a Meter)時,能夠用於流量整形(Traffic Shaping)和流量控制(Traffic Policing),漏桶算法的描述以下:

 
一、一個固定容量的漏桶,按照常量固定速率流出水滴; 

二、若是桶是空的,則不需流出水滴; 

三、能夠以任意速率流入水滴到漏桶; 

四、若是流入水滴超出了桶的容量,則流入的水滴溢出了(被丟棄),而漏桶容量是不變的。

本文介紹一種簡單的限流方式,經過配置的方式進行限流

咱們配置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/

配置文件解讀:

一、引入filte的配置-name: RequestRateLimiter,這個是使用已經實現好的RequestRateLimiterGatewayFilterFactory類進行限流;

二、key-resolver:用於獲取限流維度的實現類,能夠根據ip、uri、設備號、用戶id等進行限流,這裏使用的uriKeyResolver對應實現使用uri限流的類;

三、redis-rate-limiter.burstCapacity:令牌桶容量,就是沒秒可以同時有多少個訪問請求;

四、redis-rate-limiter.replenishRate:令牌桶每秒的填充量;

五、由於限流類依賴於redis進行統計數據的存儲,因此這裏要加上redis的鏈接配置;

微服務網關Zuul和Spring Cloud Gateway的區別和比較

產品對比

性能對比

一、低併發場景

不一樣的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,從介紹什麼是Spring Cloud Gateway開始,到詳解Spring Cloud Gateway的核心概念以及工做流程。

着重說明了Spring Cloud Gateway的路由配置,跨域訪問,重點組件過濾器,路由匹配規則,重試機制和限流機制而且和Zuul1.x作了一個詳細的對比。

本文算是比較全面的介紹了Spring Cloud Gateway相關的知識,也是我根據高頻面試題常問的幾個方面對Spring Cloud Gateway進行解讀。

總之,走過路過不要錯過,本文很長建議先馬再看。

​​​​​​​

相關文章
相關標籤/搜索