Spring Cloud Zookeeper微服务集群实例之三-网关引入及熔断与限流

在之前的文章中我们实现了服务之间的接口调用,那么集群外部的接口调用如何进行?这就必须通过网关了。网关类似其它节点一样,会将其自身注册到集群中,从而能够获取到某个服务的实例清单;然后根据我们提前配置好的规则(如按路径分发等,类似nginx),将外部过来的请求分发到对应的节点上去执行。

总的来说,网关包含有以下三大功能:

  • 路由转发:即将请求分发到合适的服务中去执行;
  • 负载均衡:类似集群内部调用负载均衡,根据配置的算法及服务的实例清单进行负载均衡处理;
  • 权限控制:可以解析登录用户信息,同时根据路径及预先配置好的规则判断用户是否有权限访问;
  • 限流:可以通过Filter过滤掉超出流量的请求,将其直接返回;
  • 熔断:调用出现异常时一段时间内减少调用对应的接口,或者全部拦截调用;

另外,我们还可以在网关层根据token信息解析出用户信息,后续集群内的接口调用都使用解析出来的用户信息进行权限方面的限制,从而使得集群内部服务能够省略权限解析的步骤,专注于核心业务逻辑的实现。

老版本的Spring Cloud中使用的是Zuul,现在我们可以使用官方的Spring Cloud Gateway来充当网关了。

1 创建网关应用

1.1 maven依赖



 ? ?4.0.0

 ? ?org.example
 ? ?gateway
 ? ?1.0-SNAPSHOT

 ? ?
 ? ? ? ?11
 ? ? ? ?11
 ? ? ? ?2.4.5
 ? ? ? ?2020.0.2
 ? ?

 ? ?
 ? ? ? ?org.springframework.boot
 ? ? ? ?spring-boot-starter-parent
 ? ? ? ?${spring.boot.version}
 ? ? ? ? 
 ? ?

 ? ?
 ? ? ? ?
 ? ? ? ? ? ?org.springframework.cloud
 ? ? ? ? ? ?spring-cloud-starter-gateway
 ? ? ? ?
 ? ? ? ?
 ? ? ? ? ? ?org.springframework.cloud
 ? ? ? ? ? ?spring-cloud-starter-zookeeper-discovery
 ? ? ? ?
 ? ?
 ? ?
 ? ? ? ?
 ? ? ? ? ? ?
 ? ? ? ? ? ? ? ?org.springframework.cloud
 ? ? ? ? ? ? ? ?spring-cloud-dependencies
 ? ? ? ? ? ? ? ?${spring-cloud.version}
 ? ? ? ? ? ? ? ?pom
 ? ? ? ? ? ? ? ?import
 ? ? ? ? ? ?
 ? ? ? ?
 ? ?
 ? ?
 ? ? ? ?
 ? ? ? ? ? ?
 ? ? ? ? ? ? ? ?org.springframework.boot
 ? ? ? ? ? ? ? ?spring-boot-maven-plugin
 ? ? ? ? ? ?
 ? ? ? ?
 ? ?

1.2 Bootstrap配置

增加bootstrap.yml文件:

spring:
  application:
 ?  name: gateway
  cloud:
 ?  gateway:
 ? ?  discovery:
 ? ? ?  locator:
 ? ? ? ?  enabled: true # 启用后,默认会通过/{serviceName}/**这样的请求,将其分发到serviceName的实例上去处理;

server:
  port: 8000

上面enabled设置成true后,会启用注册将网关注册到注册中心去,同时有请求过来时,会取host后的第一串字符当前服务名,在注册中心查找这个服务对应的实例,然后转发到对应的节点上去(这一步也会有负载均衡的动作);注意分发过去的请求会自动将{serviceName}这一串给干掉。

举个例子,我们前面启动了一个service0的应用,上面有一个/test的接口;按网关的这个配置,如果我们访问地址:
http://localhost:8000/service0/test,那么就会调用到service0服务上的test接口去。

1.3 main方法

@SpringBootApplication
public class Application {
 ? ?public static void main(String[] args) {
 ? ? ? ?SpringApplication.run(Application.class, args);
 ?  }
}

1.4 启动与测试

启动main函数,然后我们就可以通过网关来调用service0上的test接口了,访问地址:
http://localhost:8000/service0/test,不出意外可以调用成功。

2 网关处理流程

网关包含有三个关键的属性:

  • Route:即路由,包括id、目标地址、Predicate列表及Filter列表;
  • Predicate:用于判断其所在的Route能够用于哪些请求,如根据请求参数的断定或者根据请求路径进行断定等;
  • Filter:用于在调用实际服务时增加前置或后置处理;

网关的处理过程如下图所示:

客户端请求网关,网关通过Gateway Handler Mapping查找命中的Route,然后通过Web Handler进行调用,同时会在调用前后执行所配置的拦截器。

2 熔断

我们可以通过Spring Cloud Gateway来进行熔断。熔断通过网关的Filter进行。

一般会结合resilience4j来处理。

我们结合上方的示例来进行配置。

2.1 添加依赖

需要先在gateway中添加
spring-cloud-starter-circuitbreaker-reactor-resilience4j的依赖:


 ? ? ? ?
 ? ? ? ? ? ?org.springframework.cloud
 ? ? ? ? ? ?spring-cloud-starter-circuitbreaker-reactor-resilience4j
 ? ? ? ?

2.2 修改gateway配置

在bootstrap中配置:

spring:
  application:
 ?  name: gateway
  cloud:
 ?  gateway:
# ? ?  discovery:
# ? ? ?  locator:
# ? ? ? ?  enabled: true # 启用后,默认会通过/{serviceName}/**这样的请求,将其分发到serviceName的实例上去处理;但启用这种方式后,在routes中配置的就不生效了 ;
 ? ?  routes:
 ? ? ?  - id: circuitbreaker_route # 配置熔断
 ? ? ? ?  uri: lb://service0
 ? ? ? ?  filters:
 ? ? ? ? ?  - name: CircuitBreaker
 ? ? ? ? ? ?  args:
 ? ? ? ? ? ? ?  name: backendA
 ? ? ? ? ? ? ?  statusCodes:
 ? ? ? ? ? ? ? ?  - 500 ? # 必须配置,如果不配置的话不会进行熔断
# ? ? ? ? ? ? ?  fallbackUri: forward:/test # 失败后执行的请求
 ? ? ? ? ?  - StripPrefix=1
 ? ? ? ?  predicates:
 ? ? ? ? ?  - Path=/service0/**

resilience4j.circuitbreaker:
  configs:
 ?  default:
 ? ?  slidingWindowSize: 10
 ? ?  minimumNumberOfCalls: 5 # 计算错误率的最小访问次数,超过这个次数后才会计算错误率并判断是否要进行限流、熔断(但实际不会到这个值,比如配置容忍错误率为50%,配置当前值为10,那么如果前5个都失败就会直接熔断
 ? ?  permittedNumberOfCallsInHalfOpenState: 3 ?# 半开状态允许的请求数
 ? ?  automaticTransitionFromOpenToHalfOpenEnabled: true # 是否在没有请求的情况下由全开自动恢复到半开,设置成true时需要额外的线程进行监控
 ? ?  waitDurationInOpenState: 2s # 由全开到半开等待的时间
 ? ?  failureRateThreshold: 50 ?# 熔断器打开的失败阈值,也即超过指定比例时熔断器将被打开
 ? ?  eventConsumerBufferSize: 10
  instances:
 ?  backendA:
 ? ?  baseConfig: default

server:
  port: 8000

management:
  endpoints:
 ?  web:
 ? ?  exposure:
 ? ? ?  include: gateway

重启gateway;

2.3 添加测试接口

然后在service0应用的TestController中添加以下方法:

 ? ?@GetMapping("exception")
 ? ?public void testException() throws Exception {
 ? ? ? ?throw new Exception("测试异常");
 ?  }

在这里面我们直接模拟服务调用异常,因此直接抛出了一个异常;重启Service0;

2.4 测试

通过网关来调用service0中的/test/exception接口,访问地址:
http://localhost:8000/service0/test/exception,连续三次访问的时候都会访问到service0上,但第四次返回的异常将会是[904f51ba-10] There was an unexpected error (type=Service Unavailable, status=503)了,service0未收到请求,在网关侧就已经进行了拦截。

然后等待几秒再访问,又会调用到service0上;访问几次时又会被熔断。

3 限流

限流通过RequestRateLimiter类型的拦截器进行。可以使用Redis实现;使用Redis时需要本地启动的Redis,或者在bootstrap.yml中配置Redis地址;

3.1 Maven依赖


 ? ? ? ?
 ? ? ? ? ? ?org.springframework.boot
 ? ? ? ? ? ?spring-boot-starter-data-redis-reactive
 ? ? ? ?

3.2 配置限流参数

spring:
  application:
 ?  name: gateway
  cloud:
 ?  gateway:
# ? ?  discovery:
# ? ? ?  locator:
# ? ? ? ?  enabled: true # 启用后,默认会通过/{serviceName}/**这样的请求,将其分发到serviceName的实例上去处理;但启用这种方式后,在routes中配置的就不生效了 ;
 ? ?  routes:
 ? ? ?  - id: circuitbreaker_route # 配置熔断限流
 ? ? ? ?  uri: lb://service0
 ? ? ? ?  predicates:
 ? ? ? ? ?  - Path=/service0/**
 ? ? ? ?  filters:
 ? ? ? ? ?  - name: CircuitBreaker ?# 熔断
 ? ? ? ? ? ?  args:
 ? ? ? ? ? ? ?  name: backendA
 ? ? ? ? ? ? ?  statusCodes:
 ? ? ? ? ? ? ? ?  - 500 ? # 必须配置,如果不配置的话上述的配置不生效
# ? ? ? ? ? ? ?  fallbackUri: forward:/test # 失败后执行的请求
 ? ? ? ? ?  - StripPrefix=1
 ? ? ? ? ?  - name: RequestRateLimiter # 限流
 ? ? ? ? ? ?  args:
 ? ? ? ? ? ? ?  redis-rate-limiter.replenishRate: 1 # 令牌生成速度,每s生成多少个
 ? ? ? ? ? ? ?  redis-rate-limiter.burstCapacity: 2 # 令牌桶容量
 ? ? ? ? ? ? ?  redis-rate-limiter.requestedTokens: 1 # 每个请求消耗的令牌数

3.3 配置KeyResolver

需要配置KeyResolver以便进行限流,如果未配置的话,访问接口将会返回403异常。

配置方式如下:

 ? ?/**
 ? ? * 配置限流KeyResolver
 ? ? */
 ? ?@Bean
 ? ?KeyResolver userKeyResolver() {
 ? ? ? ?return exchange -> Mono.just("normal");
 ?  }

我这里是针对所有的请求使用一个限流策略;如果需要针对不同请求限制不同策略,需要修改这个KeyResolver,如根据查询参数中的值做分组限流:

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

注意如果未配置KeyResolver,限流将不会生效。

3.4 启动与测试

重启gateway,然后通过gateway调用service0的接口test:
http://localhost:8000/service0/test,根据我们的限流配置,一秒内第一次请求会成功,第二次有可能成功(如果桶中已经生成了2个令牌就会成功),第三次之后就会失败。后一秒又可以继续请求。

失败时会报:429 Too Many Request异常,成功实现限流。

4 转发规则配置

我们还可以通过routes配置一些转发规则,如:

spring:
  cloud:
 ?  gateway:
 ? ?  routes:
 ? ?  - id: before_route
 ? ? ?  uri: https://example.org
 ? ? ?  predicates:
 ? ? ?  - Before=2017-01-20T17:42:47.789-07:00[America/Denver]

意思是在指定时间前的请求转发到对应的uri上;

Spring Cloud Gateway支持很多种转发规则,其中节选比较有用的列表如下:

  • 根据权重转发即根据配置的权重决定往每个节点分发的请求比例。如:
spring:
  cloud:
 ?  gateway:
 ? ?  routes:
 ? ?  - id: weight_high
 ? ? ?  uri: https://weighthigh.org
 ? ? ?  predicates:
 ? ? ?  - Weight=group1, 8
 ? ?  - id: weight_low
 ? ? ?  uri: https://weightlow.org
 ? ? ?  predicates:
 ? ? ?  - Weight=group1, 2

即往weighthigh上分发80%的流量 ,剩下的往wieghtlow上分发。

比如我们想要做灰度测试,这种配置方式就非常有用。

另外,Spring Cloud Gateway还包含以下转发规则:

  • 指定时间之后;
  • 指定时间之前;
  • 指定时间区间;
  • 根据Cookie值转发;
  • 根据Header值转发;
  • 根据Host值转发;
  • 根据Method(GET、POST)进行转发;
  • 根据Path进行转发;
  • 根据查询条件进行转发;
  • 根据远程地址进行转发;

具体可以参考官方文档,在此不再展开。

5 拦截器配置

5.1 拦截器配置

上面我们在限流与熔断中我们已经配置过拦截器了,再来看一个示例:

spring:
  cloud:
 ?  gateway:
 ? ?  routes:
 ? ?  - id: add_request_header_route
 ? ? ?  uri: https://example.org
 ? ? ?  filters:
 ? ? ?  - AddRequestHeader=X-Request-red, blue

上面通过AddRequestHeader拦截器,在转发的请求报文头中添加了名称为X-Request-red的头,其值为blue;

5.2 通用拦截器配置

我们也可以通过default-filters来做通用的配置:

spring:
  cloud:
 ?  gateway:
 ? ?  default-filters:
 ? ?  - AddResponseHeader=X-Response-Default-Red, Default-Blue
 ? ?  - PrefixPath=/httpbin

我们可以使用这种默认的配置来优化我们之前的配置文件,看优化前的:

spring:
  application:
 ?  name: gateway
  cloud:
 ?  gateway:
# ? ?  discovery:
# ? ? ?  locator:
# ? ? ? ?  enabled: true # 启用后,默认会通过/{serviceName}/**这样的请求,将其分发到serviceName的实例上去处理;但启用这种方式后,在routes中配置的就不生效了 ;
 ? ?  routes:
 ? ? ?  - id: circuitbreaker_route # 配置熔断限流
 ? ? ? ?  uri: lb://service0
 ? ? ? ?  predicates:
 ? ? ? ? ?  - Path=/service0/**
 ? ? ? ?  filters:
 ? ? ? ? ?  - name: CircuitBreaker ?# 熔断
 ? ? ? ? ? ?  args:
 ? ? ? ? ? ? ?  name: backendA
 ? ? ? ? ? ? ?  statusCodes:
 ? ? ? ? ? ? ? ?  - 500 ? # 必须配置,如果不配置的话上述的配置不生效
# ? ? ? ? ? ? ?  fallbackUri: forward:/test # 失败后执行的请求
 ? ? ? ? ?  - StripPrefix=1
 ? ? ? ? ?  - name: RequestRateLimiter # 限流
 ? ? ? ? ? ?  args:
 ? ? ? ? ? ? ?  redis-rate-limiter.replenishRate: 1 # 令牌生成速度,每s生成多少个
 ? ? ? ? ? ? ?  redis-rate-limiter.burstCapacity: 2 # 令牌桶容量
 ? ? ? ? ? ? ?  redis-rate-limiter.requestedTokens: 1 # 每个请求消耗的令牌数

resilience4j.circuitbreaker:
  configs:
 ?  default:
 ? ?  slidingWindowSize: 10
 ? ?  minimumNumberOfCalls: 5 # 计算错误率的最小访问次数,超过这个次数后才会计算错误率并判断是否要进行限流、熔断(但实际不会到这个值,比如配置容忍错误率为50%,配置当前值为10,那么如果前5个都失败就会直接熔断
 ? ?  permittedNumberOfCallsInHalfOpenState: 3 ?# 半开状态允许的请求数
 ? ?  automaticTransitionFromOpenToHalfOpenEnabled: true # 是否在没有请求的情况下由全开自动恢复到半开,设置成true时需要额外的线程进行监控
 ? ?  waitDurationInOpenState: 2s # 由全开到半开等待的时间
 ? ?  failureRateThreshold: 50 ?# 熔断器打开的失败阈值,也即超过指定比例时熔断器将被打开
 ? ?  eventConsumerBufferSize: 10
  instances:
 ?  backendA:
 ? ?  baseConfig: default

server:
  port: 8000

management:
  endpoints:
 ?  web:
 ? ?  exposure:
 ? ? ?  include: gateway

如果有新的service,也需要修改这个配置文件,然后重启网关;这比较蛋痛,通过default-filters改造后,配置文件如下:

spring:
  application:
 ?  name: gateway
  cloud:
 ?  gateway:
 ? ?  discovery:
 ? ? ?  locator:
 ? ? ? ?  enabled: true # 启用后,默认会通过/{serviceName}/**这样的请求,将其分发到serviceName的实例上去处理;
 ? ?  default-filters: # 默认的拦截器,对所有请求都生效
 ? ? ?  - name: CircuitBreaker ?# 熔断
 ? ? ? ?  args:
 ? ? ? ? ?  name: backendA
 ? ? ? ? ?  statusCodes:
 ? ? ? ? ? ?  - 500 ? # 必须配置,如果不配置的话上述的配置不生效
 ? ? ? ? ? ?# ? ? ? ? ? ? ?  fallbackUri: forward:/test # 失败后执行的请求
 ? ? ?  - name: RequestRateLimiter # 限流
 ? ? ? ?  args:
 ? ? ? ? ?  redis-rate-limiter.replenishRate: 1 # 令牌生成速度,每s生成多少个
 ? ? ? ? ?  redis-rate-limiter.burstCapacity: 2 # 令牌桶容量
 ? ? ? ? ?  redis-rate-limiter.requestedTokens: 1 # 每个请求消耗的令牌数

resilience4j.circuitbreaker:
  configs:
 ?  default:
 ? ?  slidingWindowSize: 10
 ? ?  minimumNumberOfCalls: 5 # 计算错误率的最小访问次数,超过这个次数后才会计算错误率并判断是否要进行限流、熔断(但实际不会到这个值,比如配置容忍错误率为50%,配置当前值为10,那么如果前5个都失败就会直接熔断
 ? ?  permittedNumberOfCallsInHalfOpenState: 3 ?# 半开状态允许的请求数
 ? ?  automaticTransitionFromOpenToHalfOpenEnabled: true # 是否在没有请求的情况下由全开自动恢复到半开,设置成true时需要额外的线程进行监控
 ? ?  waitDurationInOpenState: 2s # 由全开到半开等待的时间
 ? ?  failureRateThreshold: 50 ?# 熔断器打开的失败阈值,也即超过指定比例时熔断器将被打开
 ? ?  eventConsumerBufferSize: 10
  instances:
 ?  backendA:
 ? ?  baseConfig: default

server:
  port: 8000

management:
  endpoints:
 ?  web:
 ? ?  exposure:
 ? ? ?  include: gateway

不需要再给每个服务进行配置了,添加服务的时候也不需要对网关做改动与重启。

5.3 内置拦截器

当前Spring Cloud Gateway内置以下拦截器:

  • AddRequestHeader:添加报文头
  • AddRequestParameter:添加请求参数
  • AddResponseHeader:添加返回报文头
  • DedupeResponseHeader:删除返回报文头
  • CircuitBreaker:熔断
  • FallbackHeaders:失败后调用fallbackUri地址时,并异常信息塞到请求头中;
  • MapRequestHeader:请求报文头中的报文名称转换
  • PrefixPath:为请求统一添加前缀
  • RequestRateLimit:限流
  • RedirectTo:转发
  • RemoveRequestHeader:删除请求报文头
  • RemoveResponseHeader:删除返回报文头
  • RemoveRequestParameter:删除请求参数
  • RewritePath:重定向
  • StripePrefix:自动移除路径前面的串,如指定为1时,请求路径为/test/call,那么处理后将会访问/call
  • Retry:重试
  • RequestSize:请求报文大小,默认为5M,如上传文件时超过5M,不修改此参数将会报错。
  • TokenReply:使用Spring Security OAuth2时,配置这个Filter会将前端访问传过来的Token转发给实际调用的服务;
  • ...

具体使用请参考官方文档。

6 自定义拦截器

在实际项目中,我们可能需要根据用户请求判断是否有权限访问对应的接口地址,用户与接口权限关系可能被后台管理动态的维护并且存储在MySql数据库中;此时我们可以在网关进行统一拦截,权限不足的请求直接返回前端401异常,实现如下,本实现省略了具体的登录用户信息获取及权限查询等步骤,仅做一个模拟,具体实现以业务需求为准。

6.1 定义拦截器

/**
 * @author LiuQi 2021/4/28-18:11
 * @version V1.0
 **/
@Component
public class PreGatewayFilterFactory extends AbstractGatewayFilterFactory {
 ? ?public PreGatewayFilterFactory() {
 ? ? ? ?super(Config.class);
 ?  }

 ? ?@Override
 ? ?public GatewayFilter apply(Config config) {
 ? ? ? ?return (exchange, chain) -> {
 ? ? ? ? ? ?// 模拟无权限,直接返回401 ?
 ? ? ? ? ? ?exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
 ? ? ? ? ? ?
 ? ? ? ? ? ?// 不再调用后续的拦截器
 ? ? ? ? ? ?return Mono.empty();
 ? ? ?  };
 ?  }

 ? ?public static class Config {
 ? ? ? ?//Put the configuration properties for your filter here
 ?  }
}

在这个拦截器中,我们将response的状态码设置成UNAUTHORIZED,并返回一个空的Mono;返回空的Mono会使得后续的Filter不会继续执行。

6.2 配置文件引用

然后在配置文件中引用这个Factory:

spring:
  application:
 ?  name: gateway
  cloud:
 ?  gateway:
 ? ?  discovery:
 ? ? ?  locator:
 ? ? ? ?  enabled: true # 启用后,默认会通过/{serviceName}/**这样的请求,将其分发到serviceName的实例上去处理;
 ? ?  default-filters: # 默认的拦截器,对所有请求都生效
 ? ? ?  - name: Pre

注意我们在配置文件中使用的名称是Pre,Spring Cloud Gateway将会自动在后面拼上GatewayFilterFactory然后去容器中找到对应的实例使用。也就是说,我们自定义的Factory其名称必须是以GatewayFilterFactory结尾,否则使用不了。

7 部署情况

引入网关后的集群部署情况:

这个时候我们的集群才真正成为了一个集群,能够真正的向外提供服务了。

但还是缺少一些关键东西,如整个集群的监控等。接下来的文章中将对这一内容进行讲解