gateway

1.gateway简介

API网关功能

  • 协议转换,路由转发

  • 流量聚合,对流量进行监控,日志输出

  • 作为整个系统的前端工程,对流量进行控制,有限流的作用

  • 作为系统的前端边界,外部流量只能通过网关才能访问系统

  • 可以在网关层做权限的判断--安全认证

  • 可以在网关层做缓存

  • 负载均衡

Gateway的处理流程

客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。

重要概念

  • Route(路由):这是网关的基本构建块。它由一个 ID,一个目标 URl,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。

    内置了 10 种 Router,使得我们可以直接配置一下就可以随心所欲的根据 Header、或者 Path、或者 Host、或者 Query 来做路由。

  • Predicate(断言):输入类型是一个 ServerWebExchange。我们可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。

  • 过滤器( filter),Gateway中的Fiter 分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter 将会对请求和响应进行修改处理。可以使用它拦截和修改请求,并且对上游的响应进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。

    内置了 20 种 Filter 和 9 种全局 Filter,也都可以直接用。当然自定义 Filter 也非常方便。

依赖

引入了依赖默认即开启gateway了,如果暂时不想使用这个功能,这可以配置spring.cloud.gateway.enabled=false即可。

注意:gateway搭建可能出现不兼容的情况,各种少方法,经调整 springboot 2.2.5 和 springcloud Hoxton.SR4是兼容的 和 gateway也是兼容的

<spring-boot.version>2.2.5.RELEASE</spring-boot.version> <spring-cloud.version>Hoxton.SR4</spring-cloud.version>  <dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> 

2.gateway路由配置

spring:   cloud:     gateway:       routes:       		#自定义的路由 ID,保持唯一         - id: order-server         	#目标服务地址 普通url           #uri: http://localhost:8083/           #和注册中心结合方式 lb           uri: lb://order-server           #路由条件           predicates:             - Path=/order/**           #过滤器            filters:           	#StripPrefix可以接受一个非负整数,对应实现是StripPrefixGatewayFilterFactory,作用去掉前缀,例如本例中通过gateway访问user/order/version,网关服务向后转发实际路径为/order/version             - StripPrefix=1  

3.断言 predicates

Spring Cloud Gataway 内置了很多 Predicates 功能,这些Predict的源码在org.springframework.cloud.gateway.handler.predicate包中

Spring Cloud Gateway 是通过 Spring WebFlux 的 HandlerMapping 做为底层支持来匹配到转发路由,Spring Cloud Gateway 内置了很多 Predicates 工厂,这些 Predicates 工厂通过不同的 HTTP 请求参数来匹配,多个 Predicates 工厂可以组合使用。

说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理.

每一个Predicate的使用可以理解为:当满足这种条件后才会被转发,如果是多个,那就是都满足的情况下被转发。

image-20220506214819859

常用断言:

一个请求满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发

spring:  cloud:     gateway:       routes:         - id: order-server           uri: http://localhost:8083/           predicates: #须含order参数名,例如:curl http://localhost:9066/order/createorder/1?order=xxx             - Query=order #须含order参数名,且参数值满足正则(以xx开头且长度为3位的字符串)                    - Query=order,xx.             #须header中含指定属性名和满足正则的属性值             - Header=X-Request-Id,\d+             #须Cookie中含指定属性名和满足正则的属性值             - Cookie=sessionId,test             #指定请求方式             - Method=GET,PUT             #满足路径格式             - Path=/order/{segment}             #在指定时区时间之后/前/中间才能匹配             - After=2020-01-01T12:00:00+08:00[Asia/Shanghai]             - Before=2020-01-01T12:00:00+08:00[Asia/Shanghai]             - Between=2020-01-01T12:00:00+08:00[Asia/Shanghai],2021-01-01T12:00:00+08:00[Asia/Shanghai] 

4.过滤器

4.1 内置过滤器使用

filter除了分为“pre”和“post”两种方式的filter外,在Spring Cloud Gateway中,filter从作用范围可分为另外两种,一种是针对于单个路由的gateway filter,它在配置文件中的写法同predict类似;另外一种是针对于所有路由的global gateway filer。

Spring Cloud Gateway 内置的过滤器工厂一览表如下:image-20220506220529692

源码位置:org.springframework.cloud.gateway.filter.factory

spring:   cloud:     gateway:       routes:         - id: add_request_header_route           uri: http://httpbin.org:80/get           predicates:             - Path=/get           filters:           	#会在请求头加上一对请求头,名称为X-Request-Foo,值为Bar             - AddRequestHeader=X-Request-Foo,Bar             #会在响应头加上一对请求头,名称为X-Request-Foo,值为Bar             - AddResponseHeader=X-Request-Foo,Bar             #重写路径:localhost:9066/user/order/createorder/1->localhost:8083/order/createorder/1             - RewritePath=/user/order/(?<segment>.*), /order/$\{segment} 

4.2 自定义过滤器

实现一个过滤器工厂,在打印日志的时候,可以设置参数来决定是否打印请求参数.

image-20220506221710160

过滤器工厂的顶级接口是GatewayFilterFactory,我们可以直接继承它的两个抽象类来简化开发AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这两个抽象类的区别就是前者接收一个参数(像StripPrefix和我们创建的这种),后者接收两个参数(像AddResponseHeader)。

现在需要将请求的日志打印出来,需要使用一个参数,这时可以参照RedirectToGatewayFilterFactory的写法:

public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {      private static final Logger log = LoggerFactory.getLogger(RequestTimeGatewayFilterFactory.class);     private static final String REQUEST_TIME_BEGIN = requestTimeBegin;     private static final String KEY = withParams;      @Override     public List<String> shortcutFieldOrder() {         return Arrays.asList(KEY);     } 		//在类的构造器中一定要调用下父类的构造器把Config类型传过去,否则会报ClassCastException     public RequestTimeGatewayFilterFactory() {         super(Config.class);     }      @Override     public GatewayFilter apply(Config config) {         return (exchange, chain) -> {             exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());             return chain.filter(exchange).then(                     Mono.fromRunnable(() -> {                         Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);                         if (startTime != null) {                             StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())                                     .append(: )                                     .append(System.currentTimeMillis() - startTime)                                     .append(ms);                             //判断是否打印参数                             if (config.isWithParams()) {                                 sb.append( params:).append(exchange.getRequest().getQueryParams());                             }                             log.info(sb.toString());                         }                     })             );         };     }       public static class Config {         private boolean withParams;         public boolean isWithParams() {             return withParams;         }         public void setWithParams(boolean withParams) {             this.withParams = withParams;         }      } } 

在上面的代码中 apply(Config config)方法内创建了一个GatewayFilter的匿名类,加了是否打印请求参数的逻辑,而这个逻辑的开关是config.isWithParams()。

静态内部类类Config就是为了接收那个boolean类型的参数服务的,里边的变量名可以随意写,但是要重写List shortcutFieldOrder()这个方法。

需要注意的是,在类的构造器中一定要调用下父类的构造器把Config类型传过去,否则会报ClassCastException

注入实现类

最后,需要在工程的启动文件Application类中,向Srping Ioc容器注册RequestTimeGatewayFilterFactory类的Bean。

@Bean public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory() {     return new RequestTimeGatewayFilterFactory(); } 

配置文件配置

spring:   cloud:     gateway:       routes:         - id: order-server           uri: http://localhost:8083/           predicates:             - Path=/order/**           filters:             - RequestTime=false 

4.3 全局过滤器

区别

  • GatewayFilter 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上,或通过spring.cloud.default-filters配置在全局,作用在所有路由上

  • GlobalFilter 全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。

内置GlobalFilter

image-20220506222602860

上图中每一个GlobalFilter都作用在每一个router上,能够满足大多数的需求。但是如果遇到业务上的定制,可能需要编写满足自己需求的GlobalFilter。

自定义TokenFilter

在下面的案例中将讲述如何编写自己GlobalFilter,该GlobalFilter会校验请求中是否包含了请求参数“token”,如果不包含请求参数“token”则不转发路由 . 代码如下:

复制public class TokenFilter implements GlobalFilter, Ordered {      Logger logger=LoggerFactory.getLogger( TokenFilter.class );     @Override     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {       	//从request中获取token         String token = exchange.getRequest().getQueryParams().getFirst(token);         if (token == null || token.isEmpty()) {             logger.info( token is empty... );           	//如果token不存在,终止转发,返回错误码未认证             exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);             return exchange.getResponse().setComplete();         }       	//如果token存在,校验通过         return chain.filter(exchange);     } 		//设置优先级,数字越小优先级越高     @Override     public int getOrder() {         return -100;     } } 

加入Spring容器

然后需要将TokenFilter在工程的启动类中注入到Spring Ioc容器中,代码如下:

@Bean public TokenFilter tokenFilter() {     return new TokenFilter(); } 

5.高级功能

5.1 熔断降级

依赖:

Spring Cloud Gateway可以利用Hystrix的熔断特性,在流量过大时进行服务降级,同时项目中必须加上Hystrix的依赖。

<dependency>     <groupId>org.springframework.cloud</groupId>     <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> 

配置文件:

spring:   cloud:     gateway:       routes:         - id: order-server           uri: http://localhost:8083/           predicates:             - Path=/order/**           filters:           #过滤器Hystrix,作用是通过Hystrix进行熔断降级,当上游的请求,进入了Hystrix熔断降级机制时,就会调用fallbackUri配置的降级地址。需要注意的是,还需要单独设置Hystrix的commandKey的超时时间             - name: Hystrix               args:                 name: fallbackcmd                 fallbackUri: forward:/fallback   hystrix:   command:     fallbackcmd:       execution:         isolation:           thread:             #超时时间,若不设置超时时间则有可能无法触发熔断             timeoutInMilliseconds: 5000 

降级Controller

上述配置中给出了熔断之后返回路径,因此,在Gateway服务模块添加/fallback路径,以作为服务熔断时的返回路径。

@RestController public class GatewayController {     @GetMapping(fallback)     public Map fallback() {         Map<String, String> response = new HashMap<>();         response.put(code, 500);         response.put(message, 服务暂时不可用);         return response;     } } 

测试

将被调用的服务进行休眠5S用来测试熔断,然后调用接口进行测试

5.2 重试路由器

通过简单的配置,Spring Cloud Gateway就可以支持请求重试功能,但是被调用服务需要做好幂等性处理,重试需要慎用。

配置文件配置

复制spring:   cloud:     gateway:       routes:         - id: order-server           uri: http://localhost:8083/           predicates:             - Path=/order/**           filters:             - name: Retry               args:                 retries: 3                 status: 500 

Retry GatewayFilter通过四个参数来控制重试机制,参数说明如下:

  • retries:重试次数,默认值是 3 次。
  • status:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus。
  • methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod。
  • series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5个值。

5.3 分布式限流

在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。

常见的限流方式,比如Hystrix是用线程池隔离,超过线程池的负载,走熔断的逻辑。在一般应用服务器中,比如tomcat容器也是通过限制它的线程数来控制并发的;也有通过时间窗口的平均速度来控制流量。常见的限流纬度有比如通过Ip来限流、通过uri来限流、通过用户访问频次来限流。

一般限流都是在网关这一层做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也可以在应用层通过Aop这种方式去做限流。

常见的限流算法

计数器算法

计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”

漏桶算法

漏桶算法为了消除”突刺现象”,可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。

img

在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。

这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。

令牌桶算法

从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。

img

Spring Cloud Gateway限流

在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用在Redis内的通过执行Lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:

img

引入POM依赖

Spring Cloud Gateway本身集成了限流操作,Gateway限流需要使用Redis,pom文件中添加Redis依赖

<dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> 
配置文件配置
spring:   redis:     host: 192.168.64.128     port: 6379     database: 0   cloud:     gateway:       routes:         - id: order-server           uri: http://localhost:8083/           predicates:             - Path=/order/**           filters:           	#限流过滤器,该过滤器需要配置三个参数.name必须是RequestRateLimiter。             - name: RequestRateLimiter               args:                 #用于限流的解析器的Bean对象的名字。它使用SpEL表达式#{@beanName}从Spring容器中获取bean对象。                 key-resolver: #{@hostKeyResolver}                 #令牌通每秒填充平均速率                 redis-rate-limiter.replenishRate: 1                 #令牌桶的总容量                 redis-rate-limiter.burstCapacity: 3 
key-resolver实现

Key-resolver参数后面的bean需要自己实现,然后注入到Spring容器中。

用户ID限流

这里根据用户ID限流,请求路径中必须携带userId参数

@Bean public KeyResolver userKeyResolver() {     return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst(user)); } 

KeyResolver需要实现resolve方法,比如根据userid进行限流,则需要用userid去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。

根据IP限流

如果需要根据IP限流,定义的获取限流Key的bean为:

@Bean public KeyResolver hostKeyResolver() {     return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } 
根据Path限流

还可以根据请求路径进行限流

@Bean public KeyResolver apiKeyResolver() {     return exchange -> Mono.just(exchange.getRequest().getPath().value()); }