一、      网关种类

流量型网关和业务型网关,也是自己的一个理解,流量型网关可以通常看成是nginx,kong这种更加专注于高性能进行流量分发,业务相对简单,但是对于“复杂”型业务网关,尤其系统实现使用的是java,那么使用openresty这种无疑是加大了研发成本,而且不利于调试和定位问题,毕竟需要通过规定统一接口来进行交互。

二、      网关产生背景

  1. 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  2. 认证复杂,每个服务都需要独立认证。
  3. 项目的迭代,可能需要重新划分微服务,这样客户端调用的逻辑会有调整,导致级联客户端的调整,给整个系统带来很大的麻烦,牵一发而动全身
  4. 提取通用业务到网关可以使具体的服务更专注于业务本身,通用业务如:统一身份认证,统一的数据转换处理,定向转发,以及负载均衡策略,限流,访问日志。

三、      网关技术

目前市场比较的成形网关有zuul(1.x,2.x)| spring cloud gateway | nginx | kong|other

能够支撑网关的核心技术点就是能够搞定高tps即可!

    说到这里核心的关注点来了,高tps的支撑就是要快速的去处理请求,快速的接收到请求,不能因为服务器资源的问题而拒绝请求,或造成在底层操作系统级别的排队阻塞。

    这个问题让我们想到了nodejs,而nodejs正是基于事件分发机制的reactor模型的实现,是异步非阻塞的。

    相比于传统的阻塞IO,异步非阻塞接受请求只需要一条线程即可。是的,这个线程只进行请求的接收,收到后会保存请求到一个指定的位置,然后会由一个looper来进行请求的获取,

   交给请求处理器去处理,这里的请求处理器可以理解成一个线程池的机制。

四、      Spring家族的网关

Spring cloud gateway(下面简称gateway) 是spring cloud在进一步放弃了zuul 1.x后的新作。也是spring自己的东西,相比于外部依赖更加的可控些。

SpringBoot 2.2.2. RELEASE,SpringCloud Hoxton.SR1

 

Release Train

Boot Version

Hoxton

2.2.x

Greenwich

2.1.x

Finchley

2.0.x

Edgware

1.5.x

Dalston

1.5.x

五、      Gateway启动时的自动配置

学习一个项目,或者一个技术的关键点在于,了解这个项目的运转过程,了解项目的结构,别一下进入到细节中,也不要仅仅停留于最简单的demo中。

l  GatewayAutoConfiguration  网关基础配置类,当中承载着核心的配置逻辑

l  GatewayClassPathWarningAutoConfiguration  网关类加载配置类,就是用于校验是否加载的时webFlux依赖,而不是普通的web依赖。

l  GatewayLoadBalancerClientAutoConfiguration  网关客户端负载均衡配置类

l  GatewayRedisAutoConfiguration   网关限流器配置类

我们在启动spring boot的时候基本都会使用@EnableAutoConfiguration注解,那么当你引入gateway项目的时候上面的三个配置类就会被加载。

首先看GatewayAutoConfiguration  中的NettyConfiguration,这个类为初始化netty通信的一系列流程,分别注册了3个bean。这里只是抛砖引玉,里面详细的配置做了哪些事情,可以自己找下gateway代码对号入座。

       GatewayAutoConfiguration 作为基础配置,内部又注册了这些bean

a)   NettyConfiguration

b)   GlobalFilter

c)   FilteringWebHandler

d)   GatewayProperties

e)   PrefixPathGatewayFilterFactory

f)   RoutePredicateFactory

g)   RouteDefinitionLocator

h)   RouteLocator

i)   RoutePredicateHandlerMapping

j)   GatewayWebfluxEndpoint

六、      Gateway核心概念

  • Routepredicategatewayfilter,globalfilter
  1. Route可以看成是一个请求服务器资源的对象,里面包含着请求信息,下面我会列出属性和关键方法,当然里面包含Predicate以及filter。

那么在使用的时候需要给route对象中指定属性,uri,path参数,那么predicate用来检查是否合规。

Route对象属性:

属性

含义

private final String id;

路由编号

private final URI uri;

即将路由向的 URI

private final int order;

路由顺序

Predicate <ServerWebExchange> predicate;

校验访问信息否合规,调用了包装对象的test方法,因为Predicate本身也有test方法,在gateway中又做了扩展,接口名称为:GatewayPredicate,还要注意,该字段为数组,可add操作bool表达式。

List<GatewayFilter> gatewayFilters;

过滤器链

以上的属性是通过读取配置文件得来的,或者使用Routes.locator()进行对象链式创建。目前这个阶段可以理解成一个请求信息收集。

  1. Predicate,在gateway-core包的handle中,以一个时间的Predicate来举例,AfterRoutePredicateFactory用来校验在某一时间点后生效的断言。

Predicate也很好解释,java8中的Predicate函数式关键字实质也是一个判断条件,满足条件即放行。而Route其内部是包含了关于ServerWebExchange的Boolean表达式。

在请求到了gateway,是通过DispatchHandle来进行处理的,它会去匹配HandlerMapping,gateway实现了一个RoutePredicateHandlerMapping;在这个类中的核心

方法是getHandlerInternal,这个方法中去判断当前断言是否通过,核心方法是调用每个路由断言的test方法,代码如下:

.filter(route -> route.getPredicate().test(exchange))

   

jeegcboot服务网关 服务网关gateway工作流程_jeegcboot服务网关

如果路由断言条件没有通过,则lookupRoute(ServerwebExchange)方法,返回空的集合。后面的逻辑不会在执行。

   

jeegcboot服务网关 服务网关gateway工作流程_jeegcboot服务网关_02

 

   

jeegcboot服务网关 服务网关gateway工作流程_jeegcboot服务网关_03

 

 

如果我们想自己定义一个predicate,按照官方的做法继承AbstractRoutePredicateFactory即可,不过目前原始提供的已经比较丰富,或许不用我们扩展!如果需要扩展应该想下我们的方案是否出现在了正确位置。

   那么如果能通过predicate,就会调用Mono.just(webHandler)方法继续后面的FilteringWebHandler,而这个类中会持有全局的过滤链。

  1. gateway中的网关还有一个重要的成员就是filter,分为gatewayFilter和globalFilter两种,下面详细解释下过滤器的相关问题,在此之前先打个感叹号!

        

jeegcboot服务网关 服务网关gateway工作流程_jeegcboot服务网关_04

 

  


  1. 请求接入filter

先说下请求接入的整个过程吧。这里就会涉及到gateway本身提供的全局过滤器。如有针对路由的过滤器也会根据order方法返回值顺序,与globalFilter进行统一排序。

   

jeegcboot服务网关 服务网关gateway工作流程_spring_05

 

 

 

请求实际走过handle的顺序

         i.      HttpWebHandleAdapter;

       ii.      DispatcherHandle;负责转发到具体的请求处理器;

      iii.      RoutePredicatehandlerMapping;匹配处理器后进行route的断言,成功则取执行过滤链,否则直接response;

       iv.      FilteringWebHandle;这个handle中初始化了9个spring全局的globaFilter (有一个是自定义的)

   

jeegcboot服务网关 服务网关gateway工作流程_客户端_06

 

 

DefaultGatewayFilterChain 用来处理filter过滤链的关键类,该类持有了filter链;请求在与路由匹配时,FilteringWebHandler组件创建的时候会将所有的 GlobalFilter 构建一个GatewayFilterAdapter,而该对象仅持有GlobalFilter接口方法,在转换成OrderGatewayFilter这样也持有了getOrder方法,根据getOrder方法的返回值顺序组成ArrayList。

   

jeegcboot服务网关 服务网关gateway工作流程_客户端_07

在FilteringWebHandler这个类中很关键,如果你有自定义的globalfilter那么就会加入到这个ArrayList中,首次入过滤链是通过WebClientWriteResponseFilter这个过滤器,因为这个过滤器中包含了请求和响应的全状态。整个过滤链都是在这个过滤器中进行的,代码如下:

   

jeegcboot服务网关 服务网关gateway工作流程_jeegcboot服务网关_08

    Lambda表达式中是处理响应阶段的,而chain的filter方法就是在循环ArrayList进行filter的执行;如果你在自定义filter中放行,并继续执行下面的filter那么会在代码中调用chain的filter,如果确定结束,那么需要返回一个Mono对象,由Mono对象去执行then方法,取进行响应内容的操作,最后writeWith到客户端。

系统提供的重要的全局过滤器:

  • RemoveCachedBodyFilter order为-2147483648

清除exchange的attributes中cachedRequestBody值。这个key的名称来自exchangeUtile中CACHED_REQUEST_BODY_ATTR = "cachedRequestBody";

  • AdaptCachedBodyGlobalFilter order为-2147483648+1000

作用是从exchange的attributes中获取cachedRequestBody属性值作为request的body,注意使用此功能首先必须预设cachedRequestBody属性至attributes中。

  • NettyWriteResponseFilter order为-1

NettyWriteResponseFilter将结果数据流写入ServerHttpResponse中发生在NettyRouting获取到远程调用的结果数据流之后,当NettyRouting拿到结果数据流之后会将其写入当前请求exchange的attributes中。

  • ForwardPathFilter order-0

处理uri为forword开头的服务地址,形如:forword://xxxxxx.com,否则也忽略。

  • RouteToRequestUrlFilter order为10000

过滤器RouteToRequestUrlFilter是必须的全局过滤器,主要任务是将原始的url请求根据route中配置的uri,将请求的具体资源信息组合到一起,形成一个真正往后端服务的请求,将真实的请求url路径,配置到exchange中attribute的Map中,key为“包全名.ServerWebExchangeUtils.gatewayRequestUrl”,直接发送到下一个过滤器,如果为lb://模式则会通过LoadBalancerClientFilter进行处理。

  • LoadBalancerClientFilter(我们可以在此处做自定义负载均衡)

LoadBalancerClientFilter负责服务真实ip的映射,主要针对对个服务节点的情况进行负载均衡,默认采用的netflix-ribbon作为负载均衡器,首先如果scheme不是服务节点映射的话直接过滤,获取服务节点,choose函数是真实负载均衡发生的函数,获取一个本次选出的服务server instance(如果是单节点则无负载计算),然后将服务的真实ip+port替换掉path中的lb://{serviceId}前缀。实际就是拿到一个能够真实请求的地址。那么这个过滤器如果不是lb://servername

则该过滤器也直接忽略

  • WebsocketRoutingFilter 

过滤器实现了gateway对于websocket的支持,内部通过websocketClient实现将一个http请求协议换转成websocket,如果uri不是ws开头的这种则不起作用,

ws://xxxx.com或者wss://zxxxxxx.cn

   

jeegcboot服务网关 服务网关gateway工作流程_缓存_09

  • NettyRouting   order为2147483647

NettyRouting获取到远程调用的结果数据流会将其写入当前请求exchange的attributes中,发送回DispatchHandle,又webflux处理。

  • ForwardRoutingFilter  order为2147483647

最终将exchange交还给Webhandler做http请求处理,已经准备返回数据给客户端(如果是forward则会发送到gateway本地的控制器处理)。

   

jeegcboot服务网关 服务网关gateway工作流程_客户端_10

七、      关于网关做统一认证的问题

  • 读取requestBody

gateway用于统一请求信息校验。我们可以校验header中的信息,通过exchange来获得,如果是一个post请求我们有时也需要校验请求体body的合法性。

每个filter中都持有exchange对象,获取header的时候使用exchange.getRequest().getHeaders();

那么现在如果想获取requestBody呢。你会看到网上不天盖地的各种文章,针对各种版本进行处理。拿出一种方式来举个反例:

照猫画虎:exchange.getBody()获取出来的是Flux<DataBuffer>对象,我们知道fulx使用订阅方法可以取出body,但是如果请求体过大使用sub方法没法取出。

因为sub方法只能取出发过来的第一份元素。 见了网上的各种hack方式,如果我们仅仅需要校验一个requestBody内容,则只需要在builder.routes()构建每个具体的route对象时对predicate进行readBody的设置,这里需要传入一个参数,是body的传入类型。

   

jeegcboot服务网关 服务网关gateway工作流程_客户端_11

这样在gateway启动后,一个请求过来就会去匹配我们事先定义好的route对象。嗯,我们来看下route方法的第二个参数,Function<PredicateSeqc>类型。

   

jeegcboot服务网关 服务网关gateway工作流程_spring_12

我们找到这个类,这里如果使用了readBody则会调用ReadBodyPredicateFactory的applyAsync方法,该方法为读取body的核心操作。

   

jeegcboot服务网关 服务网关gateway工作流程_缓存_13

进入applyAsync方法后我们会看到关键的对请求信息做put.attribute的操作,key为一个工具类中的常量,进入方法首先判断是否有缓存,然后我们想到了FilteringWebHandle中的第一个全局过滤器RemoveCacheBodyFilter,所以我们在这里一定能够进入到else。那就是使用exchangeUtil中的cacheBody方法,最后将body获取并存储ccHashMap。

   

jeegcboot服务网关 服务网关gateway工作流程_客户端_14

到这里我们看到了一个整体的reqbody的缓存流程,接下来可以在任意的filter中取出使用。

写到这里我有个小想法,还是想把这个predicate的readBody用filter顶替掉,因为在断言表达式成功了以后就可以进行缓存了,我们需要body可以随时拉出来校验,这个方法不会花费较长时间。不然每个路由都需要配置一次,实在是麻烦!

后来看了下gateway平台提供了一个modifiyRequestBody的全局Filter。经过改造(去除了一些修改请求的操作,仅仅是将原来的请求body订阅出来,缓存起来,然后构建一个新的exchange对象),order优先级以-2147483647+10排序时机执行,选择这个时机执行因为它处于removeCacheBody和AdapCachaeBody两个过滤器之间。即使有人使用了predicate的readBody(String.class,b-> true)方式,那么在AdapCachaeBodyGlobaleFilter全局过滤器中我们仍然遵循默认的gateway原则去执行,map中缓存的key都是spring gateway项目提供的,所以没有冲突。

   

jeegcboot服务网关 服务网关gateway工作流程_客户端_15

  

  这里敲下黑板!!!!!!,可以后面关注下这个readBody方法。

  

jeegcboot服务网关 服务网关gateway工作流程_jeegcboot服务网关_16

  • 修改requestBody

修改requestBody,由于上面的readBody的提示,我们自然而然的就想到了应该也有一个类似方法来控制,在这里我们需要注意下readBody是以predicate的形式出现的,而modifyBody是以过滤器的身份出现的,非全局过滤器。

   

jeegcboot服务网关 服务网关gateway工作流程_jeegcboot服务网关_17

如果我们需要全局对每一个请求Body都可能有监控修改的需求,建议按照modifyRequestBodyFilterFactory的内容,自己定义一个全局过滤器这样也免去了配置的麻烦。

八、      还没想好