Spring Cloud Gateway
# Gateway 简介
SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在 Spring Cloud 2.0 以上版本中,没有对新版本的 Zuul 2.0 以上最新高性能版本进行集成,仍然还是使用的 Zuul 2.0 之前的非 Reactor 模式的老版本。而为了提升网关的性能,SpringCloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
声明:Spring Cloud Gateway 底层使用了高性能的通信框架 Netty。
# Gateway 特征
基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
动态路由
Predicates 和 Filters 作用于特定路由
集成 Hystrix 断路器
集成 Spring Cloud DiscoveryClient
简单好用的 Predicates 和 Filters
限流
路径重写
不管是来自客户端的请求,还是服务内部调用。一切对服务的请求都可经过网关
网关实现鉴权、动态路由等等操作
Gateway是我们服务的统一入口
鉴权,安全控制,⽇志统⼀处理,易于监控的相关功能
# Gateway 术语解释
Route(路由):这是网关的基本模块。它由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。
Predicate(断言):这是一个 Java 8 的 Predicate。输入类型是一个 ServerWebExchange。我们可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。
Filter(过滤器):这是 org.springframework.cloud.gateway.fifilter.GatewayFilter
的实例,我们可以使用它修改请求和响应。
# Gateway 架构和 IO 模型
Spring 在 2017 年下半年迎来了 Webflux,Webflux 的出现填补了 Spring 在响应式编程上的空白,Webflux 的响应式编程不仅仅是编程风格的改变,而且对于一系列的著名框架,都提供了响应式访问的开发包,比如 Netty、Redis 等等。
SpringCloud Gateway 使用的 Webflux 中的 reactor-netty 响应式编程组件,底层使用了 Netty 通讯框架。
Springcloud 中所集成的 Zuul 版本,采用的是 Tomcat 容器,使用的是传统的 Servlet IO 处理模型。
大家知道,Servlet 由 Servlet Container 进行生命周期管理。Container 启动时构造 Servlet 对象并调用 servlet init()
进行初始化;Container 关闭时调用 servlet destory()
销毁 Servlet;Container 运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用 service()
。
弊端:Servlet 是一个简单的网络 IO 模型,当请求进入 Servlet Container 时,Servlet Container 就会为其绑定一个线程,在并发不高的场景下这种模型是适用的,但是一旦并发上升,线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单的业务场景下,不希望为每个 Request 分配一个线程,只需要 1 个或几个线程就能应对极大并发的请求,这种业务场景下 Servlet 模型没有优势。
所以 Springcloud Zuul 是基于 Servlet 之上的一个阻塞式处理模型,即 Spring 实现了处理所有 Request 请求的一个 Servlet(DispatcherServlet),并由该 Servlet 阻塞式处理处理。所以 Springcloud Zuul 无法摆脱 Servlet 模型的弊端。虽然 Zuul 2.0 开始,使用了 Netty,并且已经有了大规模 Zuul 2.0 集群部署的成熟案例,但是,Springcloud 官方已经没有集成改版本的计划了。
# Webflux 模型
Webflux 模式替换了旧的 Servlet 线程模型。用少量的线程处理 Request 和 Response IO 操作,这些线程称为 Loop 线程,而业务交给响应式编程框架处理,响应式编程是非常灵活的,用户可以将业务中阻塞的操作提交到响应式框架的 work 线程中执行,而不阻塞的操作依然可以在 Loop 线程中进行处理,大大提高了 Loop 线程的利用率。
Webflux 虽然可以兼容多个底层的通信框架,但是一般情况下,底层使用的还是 Netty,毕竟,Netty 是目前业界认可的最高性能的通信框架。而 Webflux 的 Loop 线程,正好就是著名的 Reactor 模式 IO 处理模型的 Reactor 线程,如果使用的是高性能的通信框架 Netty,这就是 Netty 的 EventLoop 线程。
关于 Reactor 线程模型,和 Netty 通信框架的知识,是 Java 程序员的重要、必备的内功,个中的原理,具体请参见尼恩编著的《Netty、Zookeeper (opens new window)、Redis高并发实战》一书,这里不做过多的赘述。
# Gateway 的处理流程
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑。
# Gateway 路由配置方式
# 基于 URI
如果请求的目标地址,是单个的 URI 资源路径,配置文件示例如下:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: path_route
uri: https://www.youngkbt.cn
predicates:
-Path=/youngkbt
2
3
4
5
6
7
8
9
10
11
12
各字段含义如下:
- id:我们自定义的路由 ID,保持唯一即可
- uri:目标服务地址
- predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)
上面这段配置的意思是,配置了一个 id 为 path_route 的 URI 代理规则,路由的规则为:当访问地址 http://localhost:8080/youngkbt/download
时,会路由到上游地址 https://www.youngkbt.cn/download
。
也就是访问 http://localhost:8080/youngkbt
,最终访问的是 https://www.youngkbt.cn
# 基于代码
转发功能同样可以通过代码来实现,我们可以在启动类 GateWayApplication
中添加方法 customRouteLocator()
来定制转发规则。
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/youngkbt")
.uri("https://www.youngkbt.cn"))
.build();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们在 yml 配置文件中注销掉相关路由的配置,重启服务,访问链接:http://localhost:8080/youngkbt
, 可以看到和上面一样的页面,证明我们测试成功。
# 基于注册中心
在 uri 的 schema 协议部分为自定义的 lb:
类型,表示从微服务注册中心(如 Nacos)订阅服务,并且进行服务的路由。
一个典型的示例如下:
server:
port: 8094
spring:
application:
name: gateway-service
cloud:
nacos: # 注册中心配置
discovery:
service: ${spring.application.name} # 使用微服务的名称作为注册的服务名称
server-addr: 172.16.138.184:8848
namespace: 5014d494-958a-4476-9aad-880c2a0c9498
group: DEFAULT_GROUP
gateway: # 网关配置
routes:
- id: provider-service-route
uri: lb://provider-service
predicates:
- Path=/provider/**
- id: consumer-service-route
uri: lb://consumer-service
predicates:
- Path=/consumer/**
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
访问 http://localhost:8094/provider/xx
或 http://localhost:8094/consumer/xx
都会去注册中心找到 provider-service
服务和 consumer-service
服务,然后调用里面的接口 /xx
。
注册中心相结合的路由配置方式,与单个 URI 的路由配置,区别其实很小,仅仅在于 URI 的 schema 协议不同。单个 URI 的地址的 schema 协议,一般为 http 或者 https 协议。而使用注册中心后,则使用 lb://
格式。
路由配置中 uri 所用的协议为 lb 时,Gateway 将把 generic-service 解析为实际的主机和端口,并通过 Ribbon 进行负载均衡。
# Gateway 匹配规则
Spring Cloud Gateway 的功能很强大,我们仅仅通过 Predicates 的设计就可以看出来,前面我们只是使用了 predicates 进行了简单的条件匹配,其实 Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。
Spring Cloud Gateway 是通过 Spring WebFlux 的 HandlerMapping 做为底层支持来匹配到转发路由,Spring Cloud Gateway 内置了很多 Predicates 工厂,这些 Predicates 工厂通过不同的 HTTP 请求参数来匹配,多个 Predicates 工厂可以组合使用。
# Predicate 断言条件介绍
Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。下面先总结了 Spring Cloud 内置的几种 Predicate 的实现:
规则 | 实例 | 说明 |
---|---|---|
Path | - Path=/gate/,/rule/ | 当请求的路径为 gate、rule 开头的时,转发到 http://localhost:8094 服务器上 |
Before | - Before=2023-04-29T15:30:00.000+08:00[Asia/Shanghai] | 在某个时间之前的请求才会被转发到 http://localhost:8094 服务器上 |
After | - After=2023-05-02T20:30:00.000+08:00[Asia/Shanghai] | 在某个时间之后的请求才会被转发 |
Between | - Between2023-04-29T15:30:00.000+08:00[Asia/Shanghai],2023-05-02T20:30:00.000+08:00[Asia/Shanghai] | 在某个时间段之间的才会被转发 |
Cookie | - Cookie=token, ch.p | 携带 Cookie 为 token 的表单或者满⾜正则 ch.p 才会被匹配到 进⾏请求转发 (opens new window) |
Header | - Header=X-Request-Id, \d+ | 携带参数 X-Request-Id 或者满⾜ \d+ 的请求头才会匹配 |
Host | - Host=youngkbt.cn | 当主机名为 youngkbt.cn 的时候直接转发到 http://localhost:8094 服务器上 |
Method | - Method=GET | 只有 GET ⽅法才会匹配转发请求,还可以限定 POST、PUT 等 |
说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来我们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。
为了方便代码,下面直接使用 predicate,其他的配置信息则忽略掉,如:
server:
port: 8094
spring:
application:
name: gateway-service
gateway: # 网关配置
routes:
- id: route
uri: https://www.youngkbt.cn
# 下面的内容具体内容具体写
2
3
4
5
6
7
8
9
10
11
# 通过请求路径匹配
Path Route Predicate 接收一个匹配路径的参数来判断是否走路由。
predicates:
- Path=/system/**
2
此时访问 /system/one
则匹配成功。
# 通过请求参数匹配
Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。
如果 Query 只有一个值表示请求头中必须包含的参数名。如果有两个值,第一个表示请求头必须包含的参数名,第二个表示请求头参数对应值。
predicates:
- Path=/system/**
- Query=username
2
3
Path 可以传入正则表达式,Query 是拼接在 url ? 后面的参数。
上面表示路径满足 /system/**
同时包含参数 username
才会触发转发到 https://www.youngkbt.cn
,如 http://localhost:8094/system/one?username=kele
。
predicates:
- Path=/system/**
- Query=username,k.
2
3
Query 要求 username = k
或者以 k 为前缀的其他值。k.
的 .
是个正则表达式,后面还可以有其他的 一个 内容。如http://localhost:9000/system/one?username=kb
。如果 username = kbt
则无法访问,因为 .
在正则表达式代表单个字符。
# 通过 Header 属性匹配
Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
如果 Header 只有一个值表示请求头中必须包含的参数名。如果有两个值,第一个表示请求头必须包含的参数名,第二个表示请求头参数对应值。
predicates:
- Header=Connection,keep-alive
- Header=X-Request-Id, \d+
2
3
此时请求头携带 Connection: keep-alive
或者 X-Request-Id: 27
都可以匹配成功。而 X-Request-Id: kbt
则匹配失败。
# 通过 Cookie 匹配
Cookie Route Predicate 可以接收两个参数,一个是 Cookie name,一个是正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
必须要有2个值,第 1 个包含的是参数名,第 2 个表示参数对应的值。
predicates:
- Cookie=sessionId, test
- Cookie=age,.*
2
3
此时 Cookie 携带 sessionId=test
或者 age=3
都可以匹配成功,其中 age=.*
,代表可以是任意值。
# 通过 Host 匹配
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 Ant 分隔的模板,用 .
号作为分隔符。它通过参数中的主机地址作为匹配规则。
- ?:匹配一个字符
- *:匹配 0 个或多个字符
- **:匹配 0 个或多个目录
predicates:
- Host=**.youngkbt.cn
2
此时 Host 携带 www.youngkbt.cn
即可匹配成功,其中 **
代表任意字符。也可以是 notes.youngkbt.cn
。
# 通过请求方式匹配
可以通过是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由,支持多个值,使用逗号分隔,多个值之间为 or 条件。
predicates:
- Method=GET,POST
2
此时以 GET 请求或者 POST 请求都可以匹配成功。
# 通过请求 IP 地址进行匹配
Predicate 也支持通过设置某个 IP 区间号段的请求才会路由,RemoteAddr Route Predicate 接受 cidr 符号(IPv4 或 IPv6 )字符串的列表(最小大小为1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子网掩码)。
predicates:
- RemoteAddr=127.0.0.1
2
请求的来源 IP 是 127.0.0.1 则匹配成功。
# 通过 Before 匹配
在指定时间点之前才能路由转发。
predicates:
- Before=2023-04-29T15:30:00.000+08:00[Asia/Shanghai]
2
在 2023-04-29
的 15:30
分之前可以匹配成功。
# 通过 After 匹配
在指定时间点之后才能路由转发。
predicates:
- After=2023-05-02T20:30:00.000+08:00[Asia/Shanghai]
2
在 2023-05-02
的 20:30
分之前可以匹配成功。
# 通过 Between 匹配
必须在设定的范围时间内,才能进行路由转发。
predicates:
- Between=2023-04-29T15:30:00.000+08:00[Asia/Shanghai], 2023-05-02T20:30:00.000+08:00[Asia/Shanghai]
2
在 2023-04-29
的 15:30
分和 2023-05-02
的 20:30
分之间可以匹配成功。
# 组合使用
server:
port: 8094
spring:
application:
name: gateway-service
gateway: # 网关配置
routes:
- id: route
uri: https://www.youngkbt.cn
predicates:
- Path=/system/**
- Query=username
- Header=X-Request-Id, \d+
- Cookie=age,.*
- Host=**.youngkbt.cn
- Method=GET,POST
- RemoteAddr=127.0.0.1
- Before=2023-04-29T15:30:00.000+08:00[Asia/Shanghai]
- After=2023-05-02T20:30:00.000+08:00[Asia/Shanghai]
- Between=2023-04-29T15:30:00.000+08:00[Asia/Shanghai], 2023-05-02T20:30:00.000+08:00[Asia/Shanghai]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 通过 Weight 控制转发比例
设置服务转发的权重,用于限制某个的请求占比。
语法:Weight=组名,负载均衡权重。
spring:
cloud:
routes:
- id: weight1
uri: lb://demo-one
predicates:
- Path=/demo/**
- Weight=group,2
- id: weight2
uri: lb://demo-two
predicates:
- Path=/demo/**
- Weight=group,8
2
3
4
5
6
7
8
9
10
11
12
13
14
此时访问 /demo/**
,weight2 的匹配成功率大于 weight1。
# 过滤(拦截)器
过滤器作为网关的其中一个重要功能,就是实现请求的鉴权。
执行顺序:Spring Cloud Gateway 的 Filter 的执行顺序有两个:pre
和 post
。pre
和 post
分别会在请求被执行前调用和被执行后调用。
# 过滤器规则
Gateway自带过滤器有几十个,常见自带过滤器有:
过滤器名称 | 说明 |
---|---|
PrefixPath | 对匹配上的请求路径添加前缀 |
StripPrefix | 对匹配上的请求路径去除前缀 |
RewritePath | 对匹配上的请求进行重写 |
SetPath | 对匹配上的请求进行覆盖 |
SetRequestHeader | 对匹配上的请求设置请求头信息 |
AddRequestHeader | 对匹配上的请求添加 Header |
AddRequestParameters | 对匹配上的请求路由添加参数 |
AddResponseHeader | 对从网关返回的响应添加 Header |
RemoveRequestHeader | 对匹配上的请求去掉某个请求头信息 |
RemoveResponseHeader | 对从网关返回的响应去掉某个请求头信息 |
DedupeResponseHeader | 对指定响应头去重复 |
CircuitBreaker | 实现熔断时使用,支持 CircuitBreaker 和 Hystrix 两种 |
FallbackHeaders | 可以添加降级时的异常信息 |
RequestRateLimiter | 限流过滤器 |
RedirectTo | 重定向过滤器,有两个参数,status 和 url。其中 status 应该 300 系列重定向状态码 |
RewriteResponseHeader | 重写响应头参数 |
SaveSession | 如果项目中使用 Spring Security 和 Spring Session 整合时,会使用到此属性 |
SecureHeaders | 具有权限验证时,建议的头信息内容 |
Retry | 设置重试次数 |
RequestSize | 请求的最大大小。包含 maxSize 参数,可以有单位 KB 或 MB,默认为 B |
ModifyRequestBody | 修改请求体内容 |
ModifyResponseBody | 修改响应体内容 |
SetRequestHeader | 替换请求参数头 |
SetResponseHeader | 替换相应头参数 |
SetStatus | 设置相应状态码 |
使用场景:
- 请求鉴权:如果没有访问权限,直接进行拦截
- 异常处理:记录异常日志
- 服务调用时长统计
# 过滤器类型
Gateway 有两种过滤器:
- 局部过滤器:只作用在当前配置的路由上
- 全局过滤器:作用在所有路由上
# PrefixPath
对匹配上的请求路径添加前缀。
spring:
cloud:
gateway:
routes:
- id: route
uri: http://127.0.0.1:9001
predicates:
- Path=/youngkbt/{segment}
filters:
- PrefixPath=/abc
2
3
4
5
6
7
8
9
10
访问 /youngkbt/123
请求被发送到 http://127.0.0.1:9001/youngkbt/abc/123
。
# StripPrefix
对匹配上的请求路径去除前缀。通过 StripPrefifix=n
来指定路由要去掉的前缀个数 n。
predicates:
- Path=/youngkbt/{segment}
filters:
- StripPrefix=1
- PrefixPath=/youngkbt
2
3
4
5
此时访问 http://localhost:8094/api/123
,⾸先 StripPrefix 过滤器去掉⼀个 /api
,然后 PrefixPath 过滤器加上⼀个 /youngkbt
,即 http://localhost:8094/youngkbt/123
,最后匹配 predicates 的 Path。
如果 StripPrefix=2
,则 /api/123
都被去掉。
# RewritePath
对匹配上的请求进行重写。
predicates:
- Path=/youngkbt/{segment}
filters:
- RewritePath=/api/(?<segment>.*), /$\{segment}
2
3
4
请求 http://localhost:8094/api/youngkbt/123
路径,RewritePath 过滤器将路径重写为 http://localhost:8094/youngkbt/123
,最后匹配 predicates 的 Path。
# SetPath
对匹配上的请求进行覆盖。
predicates:
- Path=/youngkbt/{segment}
filters:
- SetPath=/youngkbt/{segment}
2
3
4
请求 http://localhost:8094/api/youngkbt/123
路径,SetPath 过滤器将路径设置为 http://localhost:8094/youngkbt/123
,最后匹配 predicates 的 Path。
# SetRequestHeader
对匹配上的请求设置请求头信息。参数和值之间使用逗号分隔。
filters:
- SetRequestHeader=X-Request-Red, Blue
2
此时请求头会加上 X-Request-Red=Blue
。
# AddRequestParameter
添加请求表单参数,多个参数需要有多个过滤器。
filters:
- AddRequestParameter=name,bjsxt
- AddRequestParameter=age,123
2
3
其他写法都类似 ......
# DedupeResponseHeader
对指定响应头去重复。
语法:DedupeResponseHeader=响应头参数, strategy
可选参数 strategy 可取值:
- RETAIN_FIRST:默认值,保留第一个
- RETAIN_LAST:保留最后一个
- RETAIN_UNIQUE:保留唯一的,出现重复的属性值,会保留一个。例如有两个 My:bbb 的属性,最后会只留一个
filters:
- StripPrefix= 1
- DedupeResponseHeader=My Content-Type,RETAIN_UNIQUE
2
3
# 全局过滤器 default-filters
对所有的请求添加过滤器。
spring:
cloud:
gateway:
routes:
- id: route1
uri: http://127.0.0.1:9000
predicates:
- Path=/9000/{segment}
- id: route2
uri: http://127.0.0.1:9001
predicates:
- Path=/9001/{segment}
default-filters:
- StripPrefix=1
- PrefixPath=/youngkbt
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 自定义全局过滤器
exchange 代码信息
// 获取请求头信息
HttpHeaders headers = exchange.getRequest().getHeaders();
String first = exchange.getRequest().getHeaders().getFirst("Content-type");
ServerHttpRequest build = exchange.getRequest().mutate().header("Content-type","json").build();
// 获取请求方式
String name = exchange.getRequest().getMethod().name();
// 获取请求URL地址信息
String host = exchange.getRequest().getURI().getHost();
String path1 = exchange.getRequest().getURI().getPath();
String rawPath = exchange.getRequest().getURI().getRawPath();
// 获取请求属性信息
exchange.getAttributes().put("startTime",123123);
Long startTime1 = exchange.getAttribute("startTime");
// 获取返回体
ServerHttpResponse response = exchange.getResponse();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Token 过滤器
拦截 Token,假设为 null,则拦截,不允许通过。
@Configuration
public class MyGlobalFileter implements GlobalFilter, Ordered {
/**
* 自定义过滤器规则
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("-----------------全局过滤器 MyGlobalFilter---------------------");
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 定义过滤器执行顺序
* 返回值越小,越靠前执行
*/
@Override
public int getOrder() {
return 0;
}
}
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
# IP 过滤器
@Component
public class IPFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("经过IP过滤器");
ServerHttpRequest request = exchange.getRequest();
InetSocketAddress remoteAddress = request.getRemoteAddress();
System.out.println("请求的IP地址为:" + remoteAddress.getHostName());
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 请求 url 过滤器
@Component
public class UrlFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("经过url过滤器");
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
System.out.println("请求的url为:" + path);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 2;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
自定义可配置过滤器(添加到配置文件)
@Component
public class MyRouteGatewayFilterFactory extends AbstractGatewayFilterFactory<MyRouteGatewayFilterFactory.Config> {
public MyRouteGatewayFilterFactory() {
super(MyRouteGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("在这个位置写点东西传递进来的 name: " + config.getName() + ",age:" + config.getAge());
return chain.filter(exchange);
}
@Override
public String toString() {
return GatewayToStringStyler.filterToStringCreator(MyRouteGatewayFilterFactory.this).append("name", config.getName()).toString();
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("name");
}
public static class Config {
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Config() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
添加到配置文件 application.yml
spring:
application:
name: sysgateway
cloud:
routes:
- id: myFilter
uri: lb:/jqk
predicates:
- Path=/project/**
filters:
- StripPrefix=1
- name: MyRoute
args:
# 传递的参数(name、age)
name: hello
age: 12
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 其他功能
# 网关自动映射处理
- 只要请求地址符合规则:
http://gatewayIp:gatewayPort/微服务名称/微服务请求地址
,网关自动映射,把请求地址转发到http://微服务名称/微服务请求地址
- 商业开发中:enabled 一般不设置,默认为 false。避免不必要的自动转发机制
spring:
application:
name: sysgateway
cloud:
gateway:
discovery: # 配置网关发现机制
locator: # 配置处理机制
enabled: true # 开启网关自动映射处理机制
lower-case-service-id: true # 开启微服务名称小写转换。Eureka对服务名管理默认全大写。
2
3
4
5
6
7
8
9
假设存在一个微服务 generic-service。
此时请求地址:http://localhost:9999/generic-service/getArgs?name=admin&age=20
,自动转发到:http://generic-service/getArgs?name=admin&age=20
。
这样子就不需要单独配置如下 route:
server:
port: 8094
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: generic-service-route
uri: lb://generic-service
predicates:
- Path=/generic-generic/**
2
3
4
5
6
7
8
9
10
11
12
13
# 跨域配置
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" # 跨域处理:允许所有的域
allowedMethods: # ⽀持的⽅法
- GET
- POST
- PUT
- DELETE
2
3
4
5
6
7
8
9
10
11
12
# 记录执行耗时
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class UrlFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("经过url过滤器");
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
System.out.println("请求的url为:" + path);
exchange.getAttributes().put("startTime", System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute("startTime");
if (startTime != null) {
long executeTime = System.currentTimeMillis() - startTime;
log.info("请求时间:{}", exchange.getRequest().getURI().getRawPath() + "d" + executeTime + "ms");
}
}));
}
@Override
public int getOrder() {
return 2;
}
}
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
31
32
33
34
# 熔断降级
为什么要实现熔断降级?
在分布式系统中,网关作为流量的入口,因此会有大量的请求进入网关,向其他服务发起调用,其他服务不可避免的会出现调用失败(超时、异常),失败时不能让请求堆积在网关上,需要快速失败并返回给客户端,想要实现这个要求,就必须在网关上做熔断、降级操作。
为什么在网关上请求失败需要快速返回给客户端?
因为当一个客户端请求发生故障的时候,这个请求会一直堆积在网关上,当然只有一个这种请求,网关肯定没有问题(如果一个请求就能造成整个系统瘫痪,那这个系统可以下架了),但是网关上堆积多了就会给网关乃至整个服务都造成巨大的压力,甚至整个服务宕掉。因此要对一些服务和页面进行有策略的降级,以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应,所以需要网关上请求失败需要快速返回给客户端。
server:
port: 8094
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: route
uri: http://localhost:8080
predicates:
- Path=/test/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackCmdA
fallbackUri: forward:/fallbackA
hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这里的配置,使用了两个过滤器:
过滤器 StripPrefix,作用是去掉请求路径的最前面 n 个部分截取掉。
StripPrefix=1
就代表截取路径的个数为 1,比如前端过来请求 /test/good/1/view
,匹配成功后,路由到后端的请求路径就会变成 http://localhost:8888/good/1/view
。
过滤器 Hystrix,作用是通过 Hystrix 进行熔断降级
当上游的请求,进入了 Hystrix 熔断降级机制时,就会调用 fallbackUri 配置的降级地址。需要注意的是,还需要单独设置 Hystrix 的 commandKey 的超时时间。
fallbackUri 配置的降级地址的代码如下:
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 限流
高并发带来的问题
在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络 原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会 出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
服务雪崩效应
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了 问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等 待,进而导致服务瘫痪。 由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是 服务故障的「雪崩效应」。
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某 台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问 题,不会影响到其它服务的正常运行。也就是「雪落而不雪崩」。
限流,当我们的系统被频繁的请求的时候,就有可能将系统压垮,所以 为了解决这个问题,需要在每⼀个微服务中做限流操作,但是如果有了⽹关,那么就可以在⽹关系统做限流,因为所有的请求都需要先通过⽹关系统才能路由到微服务中。
# 令牌桶算法简介
令牌桶算法是⽐较常⻅的限流算法之⼀,⼤概描述如下:
- 所有的请求在处理之前都需要拿到⼀个可⽤的令牌才会被处理
- 根据限流⼤⼩,设置按照⼀定的速率往桶⾥添加令牌
- 桶设置最⼤的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝
- 请求达到后⾸先要获取令牌桶中的令牌,拿着令牌才可以进⾏其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除
- 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证⾜够的限流
# 代码实现
spring cloud gateway
默认使⽤ redis
的 RateLimter
限流算法来实现。所以我们要使⽤⾸先需要引⼊ redis
的依赖。
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
2
3
4
5
定义 KeyResolver:在 GatewayApplicatioin
引导类中添加如下代码,KeyResolver
⽤于计算某⼀个类型的限流的 KEY
,也就是说,可以通过 KeyResolver
来指定限流的 Key
。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@SpringBootApplication
public class DemoGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(DemoGatewayApplication.class, args);
}
// 定义一个KeyResolver
@Bean
public KeyResolver ipKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
};
}
}
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
修改 application.yml 中配置项,指定限制流量的配置以及 REDIS 的配置。
filters:
- PrefixPath=/test
- name: RequestRateLimiter
args:
key-resolver: "#{@ipKeyResolver}"
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
2
3
4
5
6
7
burstCapacity
:令牌桶总容量replenishRate
:令牌桶每秒填充平均速率key-resolver
:⽤于限流的键的解析器的 Bean 对象的名字。它使⽤ SpEL 表达式根据#{@beanName}
从 Spring 容器中获取 Bean 对象
通过在 replenishRate 和中设置相同的值来实现稳定的速率 burstCapacity 。设置 burstCapacity ⾼于时,可以允许临时突发 replenishRate 。在这种情况下,需要在突发之间允许速率限制器⼀段时间(根据 replenishRate),因为 2 次连续突发将导致请求被丢弃(HTTP 429 - Too Many Requests) key-resolver: "#{@userKeyResolver}"
⽤于通过 SPEL 表达式来指定使⽤哪⼀个 KeyResolver。
如上配置:
- 表示⼀秒内,允许⼀个请求通过,令牌桶的填充速率也是⼀秒钟添加⼀个令牌
- 最⼤突发状况也只允许⼀秒内有⼀次请求,可以根据业务来调整