同样是网关gateway取代了zuul,我们项目使用了这么多gateway特性!你有没有中标|Java 开发实战

3,218 阅读12分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接

相关文章

主流的四种限流策略,我都可以通过redis实现

分布式系列之网关zuul包揽全局--再也不用东拼西凑各个接口啦

特性

gateway的诞生是因为zuul2.0一直跳票,既然这样gateway可以说是zuul的替代品。既然是替代品功能肯定是包含了zuul的。

  • 上面zuul的章节主要介绍了网关的动态路由。gateway肯定也是支持的。
  • 除了路由外zuul还有四种过滤器同样gateway也有相应的过滤器
  • zuul内部携带hystrix , gateway除了和hystrix整合实现熔断、降级、限流外,内部基于令牌桶实现限流
  • zuul、gateway都支持服务发现进行路由转发

前置条件

SpringCloud Gateway是基于webflux的非阻塞式的网关。 所以gateway的环境要求 Springboot 2.x+、spring webflux、project reactor

因为是非阻塞式,所以和我们之前的阻塞式框架会有所不同。

专业名词

名词解释
Route网关的基本组成部分。它由一个ID、一个目标URI、一组谓词和一组过滤器定义。如果聚合谓词为真,则匹配路由
Predicate这是一个Java 8函数谓词。输入类型是Spring Framework serverwebeexchange。这允许您匹配HTTP请求中的任何内容,比如头或参数
Filter这些是使用特定工厂构造的GatewayFilter实例。发送下游请求之前或之后修改请求和响应

快速入门

点我官网快速开始

  • 新建模块geteway 并制定端口9091

  • 引入坐标

	<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
  • 配置路由
spring:
  cloud:
    gateway:
      routes:
        - id: route
          uri: http://localhost:8001
          predicates:
            - Path=/payment/get/**
  • 接口测试 : 我们访问http://localhost:9091/payment/get/1 会出现8001payment的信息

  • 下面我们详细来看看gateway的路由规则

路由规则

Springcloud网关匹配路由作为Spring WebFlux HandlerMapping基础设施的一部分。Spring Cloud Gateway包括许多内置的路由前置工厂。所有这些都匹配HTTP请求的不同属性。我们可以将多个路由谓词工厂与逻辑和语句组合在一起

image-20210506135529574

  • 我们也可以看出springcloud gateway内置了很多匹配工厂。我们上面的案列就是通过PathRoutePredicateFactory来实现的

path

spring:
  cloud:
    gateway:
      routes:
        - id: route
          uri: http://localhost:8001
          predicates:
            - Path=/payment/get/**

query

spring:
  cloud:
    gateway:
      routes:
        - id: route
          uri: http://localhost:8001
          predicates:
            - Path=/payment/get/**
            - Query=green
  • http://localhost:9091/payment/get/1 接口将访问不了数据,必须添加green参数 http://localhost:9091/payment/get/1?green

  • 光有参数还是不行,加入我们要求green参数必须是数组。我们可以如下配置

spring:
  cloud:
    gateway:
      routes:
        - id: route
          uri: http://localhost:8001
          predicates:
            - Path=/payment/get/**
            - Query=green,\d+
  • http://localhost:9091/payment/get/1?green=123s 不能访问 , http://localhost:9091/payment/get/1?green=123 则可以

datetime

  • 当我们在开发限时抢购接口是就可以借助datetime功能来实现
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: http://localhost:8001
          predicates:
            - Path=/payment/get/**
            - Query=green,\d+
            - After=2017-01-20T17:42:47.789-07:00[America/Denver]

ip

- RemoteAddr=10.0.20.132/140
  • 关于这个远程地址我们实际上就是填写ip , 后面的斜杠表示一个范围ip , 上面的配置表示10.0.20.132-10.0.20.140 这个段的ip都可以访问

cookie

- Cookie=zxhtom, hello
  • 请求中携带zxhtom=hello , 的cookie可以访问

服务发现

  • 在zuul中整合服务发现后有默认的路由规则的。同样的gateway也可以接入服务发现这里还是以eureka为列。

  • 这里有关服务发现的配置我就不再赘述了。在eureka专题已经介绍了,在hystrix、zuul等专题页有相关的配置,我就直接贴出gateway的开启配置

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
  • 默认的路由规则是基于在eureka中注册名进行访问 。 比如访问payment 就是如下地址http://localhost:9091/cloud-payment-service/payment/get/2
  • 内部也是ribbon进行负载均衡的。有个小问题是gateway启动时获取服务列表后续好像没有在更新服务列表。

过滤器

  • 关于gateway内置过滤器真的是很多很多。我们这里也不能一个一个的举例。下面我们看看和zuul对应的几个过滤器。在看之前我们先大概看看gateway内置有哪些

image-20210506184914527

  • 除了局部过滤器,gateway还内置几个全局过滤器

image-20210506185225830

RewiritePathGatewayFilterFactory

  • 还记得我们在zuul专题中实现了默认路由转发规则吗,就是将CLOUD-PAYMENT-SERVICECLOUD-ORDER-SERVICE 这些微服务默认代理为paymentorder 的服务前缀。在gateway中他也为我们提供了相同的功能即服务转发。但是他是针对具体的微服务的。我们可以通过自定义全局过滤器来实现zuul中的功能。下面我们来试试通过RewiritePathGatewayFilterFactory 实现它

  • 为了突出核心配置,这里我只放部分配置,需要完整的可以参考上下文。或者直接看源码。源码会在文章末尾放出

		- id: order
          uri: lb://CLOUD-ORDER-SERVICE
          predicates:
            - Query=green
          filters:
            - RewritePath=/order/(?<segment>.*), /$\{segment}
  • 和zuul的配置差不多,通过正则匹配我们的请求,然后将我们需要的部分进行抽取然后路由到真实服务进行调用

PrefixPathGatewayFilterFactory

  • 和上面``RewiritePathGatewayFilterFactory 类似而又不同的是,PrefixPathGatewayFilterFactory 是在我们匹配接口上统一加上前缀。比如如下配置
		- id: pre_route
          uri: http://localhost:8001
          predicates:
            - Path=/getTimeOut/**
          filters:
            - PrefixPath=/payment
  • http://localhost:9091/getTimeOut/123 访问这个接口的时候,我们首先通过routes 中配置匹配到pre_route 然后在接口前添加payment然后路由到真实服务上。 这里需要注意的是我们的匹配是从上至下的所以我们配置的时候注意下顺序

StripPrefixGatewayFilterFactory

  • 这个过滤器的功能就是将我们的接口进行分割,然后在路由。其实gateway为我们提供了很多的过滤器使用起来也是很方便的。我们基本上看官网提供的案列我们就能够知道如何使用了。重要的我们得了解他们内部的设计。设计才是王者
		- id: pre_route
          uri: http://localhost:8001
          predicates:
            - Path=/zxhtom/**
          filters:
            - StripPrefix=2
  • 访问http://localhost:9091/zxhtom/lqj/payment/getTimeOut/123 实际上会转发到http://localhost:8001/payment/getTimeOut/123上。注意我们uri的协议,如果是lb表示是服务发现。这里我们配置的是单节点

SetPathGatewayFilterFactory

		- id: pre_route3
          uri: http://localhost:8001
          predicates:
            - Path=/hello/{segment}
          filters:
            - SetPath=/payment/get/{segment}
  • 说白了还是将请求重新转发。只不过这边更加的生硬。上述就是将http://localhost:9091/hello/123 转发到http://localhost:8081/payment/get/123

自定义网关过滤器

  • 上面我们简单了解了gateway为我们提供的过滤器。但是内置的永远是满足不了所有的需求的。不管是为了应对需求还是满足自己的虚荣心。我们都应该来看看如果实现自己的过滤器

GatewayFilterFactory

image-20210506190710838

  • gateway内置了很多网关过滤器。我们只需要参照他们内置的过滤器实现就可以了。
  • 不知道你有没有注意到类如RewritePathGatewayFilterFactory 这些过滤器在配置的时候是配置成RewritePath 后面的GatewayFilterFactory是没有的。
  • 这是为什么呢,我们先不关这个我们依葫芦画瓢。
  • 现在我们有个需求需要实现登陆验证过滤器。实现逻辑简单点就验证用户名和密码是否匹配。用户名密码在地址中配置

新建过滤器类

@Component
public class LoginPathGatewayFilterFactory extends AbstractGatewayFilterFactory<LoginPathGatewayFilterFactory.Config> {
    public LoginPathGatewayFilterFactory() {
        super(Config.class);
    }
    @Override
    public GatewayFilter apply(Config config) {
        return (exchage,chain)->{
            String userName = config.getUserName();
            String password = config.getPassword();
            String requestUserName = exchage.getRequest().getQueryParams().getFirst("userName");
            String requestPassword = exchage.getRequest().getQueryParams().getFirst("password");
            if (userName.equals(requestUserName) && password.equals(requestPassword)) {
                return chain.filter(exchage);
            } else {
                throw new RuntimeException("用户名错误密码。。。。");
            }
        };
    }
    public static class Config {

        private String userName;

        private String password;

        public String getUserName() {
            return userName;
        }

        public Config setUserName(String userName) {
            this.userName = userName;
            return this;
        }

        public String getPassword() {
            return password;
        }

        public Config setPassword(String password) {
            this.password = password;
            return this;
        }
    }
}

新增配置

		- id: pre_route4
          uri: http://localhost:8001
          predicates:
            - Path=/login/{segment}
          filters:
            - name: LoginPath
              args:
                userName: zxhtom
                password: test
            - SetPath=/payment/get/{segment}

测试

image-20210506193533580

  • 当我们的用户名和密码不一致时就会抛出异常。如果用户名和密码和我们指定的账户相同那么就会放心至第二个过滤器进行路由转发。

GatewayFilter

  • 在上面第一种方式中我们能够观察到最终是生成GatewayFilter 对象。实际上GatewayFilter 才是真正参与过滤的对象。

构建

@Component
public class CustomLoginGatewayFilter implements GatewayFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("我进过滤器啦。。。");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

注册

@Configuration
public class RouteConfig {
    @Bean
    public RouteLocator customRouteLocator(CustomLoginGatewayFilter customLoginGatewayFilter,RouteLocatorBuilder builder) {
        return builder.routes()
                .route("path_route", r -> r.path("/baidu")
                        .uri("https://www.baidu.com")
                .filter(customLoginGatewayFilter))
                .build();
    }
}

测试

image-20210506194442468

自定义全局过滤器

image-20210507092458434

  • 还是那句话,想要自定义得看内置是如何实现的。诺、WebsocketRoutingFilter 是gateway内置的,他的实现和网关过滤器其中一种方式一样。
@Slf4j
@Component
public class GlobalCustomRoutingFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("我是全局过滤器。。。。。。");
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
  • 编写java类注册到spring中,我们在访问我们的gateway路由接口就会发现有日志打印了。当然这里只是演示全局过滤器生效。具体在全局过滤器我们可以用来做权限验证等等操作。
  • 在全局验证后我们可以将登陆用户信息写入到cookie中或者通过添加参数的方式传递到下游

过滤器名称

image-20210507100537676

  • 还记得我们在网关过滤器那边说为什么自定义名字要那么定义吗。看看上面这段源码你就理解了。在将过滤器已键值对注册到过滤器容器中

image-20210507101527587

限流

  • 关于限流笔者之前通过redis分别实现目前主流的四种限流算法。有兴趣的可以阅读下,阅读结束别忘记回到本文继续观看gateway哦。别走太远!!!

  • 上面我们主要在介绍gateway的用法即合成方向。但是作为网站的门户性接口流量比其他服务都大的很多。其他服务正常情况我们会依赖于hystrix 来实现对服务的降级等操作。同样我们也可以在网关层面上加入hystrix来实现服务的高可用。 但是除了hystrix以外, gateway本身内置了限流算法。下面我们来简单实现下限流。

  • 提到网关我们就一定绕不开限流。网关的作用就是代理但是并不是没有条件的任性代理。为什么保护我们的模块在网关中会对下游服务进行限流。有些接口处于安全和稳定考虑都会限制流量的

  • gateway实现限流也很简单。只需要我们引入redis相关模块在使用内置过滤器就可以了

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>	
@Component
public class HostAddrKeyResolver implements KeyResolver {
    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        //根據服务地址限流
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }
    @Bean
    KeyResolver apiKeyResolver() {
            //按URL限流
            return exchange -> Mono.just(exchange.getRequest().getPath().toString());
            }

    @Bean
    KeyResolver userKeyResolver() {
        //按用户限流
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userName"));
    }
}

image-20210531172056157

  • redis-rate-limiter.replenishRate : 令牌桶生成令牌的速度
  • redis-rate-limiter.burstCapaciry: 令牌桶容量
  • 下面我们看看redis中实时存储数据情况。上面我们通过jmeter等压测工具进行压力测试就会出现如下数据

image-20210531171802520

计数器

  • 计数器又分为固定时间窗口和滑动时间窗口两种策略。两者的区别其实就是时间间隔不一样。本质上是没有区别。
  • 但是因为滑动时间窗口时间间隔很短,给我们造成一种两者完全不一样的假象

固定时间窗口

image-20210508103957917

滑动时间窗口算法

漏铜

  • 漏桶算法就是提前准备了一个请求池,能够进入请求池就可以等待资源的分配。这样就解决了计数无法面对大流量的情况

令牌桶

  • 令牌桶是在漏桶基础上升级而来。我们仔细想一想正常请求还是需要花费一定时间的有的重要接口甚至需要1~10秒执行时间。但是我们生成令牌只是生成一串字符。和生成令牌相比接口的执行显然慢的多的多
  • 不知道读者有没有想过这样一种情况。漏桶算法在请求池满了之后突然遇到大流量这个时候该怎么办。这个时候漏桶算法就只能无情的拒绝多余的请求。
  • 而令牌桶则不一样了。令牌桶也有同样的池,但是内部存储的是生成好的令牌,同样在极限情况下如果令牌桶慢了遇到大流量会怎么样。
  • 在漏桶算法中遇到大流量就只能拒绝而且请求池状态跟更新很慢。但是令牌桶满了有大流量过来很快就会拿走令牌。接口的执行时间跟令牌桶就没有直接关系了。这个时候令牌桶中状态更新的很快。所以令牌桶更加能面对突如其来的大流量

整合hystrix

  • 还记得我们之前的hystrix服务熔断、降级专栏吗。仔细想想作为网关是不是比其他接口流量更大呢?那么如何对我们网关的接口进行服务熔断等操作呢?
  • 笔者这里参考了写资料仅实现全局后备的实现

功能接入

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

image-20210531170456893

  • 接入坐标后再我们上面实现的基础上我们直接添加过滤器就可以了。最后会将异常重定向到我们事先写好的接口中。这个接口返回 我们统一的数据结构
@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @RequestMapping("")
    public String fallback(){
        return "error";
    }
}

总结

  • 好了!关于网关部分笔者断断续续通过三篇文章分别从不同框架,不同角度解读。
  • zuul框架实现了我们项目中常用到的网关代理功能、本文是通过gateway实现项目中的网关代理
  • 在加上前两天通过redis分别实现四种主流限流策略问题。上面提到的两篇文章收获颇丰



努力加油

大哥大姐,留个赞在走呗!原创属实不易