在微服务架构中,一个系统往往由多个微服务组成,而这些服务可能部署在不同机房、不同地区、不同域名下。这种情况下,客户端(例如浏览器、手机、软件工具等)想要直接请求这些服务,就需要知道它们具体的地址信息,例如 IP 地址、端口号等。
这种客户端直接请求服务的方式存在以下问题:
API 网关是一个搭建在客户端和微服务之间的服务,我们可以在 API 网关中处理一些非业务功能的逻辑,例如权限验证、监控、缓存、请求路由等。
API 网关就像整个微服务系统的门面一样,是系统对外的唯一入口。有了它,客户端会先将请求发送到 API 网关,然后由 API 网关根据请求的标识信息将请求转发到微服务实例。
对于服务数量众多、复杂度较高、规模比较大的系统来说,使用 API 网关具有以下好处:
参考资料:Gateway:Spring Cloud API网关组件(非常详细) (biancheng.net)
SpringCloud Gateway 是在建立在 Spring 生态系统之上的 API 网关服务,基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,以及提供一些强大的过滤器功能,例如:安全,监控/指标、熔断、限流、重试等。
SpringCloud Gateway 的目标是替代 Zuul,在 Spring Cloud 2.0 以上版本中,没有对 Zuul 2.x 高性能版本进行集成,仍然还是使用的 Zuul 1.x 的非 Reactor 模式的老版本。而为了提升网关的性能,SpringCloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
SpringCloud 中所集成的 Zuul 1.x 版本,采用的是 Tomcat 容器,使用的是传统的 Servlet IO 处理模型。servlet 由 servlet container 进行生命周期管理:
servlet.init()
进行初始化;servlet.destory()
销毁 servlet;service()
。上述模式的缺点:servlet 是一个简单的网络 IO 模型,当请求进入 servlet container 时,servlet container 就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发,线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单的业务场景下,不希望为每个请求分配一个线程,只需要一个或几个线程就能应对极大并发的请求,这种业务场景下 servlet 模型没有优势。
所以 Zuul 1.x 是基于 servlet 阻塞 IO 模型的 API 网关,即 Spring 实现了处理所有 request 请求的一个 servlet(DispatcherServlet),并由该 servlet 阻塞式处理处理。每次 I/О 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成。所以 Zuul 1.x 无法摆脱 servlet 模型的弊端。
虽然 Zuul 2.0 开始,使用了 Netty 非阻塞和支持长连接,并且已经有了大规模 Zuul 2.0 集群部署的成熟案例,但是,SpringCloud 官方已经没有集成改版本的计划了。
传统的Web框架,比如说:Struts2,SpringMVC 等都是基于 Servlet APl 与 Servlet 容器基础之上运行的。
但是在 Servlet3.1 之后有了异步非阻塞的支持。而 WebFlux 是一个典型非阻塞异步的框架,它的核心是基于 Reactor 的相关 API 实现的。Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖 Servlet APl,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。
SpringCloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
Spring Cloud GateWay 最主要的功能就是路由转发,而在定义转发规则时主要涉及了以下三个核心概念:
核心概念 | 描述 |
---|---|
Route(路由) | 网关最基本的模块。它由一个 ID、一个目标 URI、一系列的断言(Predicate)和过滤器(Filter)组成。 |
Predicate(断言) | 路由转发的判断条件,我们可以通过 Predicate 对 HTTP 请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务。 |
Filter(过滤器) | 过滤器,可以在请求被路由前或者之后对请求进行拦截修改。 |
注意:其中 Route 和 Predicate 必须同时声明。
Spring Cloud Gateway 工作流程说明如下:
总而言之,客户端发送到 Spring Cloud Gateway 的请求需要通过一定的匹配条件,才能定位到真正的服务节点。在将请求转发到服务进行处理的过程前后(pre 和 post),我们还可以对请求和响应进行一些精细化控制。
Predicate 就是路由的匹配条件,而 Filter 就是对请求和响应进行精细化控制的工具。有了这两个元素,再加上目标 URI,就可以实现一个具体的路由了。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
client:
register-with-eureka: true
fetchRegistry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
instance:
hostname: cloud-gateway
@SpringBootApplication
@EnableEurekaClient
public class GateWayApplication {
目标:给 8001 服务套上网关
spring:
application:
name: cloud-gateway
cloud:
gateway: #网关路由配置
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议与服务名对应
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/getPaymentById/** #断言,路径相匹配的进行路由
- Method=GET #只能时GET请求时,才能访问
- id: payment_routh2 #路由的ID,没有固定规则但要求唯一,建议与服务名对应
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
测试:localhost:9527/payment/getPaymentById/31
当请求路径为 /guonei
时,转发到 https://news.baidu.com/guonei
。
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route",
r -> r.path("/guonei")
.uri("https://news.baidu.com/guonei"));
return routes.build();
}
}
测试:http://localhost:9527/guonei
默认情况下,Spring Cloud Gateway 会根据服务注册中心(例如 Eureka Server)中维护的服务列表,以服务名(spring.application.name)作为路径创建动态路由进行转发,从而实现动态路由功能。
字段 | Route URL 说明 |
---|---|
lb | 即指通过注册中心的服务名称来转发请求 |
ws | 代表这是一个websocket请求,会通过长连接的方式转发请求 |
http | 转发http请求,可以直接链接到一个web url |
我们可以在配置文件中,将 Route 的 uri 地址修改为以下形式。
lb://service-name
spring:
application:
name: cloud-gateway
cloud:
gateway: #网关路由配置
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议与服务名对应
#uri: http://localhost:8001 #匹配后提供服务的路由地址(写死)
uri: lb://provider-payment #匹配后提供服务的路由地址
predicates: #断言,路径相匹配的进行路由
- Path=/payment/getPaymentById/**
- Method=GET #只能时GET请求时,才能访问
- id: payment_routh2 #路由的ID,没有固定规则但要求唯一,建议与服务名对应
#uri: http://localhost:8001 #匹配后提供服务的路由地址(写死)
uri: lb://provider-payment #匹配后提供服务的路由地址
predicates: #断言,路径相匹配的进行路由
- Path=/payment/lb/**
测试:localhost:9527/payment/lb
Spring Cloud Gateway 通过 Predicate 断言来实现 Route 路由的匹配规则。简单点说,Predicate 是路由转发的判断条件,请求只有满足了 Predicate 的条件,才会被转发到指定的服务上进行处理。
使用 Predicate 断言需要注意以下 3 点:
常见的 Predicate 断言如下表(假设转发的 URI 为 http://localhost:8001)。
断言 | 示例 | 说明 |
---|---|---|
Path | - Path=/dept/list/** | 当请求路径与 /dept/list/** 匹配时,该请求才能被转发到 http://localhost:8001 上。 |
Before | - Before=2021-10-20T11:47:34.255+08:00[Asia/Shanghai] | 在某个时间之前的请求,才会被转发。 |
After | - After=2021-10-20T11:47:34.255+08:00[Asia/Shanghai] | 在某个时间之后的请求,才会被转发。 |
Between | - Between=2021-10-20T15:18:33.226+08:00[Asia/Shanghai],2021-10-20T15:23:33.226+08:00[Asia/Shanghai] | 在某个时间段内的请求,才会被转发。 |
Cookie | - Cookie=name,kimtou | 携带 Cookie 且 Cookie 的内容为 name=kimtou 的请求,才会被转发。 |
Header | - Header=X-Request-Id,\d+ | 请求头上携带属性 X-Request-Id 且属性值为整数的请求,才会被转发。 |
Host | - Host=www.baidu.com | 当主机名为www.baidu.com的时候直接转发 |
Method | - Method=GET | 只有 GET 请求才会被转发。 |
- id: payment_routh2 #路由的ID,没有固定规则但要求唯一,建议与服务名对应
#uri: http://localhost:8001 #匹配后提供服务的路由地址(写死)
uri: lb://provider-payment #匹配后提供服务的路由地址
predicates: #断言,路径相匹配的进行路由
- Path=/payment/lb/**
- After=2022-05-23T15:47:37.485+08:00[Asia/Shanghai] #在规定时间之后的请求,才会进行路由转发
- Cookie=name,kimtou #携带Cookie
CMD 测试:
curl http://localhost:9527/payment/lb --cookie "name=kimtou"
通常情况下,出于安全方面的考虑,服务端提供的服务往往都会有一定的校验逻辑,例如用户登陆状态校验、签名校验、token 校验等。
在微服务架构中,系统由多个微服务组成,所有这些服务都需要这些校验逻辑,此时我们就可以将这些校验逻辑写到 Spring Cloud Gateway 的 Filter 过滤器中。
Spring Cloud Gateway 提供了以下两种类型的过滤器,可以对请求和响应进行精细化控制。
过滤器类型 | 说明 |
---|---|
Pre 类型 | 这种过滤器在请求被转发到微服务之前可以对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。 |
Post 类型 | 这种过滤器在微服务对请求做出响应后可以对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。 |
按照作用范围划分,Spring Cloud gateway 的 Filter 可以分为 2 类:
GatewayFilter 是 Spring Cloud Gateway 网关中提供的一种应用在单个或一组路由上的过滤器。它可以对单个路由或者一组路由上传入的请求和传出响应进行拦截,并实现一些与业务无关的功能,比如登陆状态校验、签名校验、权限校验、日志输出、流量监控等。
- id: xxxx
uri: xxxx
predicates:
- Path=xxxx
filters:
#过滤器工厂会在匹配的请求头加上一对请求头,名称为 X-Request-Id 值为 1024
- AddRequestParameter=X-Request-Id,1024
- PrefixPath=/dept #在请求路径前面加上 /dept
GlobalFilter 是一种作用于所有的路由上的全局过滤器,通过它,我们可以实现一些统一化的业务功能,例如权限认证、IP 访问限制等。当某个请求被路由匹配时,那么所有的 GlobalFilter 会和该路由自身配置的 GatewayFilter 组合成一个过滤器链。
Spring Cloud Gateway 为我们提供了多种默认的 GlobalFilter,例如与转发、路由、负载均衡等相关的全局过滤器。但在实际的项目开发中,通常我们都会自定义一些自己的 GlobalFilter 全局过滤器以满足我们自身的业务需求,而很少直接使用 Spring Cloud Config 提供这些默认的 GlobalFilter。
@Component
@Slf4j
public class MyLogGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("come in MyLogGlobalFilter:" + new Date());
ServerHttpRequest request = exchange.getRequest();
String username = request.getQueryParams().getFirst("username");
if (username == null) {
log.info("用户名为null,非法用户");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
log.info("用户名:" + username);
return chain.filter(exchange); //传递到过滤链的下一个Filter
}
@Override
public int getOrder() {
return 0; //过滤器的顺序, 0表示第一个, 让全局过滤器优先级最高
}
}