一、 网关种类
流量型网关和业务型网关,也是自己的一个理解,流量型网关可以通常看成是nginx,kong这种更加专注于高性能进行流量分发,业务相对简单,但是对于“复杂”型业务网关,尤其系统实现使用的是java,那么使用openresty这种无疑是加大了研发成本,而且不利于调试和定位问题,毕竟需要通过规定统一接口来进行交互。
二、 网关产生背景
- 客户端会多次请求不同的微服务,增加了客户端的复杂性。
- 认证复杂,每个服务都需要独立认证。
- 项目的迭代,可能需要重新划分微服务,这样客户端调用的逻辑会有调整,导致级联客户端的调整,给整个系统带来很大的麻烦,牵一发而动全身。
- 提取通用业务到网关可以使具体的服务更专注于业务本身,通用业务如:统一身份认证,统一的数据转换处理,定向转发,以及负载均衡策略,限流,访问日志。
三、 网关技术
目前市场比较的成形网关有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核心概念
- Route,predicate,gatewayfilter,globalfilter
- 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()进行对象链式创建。目前这个阶段可以理解成一个请求信息收集。
- 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))
如果路由断言条件没有通过,则lookupRoute(ServerwebExchange)方法,返回空的集合。后面的逻辑不会在执行。
如果我们想自己定义一个predicate,按照官方的做法继承AbstractRoutePredicateFactory即可,不过目前原始提供的已经比较丰富,或许不用我们扩展!如果需要扩展应该想下我们的方案是否出现在了正确位置。
那么如果能通过predicate,就会调用Mono.just(webHandler)方法继续后面的FilteringWebHandler,而这个类中会持有全局的过滤链。
- gateway中的网关还有一个重要的成员就是filter,分为gatewayFilter和globalFilter两种,下面详细解释下过滤器的相关问题,在此之前先打个感叹号!
- 请求接入filter
先说下请求接入的整个过程吧。这里就会涉及到gateway本身提供的全局过滤器。如有针对路由的过滤器也会根据order方法返回值顺序,与globalFilter进行统一排序。
请求实际走过handle的顺序:
i. HttpWebHandleAdapter;
ii. DispatcherHandle;负责转发到具体的请求处理器;
iii. RoutePredicatehandlerMapping;匹配处理器后进行route的断言,成功则取执行过滤链,否则直接response;
iv. FilteringWebHandle;这个handle中初始化了9个spring全局的globaFilter (有一个是自定义的)
DefaultGatewayFilterChain 用来处理filter过滤链的关键类,该类持有了filter链;请求在与路由匹配时,FilteringWebHandler组件创建的时候会将所有的 GlobalFilter 构建一个GatewayFilterAdapter,而该对象仅持有GlobalFilter接口方法,在转换成OrderGatewayFilter这样也持有了getOrder方法,根据getOrder方法的返回值顺序组成ArrayList。
在FilteringWebHandler这个类中很关键,如果你有自定义的globalfilter那么就会加入到这个ArrayList中,首次入过滤链是通过WebClientWriteResponseFilter这个过滤器,因为这个过滤器中包含了请求和响应的全状态。整个过滤链都是在这个过滤器中进行的,代码如下:
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
- NettyRouting order为2147483647
NettyRouting获取到远程调用的结果数据流会将其写入当前请求exchange的attributes中,发送回DispatchHandle,又webflux处理。
- ForwardRoutingFilter order为2147483647
最终将exchange交还给Webhandler做http请求处理,已经准备返回数据给客户端(如果是forward则会发送到gateway本地的控制器处理)。
七、 关于网关做统一认证的问题
- 读取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的传入类型。
这样在gateway启动后,一个请求过来就会去匹配我们事先定义好的route对象。嗯,我们来看下route方法的第二个参数,Function<PredicateSeqc>类型。
我们找到这个类,这里如果使用了readBody则会调用ReadBodyPredicateFactory的applyAsync方法,该方法为读取body的核心操作。
进入applyAsync方法后我们会看到关键的对请求信息做put.attribute的操作,key为一个工具类中的常量,进入方法首先判断是否有缓存,然后我们想到了FilteringWebHandle中的第一个全局过滤器RemoveCacheBodyFilter,所以我们在这里一定能够进入到else。那就是使用exchangeUtil中的cacheBody方法,最后将body获取并存储ccHashMap。
到这里我们看到了一个整体的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项目提供的,所以没有冲突。
https://www.cnblogs.com/zzq-include/p/12944680.html
这里敲下黑板!!!!!!,可以后面关注下这个readBody方法。
- 修改requestBody
修改requestBody,由于上面的readBody的提示,我们自然而然的就想到了应该也有一个类似方法来控制,在这里我们需要注意下readBody是以predicate的形式出现的,而modifyBody是以过滤器的身份出现的,非全局过滤器。
如果我们需要全局对每一个请求Body都可能有监控修改的需求,建议按照modifyRequestBodyFilterFactory的内容,自己定义一个全局过滤器这样也免去了配置的麻烦。
八、 还没想好