全网最全讲解 Spring Cloud Gateway,认真看完这一篇就够了!
原文首发地址:全网最全讲解Spring Cloud Gateway,认真看完这一篇就够了!
前言
上文我们介绍过Zuul,听说SpringCloud不准备要我了,可是为什么面试还要天天问我?,虽然现在Spring Cloud已经放弃Netflix Zuul 2.x了,但是很多面试官还是会挑Zuul相关的面试题进行考察,目的就是为了了解面试者对java架构知识的全面性。
现在Spring Cloud中引用的还是Zuul 1.x版本,1.x版本基于过滤器的,是阻塞IO,不支持长连接。
虽然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 路由管理方式。
为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway旨在提供一种简单而有效的途径来发送API,并为他们提供横切关注点,例如:安全性,监控/指标和弹性。
Spring Cloud Gateway核心概念
Route(路由):这是网关的基本构建块。它由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。
Predicate(断言):这是一个 Java 8 的 Predicate。输入类型是一个 ServerWebExchange。我们可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。
Filter(过滤器):这是org.springframework.cloud.gateway.filter.GatewayFilter的实例,我们可以使用它拦截和修改请求,并且对上文的响应,进行再处理。
Spring Cloud Gateway 特性
-
基于Spring Framework 5、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: 8080spring:application:name: api-gatewaycloud:gateway:routes:-id: url-api-gatewayuri: https://www.baidu.compredicates:-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);}@Beanpublic 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分为两种:GatewayFilter和Globalfilter。
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;}@Overridepublic 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;}@Overridepublic 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方法的逻辑即可。
接下来我们就开始依次介绍这十一个实现类
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;}@Overridepublic int getOrder() {return Ordered.LOWEST_PRECEDENCE;}@Overridepublic 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: myRouteuri: lb://servicepredicates:- 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);}//保留原始urladdOriginalRequestUrl(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();}//根据获取的服务实例信息,重新组装请求的 urlURI 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;}@Overridepublic int getOrder() {return Ordered.LOWEST_PRECEDENCE;}@Overridepublic 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 responseHttpHeaders 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 NettyWriteResponseFilterexchange.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 runreturn 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 nettyfinal 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 schemeSpecificPartexchange.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_routeuri: http://localhost:3001predicates:- Path=/websocket/info/**# Normwal Websocket route- id: websocket_routeuri: ws://localhost:3001predicates:- Path=/websocket/**
Websocket 路由过滤器进行处理时,首先获取请求的URL及其前缀,判断是否满足 Websocket 过滤器启用的条件;
对于未被路由处理且请求前缀为ws或wss的请求,设置路由处理状态位,构造过滤后的头部。最后将请求通过代理转发。
// WebsocketRoutingFilter.java@Overridepublic Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain){//检查websocket 是否是 upgradechangeSchemeIfIsWebSocketUpgrade(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();}}@Overridepublic List<String> getSubProtocols() {return this.subProtocols;}@Overridepublic Mono<Void> handle(WebSocketSession session) {// pass headers along so custom headers can be sent throughreturn client.execute(url, this.headers, new WebSocketHandler() {@Overridepublic Mono<Void> handle(WebSocketSession proxySession) {// Use retain() for Reactor NettyMono<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*/@Overridepublic 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 路径。
实战总结,自定义一个 GlobalFilter,实现对 IP 地址的限制。
@Componentpublic class IPCheckFilter implements GlobalFilter, Ordered {@Overridepublic int getOrder() {return 0;}@Overridepublic 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 {//......@Beanpublic 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);}@Overridepublic 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 seriesretryableStatusCode = 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 BuilderRetry<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 iterationreturn 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) -> {// 获取已经重试的次数,默认值为-1int 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 repeatpublisher = ((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技术实现高并发和高性能的限流方案。
令牌桶算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
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-framecloud:gateway:discovery:locator:enabled: true# 服务名小写lower-case-service-id: trueroutes:- id: gateway-service# lb代表从注册中心获取服务,且已负载均衡方式转发uri: lb://gateway-servicepredicates:- Path=/service/**# 限流filter配置filters:- name: RequestRateLimiterargs:key-resolver: '#{@uriKeyResolver}'redis-rate-limiter.replenishRate: 300redis-rate-limiter.burstCapacity: 300redis:host: xxxxxxxport: 6379password: xxxxxxdatabase: 0# 注册中心eureka:instance:prefer-ip-address: trueclient: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的连接配置;
微服务网关Zuul和Spring Cloud Gateway的区别和比较
产品对比
性能对比
1、低并发场景
不同的tps,同样的请求时间(50s),对两种网关产品进行压力测试,结果如下:
并发较低的场景下,两种网关的表现差不多
2、高并发场景
配置同样的线程数(2000),同样的请求时间(5分钟),后端服务在不同的响应时间(休眠时间),对两种网关产品进行压力测试,结果如下:
Zuul网关的tomcat最大线程数为400,hystrix超时时间为100000。
Gateway在高并发和后端服务响应慢的场景下比Zuul1的表现要好。
3、官方性能对比
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进行解读。
总之,走过路过不要错过,本文很长建议先马再看。






