一.概述
1.1 简介
Spring Cloud Gateway是Spring Cloud的一个全新项目,该项目是基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,它旨在微服务架构提供一种简单有效的统一的API路由管理方式。
Spring Cloud Gateway作为Spring Cloud生态系统的网关,目标是替代Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最高性能版本进行集成,仍然还是会使用Zuul 2.0之前的非Reactor模式的老版本。而为了提升网关的性能,Spring Cloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用高性能Reactor模式通信框架Netty。
Spring Cloud Gateway的目标,不仅提供统一的路由方式,并且基于Filter链的方式提供了网关的基本功能,例如:安全,监控/指标和的限流等。
1.2 Spring Cloud Gateway和Zuul 区别
1.2.1 Spring Cloud Zuul
Spring Cloud Zuul所集成的Zuul版本,采用的Tomcat容器,使用的是传统的Servlet IO处理模型。
servlet由servlet container进行生命周期管理。container启动时构造servlet对象ing调用servlet init()进行初始化;container关闭时调用servlet destory()销毁servlet;container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取线程),然后调用service()。
缺点:servlet是一个简单的网络IO模型,当请求进入servlet container时,servelt container就会为其绑定一个线程,在并发不高的的场景下这种模型是使用。但是一旦并发一旦上升,线程数量就会上涨,而线程资源代价是昂贵的(上下文切换,内存消耗大)严重影响请求的时间。在一些简单的场景下,不希望为每个request分配一个线程,只需要1个或者几个线程就能应对极大的并发的请求。这种业务场景下servlet没有优势
Spring Cloud Zuul是基于servlet之上的一个阻塞式处理模型,即Spring实现了处理所有request的一个servlet,并且由该servlet阻塞处理。所以Spring Cloud Zuul无法摆脱servlet模型的弊端。虽然Zuul 2.0开始,使用Netty,但是Spring Cloud没有继承改版的方案。
1.2.2 Webflux服务器
Webflux替换了旧的Servlet线程模型。用少量的线程处理request和response IO操作,这些线程称为Loop线程。而业务交给响应式编程框架处理。响应式编程是非常灵活的,用户可以将业务中阻塞的操作交给响应式编程框架的work线程执行,而不阻塞操作依然在Loop线程中处理,大大提高了Loop线程的利用率。
Webflux虽然可以兼容多个底层的通信框架,但是一般情况下,底层还是使用Nettty。
1.2.3 Spring Cloud具有的特性
- 基于Spring Framework 5,Project Reactor和Spring Boot 2.0构建
- 动态路由:能够匹配任何请求属性
- 可以对指定的路由指定Predicate(断言)和Filter(过滤器)
- 集成Hystrix的断路器功能
- 集成Spring Cloud服务发现功能
- 易于编写Predicate(断言)和Filter(过滤器)
- 请求限流功能
- 支持路径重写
二.Gateway基本使用和介绍
2.1 三大核心概念
- Route(路由):路由是构建网关的基本模块,它是由ID,目标URL,一系列断言和过滤器组成。如果断言为true,则匹配该路由
- Predicate(断言):参考的是Java8的java.util.function.Predicate。开发人员可以匹配HTTP请求中的所有内容(例如请求头和请求参数),如果请求和断言匹配则进行路由
- Filter(过滤器):指的是Spring框架中GatewayFilter实例,使用过滤器,可以在请求被路由前或之后进行修改
2.2 使用(断言)
2.2.1 配置路由
1.增加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2.yml和主启动类同创建Eureka Client是一样的
3.yml增加网关配置:
server:
port: 9527
spring:
application:
name: cloud-gateway
#############################新增网关配置###########################
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
#uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
#uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
####################################################################
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
- 添加网关前 - http://localhost:8001/payment/get/1
- 添加网关后 - http://localhost:9527/payment/get/1
4.上面是使用配置文件配置路由,下来是使用代码来配置路由
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GateWayConfig
{
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder)
{
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route_atguigu",
r -> r.path("/guonei")
.uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
}
2.2.2 配置动态路由
默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上的微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
1.依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.yaml文件:需要注意的是uri协议前面的lb,表示启动Gateway的负载均衡的功能。lb://serverName是Spring Cloud Gateway在为服务中自动创建的负载均衡的uri,这里的serverName是微服务在Eureka注册中心的微服务应用名称
server:
port: 9527
spring:
application:
name: cloud-gateway
#############################新增网关配置###########################
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
####################################################################
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
2.2.3 Predicate 断言
Predicate来源于Java 8,是Java 8中引入的一个函数,Predicate接受一个输入参数,返回一个布尔值结果。该接口包含多种默认的反复将Predicate组合成复杂的逻辑。可以用于接口请求参数校验,判断新老数据是否有变化需要进行更新操作
Spring Cloud Gateway是通过Spring Webflux的HandleMapping作为底层来支持转发路由。Spring Cloud Gateway内置了许多的Predicates工厂,这些Predicates工厂通过不同的http请求参数来匹配,多个Predicates工厂可以组合使用。下面是几种常用的PrediacteFactory
转发规则:
规则 | 实例 | 说明 |
Path | - Path=/gate/,/rule/ | ## 当请求的路径为gate、rule开头的时,转发到http://localhost:9023服务器上 |
Before | - Before=2017-01-20T17:42:47.789-07:00[America/Denver] | 在某个时间之前的请求才会被转发到 http://localhost:9023服务器上 |
After | - After=2017-01-20T17:42:47.789-07:00[America/Denver] | 在某个时间之后的请求才会被转发 |
Between | - Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver] | 在某个时间段之间的才会被转发 |
Cookie | - Cookie=chocolate, ch.p | 名为chocolate的表单或者满足正则ch.p的表单才会被匹配到进行请求转发 |
Header | - Header=X-Request-Id, \d+ | 携带参数X-Request-Id或者满足\d+的请求头才会匹配 |
Host | - Host=www.hd123.com | 当主机名为www.hd123.com的时候直接转发到http://localhost:9023服务器上 |
Method | - Method=GET | 只有GET方法才会匹配转发请求,还可以限定POST、PUT等请求方式 |
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Host=**.foo.org
- Path=/headers
- Method=GET
- Header=X-Request-Id, \d+
- Query=foo, ba.
- Query=baz
- Cookie=chocolate, ch.p
各个Predicates同时存在同一个路由时,请求必须满足所有条件才能被这个路由匹配
一个请求满足多个路由的匹配条件,请求只会被成功匹配的路由转发。
2.3 Filter 过滤器
2.3.1 使用
过滤器规则:
过滤规则 | 实例 | 说明 |
PrefixPath | - PrefixPath=/app | 在请求路径前加上app |
RewritePath | - RewritePath=/test, /app/test | 访问localhost:9022/test,请求会转发到localhost:8001/app/test |
SetPath | SetPath=/app/{path} | 通过模板设置路径,转发的规则时会在路径前增加app,{path}表示原请求路径 |
RedirectTo | | 重定向 |
RemoveRequestHeader | | 去掉某个请求头信息 |
当配置多个filter,优先定义的会被调用,剩余的filter不会生效
举例:
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- PrefixPath=/mypath
通过代码配置:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.uri("http://httpbin.org"))
.route("host_route", r -> r.host("*.myhost.org")
.uri("http://httpbin.org"))
.route("rewrite_route", r -> r.host("*.rewrite.org")
.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
.uri("http://httpbin.org"))
.route("hystrix_route", r -> r.host("*.hystrix.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
.uri("http://httpbin.org"))
.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
.uri("http://httpbin.org"))
.route("limit_route", r -> r
.host("*.limited.org").and().path("/anything/**")
.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
.uri("http://httpbin.org"))
.build();
}
2.3.2 过滤器
Gateway基于过滤器实现的,有pre和post两种方式的Filter,分别处理前置逻辑和后置逻辑。客户端的请求先经过pre类型的Filter,然后将请求转发到具体的业务服务,收到业务服务响应之后,再经过post类型的filter处理,最后返回响应到客户端。
在pre类型的过滤器可以做参数校验,权限校验,流量监控,日志输出,协议转换等。在post类型的过滤器可以做响应内容,响应头的修改,日志输出,流量监控等。
Filter可以从作用范围分为两种:一种是针对单个路由的gateway filter,在配置文件中的写法同predicate类似;一个针对所有路由的global gateway filter;区别如下:
GatewayFilter:需要通过spring.cloud.routes.filters配置在具体路由下,只作用当前的路由上或者通过spring.cloud.default-filters配置在全局,左右在所有的路由上
GlobalFilter:全局过滤,不需要配置在配置文件上,作用在所有的路由。最终通过GlobalwayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换成真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化加载,并作用在每个路由上。
GateWay Filter
过滤器允许以某种方式修改传入的HTTP请求或者传出的HTTP响应。过滤器可以限定作用在某些特定的请求路径上。Spring Cloud Gateway包含了许多内置的GatewayFilter工厂。
GatewayFilter工厂同上面说的Predicates工厂一样,都是在配置文件application.yml中配置,遵循了约定大于配置,只需要在配置文件中配置GatewayFilter 工厂的名称,不需要写全类名。在配置文件中配置的最后都会由相应的过滤器工厂类处理。过滤器工厂的源码在org.springframework.cloud.geateway.filter.factory包中
定义自定义局部过滤器
1,需要实现GatewayFilter,Ordered,实现相关方法
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//@Component
@Slf4j
public class UserIdCheckGateWayFilter implements GatewayFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
String url = exchange.getRequest().getPath().pathWithinApplication().value();
log.info("请求URL:" + url);
log.info("method:" + exchange.getRequest().getMethod());
/* String secret = exchange.getRequest().getHeaders().getFirst("secret");
if (StringUtils.isBlank(secret))
{
return chain.filter(exchange);
}*/
//获取param 请求参数
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
//获取header
String userId = exchange.getRequest().getHeaders().getFirst("user-id");
log.info("userId:" + userId);
if (StringUtils.isBlank(userId))
{
log.info("*****头部验证不通过,请在头部输入 user-id");
//终止请求,直接回应
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
// 值越小,优先级越高
// int HIGHEST_PRECEDENCE = -2147483648;
// int LOWEST_PRECEDENCE = 2147483647;
@Override
public int getOrder()
{
return HIGHEST_PRECEDENCE;
}
}
2.加到过滤器工厂(自定义过滤器工厂),并注册到Spring容器中
过滤器工厂的顶级接口是GatewayFilterFactory,可以直接继承它两个抽象类来简化开发:AbstractGatewayFilterFactroy和AbstractNameValuesGatewayFilterFactory。前者接受一个参数,后者接受两个参数。
import com.crazymaker.cloud.nacos.demo.gateway.filter.UserIdCheckGateWayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
@Component
public class UserIdCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<Object>
{
@Override
public GatewayFilter apply(Object config)
{
return new UserIdCheckGateWayFilter();
}
}
3.在配置文件中进行配置,如果不配置则不启用此过滤器规则(也可以使用代码注册)
- id: service_provider_demo_route_filter
uri: lb://service-provider-demo
predicates:
- Path=/filter/**
filters:
- RewritePath=/filter/(?<segment>.*), /provider/$\{segment}
- UserIdCheck
Global Filter
Spring Cloud Gateway内置的GlobalFilter如下:
1.自定义GlobalFilter,需要实现GlobalFilter和Ordered两个接口
public class TokenFilter implements GlobalFilter, Ordered {
Logger logger=LoggerFactory.getLogger( TokenFilter.class );
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (token == null || token.isEmpty()) {
logger.info( "token is empty..." );
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
}
2.注册到IOC容器中
@Bean
public TokenFilter tokenFilter(){
return new TokenFilter();
}
2.4 实现熔断降级
为什么要实现熔断降级?
在分布式系统中,网关作为流量的入口,因此会有大量的请求进入网关,向其他服务发起调用,其他服务不可避免的会出现调用失败(超时、异常),失败时不能让请求堆积在网关上,需要快速失败并返回给客户端,想要实现这个要求,就必须在网关上做熔断、降级操作。
为什么在网关上请求失败需要快速返回给客户端?
因为当一个客户端请求发生故障的时候,这个请求会一直堆积在网关上,当然只有一个这种请求,网关肯定没有问题(如果一个请求就能造成整个系统瘫痪,那这个系统可以下架了),但是网关上堆积多了就会给网关乃至整个服务都造成巨大的压力,甚至整个服务宕掉。因此要对一些服务和页面进行有策略的降级,以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应,所以需要网关上请求失败需要快速返回给客户端。
server.port: 8082
spring:
application:
name: gateway
redis:
host: localhost
port: 6379
password: 123456
cloud:
gateway:
routes:
- id: rateLimit_route
uri: http://localhost:8000
order: 0
predicates:
- Path=/test/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackCmdA
fallbackUri: forward:/fallbackA
hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000
这里的配置,使用了两个过滤器:
(1)过滤器StripPrefix,作用是去掉请求路径的最前面n个部分截取掉。StripPrefix=1就代表截取路径的个数为1,比如前端过来请求/test/good/1/view,匹配成功后,路由到后端的请求路径就会变成http://localhost:8888/good/1/view。
(2)过滤器Hystrix(局部过滤器工厂之一),作用是通过Hystrix进行熔断降级。当上游的请求,进入了Hystrix熔断降级机制时,就会调用fallbackUri配置的降级地址。需要注意的是,还需要单独设置Hystrix的commandKey的超时时间
fallbackUri配置的降级地址的代码如下:
package org.gateway.controller;
import org.gateway.response.Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FallbackController {
@GetMapping("/fallbackA")
public Response fallbackA() {
Response response = new Response();
response.setCode("100");
response.setMessage("服务暂时不可用");
return response;
}
}
三.高级配置
3.1 分布式限流
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用在Redis内的通过执行Lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:
1.引入gateway的起步依赖和redis的reactive依赖
2.application.yml
server:
port: 8081
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://httpbin.org:80/get
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@userKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
application:
name: cloud-gateway
redis:
host: localhost
port: 6379
database: 0
在上面的配置文件,指定程序的端口为8081,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
- burstCapacity,令牌桶总容量。
- replenishRate,令牌桶每秒填充平均速率。
- key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
3.KeyResolver需要实现resolve方法,比如根据userid进行限流,则需要用userid去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
3.2 统一配置跨域请求
什么是跨域请求:跨域就等于从百度访问谷歌的资源,URL由协议、域名、端口和路径组成,如果两个URL的协议、域名和端口相同,则表示他们同源。相反,只要协议
,域名
,端口
有任何一个的不同,就被当作是跨域。
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "*"
allowed-headers: "*"
allow-credentials: true
allowed-methods:
- GET
- POST
- DELETE
- PUT
- OPTION