关于网关gateway的初步了解

一、背景

1、为什么需要网关

网关层是浏览器与服务器交互时经过的第一个服务节点,它主要起屏蔽下游业务服务的作用,对于浏览器而言,只需要跟网关交互就相当于在与下游多个业务服务节点交互,让浏览器觉得他在和一台服务器交互。

2、好处

不管是下游业务服务、支撑服务、基础服务,都对于浏览器屏蔽,与服务器的交互变的非常简单,浏览器无需关心各个节点的依赖关系、如何协同工作,浏览器只会了解到本次请求是否成功;开发者可以灵活的增加业务服务模块;可以在网关层做一些最上层的公用的操作,如过滤恶意请求、设置ip黑白名单、做身份认证、限流、负载均衡、单点登录以及跨域等。

二、选型说明

1、gateway和zuul的简单源码分析

1、zuul源码

1⃣️com.netflix.zuul.http.ZuulServlet

public class ZuulServlet extends HttpServlet {

private ZuulRunner zuulRunner;

public void init(ServletConfig config) throws ServletException {
    ...
}
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
    ...
}
它本质上用了java.servlet API,实现了一个有网关功能的servlet

2⃣️com.netflix.zuul.http.ZuulServlet#service

try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
    preRoute();
} catch (ZuulException e) {
    error(e);
    postRoute();
    return;
}
try {
    route();
} catch (ZuulException e) {
    error(e);
    postRoute();
    return;
}
try {
    postRoute();
} catch (ZuulException e) {
    error(e);
    return;
     }
} catch (Throwable e) {
	error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
	RequestContext.getCurrentContext().unset();
}
preRoute()、route()、postRoute()、error(),它们最终调用了com.netflix.zuul.FilterProcessor#runFilters

3⃣️com.netflix.zuul.FilterProcessor#runFilters

public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
    Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
    for (int i = 0; i < list.size(); i++) {
        //ZuulFilter list
        //轮流执行ZuulFilter的逻辑,result=false或执行完所有ZuulFilter时调用链结束
        ZuulFilter zuulFilter = list.get(i);
        Object result = processZuulFilter(zuulFilter);
        if (result != null && result instanceof Boolean) {
            bResult |= ((Boolean) result);
        }
    }
}
return bResult;
}
从FilterRegistry(相当于内存中的filter)加载Zuulfilter list
编译groovy文件并加载Zuulfilter
对于FilterRegistry,则是用于内存中保存filter,可以动态变化的,注册新的filter以及移除filter等,可提供给jmx、endpoint做远程控制。

4⃣️com.netflix.zuul.FilterProcessor#processZuulFilter

public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

RequestContext ctx = RequestContext.getCurrentContext();
boolean bDebug = ctx.debugRouting();
final String metricPrefix = "zuul.filter-";
long execTime = 0;
String filterName = "";
try {
    long ltime = System.currentTimeMillis();
    filterName = filter.getClass().getSimpleName();
    
    RequestContext copy = null;
    Object o = null;
    Throwable t = null;

    if (bDebug) {
        Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
        copy = ctx.copy();
    }
    //执行ZuulFilter的runFilter逻辑
    ZuulFilterResult result = filter.runFilter();
    ExecutionStatus s = result.getStatus();
    //执行耗时统计(可以发现Zuul还没有完善这个功能,只是形成了框架)
    execTime = System.currentTimeMillis() - ltime;
	//处理执行结果,无论成功与否,都记录了debug日志
    switch (s) {
        case FAILED:
            t = result.getException();
            ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
            break;
        case SUCCESS:
            o = result.getResult();
            ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
            if (bDebug) {
                Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
                Debug.compareContextState(filterName, copy);
            }
            break;
        default:
            break;
    }
    
    if (t != null) throw t;
	//目前作为空壳存在,可见是为了方便扩展
    usageNotifier.notify(filter, s);
    return o;

} catch (Throwable e) {
    if (bDebug) {
        Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
    }
    usageNotifier.notify(filter, ExecutionStatus.FAILED);
    if (e instanceof ZuulException) {
        throw (ZuulException) e;
    } else {
        ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
        ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
        throw ex;
    }
}

实际上整个调用链是由ZuulFilter来组成,对于用户而言,只需要关心如果构建自定义的ZuulFilter以及它们之间的顺序。
由于底层是servlet,Zuul处理的是http请求,Zuul的抽象写的非常简单易懂,易于扩展,但是zuul-core包不依赖Spring,依赖的包很少,没有提供异步支持,流控等均由hystrix支持

2、gateway

在这里插入图片描述
在Spring mvc是通过HandlerMapping解析请求链接,然后根据请求链接找到执行这个请求Controller类 。而在Spring GateWay中也是使用HandlerMapping对请求的链接进行解析匹配对应的Route进行代理转发到对应的服务。
用户请求先通过DispatcherHandler找到对应GateWwayHandlerMapping,再通过GateWwayHandlerMapping解析匹配到对应的Handler。Handler处理完后,再经过Filter,最终到Proxied Service
在这里插入图片描述
在这里插入图片描述

1⃣️org.springframework.cloud.gateway.config.GatewayAutoConfiguration

@Configuration
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@EnableConfigurationProperties
@AutoConfigureBefore(HttpHandlerAutoConfiguration.class)
@AutoConfigureAfter({GatewayLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class})
@ConditionalOnClass(DispatcherHandler.class)
public class GatewayAutoConfiguration {

...

@Bean
public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties, Environment environment) {
    return new RoutePredicateHandlerMapping(webHandler, routeLocator, globalCorsProperties, environment);//webHandler是一个封装了Global Filter的对象,routeLocator保存了所有Route的对象
}

...

}

2⃣️org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping处理获取路由

private final RouteLocator routeLocator;存储了我们启动的时候加载的路由对象信息。

先看下org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#getHandlerInternal 从RouteLocator获取路由存放在ServerWebExchange中,返回webFilter

protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
    if (this.managementPortType == RoutePredicateHandlerMapping.ManagementPortType.DIFFERENT && this.managementPort != null && exchange.getRequest().getURI().getPort() == this.managementPort) {
        return Mono.empty();
    } else {
        exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_HANDLER_MAPPER_ATTR, this.getSimpleName());
        return this.lookupRoute(exchange).flatMap((r) -> {
            exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Mapping [" + this.getExchangeDesc(exchange) + "] to " + r);
            }

            exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR, r);
            return Mono.just(this.webHandler);
        }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
            exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("No RouteDefinition found for [" + this.getExchangeDesc(exchange) + "]");
            }

        })));
    }
}

继续看
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#lookupRoute

protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
    return this.routeLocator.getRoutes().concatMap((route) -> {
        return Mono.just(route).filterWhen((r) -> {
            exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
            return (Publisher)r.getPredicate().apply(exchange);
        }).doOnError((e) -> {
            this.logger.error("Error applying predicate for route: " + route.getId(), e);
        }).onErrorResume((e) -> {
            return Mono.empty();
        });
    }).next().map((route) -> {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Route matched: " + route.getId());
        }

        this.validateRoute(route, exchange);
        return route;
    });
}

继续看org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getRoutes

public Flux<Route> getRoutes() {
    Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);
    if (!this.gatewayProperties.isFailOnRouteDefinitionError()) {
        routes = routes.onErrorContinue((error, obj) -> {
            if (this.logger.isWarnEnabled()) {
                this.logger.warn("RouteDefinition id " + ((RouteDefinition)obj).getId() + " will be ignored. Definition has invalid configs, " + error.getMessage());
            }

        });
    }

    return routes.map((route) -> {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("RouteDefinition matched: " + route.getId());
        }

        return route;
    });
}

继续看org.springframework.cloud.gateway.route.RouteDefinitionLocator

public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();

}
RouteDefinitionLocator用于存放route信息,而真正可以执行route逻辑的则是Route,Route中包含路由filter链。
route包含:
URI:路由地址uri。
Predicate:包含接受请求的方法、path等信息
Filter:Route自定义的过滤器

2、gateway和zuul对比

1、zuul是基于servlet 2.5,兼容servlet3.0,使用的是阻塞API,不支持长连接如websocket,
虽然zuul2支持非阻塞API,但并没有和spring cloud集成,而且不再维护,所以不考虑使用

2、Gateway基于spring5,Reactor和Spring boot2,使用了非阻塞API,支持websocket,和spring完美集成,对开发者友好。
Reactor:提供了一个非阻塞的,高并发的基于健壮的Netty框架的网络运行API,包括本地tcp/http/udp 客户端和服务端。
Flux:表示的是包含 0 到 N 个元素的异步序列。在该序列中可以包含三种不同类型的消息通知:正常的包含元素的消息、序列结束的消息和序列出错的消息。当消息通知产生时,订阅者中对应的方法 onNext(), onComplete()和 onError()会被调用
Mono:Mono 表示的是包含 0 或者 1 个元素的异步序列。该序列中同样可以包含与 Flux 相同的三种类型的消息通知。
Flux 和 Mono 之间可以进行转换。对一个 Flux 序列进行计数操作,得到的结果是一个 Mono对象。把两个 Mono 序列合并在一起,得到的是一个 Flux 对象。
简单说Mono返回单个元素,Flux返回多个元素

3、因为zuul使用了阻塞API,所以在spring项目使用中需要依赖spring-web,而gateway使用非阻塞,所以需要依赖spring-webflux,两者是冲突的,需要考虑包冲突问题。
Spring WebFlux:是Spring Framework 5.0中引入的新的反应式Web框架,它不需要Servlet API,完全异步和非阻塞, 并通过Reactor项目实现Reactive Streams规范。 并且可以在诸如Netty,Undertow和Servlet 3.1+容器的服务器上运行,核心控制器DispatcherHandler,等同于阻塞方式的DispatcherServlet。
webflux流程:
1.通过 HandlerMapping获取到HandlerAdapter放到ServerWebExchange的属性中
2.获取到HandlerAdapter后触发handle方法,得到HandlerResult
3.通过HandlerResult,触发handleResult,针对不同的返回类找到不同的HandlerResultHandler如视图渲染ViewResolutionResultHandler,ServerResponseResultHandler,ResponseBodyResultHandler,ResponseEntityResultHandler

4、gateway在spring的支持下,功能更强大,内部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于Spring Cloud套件,而zuul则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等

5、gateway很好的支持异步,那么理论上gateway则更适合于提高系统吞吐量,从框架设计的角度看,gateway具有更好的扩展性,稳定性也是非常好的

总的来说,在微服务架构,如果使用了Spring Cloud生态的基础组件,则Spring Cloud Gateway相比而言更加具备优势,流式编程+支持异步太吸引人。
而对于小型微服务架构,zuul是一个不错的选择

二、整合过程

1、导入maven依赖

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

2、配置说明

spring:
  cloud:
    gateway:
      discovery:
        locator:
          # 是否与服务发现组件进行结合,通过serviceId转发到具体的服务实例。默认为false,为true代表开启基于服务发现的路由规则。
          enabled: false
          # 配置之后访问时无需大写
          lower-case-service-id: true
      routes:
        - id: homework-user
          # lb: 使用loadBalanceClient实现负载均衡,后面homework-user是微服务的名称,用于集群
          uri: lb://homework-user
          predicates:
            - Path=/homework-user/**  #类型路径路由至homework-user微服务
          filters:
            - StripPrefix=1  #请求微服务,自动去掉第一个前缀
            - name: RequestRateLimiter  #限流过滤器,使用默认factory
              args:
                #限流解析器bean对象
                key-resolver: '#{@iPKeyResolver}'
                #令牌桶每秒填充平均速率,每秒钟只允许一个请求
                redis-rate-limiter.replenishRate: 1
                #令牌桶容量,允许并发三个个请求
                redis-rate-limiter.burstCapacity: 3

    - id: homework-openapi
      uri: lb://homework-openapi
      predicates:
        - Path=/homework-openapi/**
      filters:
        - StripPrefix=1

  globalcors:
    cors-configurations:
      '[/**]': #匹配所有请求
        allowCredentials: true
        allowedOrigins: "*"
        allowedMethods: "*"
        allowedHeaders: "*"

3、实现功能

登陆过滤、跨域、服务端响应处理、白名单过滤、验签过滤以及服务通用fallback处理

三、遇到问题

1、版本问题

由于nacos原因,所以采用最新版本的Springboot以及cloud版本
之前Hoxton.SR6版本的springcloud对长连接处理有问题,所以一直出现too many files问题,后来提出issue,问题得到解决,目前用到的版本是:
springboot:2.3.2.RELEASE
springcloud:Hoxton.SR6
nacos:2.2.1.RELEASE

2、分组、包问题

分组:微服务的discovery分组必须和网关分组一致
包:由于webflux和web冲突问题,所以必须提出所有web包

3、客户端请求问题

客户端请求到网关时,如果数据包很大,默认会分两次请求导致问题,可以在客户端做配置处理,HTTP请求框架将 httpclient 设置 Expect: 100-continue =false

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值