spring cloud实践(六) gateway

什么是API网关

API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。

API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务。

常见API网关

  • Nginx
  • OpenResty
  • Kong
  • ZUUL
  • Spring Cloud Gateway

Spring Cloud Gateway

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

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

  • Filter(过滤器): 这是由工厂产出的(Spring Framework)GatewayFilter的实例,我们可以使用它修改请求和响应。

工作流程

spring_cloud_gateway_diagram

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

项目搭建

在一个Spring Boot项目中引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

配置

1
2
3
4
5
server:
port: 8020
spring:
application:
name: spring-cloud-gateway
路由规则

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

时间匹配
  • After Route Predicate Factory

在 application.yml 中配置:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://lyyljs.site
predicates:
- After=2019-04-01T00:00:00.000+08:00[Asia/Shanghai]

该断言指定只有在19-04-01以后的请求才转发到lyyljs.site。这里比较的时间是ZonedDateTime类。

  • Before Route Predicate Factory
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://lyyljs.site
predicates:
- Before=2019-04-01T00:00:00.000+08:00[Asia/Shanghai]

该配置指定在19-04-01之前的请求转发到指定网址。

  • Between Route Predicate Factory
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://lyyljs.site
predicates:
- Between=2019-04-01T00:00:00.000+08:00[Asia/Shanghai], 2019-05-01T00:00:00.000+08:00[Asia/Shanghai]

该断言指定在4月1日与5月1日之间的请求转发到指定地址。

通过cookie匹配

Cookie Route Predicate Factory接收两个参数, cookie name 和 正则表达式,路由规则会通过获取对应的 cookie 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://lyyljs.site
predicates:
- Cookie=chocolate, ch.p

该断言会匹配cookie名为chocolate,其值与所给正则ch.p匹配。
使用 curl http://localhost:8020 –cookie “chocolate=chap” 测试,拉去到页面代码则匹配上,去掉cookie则404。

通过Header匹配

同 Cookie Route Predicate Factory,Header Route Predicate Factory同样接收两个参数,name和对应value值的正则。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://lyyljs.site
predicates:
- Header=X-Request-Id, \d+

curl 进行测试 curl http://localhost:8020 -H “X-Request-Id:123”;返回页面代码则成功,去掉Header则返回404。

通过Host匹配

Host Route Predicate Factory 仅接收一个参数:一个host name 模式列表,这个模式是Ant风格并以 . 号作为分隔符。它使用此参数去匹配请求中的 Host header。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://lyyljs.site
predicates:
- Host=**.google.com,**.github.com

该断言匹配请求中是否包含Host Header 如www.google.com, translate.google.com等。

使用 curl http://localhost:8020 -H “Host: www.google.com"进行测试。

通过请求方式匹配

Method Route Predicate Factory 接收 HTTP 请求方式(GET,POST,PUT,DELETE…)作为参数。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://lyyljs.site
predicates:
- Method=POST

使用 curl -X POST http://localhost:8020 测试。

通过请求路径匹配

Path Route Predicate Factory 接收两个参数:一个路径匹配列表和一个可选的尾随分隔符。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://lyyljs.site
predicates:
- Path=/foo/{segment},/bar/{segment}

该路由匹配请求路径如/foo/1 , /foo/bar , /bar/baz。

URI模版参数如例子中的segment会存储起来以供后续Filter使用,可以通过以下代码获取该值。

1
2
3
Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange);

String segment = uriVariables.get("segment");
通过请求参数匹配

Query Route Predicate Factory 接收两个参数:param 和可选的其值正则。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://lyyljs.site
predicates:
- Query=baz

这条路由会匹配含参数baz的请求, 如 http://localhost:8020?baz=1

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://lyyljs.site
predicates:
- Query=foo, ba.

这条路由会匹配参数名为foo,其值匹配正则ba.的请求,如 http://localhost:8020?foo=bar

通过请求ip地址匹配

RemoteAddr Route Predicate Factory 接收一个CIDR地址的列表。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: http://lyyljs.site
predicates:
- RemoteAddr=192.168.1.1/24

这条路由会匹配来自 ip 为 192.168.1.1-254的请求。

GatewayFilter Factories

Gateway 内置了许多Filter。

由生命周期Filter 可分为两类,发出代理请求前的pre Filter 和 收到回应后的 post Filter。可参照前文工作流程图。

由应用范围Filter 可分为应用于单个或单组路由上的 GatewayFilter 和 应用于全局的 GlobalFilter。

截止2.1.0.release,Gateway 内置了 24 种Gateway Filter Factory 和 9 种Global Filter,详细可参见GatewayFilter FactoriesGlobal Filters

这里使用 RewritePath GatewayFilter Factory 来快速上手 Filter的使用。

RewritePath GatewayFilter Factory

RewritePath GatewayFilter Factory 接收一个路径的正则参数和一个替换的参数。这里使用Java正则来重写请求路径。

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: http://localhost:9001
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo/(?<segment>.*), /hello/$\{segment}

访问 http://localhost:8020/foo/lyyljs, 路径将重写为 http://localhost:9001/hello/lyyljs。

高可用

注册中心与负载均衡

在微服务集群中,不可能每次手动指定单个实例。我们需要从注册中心自动获取。

  • 添加依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • 添加配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
server:
port: 8020
register:
port1: 8000
port2: 8001
port3: 8002

eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.register.port1}/eureka/

spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: rewritepath_route
uri: lb://spring-cloud-consumer # 使用 lb://service 来提供负载均衡
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo/(?<segment>.*), /hello/$\{segment}

重启,

代理规则:/serviceId/(?.*),会转换为/${remaining}。如访问http://localhost:8020/SPRING-CLOUD-CONSUMER/hello/lyyljs会经过负载均衡访问spring-cloud-consumer服务的/hello/lyyljs。

在重写路径规则下,访问http://localhost:8020/foo/lyyljs可获得正常响应信息。

重试

使用 Retry GatewayFilter Factory 来进行重试。
参数列表

  • retries 重试次数
  • statuses 什么响应码时重试,参见 org.springframework.http.HttpStatus
  • methods 指定哪些方法的请求需要进行重试逻辑,参见 org.springframework.http.HttpMethod;默认 GET
  • series 哪列状态码才进行重试,参见 org.springframework.http.HttpStatus.Series;默认是 SERVER_ERROR,即状态码为5xx时。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: lb://spring-cloud-consumer
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo/(?<segment>.*), /hello/$\{segment}
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
熔断

Spring Cloud Gateway 同样可以通过使用 Hystrix 来进行熔断以及降级。

  • 添加依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  • 添加fallback方法
1
2
3
4
@RequestMapping("/fallback")
public Mono<String> fallback() {
return Mono.just("msg: fallback");
}
  • 添加配置

通过配置使用 Hystrix GatewayFilter Factory 来启用 Hystrix。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: fallbacktest
uri: lb://spring-cloud-consumer
predicates:
- Path=/sleep
filters:
- name: Hystrix
args:
name: sleepfail
fallbackUri: forward:/fallback

hystrix:
command:
default:
circuitBreaker:
requestVolumeThreshold: 3
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 10
  • 启动测试

访问http://localhost:8020/sleep,得到fallback信息。

限速

可以使用 RequestRateLimiter GatewayFilter Factory 来进行限速。

RequestRateLimiter使用RateLimiter实现是否允许继续执行当前请求。如果不允许继续执行,则返回HTTP 429 - Too Many Requests (默认情况下)。

Redis RateLimiter 基于 Stripe 实现,使用的是桶令牌算法。

使用以下步骤快速添加一个限速策略。

  • 添加依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
  • 添加限流键的解析器

这里根据host address进行限流。

1
2
3
4
5
6
7
@Bean
KeyResolver hostAddrKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest()
.getRemoteAddress()
.getAddress().getHostAddress());
}
  • 添加配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
application:
name: spring-cloud-gateway
redis:
host: localhost
port: 6379
pass: 123456
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: fallbacktest
uri: lb://spring-cloud-consumer
predicates:
- Path=/sleep
filters:
- name: RequestRateLimiter
args:
redis-rate-limter.replenishRate: 1 #允许用户每秒处理多少个请求;令牌桶的填充速率。
redis-rate-limiter.burstCapacity: 2 # 令牌桶的容量,允许在一秒钟内完成的最大请求数
key-resolver: "#{@addrKeyResolver}" # 用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象
  • 启动测试

访问http://localhost:8020/sleep,正常返回。频繁刷新则会失败。


参考链接