zuul1.x原理剖析

zuul网关 = 路由转发 + 过滤器
1、路由转发:接收一切外界请求,转发到后端的微服务上去;
2、过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成(其实路由转发也是通过过滤器实现的)

pom配置:

org.springframework.cloud spring-cloud-starter-netflix-zuul

启动类:@EnableZuulProxy或@EnableZuulServer
@ @EnableZuulServer创建一个SimpleRouteLocator从Spring Boot配置文件加载路由定义。

@EnableZuulProxy
创建一个DiscoveryClientRouteLocator从DiscoveryClient(例如Eureka)以及属性加载路径定义的方法。路线为每个创建serviceId从所述DiscoveryClient。添加新服务后,将刷新路由。

Zuul拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡,传统路由也就是使用path与url映射关系来配置路由规则的时候,对于路由转发的请求不会使用HystrixCommand来包装,没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。
yml配置文件:
传统路由配置, : http://zuul的Host地址:zuul端口/path/服务方法地址
访问方式:http://localhost:9066/api-test/api-test/hello?accessToken=
zuul.host.connect-timeout-millis : 请求连接超时时间 (以毫秒为单位)。默认为2000,
zuul.host.socket-timeout-millis : 请求处理超时时间(以毫秒为单位的套接字超时)。默认为10000。

通过eureka来访问,默认支持负载均衡
访问方式:http://localhost:9066/api-a/api-test/hello?accessToken=3

面向服务路由会从注册到服务治理框架中取出服务实例清单,通过清单直接找到对应的实例地址清单,从而通过Ribbon进行负载均衡选取实例进行路由(请求转发),Zuul在注册到Eureka服务中心之后,它会为Eureka中的每个服务都创建一个默认的路由规则,默认规则的path会使用serviceId配置的服务名作为请求前缀.如果不希望开放的服务被外部访问到.可以使用zuul.ignored-services参数来设置一个不自动创建该服务的默认路由。
Zuul在自动创建服务路由的时候会根据这个表达式进行判断,如果服务名匹配表达式,那么Zuul将跳过此服务,不为其创建默认路由
如:zuul:
ignored-services: ‘’ 或指定的服务名,逗号隔开或者

通过zuul路由访问服务错误:

这个错误是因为ribbon默认超时时间比较小.
走网关的话,有三层的超时时间,一个是zuul的,一个是ribbon的,还有一个是hystrix的。

核心实现:

核心类图

核心类分析
ZuulConfiguration

过滤器:
类型 描述
PRE 在请求被路由之前调用。可用来实现身份验证、在集群中选择请求的微服务、记录调试信息等。
ROUTING 将请求路由到微服务。用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
POST 在路由到微服务以后执行。可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
ERROR 在其他阶段发生错误时执行该过滤器。

从上图中可以看出,当外部HTTP请求到达API网关服务的时候,首先它会进入第一个阶段pre,在这里它会被pre类型的过滤器进行处理,该类型的过滤器主要目的是在进行请求路由之前做一些前置加工,比如请求的校验等。在完成了pre类型的过滤器处理之后,请求进入第二个阶段routing,也就是之前说的路由请求转发阶段,请求将会被routing类型过滤器处理,这里的具体处理内容就是将外部请求转发到具体服务实例上去的过程,当服务实例将请求结果都返回之后,routing阶段完成,请求进入第三个阶段post,此时请求将会被post类型的过滤器进行处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以在post类型的过滤器中,可以对处理结果进行一些加工或转换等内容。另外,还有一个特殊的阶段error,该阶段只有在上述三个阶段中发生异常的时候才会触发,但是它的最后流向还是post类型的过滤器,因为它需要通过post过滤器将最终结果返回给请求客户端

类型 顺序 过滤器 功能
pre -3 ServletDetectionFilter 标记请求是否通过spring DispatchServlet而来
pre -2 Servlet30WrapperFilter 包装HttpServletRequest请求为Servlet 3.0兼容请求
pre -1 FormBodyWrapperFilter 解析form请求体并重新编码包装
pre 1 DebugFilter 根据请求的调试参数标记调试标志
pre 5 PreDecorationFilter 根据请求URI获取的route设置请求上下文(设置请求的serviceId或者请求地址或者重定向地址)
route 10 RibbonRoutingFilter 使用ribbon及hystrix实现负载均衡、熔断降级的请求调用
route 100 SimpleHostRoutingFilter 根据Route的path进行请求调用
route 500 SendForwardFilter 根据Route重定向地址进行请求重定向,通常转发到当前应用
post 0 SendErrorFilter filter执行出错时输出响应
post 1000 SendResponseFilter filter执行成功输出响应

3、Zuul的核心:ZuulServlet
Zuul默认是使用信号量隔离,并且信号量的大小是100,也就是当一个路由请求的信号量高于100那么就拒绝服务了,返回500。
也可以改为使用线程隔离,调大hystrix线程池线程大小,该线程池默认10个线程,每个路由都有自己的线程池,而不是共享一个。
Zuul基于Servlet框架,ZuulServlet用于处理所有的Request。其可以认为是所有Http Request的入口。
其类图如下:

RequestContext:用于在过滤器之间传递消息。它的数据保存在每个请求的ThreadLocal中。它用于存储请求路由到哪里、错误、HttpServletRequest、HttpServletResponse都存储在RequestContext中。RequestContext扩展了ConcurrentHashMap,所以,任何数据都可以存储在上下文中。RequestContext类中有ThreadLocal变量来记录每个Request所需要传递的数据.

三个核心的方法preRoute(),route(), postRoute(),zuul对request处理逻辑都在这三个方法里,ZuulServlet交给ZuulRunner去执行。由于ZuulServlet是单例,因此ZuulRunner也仅有一个实例。而ZuulRunner直接将执行逻辑交由FilterProcessor处理, FilterProcessor也是单例。 FilterProcessor功能就是依据filterType执行filter的处理逻辑。

每个filter的处理逻辑

上面两段代码是FilterProcessor对filter的处理逻辑。
首先根据Type获取所有输入该Type的filter,List list。
遍历该list,执行每个filter的处理逻辑,processZuulFilter(ZuulFilter filter)
RequestContext对每个filter的执行状况进行记录,应该留意,此处的执行状态主要包括其执行时间、以及执行成功或者失败,如果执行失败则对异常封装后抛出。
到目前为止,zuul框架对每个filter的执行结果都没有太多的处理,它没有把上一filter的执行结果交由下一个将要执行的filter,仅仅是记录执行状态, 如果执行失败抛出异常并终止执行。
ZuulFilter.runFilter()方法

zuul框架对filter的处理到此结束。ZuulFilterResult 记录了该filter的执行状态,run() 中返回的Object其实在zuul框架中没有用到过。

执行流程:

PreDecorationFilter 前置过滤器

SimpleHostRoutingFilter 转发过滤器

RibbonRoutingFilter

SendResponseFilter:主要是处理header跟response

ribbon:
ReadTimeout: 3000
ConnectTimeout: 3000
MaxAutoRetries: 1 #同一台实例最大重试次数,不包括首次调用
MaxAutoRetriesNextServer: 1 #重试负载均衡其他的实例最大重试次数,不包括首次调用
OkToRetryOnAllOperations: false #是否所有操作都重试
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 断路器超时时间 默认1000.

ribbon.ConnectTimeout:请求连接超时时间。若出现路由请求连接超时,会自动进行重试路由请求,如果重试依然失败,Zuul会抛出异常。
ribbon.ReadTimeout:请求处理的超时时间 ,下级服务响应最大时间,超出时间消费方(路由也是消费方)返回timeout
一般情况下 都是 ribbon 的超时时间(<)hystrix的超时时间,如果在重试期间,时间超过了hystrix的超时时间,便会立即执行熔断,fallback。所以要根据上面配置的参数计算hystrix的超时时间,使得在重试期间不能达到hystrix的超时时间,不然重试机制就会没有意义 .当ribbon超时后且hystrix没有超时,便会采取重试机制

ribbon的重试机制:当一个请求过来被分发到其中的一个实例处理,当这个实例出现问题无法响应或者响应超时的时候,继续请求当前实例,或者请求其他实例,
重试的场景分为三种:
okToRetryOnConnectErrors:只重试网络错误
okToRetryOnAllErrors:重试所有错误
OkToRetryOnAllOperations:重试所有操作,为true,则所有的请求都进行重试.如果为false,不是get请求,就关闭重试.如果是get请求,则开启重试。
重试的次数有两种:
MaxAutoRetries:每个节点的最大重试次数
MaxAutoRetriesNextServer:更换节点重试的最大次数

AbstractLoadBalancerAwareClient: // 获取请求重试handler 构建ribbon的负载均衡请求

步骤说明:
1.从ribbon的配置里面获取OkToRetryOnAllOperations的配置,默认为false,如果配置为true,那么设置okToRetryOnConnectErrors = true , okToRetryOnAllErrors = true , 也就是说 配置了这个,只要发现实例不可达,那么都会进行重试
2.判断请求方式是否是POST请求,如果是POST 请求,则设置okToRetryOnConnectErrors = true , okToRetryOnAllErrors = false , 这个设置的意思,只要在出现connectErrors的时候才会进行重试。
3.第三步,基本上表示该请求为GET请求了,这个时候是设置okToRetryOnConnectErrors = true , okToRetryOnAllErrors = true , 跟第一步的效果一样。
默认ribbon重试一次,POST请求默认是在connect异常的时候会重试,GET请求是都会进行重试,包括请求超时之类的。所以尽量不要用GET请求做增加,修改,删除的操作,

getRequestSpecificRetryHandler 由于这个ribbon是结合feign使用的,所以这个获取重试机制的方法是实现在下面这个类里面:

LoadBalancerCommand:
无论上层是Feign调用还是Zuul调用,到了Ribbon这一层都是创建一个LoadBalancerCommand,调用其中的submit方法执行http请求,这里利用了RxJava机制:
1:读取配置,最多重试多少台服务器以及每台服务器最多重试次数.
2:利用RxJava生成一个Observable(发射源,可观察对象)用于后面的回调,选择一个server进行调用
3:获取本次server调用的回调入口,用于重试同一实例的重试回调
4:最后设置重试超过次数则终止调用并设置对应异常的回调

设置重试的回调:
1:只有返回为true的时候才会retry
2:超过最大重试次数则不重试

3:判断是否是可以重试的exception

动态路由:

ZuulConfiguration: 核心类,路由定位器.默认配置的实现是SimpleRouteLocator.class

注册了一个路由刷新监听器,默认实现是ZuulRefreshListener.class,

SimpleRouteLocator

路由定位器和其他组件的交互,最终把定位的Routes以list的方式提供出去

定位路由信息

zuul默认的路由都是在properties里配置的,实现动态路由需要实现可刷新的路由定位器接口(RefreshableRouteLocator),并可以继承默认的实现(SimpleRouteLocator)再进行扩展
实现动态路由主要关注两个方法
protected Map<String, ZuulRoute> locateRoutes():此方法是加载路由配置的,父类中是获取properties中的路由配置,可以通过扩展此方法,达到动态获取配置的目的
public Route getMatchingRoute(String path):此方法是根据访问路径,获取匹配的路由配置,父类中已经匹配到路由,可以通过路由id查找自定义配置的路由规则,以达到根据自定义规则动态分流的效果

zuul1
(1)优势:
同步阻塞模式的编程模型比较简单,整个请求->处理->响应的流程(call flow)都是在一个线程中处理的,这样开发调试比较方便易于理解,比如出了问题跟踪调试比较方便。另外,线程局部变量(ThreadLocal)机制在同步多线程模式下可以工作,有些监控产品,例如CAT调用链依赖于ThreadLocal,在同步多线程模式下,CAT埋点比较方便,调用链关系的展示也比较直观。
(2)不足:
线程本身需要消耗CPU和内存资源,且多线程之间切换是有开销的(所谓的上下文切换Context Switch开销),线程越多,这种上下文切换的开销就越大,同步阻塞模式一般会启动很多的线程,必然引入线程切换开销。另外,同步阻塞模式下,容器线程池的数量一般是固定的,造成对连接数有一定限制,当后台服务慢,容器线程池易被耗尽,一旦耗尽容器会拒绝新的请求,这个时候容器线程其实并不忙,只是被后台服务调用IO阻塞,但是干不了其它事情。

总体上,同步阻塞模式比较适用于计算密集型(CPU bound)应用场景。对于IO密集型场景(IO bound),同步阻塞模式会白白消耗很多线程资源,它们都在等待IO的阻塞状态,没有做实质性工作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值