zoukankan      html  css  js  c++  java
  • Spring Cloud Gateway入门demo

    Spring Cloud Gateway入门demo

    网关描述

    ​ 在微服务的架构中,每一个服务都是在独立的运行的,而一个完整的微服务系统,都是由这些一个个独立运行的服务组成的。每个服务各施其职。各个微服务之间的联系通过REST API或者RPC完成通信。 比如一个场景是: 用户要查看一个商品信息,我们知道一个商品的页面会有: 商品的信息,广告,评论,库存等等。到这里就会涉及到有4个服务了,如果我们没有网关的话,可能就要调用多个服务去获取信息,但是可能会出现一些问题,问题如下:

    • 客户端需要发起多次请求,增加了网络通信的成本及客户端处理的复杂性。(如:多个服务的话就要知道多个服务的url地址)
    • 服务的鉴权会分布在每个微服务中处理,客户端对于每个服务的调用都需要重复鉴权。
    • 在后端的微服务架构中,可能不同的服务采用的协议不同,比如有 HTTP、RPC 等。客户端如果需要调用多个服务,需要对不同协议进行适配

    网关的功能

    • 性能:API高可用,负载均衡,容错机制。
    • 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。
    • 日志:日志记录(spainid,traceid)一旦涉及分布式,全链路跟踪必不可少。
    • 缓存:数据缓存。
    • 监控:记录请求响应数据,api耗时分析,性能监控。
    • 限流:流量控制,错峰流控,可以定义多种限流规则。
    • 灰度:线上灰度部署,可以减小风险。
    • 路由:动态路由规则

    常见的网关方案:

    • OpenResty(Nginx+lua)
    • Kong,是基于openresty之上的一个封装,提供了更简单的配置方式。 它还提供了付费的商业插件
    • Tyk(开源、轻量级),Tyk 是一个开源的、轻量级的、快速可伸缩的 API 网关,支持配额和速度限制,支持认证和数据分析,支持多用户多组织,提供全 RESTful API。它是基于go语言开发的组件。
    • Zuul,是spring cloud生态下提供的一个网关服务,性能相对来说不是很高
    • Spring Cloud Gateway,是Spring团队开发的高性能网关

    Spring Cloud Gateway概述:

    spring-cloud-Gatewayspring-cloud的一个子项目。而zuul则是netflix公司的项目,只是spring将zuul集成在spring-cloud中使用而已。因为zuul2.0连续跳票和zuul1的性能表现不是很理想,所以催生了spring团队开发了Gateway项目。

    zuul1.x和spring gateway对比:

    • Zuul1.x构建于 Servlet 2.5,兼容 3.x,使用的是阻塞式的 API,不支持长连接,比如 websockets。
    • Spring Cloud Gateway构建于 Spring 5+,基于 Spring Boot 2.x 响应式的、非阻塞式的 API。同时,它支持 websockets,和 Spring 框架紧密集成,开发体验相对来说十分不错。

    ​ 注意:现在zuul2.x已经开发出来了。但是spring cloud没有将zuul2.x集成到spring cloud当中,现在的spring cloud zuul组件还是1.x的。所以用网关还是优先使用spirng cloud gateway把

    spring cloud gateway组成和执行过程

    ​ spring cloud 的路由信息可以通过RouteDefinition 这个类去查看。 这个类里面包含了 id, predicates断言, filters过滤器,uri 转发的地址等等这几个的成员变量,当请求到达网关时 ,首先会基于predicates断言去判断该请求满不满足需求,满足的话就进行下面一系列的filter , 然后再转发到目标uri去

    image-20201004145935412

    spring cloud gateway的demo搭建

    pom.xml

    <dependencies>
           <dependency>
               <groupId>org.springframework.cloud</groupId>
               <artifactId>spring-cloud-dependencies</artifactId>
               <version>Hoxton.SR4</version>
               <type>pom</type>
               <scope>import</scope>
           </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
       		</dependency>
    </dependencies>
    

    注意:Hoxton.SR4这个版本是需要添加spring-boot-starter-validation的,不然会报错的。其他版本不清楚会不会

    yml配置:

    spring:
      application:
        name: gateway-demo
      cloud:
        gateway:
          enabled: true
          routes: 
          #路径的匹配,StripPrefix代表信
          - id: path_route
            predicates:
            - Path=/baidu
            filters:
            - StripPrefix=1  
            uri: https://www.bilibili.com
            #cookie的匹配
          - id: cookie_route
            predicates:
            - Cookie=chocolate,ch.p   
            uri: https://www.bilibili.com
          #请求头匹配
          - id: header_route
            predicates:
            - Header=X-Request-Id, d+
            uri: https://www.bilibili.com   
          # 组合匹配
          - id: compose
            predicates:
            - Path=/compose
            - Header=name, cong
            uri: https://www.bilibili.com
            filters:
            - StripPrefix=1
    

    ​ 可以看到有许多的路由信息,大概都是会有:id,predicates,filters,uri这几个参数。

    注意点:

    例如下面这个例子:

     - id: path_route1
            predicates:
            - Header=name, cong
            uri: https://www.bilibili.com
          - id: path_route2
            predicates:
            - Path=/abc
            filters:
            - StripPrefix=1  
            uri: https://baidu.com
     
    

    image-20201004154147875

    ​ 至于为什么为什么没跳到bilbili,是因为最终访问的是https://www.bilibili.com/abc 所以肯定是返回出错的.这里也验证了注意点的第二点了

    断言和过滤器配置方式:

    ​ 断言和过滤配置方式分成两种:一种是Shortcut Configuration,一种是Fully Expanded Arguments,以下的配置方式功能是相同的,都是如果cookie存在mycookie=mycookievalue的话,断言判断成功的。

          - id: Fully_Expanded
            predicates:
            - name: Cookie
              args:
                name: mycookie
                regexp: mycookievalue
            uri: https://www.bilibili.com
          - id: short_cut
            predicates:
            - Cookie=mycookie,mycookievalue
            uri: https://www.bilibili.com
    

    至于args的信息在哪里找。因为是通过Cookie断言的,所以可在CookieRoutePredicateFactory.Config类上面看到相应的参数。

    断言的解析

    ​ 常用的断言有:

    • 路径匹配,实现的类是PathRoutePredicateFactory
    • cookie匹配, 实现的类是CookieRoutePredicateFactory
    • 头部匹配, 实现的类是HeaderRoutePredicateFactory

    其他的可以去到官网上面看。

    官网地址:https://docs.spring.io/spring-cloud-gateway/docs/3.0.0-SNAPSHOT/reference/html/#gateway-request-predicates-factories

    自定义断言

    ​ 自定义一个自己的断言,其实这个断言跟Header头部匹配差不多一样的。功能就是判断头部有没有key为Authorization,value为token

    仿照HeaderRoutePredicateFactory,写了一个自己的自定义类

    代码实现:

     * project name : cloud-demo
     * Date:2020/10/3
     * Author: yc.guo
     * DESC:  需要头部带上authentication才能让其通过
     */
    @Component
    public class AuthRoutePredicateFactory extends AbstractRoutePredicateFactory<AuthRoutePredicateFactory.Config> {
    
        public AuthRoutePredicateFactory() {
            super(AuthRoutePredicateFactory.Config.class);
        }
    
        public List<String> shortcutFieldOrder() {
            return Arrays.asList("name", "value");
        }
        
        @Override
        public Predicate<ServerWebExchange> apply(AuthRoutePredicateFactory.Config config) {
            return serverWebExchange -> {
                List<String> list = serverWebExchange.getRequest().getHeaders().get(config.name);
                if(list!=null && list.size() > 0 && list.contains(config.value)){
                    return true;
                }
                return false;
            };
        }
        @Validated
        public static class Config {
            @NotEmpty
            private String name;
    
            private String value;
    
            public String getName() {
                return name;
            }
    
            public AuthRoutePredicateFactory.Config setName(String name) {
                this.name = name;
                return this;
            }
    
            public String getValue() {
                return value;
            }
    
            public AuthRoutePredicateFactory.Config setValue(String value) {
                this.value = value;
                return this;
            }
    
            public Config() {
            }
            
        }
    }
    

    yml:

          - id: define
            predicates:
            - Auth=Authorization,token
            uri: https://www.bilibili.com
    

    注意点:

    • 要加上@Component,注入到容器中
    • 至于Auth=Authorization,token中的Auth表达式是怎样得到的,程序会把实现类AuthRoutePredicateFactory后面RoutePredicateFactory的这部分截取掉,最后就只剩下Auth,所以就是这样子得到的Auth表达式的
    • 如果想使用shortcut配置方式的话,要重写shortcutFieldOrder这个方法。比如上面的配置- Auth=Authorization,token , 因为我重写的实现是这样的Arrays.asList("name", "value"); 最后按照顺序来解析就成了: name=Authorization value=token

    过滤器

    ​ Filter分为全局过滤器和路由过滤器。路由过滤器只是针对单个路由的,全局过滤器是针对于所有的路由的,优先级应该是路由过滤器先执行,再到全局过滤器执行

    路由过滤器

    RouteFilter路由过滤器基本有:

    • StripPrefix - 实现类:StripPrefixGatewayFilterFactory 。

      列子:路径是http://127.0.0.1/a/b/c/d ,StripPrefix=2的话 ,得到的结果http://127.0.0.1/c/d

    • 限流过滤器RequestRateLimiter-实现类:RequestRateLimiterGatewayFilterFactory,注意:这个类不能使用shortCut方式因为他没有实现shortcutFieldOrder

    限流过滤器

    ​ 限流过滤器通过redis还有令牌桶算法去实现的。 令牌桶算法简单的可以把他认为是: 一个很有特色的奶茶店,但是他是每天只能供应50杯奶茶。 那每天只卖50杯了,想喝的话明天请早。令牌桶的意思就是比如,每秒钟可以补充10个令牌桶(),总令牌桶容量可以达到30个。

    第一秒内拿走了25个 30-25=5; 第一秒之后补充10个:15个

    第二秒没人拿走: 15 第二秒后补充10个: 15+10=25

    第三秒没人拿走: 25 第三秒后补充10个: 这里因为容量是30,所以就是30

    第四秒需要拿走35: 0,还有5个吃闭门羹了 第四秒后补充10个: 10个

    pom.xml文件:

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
            </dependency>
    

    yml配置:

              filters:
                - StripPrefix=1
                - name: RequestRateLimiter
                  args:
                    deny-empty-key: true
                    keyResolver: '#{@ipAddressKeyResolver}'     #通过ip作为key值
                    redis-rate-limiter.replenishRate: 1         #每秒补充的令牌数
                    redis-rate-limiter.burstCapacity: 2			#令牌容量大小                
                    
    
    #redis的配置
    spinrg:
      redis:
        host: 127.0.0.1
        port: 6379
    
    • ​ KeyResolver默认的实现是PrincipalNameKeyResolver,它从ServerWebExchange中检索Principal,并调用Principal.getName()方法。如果你直接限流请求这个路径的所有请求,keyResolver感觉用默认就行了。当然也有一种比较常见的keyResolver情况,比如: ?userId=admin 这种带参数的,这里我们也可以,拿取userId去做为key,这样就可以做到用户的限流。

    ​ 注意点: redis-rate-limiter.replenishRate:2 , redis-rate-limiter.burstCapacity: 1 这样子虽然每秒补充2个,但是容量只有1个的话,也是只允许一个请求,所以这里还是一秒钟只能访问一个请求。

    ipAddressKeyResolver:

    @Component
    public class ipAddressKeyResolver implements KeyResolver {
    
        @Override
        public Mono<String> resolve(ServerWebExchange exchange) {
            return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
        }
    }
    
    

    现象如下:

    spring-cloud-gateway限流现象截图

    可以看到如果不允许访问的时候会返回一个429的状态码。HTTP 429 - Too Many Requests

    自定义一个自己的路由过滤器

    实现的功能是路由的时候打印一下日志:

    /**
     * project name : cloud-demo
     * Date:2020/10/4
     * Author: yc.guo
     * DESC:
     */
    @Component
    public class LogsGatewayFilterFactory extends AbstractGatewayFilterFactory<LogsGatewayFilterFactory.Config> {
    
    
        Logger logger= LoggerFactory.getLogger(LogsGatewayFilterFactory.class);
    
        @Override
        public List<String> shortcutFieldOrder() {
            return Arrays.asList("name");
        }
    
        public LogsGatewayFilterFactory() {
            super(LogsGatewayFilterFactory.Config.class);
        }
    
        @Override
        public GatewayFilter apply(Config config) {
            return (exchange,  chain) ->{
                logger.info("pre: 执行前的日志!" + config.name);
                chain.filter(exchange);  //继续走下去的方法
                logger.info("post: 执行后的日志" + config.name);
                return Mono.empty();
            };
        }
    
    
        public static class Config {
            String name;
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
        }
    }
    
    

    yml

      - id: logs_filter
            predicates:
            - Path=/logs/orders
            filters:
            - StripPrefix=1  
            uri: http://127.0.0.1:8071
    

    这个注意的点和上面的自定义断言的一样的。要按照上面自定义断言的注意点去做。

    全局过滤器

    全局过滤器常用到的有:

    • 负载均衡的全局过滤器

    负载均衡全局过滤器

    ​ spirng cloud集成的负载均衡器有ribbon,所以gateway应该是可以无缝对接ribbon的。ribbon可以从yml配置文件上得到负载均衡的地址,也可以读取eureka上面得到负载均衡的地址,

    spirng cloud gateway和ribbon集成

    pom.xml

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

    application.yml:

          #结合ribbon的负载均衡
          - id: lb_fiter
            predicates:
            - Path=/lb/**
            filters:
            - StripPrefix=1
            uri: lb://service1
    service1:
      ribbon:
        listOfServers: 127.0.0.1:8071        
    

    提醒:uri是lb://serviceId ,负债均衡需要lb://开头才能识别得鸟

    官网上建议使用ReactiveLoadBalancerClientFilter,只需要这样spring.cloud.loadbalancer.ribbon.enabledtofalse`就行了

    注意点:

    ​ 如果通过文件配置的方式实现负载均衡,这个不能注册上eureka。一注册上了服务地址就会读取eureka上面了。不会读取本地配置文件了。之前我实现ribbon成功了,然后去实验去读取eureka的时候,然后回过头去测试ribbon就不行了

    spirng cloud gateway通过读取eureka上的地址列表实现负载均衡

    pom.xml

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
    
    @SpringBootApplication
    @EnableEurekaClient //开启注册到eureka上
    public class GatewayApp {
        public static void main(String[] args) {
            
            SpringApplication.run(GatewayApp.class,args);
        }
    }
    
    spring:
      application:
        name: gateway-demo
      cloud:
        gateway:
          enabled: true
          routes: 
          #结合eureka的负载均衡
          - id: discovery_filter
            predicates:
            - Path=/eureka/lb/**
            filters: 
            - StripPrefix=2
            uri: lb://eureka-client
            #设置发现读取远端地址为true
          discovery:
            locator:
              enabled: true          
              
    

    这里也是需要lb://开头的

    默认情况下,当一个服务实例在LoadBalancer中没有找到时,将返回503。你可以通过配spring.cloud.gateway.loadbalancer.use404=true来让它返回404。

    动态路由

    ​ 如果使用正常的配置方式,我们都是通过配置application.yml,来配置路由的,但是这样不太好的就是如果我们需要改变路由信息的话,就得需要重新修改application.yml并且重新启动项目才能让路由生效(我现在的公司用的zuul,也是这样,这样做的话缺点很明显的,因为你修改一次路由就得重启一次项目)。

    ​ 按照正常的设定我的想法是:spring cloud通过读取配置文件的路由信息,创建了一些路由对象放到内存中,然后当请求网关时,再通过这些路由信息进行一个处理。当项目启动的时候,那我们也可以直接修改他内存里的路由信息,不就可以完成动态路由了吗?

    ​ spring cloud真的提供了一个接口出来给我们做路由信息的管理,接口名字就是:RouteDefinitionRepository,而默认的实现类就只有一个:InMemoryRouteDefinitionRepository(利用内存管理路由信息)。下面这个图是RouteDefinitionRepository的结构图:

    image-20201007163706036

    ​ 动态路由实际上运行的流程:

    • 首先读取配置文件的路由信息和RouteDefinitionRepository的路由信息,加载到内存中
    • 然后定时去读取RouteDefinitionRepository的路由信息(默认30s),做一个合并。提示:RouteDefinitionRepository里面不会存有配置文件配置的路由信息

    使用redis来存储路由信息的类:(通过仿照InMemoryRouteDefinitionRepository来实现的)

    @Component
    public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
    
        private final static String GATEWAY_ROUTE_KEY="gateway_dynamic_route";
    
        Logger logger= LoggerFactory.getLogger(RedisRouteDefinitionRepository.class);
    
    
    
        private ObjectMapper objectMapper = new ObjectMapper();
        
        
        @Autowired
        private RedisTemplate<String,String> redisTemplate;
    
        @Override
        public Flux<RouteDefinition> getRouteDefinitions() {
            List<RouteDefinition> list = new ArrayList();
            redisTemplate.opsForHash().values(GATEWAY_ROUTE_KEY).stream().forEach(route ->{
                try {
                    list.add(objectMapper.readValue((String) route,RouteDefinition.class));
                }catch (Exception e){
                    logger.error(e.getMessage(),e);
                    throw new RuntimeException("解析失败");
                }
            });
            return Flux.fromIterable(list);
        }
    
        @Override
        public Mono<Void> save(Mono<RouteDefinition> route) {
            return route.flatMap(routeDefinition -> {
                    try {
                        System.out.println(objectMapper.writeValueAsString(route));
                        redisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY,
                                routeDefinition.getId(),
                                objectMapper.writeValueAsString(routeDefinition));
                    }catch (Exception e){
                        logger.error(e.getMessage(),e);
                        throw new RuntimeException("保存失败");
                    }
                    return Mono.empty();
                }
            );
        }
    
        @Override
        public Mono<Void> delete(Mono<String> routeId) {
            return routeId.flatMap( id -> {
                redisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY,id);
                return Mono.empty();
            });
        }
    }
    
    

    ​ 存储redis的数据结构,使用hash来存储。value我是直接使用json格式来存储路由信息,路由信息的json格式可以参考一下:actuator下面添加路由信息的body里面的结构。

    存储redis的数据结构:image-20201007164927225

    使用nacos来存储路由信息的类:(等我学习完nacos时补上)

    
    
    

    提示:如果我们没有引入actuator的话,我们可以直接操作存储的媒介来达到目的,比如redis,nacos。

    按照官网的说法,可以通过Restful Api来管理动态路由的信息,不过需要引入spring-boot-starter-actuator。
    xml:

      <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
       </dependency>
    

    配置文件:

    management.endpoint.gateway.enabled=true # default value
    management.endpoints.web.exposure.include=gateway
    

    查看所有的路由信息:

    http://127.0.0.1:8080/actuator/gateway/routes

    添加路由信息

    post请求,url: http://127.0.0.1:8080/actuator/gateway/routes/first_route

    body

    {
      "id": "first_route",
      "predicates": [{
        "name": "Path",
        "args": {"_genkey_0":"/first"}
      }],
      "filters": [{
          "name" : "StripPrefix",
          "args":{"parts":"1"}
      }],
      "uri": "https://www.bilibili.com",
      "order": 0
    }
    

    删除一个路由信息(delete请求)

    http://127.0.0.1:8080/actuator/gateway/routes/first_route

    刷新加载RouteDefinitionRepository的路由信息到缓存中(post请求):

    http://127.0.0.1:8080/actuator/gateway/refresh

    一些小发现:查看路由信息:

    image-20201007170154214

    ​ 这两个路由信息我是没有没有配的,但他却出现在路由信息上面,按照我的想法,应该是如果配置了读取eureka上的地址列表实现负载均衡的话,网关就会读取eureka上面的apps信息,并且把apps信息转换成相应的路由信息,保存到内存上去。

  • 相关阅读:
    C# 之 FTPserver中文件上传与下载(一)
    net-snmp-5.7.3配置编译安装
    Linux下编译安装Apache Http Server
    linux回收站设计
    String封装——读时共享,写时复制
    4-python学习——数据操作
    3-python学习——变量
    2-python学习——hello world
    1 python学习——python环境配置
    04-树7. Search in a Binary Search Tree (25)
  • 原文地址:https://www.cnblogs.com/dabenxiang/p/13778128.html
Copyright © 2011-2022 走看看