zuul网关实现解析

徐键
介绍

    zuul是spring cloud的网关组件,可以构建动态路由、服务降级、负载均衡的服务网关,通过filter链式调用进行扩展,实现统一认证、调用监控、日志管理等等功能。

    本文主要介绍zuul 1.x实现,现在zuul 2也已经开源,并且spring官方也推出了spring cloud gateway,后两者都支持非阻塞io,性能上有了一定提升,提升主要来自对于netty的引入,通过netty实现请求的接收、响应、及调用其他服务调用的io操作都是基于非阻塞io。 下图比较了三者高并发下的性能比较(下图的zuul为1.x):

实现

    下面主要介绍下zuul的组件及怎么通过依赖ribbon(spring cloud的负载均衡组件)及hystrix(spring cloud的熔断降低服务)实现负载均衡、熔断降级功能。

主要组件:

  • ZuulFilter 这是zuul的核心,分为pre、route、post三种类型,分别对应服务调用之前、之中、之后的处理。
  • ZuulServlet 一个HttpServlet,执行所有ZuulFilter的入口。
  • Route 路由的一个节点,通常对应某个微服务。
  • RouteLocator 获取所有Route的接口,有SimpleRouteLocator和DiscoveryClientRouteLocator两种实现,后者继承自前者,前者通过配置文件配置获得,后者扩展了通过服务发现找到所有服务,并默认为所有服务生成path为/serviceId/**的Route。
  • ZuulController 需要路由的请求的统一处理器,继承自ServletWrappingController,把请求统一代理给ZuulServlet处理。
  • ZuulHandlerMapping 继承自spring mvc模块的AbstractUrlHandlerMapping,通过RouteLocator 获取到所有Route之后把route的fullPath和ZuulController注册到到HandlerMap中,把网关需要路由的路径统一映射到ZuulController

    通过以上组件zuul实现了服务的动态路由,而负载均衡、熔断降级这是通过Zuul内置的ZuulFilter实现的,下图展示了filter如何运行: 来看看Zuul有哪些内置的filter:

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

    下面来看看RibbonRoutingFilter怎么实现负载均衡、熔断降级的服务调用,RibbonRoutingFilter内部使用一个RibbonCommandFactory来构建一个RibbonCommand,使用了工厂方法模式,RibbonCommand继承自HystrixCommond,有三种具体实现,每种实现内部包含一个http请求的client:ribbon自己实现的RestClient,RibbonLoadBalancingHttpClient,OkHttpLoadBalancingClient,通过spring boot自动化配置根据classpath是否引入对应依赖来判断具体使用。 这三个client继承自AbstractLoadBalancingClient,而AbstractLoadBalancingClient又继承自AbstractLoadBalancerAwareClient,内部会构建一个LoadBalancerCommand,comond内部使用ILoadBalancer来做到负载均衡。 ILoadBalancer是ribbon的负载均衡器,不同的负载均衡策略有IRule来实现,来看看ribbon实现了哪些策略:

ribbon rule

AbstractLoadBalancerRule
负载均衡策略的抽象类,在该抽象类中定义了负载均衡器ILoadBalancer对象,可通过getReachableServers获取up的实例列表,通过getAllServers获得所有实例列表,为负载策略提供所需实例。

RandomRule
该策略实现了从服务实例清单中随机选择一个服务实例的功能。通过rand.nextInt(serverCount)函数来获取一个随机数,获取一个随机应用实例。

RoundRobinRule
该策略实现了按照线性轮询的方式依次选择每个服务实例的功能。通过一个count计数变量,线性轮询获取应用实例。

RetryRule
该策略实现了一个具备重试机制的实例选择功能。从下面的实现中我们可以看到,在其内部还定义了一个IRule对象,默认使用了RoundRobinRule实例。而在choose方法中的则实现了对内部定义的策略进行反复尝试的策略,若期间能够选择到具体的服务实例就返回,若选择不到就根据设置的尝试结束时间为阈值(maxRetryMillis参数定义的值 + choose方法开始执行的时间戳),当超过该阈值后就返回null。

WeightedResponseTimeRule
WeightedResponseTimeRule策略在初始化的时候会通过serverWeightTimer.schedule(new DynamicServerWeightTask(), 0, serverWeightTaskTimerInterval)启动一个定时任务,用来为每个服务实例计算权重,该任务默认30秒执行一次。
通过收集每个实例的平均耗时来计算权重,假设有4个实例A、B、C、D,他们的平均响应时间为:10、40、80、100,所以总响应时间是10 + 40 + 80 + 100 = 230,每个实例的权重为总响应时间与实例自身的平均响应时间的差的累积获得,所以实例A、B、C、D的权重分别为:
实例A:230 - 10 = 220
实例B:220 + (230 - 40)= 410
实例C:410 + (230 - 80)= 560
实例D:560 + (230 - 100)= 690
获得权重区间:
实例A:[0, 220]
实例B:(220, 410]
实例C:(410, 560]
实例D:(560,690)
通过0到690的一个随机数定位的在哪个区间来选择实例。

ClientConfigEnabledRoundRobinRule
内部使用RoundRobinRule进行负载均衡,扩展该类可以实现更丰富的负载策略,并以RoundRobinRule为降级策略。

BestAvailableRule
该策略继承自ClientConfigEnabledRoundRobinRule,在实现中它注入了负载均衡器的统计对象:LoadBalancerStats,同时在具体的choose算法中利用LoadBalancerStats保存的实例统计信息来选择满足要求的实例。它通过遍历负载均衡器中维护的所有服务实例,会过滤掉故障的实例,并找出并发请求数最小的一个,所以该策略的特性是选出最空闲的实例。

PredicateBasedRule
这是一个抽象策略,它也继承了ClientConfigEnabledRoundRobinRule,从其命名中可以猜出他是一个基于Predicate实现的策略,Predicate是Google Guava Collection工具对集合进行过滤的条件接口,通过Predicate实现来过滤出要选择的应用实例列表再从中随机一个实例。

AvailabilityFilteringRule
该策略继承自上面介绍的抽象策略PredicateBasedRule,通过RoundRobinRule选择一台实例后,使用AvailabilityPredicate匹配,如果符合条件则返回当前实例,匹配的条件是实例可用,并且ServerStats统计的活跃请求数少于配置的最大请求数,默认请求数统计窗口是10分钟。

ZoneAvoidanceRule
该策略也继承自上面介绍的抽象策略PredicateBasedRule,它使用了CompositePredicate来进行服务实例清单的过滤。这是一个组合过滤条件,在其构造函数中,它以ZoneAvoidancePredicate为主过滤条件,AvailabilityPredicate为次过滤条件初始化了组合过滤条件的实例,ZoneAvoidancePredicate该过滤条件需要实例具备区域属性,如果没有直接返回true,有的话判断所在区域是否可用(判断条件是故障实例比例小于配置值并且实例负载不为0),该策略为ribbon默认策略。

负载均衡使用ribbon实现,而熔断降级则由hystrix实现,上面也介绍了RibbonRoutingFilter内部是使用一个RibbonCommandFactory来构建一个RibbonCommand,是继承自HystrixCommond,即拥有了hystrix的熔断降级能力,对于hystrix,本文不做展开了,简单讲就是使用rxjava对RibbonCommand的执行进行各个执行状态的统计,通过判断统计结果来确定是否熔断降级,稍微介绍下rxjava。 RxJava 是一个响应式编程框架,采用观察者设计模式,主要组件如下:

  • Observable:发射源,英文释义“可观察的”,在观察者模式中称为“被观察者”或“可观察对象”;
  • Observer:接收源,英文释义“观察者”,没错!就是观察者模式中的“观察者”,可接收Observable、Subject发射的数据;
  • Subject:Subject是一个比较特殊的对象,既可充当发射源,也可充当接收源;
  • Subscriber:“订阅者”,也是接收源,那它跟Observer有什么区别呢?Subscriber实现了Observer接口,比Observer多了一个最重要的方法unsubscribe( ),用来取消订阅,当你不再想接收数据了,可以调用unsubscribe( )方法停止接收,Observer 在 subscribe() 过程中,最终也会被转换成 Subscriber 对象,一般情况下,建议使用Subscriber作为接收源;
  • Subscription :Observable调用subscribe( )方法返回的对象,同样有unsubscribe( )方法,可以用来取消订阅事件;
  • Action0:RxJava中的一个接口,它只有一个无参call()方法,且无返回值,同样还有Action1,Action2…Action9等,Action1封装了含有 1 个参的call()方法,即call(T t),Action2封装了含有 2 个参数的call方法,即call(T1 t1,T2 t2),以此类推;
  • Func0:与Action0非常相似,也有call()方法,但是它是有返回值的,同样也有Func0、Func1…Func9;

本文是对zuul实现的简单介绍,顺便引出了ribbon,hystrix,rxjava等,这里的每个组件都可以单独使用。