Gateway网关详解及实践
- 一 简介
- 1.1介绍
- 1.2.GateWay核⼼概念
- 1.3.GateWay核心功能
- 二Gateway入门案例
- 2.1 gateway依赖
- 2.2 gateway配置
- 2.3 gateway测试
- 三Gateway面向服务的路由
- 3.1.门案例问题
- 3.2.gateway-server结合eureka步骤
- 3.2.1.添加Eureka客户端依赖
- 3.2.2.添加Eureka配置
- 3.2.3修改映射配置
- 3.2.4.启动测试
- 3.3局部过滤器
- 3.3.1.Hystrix
- 1)引入Hystrix的依赖
- 2)开启Hystrix,添加@EnableHystrix
- 3)定义降级处理规则
- 4)定义降级的处理函数
- 5)测试
- 3.3.2.路由前缀
- 1)问题演示
- 2)去除路由前缀
- 3.4全局过滤器
- 3.4.1.GlobalFilter接口
- 3.4.2.过滤器顺序
- 3.4.3.自定义过滤器
- 3.4.3.1.登录拦截器(实现GlobalFilter接口方式)
- 3.4.3.2.多过滤器演示(lambda表达式)
- 3.5.网关限流
- 3.5.1.令牌桶算法原理
- 3.5.2.Gateway中限流
- 1) 引入redis
- 2) 配置过滤条件key
- 3) 配置桶参数
- 3.8.3.测试
- 源码地址
一 简介
1.1介绍
Spring Cloud GateWay是Spring Cloud的⼀个全新项⽬,⽬标是取代Netflix Zuul, 它基于Spring5.0+SpringBoot2.0+WebFlux(基于⾼性能的Reactor模式响应式通信 框架Netty,异步⾮阻塞模型)等技术开发,性能⾼于Zuul,官⽅测试,GateWay是 Zuul的1.6倍,旨在为微服务架构提供⼀种简单有效的统⼀的API路由管理⽅式。
Spring Cloud GateWay不仅提供统⼀的路由⽅式(反向代理)并且基于 Filter(定义 过滤器对请求过滤,完成⼀些功能) 链的⽅式提供了⽹关基本的功能,例如:鉴权、 流量控制、熔断、路径重写、⽇志监控等。
⽹关在架构中的位置
1.2.GateWay核⼼概念
Zuul1.x 阻塞式IO 2.x 基于Netty
Spring Cloud GateWay天⽣就是异步⾮阻塞的,基于Reactor模型
⼀个请求—>⽹关根据⼀定的条件匹配—匹配成功之后可以将请求转发到指定的服务地址;⽽在这个过程中,我们可以进⾏⼀些⽐较具体的控制(限流、⽇志、⿊⽩名 单)
路由(route)
: ⽹关最基础的部分,也是⽹关⽐较基础的⼯作单元。路由由⼀个ID、⼀个⽬标URL(最终路由到的地址)、⼀系列的断⾔(匹配条件判断) 和 Filter过滤器(精细化控制) 组成。如果断⾔为true,则匹配该路由。断⾔(predicates)
:参考了Java8中的断⾔java.util.function.Predicate,开发⼈员可以匹配Http请求中的所有内容(包括请求头、请求参数等)(类似于 nginx中的location匹配⼀样),如果断⾔与请求相匹配则路由。过滤器(filter)
:⼀个标准的Spring webFilter,使⽤过滤器,可以在请求之前或者之后执⾏业务逻辑。
来⾃官⽹的⼀张图
其中,Predicates断⾔就是我们的匹配条件,⽽Filter就可以理解为⼀个⽆所不能的拦截器,有了这两个元素,结合⽬标URL,就可以实现⼀个具体的路由转发。
1.3.GateWay核心功能
- 路由:gateway加入后,一切请求都必须先经过gateway,因此gateway就必须根据某种规则,把请求转发到某个微 服务,这个过程叫做路由。
- 权限控制:请求经过路由时,我们可以判断请求者是否有请求资格,如果没有则进行拦截。
- 限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
二Gateway入门案例
源码地址
Gateway的路由功能,基本步骤如下:
- 创建SpringBoot工程gateway_server,引入网关依赖
- 编写启动类
- 编写基础配置:服务端口,应用名称
- 编写路由规则
- 启动网关服务进行测试
2.1 gateway依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2.2 gateway配置
server:
port: 10010
spring:
application:
name: gateway-server # 服务名
cloud:
gateway:
routes: #路由规则的列表
- id: consumer-service # 当前路由的唯一标识
uri: http://127.0.0.1:8080 # 路由的目标微服务地址
predicates: # 断言,定义请求的匹配规则
- Path=/consumer/** # Path代表按照路径匹配的规则,/consumer/**是指路径必须以/consumer开头
2.3 gateway测试
启动下列三个服务
当我们访问http://localhost:10010/consumer/depart/get/1时,首先会进入网关服务,断言判断符合=/consumer/**,因此请求会被代理到http://localhost:8080/consumer/depart/get/1
三Gateway面向服务的路由
3.1.门案例问题
在入门案例中路由的目标地址是写死的,在微服务的情况下,可能目标服务是个集群那么这样做显然不合理。我们应该根据服务的名称去Eureka注册中心查找 服务对应的所有实例列表,并且对服务列表进行负载均衡才对!
案例模块
3.2.gateway-server结合eureka步骤
feign-consumer-8080 feign-eureka-server feign-provider-8081这三个模块沿用了之前旧的部分,具体搭建步骤不在赘述。着重讲解gateway-server结合eureka。
3.2.1.添加Eureka客户端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
3.2.2.添加Eureka配置
将gateway服务注到eure中
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka/
instance:
prefer-ip-address: true #偏好使用ip地址,而不是host主机名
ip-address: 127.0.0.1
instance-id: ${spring.application.name}.${eureka.instance.ip-address}.${server.port}
3.2.3修改映射配置
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
server:
port: 10010
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka/
instance:
prefer-ip-address: true #偏好使用ip地址,而不是host主机名
ip-address: 127.0.0.1
instance-id: ${spring.application.name}.${eureka.instance.ip-address}.${server.port}
spring:
application:
name: gateway-server # 服务名
cloud:
gateway:
routes: #路由规则的列表,可以有多个
- id: feign-consumer # 当前路由的唯一标识
uri: lb://feign-consumer # 路由的目标微服务,lb:代表负载均衡,feign-consumer:代表服务id
predicates: # 断言,定义请求的匹配规则
- Path=/consumer/** # Path代表按照路径匹配的规则,/consumer/**是指路径必须以/consumer开头
这里修改了uri的路由方式:
- lb:负载均衡的协议,将来会使用Ribbon实现负载均衡
- feign-consumer:服务的id
3.2.4.启动测试
-
启动模块
-
查看eureka客户端
-
访问并观察结果
当我们访问http://localhost:10010/consumer/depart/get/1时,首先会进入网关服务,断言判断符合=/consumer/**,因此请求会被代理到http://localhost:8080/consumer/depart/get/1
3.3局部过滤器
GatewayFilter Factories是Gateway中的局部过滤器工厂,作用于某个特定路由,允许以某种方式修改传入的HTTP请求或返回的HTTP响应。
3.3.1.Hystrix
网关做请求路由转发,如果被调用的请求阻塞,需要通过Hystrix来做线程隔离和熔断,防止出现故障。
1)引入Hystrix的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2)开启Hystrix,添加@EnableHystrix
@EnableHystrix
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
3)定义降级处理规则
可以通过default-filter来配置,会作用于所有的路由规则。
spring:
application:
name: gateway-server # 服务名
cloud:
gateway:
routes: #路由规则的列表,可以有多个
- id: feign-consumer # 当前路由的唯一标识
uri: lb://feign-consumer # 路由的目标微服务,lb:代表负载均衡,feign-consumer:代表服务id
predicates: # 断言,定义请求的匹配规则
- Path=/consumer/** # Path代表按照路径匹配的规则,/consumer/**是指路径必须以/consumer开头
default-filters: # 默认过滤项
- name: Hystrix # 指定过滤工厂名称(可以是任意过滤工厂类型)
args: # 指定过滤的参数
name: fallbackcmd # hystrix的指令名
fallbackUri: forward:/fallbackTest # 失败后的跳转路径
hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 1000 # 失败超时时长
- default-filters:默认过滤项,作用于所有的路由规则
- name:过滤工厂名称,这里指定Hystrix,意思是配置Hystrix类型
- args:配置过滤工厂的配置
- name:Hystrix的指令名称,用于配置例如超时时长等信息
- fallbackUri:失败降级时的跳转路径
4)定义降级的处理函数
定义一个controller,用来编写失败的处理逻辑:
@RestController
public class FallbackController {
@RequestMapping(value = "/fallbackTest")
public Map<String, String> fallBackController() {
Map<String, String> response = new HashMap<>();
response.put("code", "502");
response.put("msg", "服务超时");
return response;
}
}
5)测试
重启gateway,不启动consumer,访问http://localhost:10010/consumer/depart/get/1一秒后观察结果发现走了超时方法
3.3.2.路由前缀
1)问题演示
我们之前用/consumer/**这样的映射路径代表feign-consumer这个服务。因此请求feign-consumer服务的一切路径要以/consumer/**开头
比如,访问:localhost:10010/consumer/depart/get/1会被代理到:http://localhost:8080/consumer/depart/get/1
现在,我们在feign-consumer中的controller中定义一个新的接口:
@RestController
@RequestMapping("/test/depart")
public class TestController {
@GetMapping("/get/{id}")
public DepartVO getHandle(@PathVariable("id") int id) {
DepartVO departVO = new DepartVO();
departVO.setId(id);
departVO.setName("测试名称");
return departVO;
}
}
这个接口的路径是/test/depart/get/1,并不是以/consumer/开头。当访问:localhost:10010/test/depart/get/时,并不符合映射路径,因此会得到404.
无论是 /consumer/**还是/test/**都是feign-consumer中的一个controller路径,都不能作为网关到feign-consumer的映射路径。
因此我们需要定义一个额外的映射路径,例如:/feign-consumer,配置如下
# 路由前缀配置
spring:
application:
name: gateway-server # 服务名
cloud:
gateway:
routes: #路由规则的列表,可以有多个
- id: feign-consumer # 当前路由的唯一标识
uri: lb://feign-consumer # 路由的目标微服务,lb:代表负载均衡,feign-consumer:代表服务id
predicates: # 断言,定义请求的匹配规则
- Path=/feign-consumer/** # Path代表按照路径匹配的规则,feign-consumer:代表服务id。/feign-consumer/**是指路径必须以/feign-consumer
default-filters: # 默认过滤项
- name: feign-consumer # 指定过滤工厂名称(可以是任意过滤工厂类型)
args: # 指定过滤的参数
name: fallbackcmd # hystrix的指令名
fallbackUri: forward:/fallbackTest # 失败后的跳转路径
那么问题来了:
当我们访问::localhost:10010/feign-consumer/consumer/depart/get/1
,映射路径/feign-consumer指向用户服务,会被代理到:http://localhost:8080/feign-consumer/consumer/depart/get/1
当我们访问:localhost:10010/feign-consumer/test/depart/get/1
,映射路径/feign-consumer指向用户服务,会被代理到:http://localhost:8080/feign-consumer/test/depart/get/1
而在feign-consumer
中,无论是/feign-consumer/consumer/depart/get/1
还是/feign-consumer/test/depart/get/1
都是错误的,因为多了一个 /feign-consumer 。
这个 /feign-consumer 是gateway中的映射路径,不应该被代理到微服务,怎办吧?
2)去除路由前缀
解决思路很简单,当我们访问http://localhost:10010/feign-consumer/consumer/depart/get/1
时,网关利用 /feign-consumer 这个映射路径匹配到了用户微服务,请求代理时,只要把 /feign-consumer 这个映射路径去除不就可以了吗。
恰好有一个过滤器:StripPrefixFilterFactory
可以满足我们的需求。
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.3.RELEASE/reference/html/#the-stripprefix-gatewayfilter-factory
我们修改刚才的路由配置:
# 路由前缀配置
spring:
application:
name: gateway-server # 服务名
cloud:
gateway:
routes: #路由规则的列表,可以有多个
- id: feign-consumer # 当前路由的唯一标识
uri: lb://feign-consumer # 路由的目标微服务,lb:代表负载均衡,feign-consumer:代表服务id
predicates: # 断言,定义请求的匹配规则
- Path=/feign-consumer/** # Path代表按照路径匹配的规则,feign-consumer:代表服务id。/feign-consumer/**是指路径必须以/feign-consumer
filters:
- StripPrefix=1
default-filters: # 默认过滤项
- name: feign-consumer # 指定过滤工厂名称(可以是任意过滤工厂类型)
args: # 指定过滤的参数
name: fallbackcmd # hystrix的指令名
fallbackUri: forward:/fallbackTest # 失败后的跳转路径
此时,网关做路由的代理时,就不会把/feign-consumer作为目标请求路径的一部分了。
当我们访问::localhost:10010/feign-consumer/consumer/depart/get/1
,会被代理到:http://localhost:8080/consumer/depart/get/1
当我们访问:localhost:10010/feign-consumer/test/depart/get/1
,映射路径/feign-consumer指向用户服务,会被代理到:http://localhost:8080/test/depart/get/1
访问测试
3.4全局过滤器
全局过滤器Global Filter 与局部的GatewayFilter会在运行时合并到一个过滤器链中,并且根据org.springframework.core.Ordered
来排序后执行,顺序可以通过getOrder()
方法或者@Order
注解来指定。
3.4.1.GlobalFilter接口
全局过滤器的顶级接口:
实现接口,就要实现其中的filter方法,在方法内部完成过滤逻辑,其中的参数包括:
-
ServerWebExchange:一个类似于Context的域对象,封装了Request、Response等服务相关的属性
-
GatewayFilterChain:过滤器链,用于放行请求到下一个过滤器
3.4.2.过滤器顺序
通过添加@Order
注解,可以控制过滤器的优先级,从而决定了过滤器的执行顺序。
一个过滤器的执行包括
"pre"
和"post"
两个过程:
在GlobalFilter.filter()
方法中编写的逻辑属于pre阶段,
在使用GatewayFilterChain.filter().then()
的阶段,属于Post阶段。
优先级最高的过滤器,会在pre过程的第一个执行,在post过程的最后一个执行,如图:
我们可以在pre阶段做很多事情,诸如:
- 登录状态判断
- 权限校验
- 请求限流等
3.4.3.自定义过滤器
定义过滤器只需要实现GlobalFilter即可,不过我们有多种方式来完成:
- 方式一:定义过滤器类,实现GlobalFilter接口
- 方式二:通过@Configuration类结合lambda表达式
3.4.3.1.登录拦截器(实现GlobalFilter接口方式)
现在,通过自定义过滤器,模拟一个登录校验功能,逻辑非常简单:
- 获取用户请求参数中的 access-token 参数
- 判断是否为"admin"
- 如果不是,证明未登录,拦截请求
- 如果是,证明已经登录,放行请求
代码如下:
@Order(0) // 通过注解声明过滤器顺序
@Component
public class LoginFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取token
String token = exchange.getRequest().getQueryParams().toSingleValueMap().get("access-token");
// 判断请求参数是否正确
if(StringUtils.equals(token, "admin")){
// 正确,放行
return chain.filter(exchange);
}
// 错误,需要拦截,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 结束任务
return exchange.getResponse().setComplete();
}
}
测试:
-
带错误参数的情况:
http://localhost:10010/feign-consumer/consumer/depart/get/1 -
带正确参数的情况:
http://localhost:10010/feign-consumer/consumer/depart/get/1?access-token=admin
3.4.3.2.多过滤器演示(lambda表达式)
@Configuration
public class FilterConfiguration {
@Bean
@Order(-2)
public GlobalFilter globalFilter1(){
return ((exchange, chain) -> {
System.out.println("过滤器1的pre阶段!");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
System.out.println("过滤器1的post阶段!");
}));
});
}
@Bean
@Order(-1)
public GlobalFilter globalFilter2(){
return ((exchange, chain) -> {
System.out.println("过滤器2的pre阶段!");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
System.out.println("过滤器2的post阶段!");
}));
});
}
@Bean
@Order(0)
public GlobalFilter globalFilter3(){
return ((exchange, chain) -> {
System.out.println("过滤器3的pre阶段!");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
System.out.println("过滤器3的post阶段!");
}));
});
}
}
http://localhost:10010/feign-consumer/consumer/depart/get/1?access-token=admin
3.5.网关限流
网关除了请求路由、身份验证,还有一个非常重要的作用:请求限流。当系统面对高并发请求时,为了减少对业务处理服务的压力,需要在网关中对请求限流,按照一定的速率放行请求。
常见的限流算法包括:
- 计数器算法
- 漏桶算法
- 令牌桶算法
3.5.1.令牌桶算法原理
SpringGateway中采用的是令牌桶算法,令牌桶算法原理:
- 准备一个令牌桶,有固定容量,一般为服务并发上限
- 按照固定速率,生成令牌并存入令牌桶,如果桶中令牌数达到上限,就丢弃令牌。
- 每次请求调用需要先获取令牌,只有拿到令牌,才继续执行,否则选择选择等待或者直接拒绝。
3.5.2.Gateway中限流
SpringCloudGateway是采用令牌桶算法,其令牌相关信息记录在redis中,因此我们需要安装redis,并引入Redis相关依赖。
1) 引入redis
引入Redis有关依赖:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
注意:这里不是普通的redis依赖,而是响应式的Redis依赖,因为SpringGateway是基于WebFlux的响应式项目。
在application.yml中配置Redis地址:
spring:
redis:
host: localhost
2) 配置过滤条件key
Gateway会在Redis中记录令牌相关信息,我们可以自己定义令牌桶的规则,例如:
- 给不同的请求URI路径设置不同令牌桶
- 给不同的登录用户设置不同令牌桶
- 给不同的请求IP地址设置不同令牌桶
Redis中的一个Key和Value对就是一个令牌桶。因此Key的生成规则就是桶的定义规则。SpringCloudGateway中key的生成规则定义在KeyResolver
接口中:
public interface KeyResolver {
Mono<String> resolve(ServerWebExchange exchange);
}
这个接口中的方法返回值就是给令牌桶生成的key。API说明:
- Mono:是一个单元素容器,用来存放令牌桶的key
- ServerWebExchange:上下文对象,可以理解为ServletContext,可以从中获取request、response、cookie等信息
比如上面的三种令牌桶规则,生成key的方式如下:
-
给不同的请求URI路径设置不同令牌桶,示例代码:
return Mono.just(exchange.getRequest().getURI().getPath());// 获取请求URI
-
给不同的登录用户设置不同令牌桶
return exchange.getPrincipal().map(Principal::getName);// 获取用户
-
给不同的请求IP地址设置不同令牌桶
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());// 获取请求者IP
这里我们选择最后一种,使用IP地址的令牌桶key。
我们在config
中定义一个类,配置一个IpKeyResolver 的Bean实例:
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class IpKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
3) 配置桶参数
另外,令牌桶的参数需要通过yaml文件来配置,参数有2个:
-
replenishRate
:每秒钟生成令牌的速率,基本上就是每秒钟允许的最大请求数量 -
burstCapacity
:令牌桶的容量,就是令牌桶中存放的最大的令牌的数量
完整配置如下:
spring:
application:
name: ly-gateway
cloud:
gateway:
default-filters: # 默认过滤项
- StripPrefix=1 # 去除路由前缀
- name: Hystrix # 指定过滤工厂名称(可以是任意过滤工厂类型)
args: # 指定过滤的参数
name: fallbackcmd # hystrix的指令名
fallbackUri: forward:/hystrix/fallback # 失败后的跳转路径
- name: RequestRateLimiter #请求数限流 名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}" # 指定一个key生成器
redis-rate-limiter.replenishRate: 2 # 生成令牌的速率
redis-rate-limiter.burstCapacity: 2 # 桶的容量
这里配置了一个过滤器:RequestRateLimiter,并设置了三个参数:
-
key-resolver
:"#{@ipKeyResolver}"
是SpEL表达式,写法是#{@bean的名称}
,ipKeyResolver就是我们定义的Bean名称 -
redis-rate-limiter.replenishRate
:每秒钟生成令牌的速率 -
redis-rate-limiter.burstCapacity
:令牌桶的容量
这样的限流配置可以达成的效果:
- 每一个IP地址,每秒钟最多发起2次请求
- 每秒钟超过2次请求,则返回429的异常状态码
3.8.3.测试
我们快速在浏览器多次访问http://localhost:10010/feign-consumer/consumer/depart/get/1?access-token=admin,就会得到一个错误:
429:代表请求次数过多,触发限流了。
源码地址
源码地址
05-gateway-eureka
上一篇:Hystrix详解及实践—SpringCloud组件(四)