Spring Cloud笔记-Gateway新一代网关(十二)

1.概述简介

Gateway官方文档:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/

在Spring Cloud 1.x中,网关使用的是Zuul 1,在Spring Cloud 2.x中,网关使用的是Gateway,因为Zuul 2版本进展缓慢,所以Spring Cloud自己研发了网关,Gateway是原Zuul 1的替代版。Gateway采用异步非阻塞模型开发,性能上不需要担心,虽然Netflix发布了Zuul 2版本,但是Spring Cloud并没有整合的计划,所以才自己推出了Gateway的方案。

Spring Cloud Gateway是Spring Cloud的一个全新项目,基于Spring 5+Spring Boot 2.x+Project Reactor等技术开发的网关,旨在为微服务架构提供一种简单有效的统一API路由管理方式。

Spring Cloud Gateway作为Spring Cloud生态系统中的网关,目标是替代Zuul 1,在Spring Cloud 2.x版本中,没有对新版本Zuul 2最新高性能版本进行集成,仍然使用的Zuul 1非Reactor模式的老版本,为了提升网关性能,Spring Cloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层使用了高性能的Reactor模式通信框架Netty。

Spring Cloud Gateway目标是提供统一的路由方式,并且基于Filter链方式提供网关基本功能:安全、监控、指标、限流。

网关的作用:反向代理、鉴权、流量控制、熔断、日志监控等。

在微服务架构中,网关的位置,位于各个微服务的上一层,通过网关后,就访问到了具体的微服务了。

Spring Cloud Gateway特性:

  • 基于Spring Framework 5+Project Reactor+Spring Boot 2.x进行构建
  • 动态路由:能够匹配任何请求属性
  • 支持路由指定Predicate(断言)和Filter(过滤器),而且编写容易
  • 集成了Hystrix断路器功能
  • 集成Spring Cloud服务发现功能
  • 支持请求限流功能
  • 支持路径重写功能

Spring Cloud Gateway和Zuul的区别:

  • Zuul 1采用的是阻塞I/O的API网关
  • Zuul 1使用Servlet2.5的阻塞架构实现,不支持任何长连接(如WebSocket),Zuul的设计模式和Nginx比较像,每次I/O都从工作线程中选择一个执行,请求线程在工作线程完成之前一直是阻塞的,Nginx是C++实现,Zuul是Java实现,JVM在第一次的加载时候,会比较慢,所以Zuul的性能比较差
  • Zuul 2设计理念跟先进,基于Netty非阻塞和支持长连接,但是目前Spring Cloud没有整合它,Zuul 2的性能比Zuul 1性能有较大提升,Spring Cloud Gateway的性能也不错,官方测试数据表示,Spring Cloud Gateway的RPS(每秒请求数)是Zuul 1的1.6倍
  • Spring Cloud Gateway建立在Spring Framework 5+Project Reactor+Spring Boot 2.x之上,使用非阻塞API
  • Spring Cloud Gateway还支持WebSocket,与Spring紧密集成拥有更好的开发体验

Zuul 1模型缺点:Zuul 1采用传统Servlet I/O处理模型,但请求进入servlet容器时,servlet容器会为其绑定一个线程,在并发不高的情况下是适用的,当并发量增加,线程数就会增加,但是线程资源是非常昂贵的(线程上下文切换内存消耗大),会影响到请求处理时间。有些简单的业务场景,并不需要每个请求分配一个线程,简单业务的高并发下,实际可能只需要几个线程就能扛得住,因此,每个请求分配一个线程,在高并发环境下并没有优势。

Gateway模型:Gateway模型采用的是WebFlux框架,这个框架是一个典型的非阻塞异步的框架,并且在Servlet 3.1后,支持了异步非阻塞,框架的核心是基于Reactor相关API实现的。相对于传统Web框架,它可以运行在支持Servlet 3.1容器上的组件中(如Netty、Undertow等)。Spring WebFlux是Spring 5引入的新的响应式框架,区别于Spring MVC,不需要依赖Servlet API,完全异步非阻塞,并且基于Reactor来实现响应式流规范。

Spring WebFlux可以参考官网:https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html

2.三大核心概念

Route(路由)

路由是构建网关的基本模块,由ID,目标URI一系列断言和过滤器组成,如果断言为true,则匹配该路由。

Predicate(断言)

参考Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(比如请求头,请求参数等),如果请求与断言匹配,则进行路由。

Filter(过滤)

类似于Web开发中的过滤器,这里指的是Spring框架中GatewayFilter实例,使用过滤器可以在请求被路由之前或之后对请求进行修改。

总结

路由的功能是由断言和过滤组合来实现的,一个Web请求发送后,先经过网关,网关里的断言和过滤用来判断这个请求是否需要路由转发,当断言为true,过滤器放行时候,这个请求进行路由转发,此时,请求才到达具体的微服务模块。

3.Gateway工作流程

Spring Cloud Gateway Diagram

客户端向Spring Cloud Gateway发送请求,然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其Web Handler。Handler通过指定的过滤器链将请求发送到实际服务,执行业务逻辑,然后返回。如果有post类型的过滤器,执行过滤器逻辑。

pre类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等功能。

post类型的过滤器可以做响应内容、响应头的修改、日志输出、流量监控等功能。

4.入门配置

创建cloud-gateway-gateway9527模块,修改pom.xml,加入spring-cloud-starter-gateway和spring-cloud-starter-netflix-eureka-client的坐标。



    
        cloud2020
        com.atguigu.springcloud
        1.0-SNAPSHOT
    
    4.0.0
    cloud-gateway-gateway9527
    
        
            org.springframework.cloud
            spring-cloud-starter-gateway
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        
        
            org.springframework.boot
            spring-boot-devtools
            runtime
            true
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

添加application.yml配置文件。

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    register-with-eureka: true # true:将自己注册进Eureka
    fetch-registry: true # true:需要去注册中心获取其他服务地址
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

添加主启动类。

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GatewayMain9527.class, args);
    }
}

通过yml给Gateway添加路由规则。

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
        # - 是yml语法,代表数组的意思
        - id: payment_route1 # 路由ID,没有固定规则,需要保证唯一
          uri: http://localhost:8001 # 路由到哪个地址,实际提供微服务的地址
          predicates:
            - Path=/payment/get/** # 路径匹配断言
        - id: payment_route2 # 路由ID,没有固定规则,需要保证唯一
          uri: http://localhost:8001 # 路由到哪个地址,实际提供微服务的地址
          predicates:
            - Path=/payment/loadbalance # 路径匹配断言
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    register-with-eureka: true # true:将自己注册进Eureka
    fetch-registry: true # true:需要去注册中心获取其他服务地址
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

相比之前的yml,这里的yml多了Gateway结点,配置这个的目的在于隐藏localhost:8001的地址,如果想访问payment8001服务的/payment/get/**地址,之前需要访问http://localhost:8001/payment/get/1,配置了路由之后,访问http://localhost:9527/payment/get/1也可以实现,于是,原来的8001端口就隐藏了。

配置路由有两种方式,一种是上面的yml配置文件,另一种是是编码方式配置,uri中记得带http,否则访问不到,这里的uri可以写任意地址,代表一个路由规则,访问path路径的时候,被路由到uri地址。

package com.atguigu.springcloud.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
        return routeLocatorBuilder.routes()
                // 当访问http://localhost:9527/payment/get/**的时候,请求会被路由到http://localhost:8001/payment/get/**
                .route("payment_route1", r -> r.path("/payment/get/**").uri("http://localhost:8001/payment/get/**"))
                // 当访问http://localhost:9527/payment/loadbalance的时候,请求会被路由到http://localhost:8001/payment/loadbalance
                .route("payment_route2", r -> r.path("/payment/loadbalance").uri("http://localhost:8001/payment/loadbalance"))
                .build();
    }
}

5.通过微服务名实现动态路由

现在存在的问题:真实服务地址写死了,我们应该通过服务名来找服务,而不是通过服务地址找服务。

默认情况下,Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由转发,从而实现动态路由的功能。

为了演示网关的负载均衡,修改Provider8001和Provider8002的yml配置文件,将它们的注册地址改成eureka7001的地址。启动Eureka7001和Provider8001和Provider8002服务。修改Gateway9527模块的application.yml配置文件,修改为如下内容。因为要修改application.yml,所以先把GatewayConfig类进行屏蔽,避免产生影响。

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
        # - 是yml语法,代表数组的意思
        - id: payment_route1 # 路由ID,没有固定规则,需要保证唯一
          # uri: http://localhost:8001 # 路由到哪个地址,实际提供微服务的地址
          uri: lb://cloud-payment-service # 使用微服务名称查找路由地址,lb是用于识别负载均衡的前缀,不能修改
          predicates:
            - Path=/payment/get/** # 路径匹配断言
        - id: payment_route2 # 路由ID,没有固定规则,需要保证唯一
          # uri: http://localhost:8001 # 路由到哪个地址,实际提供微服务的地址
          uri: lb://cloud-payment-service # 使用微服务名称查找路由地址,lb是用于识别负载均衡的前缀,不能修改
          predicates:
            - Path=/payment/loadbalance # 路径匹配断言
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称进行路由
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    register-with-eureka: true # true:将自己注册进Eureka
    fetch-registry: true # true:需要去注册中心获取其他服务地址
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

通过浏览器访问http://localhost:9527/payment/loadbalance,在页面端可以看到端口号的不断变化,如果要实现负载均衡,注意uri中的lb是固定的,它是识别开启负载均衡的标志。

6.Predicate的使用

在Gateway9527启动的时候,在Console可以看到如下内容,代表执行加载Predicate,我们目前使用的Predicate是Path。

2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [After]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Before]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Between]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Cookie]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Header]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Host]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Method]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Path]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Query]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [ReadBodyPredicateFactory]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [RemoteAddr]
2020-06-17 07:31:15.501  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [Weight]
2020-06-17 07:31:15.502  INFO 10848 --- [  restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator    : Loaded RoutePredicateFactory [CloudFoundryRouteService]

官方文档:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#gateway-request-predicates-factories

Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。

Spring Cloud Gateway包括许多内置的Route Predicate Factory,这些Predicate斗鱼HTTP请求的不同属性匹配,多个Route Predicate Factory可以进行组合。

Spring Cloud Gateway创建Route对象时,使用Route Predicate Factory创建Predicate对象,Predicate对象可以赋值给Route。

根据官方文档里的说明,在application.yml里配置上即可生效,这相当于加了一个条件,条件满足就做路由转发,不满足就不路由转发。

简单说下吧,After Route Predicate、Before Route Predicate、Between Route Predicate需要用到一个DateTime值,这个值要使用ZonedDateTime类来获取。Cookie Route Predicate、Header Route Predicate、Host Route Predicate的测试使用cmd来测试,通过发送curl命令即可完成,具体可以看一下curl命令的参数介绍。

测试Cookie Route Predicate
curl http://localhost:9527/payment/loadbalance --cookie "username=wangshaoyang"
测试Header Route Predicate
curl http://localhost:9527/payment/loadbalance -H "X-Request-Id:123"
测试Header Route Predicate
curl http://localhost:9527/payment/loadbalance -H "Host: www.atguigu.com"

Method Route Predicate是根据GET或POST做断言,Path Route Predicate就是我们最初使用的方式,Query Route Predicate是根据请求里所带Query参数进行判断的,RemoteAddr Route Predicate是对请求发起ip做的校验,Weight Route Predicate是给负载均衡分配权重的,可以指定请求发到某一个uri的权重。

所以说,Predicate可以提供一组匹配规则,每当一个请求过来的时候,去检验这些规则,如果满足规则,进行路由,否则,就提示出错。

7.Filter的使用

路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用,Spring Cloud Gateway内置了多种路由过滤器,它们是通过Gateway Filter Factory产生的。

从生命周期来分类,可以分为Pre和Post,类似于Spring AOP的前置通知和后置通知。从种类上来分类,可以分为Gateway Filter和Global Filter。具体可以参考官网,这个的配置也是在application.yml里加配置即可。

Gateway Filter:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#gatewayfilter-factories

Global Filter:https://cloud.spring.io/spring-cloud-gateway/2.2.x/reference/html/#global-filters

下面来介绍自定义全局过滤器,我们可以根据业务需求做定制化处理。

package com.atguigu.springcloud.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class MyGatewayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取Request,获取参数
        MultiValueMap queryParams = exchange.getRequest().getQueryParams();
        System.out.println(queryParams);
        // 这里可以对queryParams里的值做一些校验
        if ("".equals(queryParams.getFirst("username"))) {
            System.out.println("非法用户");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            // 表示该请求已经处理完成
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    /**
     * 过滤器的优先级,值越小优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

 

你可能感兴趣的:(Spring,Cloud)