八、Spring Cloud Gateway 微服务网关

官网: Spring Cloud Gateway

一、网关的介绍

1. 网关的整体概述

在单体架构中,随着数据量和并发量的提升,会将之拆分为多个微服务,而每个微服务需要的诸如用户权限校验、熔断、限流、日志监控等功能,如果在每个微服务内实现,肯定会有一部分的代码与逻辑的冗余。此时可以在客户端与微服务之间,建立网关,统一做这些事情并在验证通过后路由到指定微服务。

在微服务架构中,每个服务都是一个可以独立开发和运行的组件,而一个完整的微服务架构由一系列独立运行的微服务组成。其中每个服务都只会完成特定领域的功能,比如订单服务提供与订单业务场景有关的功能、商品服务提供商品展示功能等。各个微服务之间通过轻量级通信机制 REST API 或者 RPC 完成通信。 微服务之后在某些层面会带来一定的影响,比如,一个用户查看一个商品的详情,对于客户端来说,可能需要调用商品服务、评论服务、库存服务、营销服务等多个服务来完成数据的渲染。在这个场景中,客户端虽然能通过调用多个服务实现数据的获取,但是会存在一 些问题,比如:

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

2. 微服务网关的作用

所以,我们可以在微服务之前增加一个前置节点,这个节点就是网关。

对于商品详情展示的场景来说,增加了 API 网关之后,在 API 网关层可以把后端的多个服务进行整合,然后提供一个唯一的业务接口,客户端只需要调用这个接口即可完成数据的获取及展示。在网关中会再去消费后端的多个微服务进行统一的整合,给客户端返回一个唯一的响应。当然,网关不仅只是做一个请求转发以及服务整合,有了网关这个统一的入口之后,它还能提供:

  • 针对所有请求进行统一鉴权、限流、熔断、日志
  • 协议转化。针对后端多种不同的协议,在网关层统一处理后以 HTTP 协议对外提供服务
  • 统一错误码处理
  • 请求转发,并且可以基于网关实现内外网隔离
网关的作用:
性能:API高可用,负载均衡,容错机制。
安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。
日志:日志记录(spainid,traceid)一旦涉及分布式,全链路跟踪必不可少。
缓存:数据缓存。
监控:记录请求响应数据,api耗时分析,性能监控。
限流:流量控制,错峰流控,可以定义多种限流规则。
灰度:线上灰度部署,可以减小风险。
路由:动态路由规则。

3. 服务网关的要求

从上面的这个架构图来看,网关成了所有流量的入口,那么对于这样一个角色来说,它需要在某些方面有很高的要求。

  • 稳定性,
  • 安全性,防止恶意请求,以及保障数据传输的安全性
  • 高性能、可用性,
      网关作为所有流量的入口,那么对于性能这块的要求就非常高了,因为一旦网关的性能出现瓶颈,即便后端的服务性能再高,意义也不大
      网关必须要支持集群部署,这个是分布式架构的基本要求。否则网关服务挂掉就会导致整个系统不可用
  • 扩展性,可维护性,对于定制化需求方面,如何实现可扩展;

4. 常见的网关方案

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

5. 网关选型

对于网关选型,主要关注几个方面

  • 部署和维护成本
  • 开源还是闭源
  • 是否私有化部署
  • 功能是否满足当前需求
  • 社区资料的完善以及版本迭代和功能维护

整体架构:

二、Gateway使用

1. Spring Cloud Gateway的核心概念

Route 路由 (id、predicate、filter、uri)

  Route是网关的基础元素,包含ID、目标URI、断言、过滤器组成,当前请求到达网关时,会通过Gateway Handler Mapping,基于断言进行路由匹配,当断言为true时,匹配到路由进行转发

  • Predicate,断言
      学过java8的同学应该知道这个函数,它可以允许开发人员去匹配HTTP请求中的元素,一旦匹配为true,则表示匹配到合适的路由进行转发
  • Filter,过滤器
      可以在请求发出的前后进行一些业务上的处理,比如授权、埋点、限流等。 可分为前置处理器、后置处理器 PreFilter/PostFilter
    过滤器内可以进行:授权认证、限流、更改请求报文
    它的整体工作原理如下。
    其中,predicate就是我们的匹配条件;而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。
    客户端向 Spring Cloud Gateway 发出请求,如果请求与网关程序定义的路由匹配,则该请求就会被发送到网关 Web 处理程序,此时处理程序运行特定的请求过滤器链。
    过滤器之间用虚线分开的原因是过滤器可能会在发送代理请求的前后执行逻辑。所有 pre 过滤器逻辑先执行,然后执行代理请求;代理请求完成后,执行 post 过滤器逻辑

Predicate 断言

官方提供了11种不同的断言,包括
Before : 请求在某个时间之前
After : 请求在某个时间之后
Between : 再某个时间段内
Cookie : cookie内是否包含某个键值对
Header: 请求头格式
Host :地址
等,具体可见 Route Predicate Factories

2. 案例使用

a. 匹配url规则进行路由

  1. 首先创建SpringBoot项目,端口号为8080,添加Gateway依赖:
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
  1. 配置文件内进行路由配置
    目的:
    现在已有本地订单服务在启动,端口号8082,其中查询订单的接口为: http://localhost:8082/order;
    需要通过网关Gateway项目路由到订单服务接口进行正常请求:
    配置如下:
    application.yml
spring:
  application:
    name: spring-cloud-gateway-8080
  cloud:
    gateway:
      routes:
        - predicates: # 断言,进行拦截判断;  - 表示集合,可以配置多个
           - Path=/gateway/**  #  请求url规则: /gateway/**
          filters: # 过滤
           - StripPrefix=1  # 去掉第一个前缀, 以 订单服务的:http://localhost:8082/order 为例, 经过网关时的请求为:http://localhost:8080/gateway/order
          uri: http://localhost:8082  # 匹配成功之后,跳转的路径


server:
  port: 8080

我们定义了一个路由route,断言匹配所有/gateway/**的请求: 如 http://localhost:8080/gateway/order
匹配成功之后通过过滤器filters将第一个前缀去除: /gateway/order 的请求就会转变为 /order
最后,指定跳转uri:最终路径即为 http://localhost:8082/order 即为订单服务的正常请求。
通过结果也可看出,我们访问的虽是网关服务8080的地址,返回的数据确实是订单服务8082的接口提供的:

b. 根据cookie内容来路由

  1. application.yml内定义cookie路由
spring:
  application:
    name: spring-cloud-gateway-8080
  cloud:
    gateway:
      routes:
        - id : test_route
          predicates: # 断言,进行拦截判断;  - 表示集合,可以配置多个
           - Path=/gateway/**  #  请求url规则: /gateway/**
          filters: # 过滤
           - StripPrefix=1  # 去掉第一个前缀, 以 订单服务的:http://localhost:8082/order 为例, 经过网关时的请求为:http://localhost:8080/gateway/order
          uri: http://localhost:8082  # 匹配成功之后,跳转的路径
        - id : cookie_route
          predicates:
           - Cookie=myName,shen
          uri: https://www.baidu.com/

当请求的cookie内包含 key为myName,value为shen的内容时,跳转至 百度
2. Postman添加cookie模拟

重启服务发起请求,结果也是成功路由至 baidu:

c. 负载均衡来进行路由

  1. 当需要通过网关路由到集群节点时,肯定需要从注册中心将服务提供者列表获取到本地,进行负载均衡。此时我们需要引入服务注册中心Eureka-client
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-netflix-eureka-client</artifactId>
        </dependency>
  1. application.yml内进行负载均衡route的配置
...
  ...
        - id: lb_route # lb: load balance 负载均衡,从eureka服务注册中心获取服务提供者地址列表
          predicates:
            - Path=/lb/**
          filters:
            - StripPrefix=1
          uri: lb://shen-order-service  # 集群服务名,eureka中服务的key
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true # 小写方式进行匹配
server:
  port: 8080

eureka:
  client:
    service-url:
      defaultZone: http://localhost:9090/eureka  # eurkea注册中心

此时访问gateway服务,http://localhost:8080/lb/order 接口时,会匹配到id为lb_route的路由route,从eureka获取到服务名称为shen-order-service的服务列表,负载均衡获取其中一个服务提供者地址,再拼接/order进行调用。

d. 对请求进行限流的过滤器

requestratelimiter-gatewayfilter-factory
官方提供了RequestRateLimiterGatewayFilterFactory类型的过滤器,可以针对请求限流,默认使用Redis进行限流,限流的实现使用令牌桶算法(和semaphore类似,每秒桶内生成一定数量的令牌,当令牌消耗完毕则限流,之前的消耗不完会堆积一部分令牌)

本次针对同一host的请求进行限流

  1. 首先引入redis依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  1. 自定义限流规则,根据host限流
    需要实现KeyResolver接口
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 IpAddressKeyResolver implements KeyResolver {

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        // 根据id作为限流的Key
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }

}

  1. application.yml内配置
...
  ...
        - id: ratelimiter_route
          predicates:
          - Path=/ratelimiter/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                deny-empty-key: true
                keyResolver: '#{@ipAddressKeyResolver}'
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 2
          uri: lb://shen-order-service


  redis:
    host: 192.168.197.1
    port: 6379

重启服务,当我们对同一个请求联系进行多次请求时,就会出现限流的标识符:HTTP ERROR 429

e. 动态路由

actuator-api

查询路由信息
官方提供了actuator可以让我们通过/actuator/gateway/routes请求来查询路由的配置; http://localhost:8080/actuator/gateway/routes
/actuator/gateway/routes路径后添加某一个路由id可以查看具体某一路由的详细信息,如之前配置的限流路由: http://localhost:8080/actuator/gateway/routes/ratelimiter_route

新增、删除路由信息
creating-and-deleting-a-particular-route

发送post请求新增一个路由:

返回1表示新增成功,从上一步骤的查询接口可以根据路由Id来查看是否新增了该路由。

引入actuator进行路由查看:

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

f. 路由持久化

使用Redis进行路由持久化:


import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

/**
 * 扩展路由数据的持久化
 * 根据Redis实现
 */
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    private static final String GATEWAY_ROUTE_KEY = "gateway_dynamic_route";

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    /**
     * 获取路由信息
     * @return
     */
    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> routeDefinitionList = new ArrayList<>();
        redisTemplate.opsForHash().values(GATEWAY_ROUTE_KEY).stream().forEach(route -> {
            routeDefinitionList.add(JSON.parseObject(route.toString(), RouteDefinition.class));
        });
        return Flux.fromIterable(routeDefinitionList);
    }

    /**
     * 保存路由信息
     * @param route
     * @return
     */
    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(routeDefinition -> {
            redisTemplate.opsForHash().put(GATEWAY_ROUTE_KEY, routeDefinition.getId(), JSON.toJSONString(routeDefinition));
            return Mono.empty();
        });
    }

    /**
     * 删除路由信息
     * @param routeId
     * @return
     */
    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id -> {
            if (redisTemplate.opsForHash().hasKey(GATEWAY_ROUTE_KEY, id)) {
                redisTemplate.opsForHash().delete(GATEWAY_ROUTE_KEY, id);
                return Mono.empty();
            }
            return Mono.defer(() -> Mono.error(new Exception("RouteDefinition not found: " + routeId)));
        });
    }
}

这样做的好处显而易见,之前动态添加的路由,服务重启后仍然存在。

三、源码及自定义

1. 源码逻辑

断言类的匹配逻辑

以 cookie为例:
断言类CookieRoutePredicateFactory内有GatewayPredicate.apply()方法的实现,当内部回调test方法返回true表示匹配。


	@Override
	public Predicate<ServerWebExchange> apply(Config config) {
		return new GatewayPredicate() {
			@Override
			public boolean test(ServerWebExchange exchange) {
				List<HttpCookie> cookies = exchange.getRequest().getCookies()
						.get(config.name);
				if (cookies == null) {
					return false;
				}
				for (HttpCookie cookie : cookies) {
					if (cookie.getValue().matches(config.regexp)) {
						return true;
					}
				}
				return false;
			}

			@Override
			public String toString() {
				return String.format("Cookie: name=%s regexp=%s", config.name,
						config.regexp);
			}
		};
	}

加入断点看着就更为清晰了

断言类的定义规则

其中之前提到的11种断言规则,在gateway源码内都对应具体的工厂类,如这里用到的cookie断言,对应org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactory ,在该类所在目录下定义了一系列不同的断言工厂。

这些类的命名其实是有一定的规则的,如我们之前使用到了 Path 和 Cookie 两种断言,在yml内配置时key分别为 - Path=/gateway/** - Cookie=myName,shen,则对应类名为 Path + RoutePredicateFactoryCookie + RoutePredicateFactory
反言之当我们自定义断言工厂时,如ShenRoutePredicateFactory,在yml内使用时,key即为Shen

过滤器类

之前我们用过了- StripPrefix=2类型的过滤器,可以从请求的url总截取掉一部分,在进行跳转,这是官方提供的。官方提供的StripPrefixGatewayFilterFactory类型的过滤器提供的,官方也提供了很多其余的过滤器:
主要有:Gateway Filters(包含31中过滤器)和Global Filters(包含10中常用过滤器)两种类型。

这些过滤器对应的实现类,定义的规则是: key + GatewayFilterFactory
如:用到的filters: - StripPrefix=1,key为StripPrefix,实现类为StripPrefixGatewayFilterFactory。可以根据这种规则查找官方提供的几十种过滤器的实现原理,当自定义过滤器时也根据此种规则来即可。

2. 自定义断言、过滤器

自定义断言

  1. 自定义一个断言: 匹配当请求头header里包含 key=myHeader,value=test123时,路由至 博客首页

模仿cookie等断言,继承抽象类AbstractRoutePredicateFactoryapply方法内对请求头进行匹配

import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

@Component
public class ShenRoutePredicateFactory extends AbstractRoutePredicateFactory<ShenRoutePredicateFactory.Config> {

    public ShenRoutePredicateFactory() {
        super(Config.class);
    }

    private static final String NAME_KEY="name";
    private static final String VALUE_KEY="value";

    // 定义 key、value顺序
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(NAME_KEY,VALUE_KEY);
    }


    @Override
    public Predicate<ServerWebExchange> apply(Config config) {

        return new GatewayPredicate() {

            @Override
            public boolean test(ServerWebExchange exchange) {
                // 匹配请求头 header 里是否包含对应key、value
                HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
                List<String> value = httpHeaders.get(config.getName());
                return !CollectionUtils.isEmpty(value);
            }

            @Override
            public String toString() {
                return String.format("Cookie: name=%s value=%s", config.name,config.value);
            }

        };
    }

    public static class Config {

        private String name;

        private String value;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }

    }

}
  1. 配置文件内指定匹配规则
    定义路由id: shenHeader_route
    匹配规则,两个断言: 请求url为/shenGateway/**格式;自定义断言Shen匹配请求头内有:myHeader:test123的请求;
    过滤器: 截取url的首个单词
    跳转url: 博客首页 https://www.cnblogs.com/Qkxh320

application.yml

...
  ...
        - id : shenHeader_route
          predicates:
           - Path=/shenGateway/**
           - Shen=myHeader,test123
          filters:
           - StripPrefix=1
          uri: https://www.cnblogs.com/Qkxh320

路由结果:

自定义过滤器

package com.bigshen.springcloud.demo.springcloudgateway8080;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

@Component
public class ShenDefineGatewayFilterFactory extends AbstractGatewayFilterFactory<ShenDefineGatewayFilterFactory.ShenConfig> {

    Logger logger = LoggerFactory.getLogger(ShenDefineGatewayFilterFactory.class);

    private static final String NAME_KEY = "name";

    public ShenDefineGatewayFilterFactory() {
        super(ShenConfig.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList(NAME_KEY);
    }

    @Override
    public GatewayFilter apply(ShenConfig config) {

        // Filter pre/post

        return (((exchange, chain) -> {
            // 过滤器 pre
            logger.info("[Pre] Filter Request, name: " + config.getName());
            // TODO before
            return chain.filter(exchange) // 处理
                    .then(Mono.fromRunnable(()-> { // then,响应式编程,当前一步骤 chain.filter 处理结束,进入then逻辑进行处理
                        // 内部回调
                        // 过滤器 post

                        // TODO after
                        logger.info("[post]: Response Filter");
                    }));

        }));
    }

    public static class ShenConfig {

        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

}

application.yml:

...
  ...

        - id : shenHeader_route
          predicates:
           - Path=/shenGateway/**
           - Shen=myHeader,test123
          filters:
           - StripPrefix=1
           - ShenDefine=Shen World
          uri: https://www.cnblogs.com/Qkxh320

本次演示案例地址

posted @ 2019-09-08 21:27  BigShen  阅读(552)  评论(0编辑  收藏  举报