SpringCloud-Gateway网关

在微服务架构中,每个服务都是一个可以独立开发和运行的组件,而一个完整的微服务架构由一系列独 立运行的微服务组成。其中每个服务都只会完成特定领域的功能,比如订单服务提供与订单业务场景有 关的功能、商品服务提供商品展示功能等。各个微服务之间通过轻量级通信机制 REST API 或者 RPC 完 成通信。 微服务之后在某些层面会带来一定的影响,比如,一个用户查看一个商品的详情,对于客户端 来说,可能需要调用商品服务、评论服务、库存服务、营销服务等多个服务来完成数据的渲染

在这个场景中,客户端虽然能通过调用多个服务实现数据的获取,但是会存在一 些问题,比如:

  • 客户端需要发起多次请求,增加了网络通信的成本及客户端处理的复杂性。
  • 服务的鉴权会分布在每个微服务中处理,客户端对于每个服务的调用都需要重复鉴权。
  • 在后端的微服务架构中,可能不同的服务采用的协议不同,比如有 HTTP、RPC 等。客户端如果需要调用多个服务,需要对不同协议进行适配

所以,我们可以在微服务之前增加一个前置节点,这个节点就是网关,

什么是网关呢? 大家都知道,从一个房间走到另一个房间,必然要经过一扇门,同样,从一个网络向另一个网络发送信息,也必须经过一道关口,顾名思义,网关就是一个网络连接到另一个网络的“关口”。 也就是网络。

而在微服务架构中,它不仅仅只是一个网络互连的一个关口,还有更多的作用,以前面分析的这个场景 为例,增加网关之后。

对于商品详情展示的场景来说,增加了 API 网关之后,在 API 网关层可以把后端的多个服务进行整合,然后提供一个唯一的业务接口,客户端只需要调用这个接口即可完成数据的获取及展示。在网关中会再去消费后端的多个微服务进行统一的整合,给客户端返回一个唯一的响应。

当然,网关不仅只是做一个请求转发以及服务整合,有了网关这个统一的入口之后,它还能提供

  • 针对所有请求进行统一鉴权、限流、熔断、日志。
  • 协议转化。针对后端多种不同的协议,在网关层统一处理后以 HTTP 协议对外提供服务。

  • 用过 Dubbo 框架应该知道,针对 Dubbo 服务还需要提供一个 Web 应用来进行协议 转 化。

  • 统一错误码处理。

  • 请求转发,并且可以基于网关实现内外网隔离

1. 网关的作用以及要求

作用:

  1. 性能:API高可用,负载均衡,容错机制。
  2. 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。
  3. 日志:日志记录(spainid,traceid)一旦涉及分布式,全链路跟踪必不可少。 缓存:数据缓存。

  4. 监控:记录请求响应数据,api耗时分析,性能监控。 限流:流量控制,错峰流控,可以定义多种限流规则。

  5. 灰度:线上灰度部署,可以减小风险。

  6. 路由:动态路由规则。

要求:

网关成了所有流量的入口,那么对于这样一个角色来说,它需要在某些方面 有很高的要求。

  • 稳定性,

  • 安全性,防止恶意请求,以及保障数据传输的安全性

  • 高性能、可用性,

    • 网关作为所有流量的入口,那么对于性能这块的要求就非常高了,因为一旦网关的性能出现 瓶颈,几遍后端的服务性能再高,意义也不大
    • 网关必须要支持集群部署,这个是分布式架构的基本要求。否则网关服务挂掉就会导致整个 系统不可用
  • 扩展性,可维护性,对于定制化需求方面,如何实现可扩展;

2. 常见的网关解决方案以及选型

  1. OpenResty(Nginx+lua)
  2. Kong,是基于openresty之上的一个封装,提供了更简单的配置方式。 它还提供了付费的商业插 件

  3. Tyk(开源、轻量级),Tyk 是一个开源的、轻量级的、快速可伸缩的 API 网关,支持配额和速度 限制,支持认证和数据分析,支持多用户多组织,提供全 RESTful API。它是基于go语言开发的组 件。

  4. Zuul,是spring cloud生态下提供的一个网关服务,性能相对来说不是很高

  5. Spring Cloud Gateway,是Spring团队开发的高性能网关

网关选型主要关注几个方面

  • 部署和维护成本

  • 开源还是闭源

  • 是否私有化部署

  • 功能是否满足当前需求

  • 社区资料的完善以及版本迭代和功能维护

3. Spring Cloud Gateway的核心概念

  1. Route 路由,它是网关的基础元素,包含ID、目标URI、断言、过滤器组成,当前请求到达网关 时,会通过Gateway Handler Mapping,基于断言进行路由匹配,当断言为true时,匹配到路由 进行转发
  2. Predicate,断言,学过java8的同学应该知道这个函数,它可以允许开发人员去匹配HTTP请求中 的元素,一旦匹配为true,则表示匹配到合适的路由进行转发
  3. Filter,过滤器,可以在请求发出的前后进行一些业务上的处理,比如授权、埋点、限流等。

它的整体工作原理如下。 其中,predicate就是我们的匹配条件;而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。

客户端向 Spring Cloud Gateway 发出请求,如果请求与网关程序定义的路由匹配,则该请求就会被发送到网关 Web 处理程序,此时处理程序运行特定的请求过滤器链。

过滤器之间用虚线分开的原因是过滤器可能会在发送代理请求的前后执行逻辑。所有 pre 过滤器逻辑先 执行,然后执行代理请求;代理请求完成后,执行 post 过滤器逻辑

gateway执行流程图(来源官网)

路由规则示例:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: lb_route # 指定id,用户自定义
pridicates:
- Path=/lb/** # 断言,请求网关的请求uri要满足此要求,还有其他断言类型
filters:
- StripPrefix=1 # 过滤器,跳过url第一个前缀
uri: lb://order-service # 目标uri,lb是负载均衡协议,需要配置注册中心客户端

3.1 断言

gateway为我们定义了14种断言工厂,在包org.springframework.cloud.gateway.handler.predicate中。

示例中配置的断言Path=/lb/**实际用到的是PathRoutePredicateFactory工厂类,配置时只需要使用工厂类的前缀path。

  • AfterRoutePredicateFactory 接收一个时间参数,当前时间在此时间之后,返回true
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- After=2017-01-20T17:42:47.789-07:
  • BeforeRoutePredicateFactory 接收一个时间参数,当前时间在此时间之前,返回true
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: before_route
uri: https://example.org
predicates:
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]
  • BetweenRoutePredicateFactory 接收两个时间参数,当前时间在两时间之内,返回true
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: between_route
uri: https://example.org
predicates:
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

此路由匹配在2017年1月20日17点42分(丹佛)之后和2017年1月21日17点42分(丹佛)之前发出的请求。这对于维护窗口非常有用。

  • CookieRoutePredicateFactory 工厂接受两个参数,Cookie名称和regexp(一个Java正则表达式)。此断言匹配具有给定名称且其值与正则表达式匹配的cookie。下面的示例配置了一个cookie路由谓词工厂:
1
2
3
4
5
6
7
8
spring:  
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=chocolate, ch.p
  • HeaderRoutePredicateFactory 消息头路由工厂接受两个参数,消息头名称和regexp(一个Java正则表达式)。此谓词与具有给定名称的头匹配,该头的值与正则表达式匹配。下面的示例配置了一个头路由谓词:
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+
  • HostRoutePredicateFactory主机路由工厂接受一个参数:主机名模式列表。该模式是一个ant样式的模式。作为分隔符。此谓词匹配与模式匹配的主机头。下面的示例配置了一个主机路由谓词:
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://example.org
predicates:
- Host=**.somehost.org,**.anotherhost.org
  • MethodRoutePredicateFactory 方法路由工厂接受一个方法参数,该参数是一个或多个参数:要匹配的HTTP方法。下面的示例配置了一个方法路由谓词:
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: method_route
uri: https://example.org
predicates:
- Method=GET,POST
  • PathRoutePredicateFactory 路径路由工厂接受两个参数:Spring PathMatcher模式列表和一个称为matchOptionalTrailingSeparator的可选标志。下面的示例配置了一个路径路由谓词:
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: path_route
uri: https://example.org
predicates:
- Path=/red/{segment},/
  • QueryRoutePredicateFactory 查询路由工厂接受两个参数:一个必需的param和一个可选的regexp(一个Java正则表达式)。下面的示例配置了一个查询路由谓词:
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://example.org
predicates:
- Query=green

如果请求包含green查询参数,则匹配成功。

  • RemoteAddrRoutePredicateFactory 匹配请求源的ip地址
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: https://example.org
predicates:
- RemoteAddr=192.168.1.1/24
  • WeightRoutePredicateFactory 权重路由工厂有两个参数:group和weight(int)。权重按组计算。下面的示例配置了一个权重路由谓词:
1
2
3
4
5
6
7
8
9
10
11
12
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

这路由配置将把~80%的流量转发到weighthigh.org,并将~20%的流量转发到weighlow.org

注意:一个router中可以配置多个predicate,他们之间是&关系,即所有的条件都满足才会进入filter

自定义断言

继承AbstractRoutePredicateFactory

3.2 过滤器

3.2.1全局过滤器

自定义全局过滤器:

1
implements GlobalFilter
3.2.2 过滤器工厂

gateway为我们定义了30种过滤器工厂,在包org.springframework.cloud.gateway.filter.factory中。

示例中配置的StripPrefix=1实际使用的是StripPrefixGatewayFilterFactory工厂,配置时只需要使用工厂类的前缀StripPrefix

  • AddRequestHeaderGatewayFilterFactory
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue

该过滤器向请求头中添加参数 X-Request-red = blue ,参数的值可以动态指定:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
predicates:
- Path=/red/{segment} # 请求uri中传入segment
filters:
- AddRequestHeader=X-Request-Red, Blue-{segment} #将segment添加到请求头中
  • AddRequestParameterGatewayFilterFactory 添加请求参数
  • AddResponseHeaderGatewayFilterFactory 添加响应头

  • RemoveRequestHeaderGatewayFilterFactory 移除

  • RemoveRequestParameterGatewayFilterFactory 移除
  • RemoveResponseHeaderGatewayFilterFactory 移除
  • SetRequestHeaderGatewayFilterFactory 修改
  • SetResponseHeaderGatewayFilterFactory 修改

  • DedupeResponseHeaderGatewayFilterFactory

DedupeResponseHeader网关过滤器工厂接受一个name参数和一个可选的策略参数。name可以包含一个以空格分隔的标题名称列表。下面的示例配置了DedupeResponseHeader网关过滤器:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: dedupe_response_header_route
uri: https://example.org
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

当网关CORS逻辑和下游逻辑都添加Access-Control-Allow-Credentials和访Access-Control-Allow-Origin响应头时,这将删除它们的重复值。

DedupeResponseHeader过滤器还接受一个可选的策略参数。接受的值是RETAIN_FIRST(默认)、RETAIN_LASTRETAIN_UNIQUE

  • HystrixGatewayFilterFactory

该工厂需要一个name参数,它是HystrixCommand的名称。以下示例配置HystrixGatewayFilter

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: hystrix_route
uri: https://example.org
filters:
- Hystrix=myCommandName

这会将其余过滤器(配置在它之后的过滤器)包装HystrixCommand,并命名为myCommandName

Hystrix过滤器也可以接受可选fallbackUri参数。当前,仅支持forward:格式的URI。

如果调用了fallback,则请求将转发到与URI匹配的控制器。以下示例配置了这种后备:

1
2
3
4
5
6
7
8
9
10
- id: hystrix_route
uri: lb://backing-service:8088
predicates:
- Path=/consumingserviceendpoint
filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/incaseoffailureusethis
- RewritePath=/consumingservic

调用Hystrix fallback时将转发到URI/incaseoffailureusethis。请注意,此示例还演示了(可选)Spring Cloud Netflix Ribbon负载平衡(lb在目标URI上定义了前缀)。

主要方案是通过fallbackUri跳转到网关应用程序中的内部控制器或处理程序。但是,您还可以将请求重新路由到外部应用程序中的控制器或处理程序,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: Hystrix
args:
name: fetchIngredients
fallbackUri: forward:/fallback
- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback

在此示例中,网管服务中没有定义fallback处理程序,而是将fallback转发到了http://localhost:9994上

万一请求转发给fallback,则Hystrix网关过滤器还会提供引起fallback的Throwable详细信息。它作为ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR属性添加到ServerWebExchange中,可以在处理网关应用程序中的fallback时使用该属性。

对于外部控制器/处理程序方案,您可以添加带有异常详细信息的标头。详见下面的FallbackHeadersGatewayFilterFactory介绍

您可以使用使用全局默认值或逐条路由配置Hystrix设置(例如超时)来hystrix的配置。

要为前面显示的示例路由设置五秒钟的超时时间,可以使用以下配置:

1
hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 5000
  • FallbackHeadersGatewayFilterFactory

通过FallbackHeaders工厂,可以将Hystrix或Spring Cloud CircuitBreaker执行异常的详细信息添加到fallbackUri指定请求头中,如以下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
cloud:
gateway:
routes:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: CircuitBreaker
args:
name: fetchIngredients
fallbackUri: forward:/fallback # 发生熔断转发到 /fallback

- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback # fallback 路由到http://localhost:9994,且转发请求投中添加熔断详细信息
filters:
- name: FallbackHeaders
args:
executionExceptionTypeHeaderName: Test-Header

在此示例中,在运行断路器时发生执行异常后,该请求将转发到在上fallback运行的应用程序中的端点或处理程序localhost:9994FallbackHeaders过滤器会将具有异常类型,消息和(如果有)根本原因异常类型和消息的标头添加到该请求。

您可以通过设置以下参数的值(以其默认值显示)来覆盖配置中标头的名称:

executionExceptionTypeHeaderName"Execution-Exception-Type"

executionExceptionMessageHeaderName"Execution-Exception-Message"

rootCauseExceptionTypeHeaderName"Root-Cause-Exception-Type"

rootCauseExceptionMessageHeaderName"Root-Cause-Exception-Message"

  • SpringCloudCircuitBreakerFilterFactory
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: https://example.org
filters:
- CircuitBreaker=myCircuitBreaker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: lb://backing-service:8088
predicates:
- Path=/consumingServiceEndpoint
filters:
- name: CircuitBreaker
args:
name: myCircuitBreaker
fallbackUri: forward:/inCaseOfFailureUseThis
- RewritePath=/consumingServiceEndpoint, /backingServiceEndpoint
1
2
3
4
5
6
7
8
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint")
.filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis"))
.rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088")
.build();
}
  • MapRequestHeaderGatewayFilterFactory
  • PrefixPathGatewayFilterFactory 对网关请求的uri添加前缀
1
2
3
4
5
6
7
8
pring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- PrefixPath=/mypath
  • PreserveHostHeaderGatewayFilterFactory 此过滤器设置一个请求属性,对该属性进行检查,以确定是否应该发送原始host 头,而不是由HTTP客户机确定的host头
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: preserve_host_route
uri: https://example.org
filters:
- PreserveHostHeader

Host 是 HTTP 1.1 协议中新增的一个请求头,主要用来实现虚拟主机技术。一台主机内使用端口号区分不同的应用进程,在一台部署了很多虚拟机的主机上,使用host头来决定请求发往哪一台虚拟机。

举个栗子,有一台 ip 地址为 61.135.169.125 的服务器,在这台服务器上使用虚拟主机部署着谷歌、百度、淘宝的网站。为什么我们访问 https://www.google.com 时,看到的是 Google 的首页而不是百度或者淘宝的首页?原因就是 Host 请求头决定着访问哪个虚拟主机。

  • RedirectToGatewayFilterFactory

  • RequestHeaderSizeGatewayFilterFactory

  • RequestHeaderToRequestUriGatewayFilterFactory

  • RequestRateLimiterGatewayFilterFactory 该过滤器使用一个RateLimiter的实现来决定当前的请求是否能发送出去,如果不能访问,返回429(too many request)

    该过滤器有一个可选的参数keyResolver用于指定RateLimiter限制的内容,默认是用户名,即一个用户在一定的时间内访问次数是受限制的,可以自定义keyResolver,例如设置成IP地址,用keyResolver: '#{@ipAddressKeyResolver}'的方式配置进去,@符号后面是beanName

1
2
3
4
5
6
7
8
9
10
11
12
- id: ratelimiter_route
predicates:
- Path=/ratelimiter/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
deny-empty-key: true
keyResolver: '#{@ipAddressKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 2
uri: lb://order-service

基于redis的流量限制策略:

参数说明:

  1. redis-rate-limiter.replenishRate希望用户每秒可以执行多少请求,而不需要丢弃任何请求。这是令牌桶被填充的速率。
  2. redis-rate-limiter.burstCapacity一个用户在一秒内允许执行的最大请求数。这是令牌桶可以容纳的令牌的数量。将此值设置为零将阻止所有请求。
  3. redis-rate-limiter.requestedTokens一个请求需要多少开销。这是每个请求从桶中取出的令牌的数量,默认值为1。
  • RequestSizeGatewayFilterFactory
  • RetryGatewayFilterFactory
  • RewriteLocationResponseHeaderGatewayFilterFactory
  • RewritePathGatewayFilterFactory
  • RewriteResponseHeaderGatewayFilterFactory
  • SaveSessionGatewayFilterFactory
  • SecureHeadersGatewayFilterFactory
  • SetPathGatewayFilterFactory
  • SetStatusGatewayFilterFactory
  • SpringCloudCircuitBreakerHystrixFilterFactory
  • SpringCloudCircuitBreakerResilience4JFilterFactory
  • StripPrefixGatewayFilterFactory
3.2.3 自定义过滤器工厂

自定义过滤器

4. 高级用法

4.1 负载均衡

网关需要接入注册中心,路由配置中的uri采用 lb:// 协议,框架会使用ribbon实现负载均衡。

4.3 限流

基于redis,使用方式参考官网。

  • 引入redis的jar包

  • 配置redis服务器信息

  • 配置路由:

1
2
3
4
5
6
7
8
9
10
11
12
- id: redis_rate_limiter
predicates:
- Path=/ratelimiter/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
keyResolver: '#{@ipAddressKeyResolver}'
deny-empty-key: true
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 2
uri: lb://order-service

这里使用自定义的KeyResolver,这里的自定义就需要根据应用场景进行自定义了,比如想要配置基于IP地址的限流策略,那么就需要使用Ip地址作为key

RequestRateLimiter 使用令牌桶算法,

配置的参数 replenishRate 表示:令牌生成速度,也就是每秒允许请求的次数。

burstCapacity 表示: 令牌桶的容量。

4.4 动态路由

添加actuator组件

spring gateway 提供了了一个endpoint用于动态路由的实现

  • 使用GET 请求 actuator/gateway/routes 可以查看已配置的路由信息

  • 使用POST请求 actuator/gateway/routes/{router_id} 动态添加路由信息

  • 使用DELETE请求 actuator/gateway/routes/{router_id} 动态删除

修改完路由表 需要调用 actuator/gateway/refresh 刷新使之生效

默认动态路由只在应用运行期间存在,重启就消失

但是这里可以扩展:

默认动态信息是添加到内存中,我我们只要扩展RouteDifinitionRepository接口进行配置,就可以将动态的路由信息持久化。

将自定义的RouteDifinitionRepository注入到spring容器中,然后重启网关,该配置就生效了