您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
浅谈zuul网关实现原理及应用
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
浅谈zuul网关实现原理及应用
自猿其说Tech
2022-05-05
IP归属:未知
544360浏览
### 1 前沿 #### 1.1 Api网关 api 网关是微服务体系下的一种服务,提供系统唯一的流量入口,所有的客户端和服务端通过统一的网关接入到微服务,同时网关也具有鉴权校验、负载均衡、限流熔断、监控等功能。 #### 1.2 技术对比 api网关的开源框架繁多,但主流的开源框架有Kong、Traefik、Ambassador、Tyk以及Zuul等,以下就几方面进行进行api网关的对比 ![](//img1.jcloudcs.com/developer.jdcloud.com/05da21eb-0cef-4273-8944-e0d0c1b7968d20220505143245.png) 以上简单对比可以看出,Kong、Traefik社区活跃度最高,且体系成熟,有丰富的插件,性能上也不错,zuul网关插件需要自研或者集成他组件,但是与spring cloud体系深度集成,使用度也很高,本文就zuul网关的相关实现原理与简单使用做了以下介绍 ### 2 Zuul网规简介 zuul网关作为spring cloud中的微服务网关,是系统流量的统一入口,所有请求都需要通过网关,进行路径的路由,定位到具体的后端微服务 - 网关本身也是微服务,所以也需要注册到注册中心,常用的注册中心有cp系列(zookeeper,consul),ap系列(eureka)等,也可以与ribbon,hystrix等组件配合使用 网关本身并不是必要的,是推荐使用的。 ### 3 Zuul网关原理 #### 3.1 为什么需要网关 - 入口统一:外部请求均需通过网关才能到达后端服务,保障了后台服务的安全性 - 鉴权校验:对请求进行权限校验,拒绝不符合要求的请求 - 动态路由:根据负载均衡算法动态的将请求路由到不同的后端服务集群中 - 限流熔断:请求高峰时可以多维度的对服务进行限流以及熔断策略,保证后端服务的安全性 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/f74cc409-35d0-4eaf-8fa2-878eb1d771c520220505143445.png) 图1 网关部署图</center> ### 3.2 网关架构 #### 3.2.1 过滤器种类 zuul网关通过过滤器来实现认证、限流等功能,这些过滤器通过groovy编写,并保存在固定目录中,服务器开另外开启一个线程采用定时轮询的方式来发现已经更改的过滤器,然后动态地进行编译加载,等后续新的请求进来更改的过滤器便会生效,过滤器有如下种类: - PRE filters(前置过滤器): 请求转发到后端服务器之前会先经过这个过滤器,在这种过滤器可以做请求鉴权、选择路由、以及设置日志信息等。 - ROUTE filters(路由过滤器): 这个过滤器负责把请求转发到后端服务器上,在这里进行请求的组装和以及使用Apache httpClient 和 Ribbon发送请求。 - POST filters(后置过滤器):请求路由到后端服务之后的过滤器,在这里可以添加response响应头,数据收集等。 - ERROR filters(错误过滤器): 以上任何过滤器执行过程中出现错误会执行这个过滤器 <center>![](//img1.jcloudcs.com/developer.jdcloud.com/128ffe25-d132-4f41-9fe1-05dd5ba7108620220505151053.png) 图2 zuul 网关架构图</center> ##### 3.2.2 过滤器加载机制 Zuul filter加载实际上是通过groovy,基于最后更新时间来进行热加载的,先来看下Zuul filter如何进行缓存 (com.netflix.zuul.filters.FilterRegistry) ![](//img1.jcloudcs.com/developer.jdcloud.com/94155f01-eea3-4392-bab4-5663a7c7322520220505151133.png) 首先filter的缓存类是单例的(饿汉模式),然后用一个concurrentHashMap来缓存filter的对应关系,这些缓存除非主动调用remove方法,否则不会自动清理;Zuul提供默认的动态代码编译器,接口是DynamicCodeCompiler,目的是把代码编译为Java的类,默认实现是GroovyCompiler,功能就是把Groovy代码编译为Java类,这就涉及到了一个总要的工厂接口类FilterFactory,它定义了ZuulFilter类生成ZuulFilter实例的逻辑,默认实现是DefaultFilterFactory,实际上就是利用Class#newInstance()反射生成ZuulFilter实例。接着,我们可以进行分析FilterLoader的源码,这个类的作用就是加载文件中的ZuulFilter实例 ![](//img1.jcloudcs.com/developer.jdcloud.com/16066515-ae9d-4453-bdef-d6c111cf6efa20220505151145.png) FilterLoader里定义了几个Map,具体作用如注释,FilterLoader里比较重要的方法就是 putFilter,主要作用是通过文件加载ZuulFilter实例 ![](//img1.jcloudcs.com/developer.jdcloud.com/28213a89-6c2e-41cc-92d5-a524d2d64d2b20220505151200.png) putFilter会读取通过文件路径读取文件,如果文件有修改过,则会重新删除缓存里的数据重新加载,读取到文件内容后,用默认的编译器把groovy编译成java class,然后通过FilterFactory进行实例化,那么问题来了, 我们知道了加载Filter的具体实现,那么如何触发这个方法呢?通过FilterFileManager! ![](//img1.jcloudcs.com/developer.jdcloud.com/b4de59b8-de10-463d-b33f-eb1d9db76dfd20220505151215.png) 原理就是后台启动一个守护线程,定时轮询指定文件夹里面的文件,如果文件存在变更,则尝试更新指定的ZuulFilter缓存,FilterFileManager的init方法调用的时候在启动后台线程之前会进行一次预加载,Zuulj就是通过这样的机制实现了过滤器的加载。 zuul是通过request context来进行过滤器之间的信息传递,具体底层原理其实就是通过threadlocal来实现,过滤器之间遵循 前置过滤器 -> 路由过滤器 -> 后置过滤器的顺序执行,当这些过滤器执行过程中出现错误,会执行error过滤器,以下是zuul servlet源码: ```java @Override public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { try { init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); RequestContext context = RequestContext.getCurrentContext(); context.setZuulEngineRan(); // 执行前置过滤器 try { preRoute(); } catch (ZuulException e) { error(e); postRoute(); return; } // 执行route过滤器 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(); }} ``` ##### 3.2.3 过滤器工作原理 关键方法: 过滤器继承父类zuulFilter,通过重写父类提供的模板方法自定义过滤器,zuulFilter主要方法有: - filterType: 返回字符串类型,表明当前过滤器是pre/route/post/error过滤器 - filterOrder: 返回int类型,用于定制同类型过滤器的执行顺序,数字越小,执行优先级越高 - shouldFilter:返回boolean类型,表明当前过滤器是否生效 - run: 具体的过滤执行逻辑 以下是zuulFilter源码: ```java @Component public class MyFilter extends ZuulFilter { // 过滤器类型 @Override public String filterType() { return null; } // 过滤器执行顺序 @Override public int filterOrder() { return 0; } // 当前过滤器是否有效 @Override public boolean shouldFilter() { return false; } // 当前过滤器过滤逻辑 @Override public Object run() { return null; }} //实现Comparable,基于filterOrder升序排序,也就是filterOrder越大,执行优先度越低 public int compareTo(ZuulFilter filter) { return Integer.compare(this.filterOrder(), filter.filterOrder()); } ``` 这里注意几个地方,第一个是filterOrder()方法和compareTo(ZuulFilter filter)方法,子类实现ZuulFilter时候,filterOrder()方法返回值越大,或者说Filter的顺序系数越大,ZuulFilter执行的优先度越低。第二个地方是可以通过zuul.${全类名}.${filterType}.disable=false通过类名和Filter类型禁用对应的Filter。第三个值得注意的地方是Zuul中定义了四种类型的ZuulFilter,每种filter处理请求的顺序不同。ZuulFilter实际上就是使用者扩展的核心组件,通过实现ZuulFilter的方法可以在一个请求处理链中的特定位置执行特定的定制化逻辑。第四个值得注意的地方是runFilter()方法执行不会抛出异常,如果出现异常,Throwable实例会保存在ZuulFilterResult对象中返回到外层方法,如果正常执行,则直接返回runFilter()方法的结果。 四种过滤器的执行顺序总结如下: 1. pre、route、post都不抛出异常,顺序是:pre->route->post,error不执行。 2. pre抛出异常,顺序是:pre->error->post。 3. route抛出异常,顺序是:pre->route->error->post。 4. post抛出异常,顺序是:pre->route->post->error。 ### 4 zuul实际应用 #### 4.1如何引用zuul网关组件? 1)pom文件中引入以下starter: ```java <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency> ``` 2)启动类上加上@EnableZuulProxy注解 ![](//img1.jcloudcs.com/developer.jdcloud.com/01865e8e-46c4-494e-b8e0-9ab2e580226b20220505151600.png) #### 4.2 zuul网关常用场景 ##### 4.2.1 路由场景 路由功能可以在application.yml中进行配置,且用法灵活简便,以下是在本地机器上测试的简单例子 说明:测试服务provider-demo提供测试接口,并且已经在eureka上注册 以下方式可以指定以【/api/**】的路径进行请求时会去请求provider-demo服务,【**】代表任意层级的url的请求,【*】则只请求一级 ![](//img1.jcloudcs.com/developer.jdcloud.com/fee0c35c-0d34-43c2-b662-2581efe9784820220505151627.png) 以下方式请求指定了【path】和【serviceId】,代表请求指定path时会去请求serviceId的服务,serviceId为服务在注册中心的注册名称 ![](//img1.jcloudcs.com/developer.jdcloud.com/6629fc24-b89c-41cc-9c21-3b05443047cb20220505151740.png) 以下方式请求指定了【path】和【url】,代表请求指定path时会去请求url的服务,但是注意这种方式会使hystrix和ribbon负载均衡失效 ![](//img1.jcloudcs.com/developer.jdcloud.com/2b09ee1c-24da-4a95-bb04-633ef7cbb06e20220505151757.png) 以下方式配置【hystrix】和【ribbon】的相关配置 说明:hystrix 是spring cloud的体系中的熔断器组件,当zuul网关调用具体业务逻辑时可能会受到多种因素影响,如网络延时、GC等原因,会使当前线程阻塞,这时候配置熔断策略使当前线程调用返回,保护后端服务集群;Ribbon是负载均衡组件,当请求来时可以根据具体的负载策略选择最合适的后端实例进行调用 ![](//img1.jcloudcs.com/developer.jdcloud.com/6f92e6e1-3ff2-494e-bd92-9eeb9e48cadd20220505151813.png) - retryable: 是否开启路由重试 - ribbon.MaxAutoRetries: 当前实例的重试次数 - ribbon.MaxAutoRetriesNextServer: 切换实例的重试次数 - ribbon.ReadTimeout: 请求处理的超时时间 - ribbon.ConnectTimeout: 请求连接的超时时间 - ribbon.OkToRetryOnAllOperations: 是否对所有请求都进行重试 - hystrix.command.default.execution.isoladtion.thread.timeoutInMilliseconds: 熔断时间 说明: - ribbon.ReadTimeout 和 connectTimeout 是路由服务时走注册中心的配置,如果路由服务时走url则使用zuul.host.connect-timeout-mills和zuul.host.socket-timeout-mills - 对指定服务配置hystrix时使用 hystrix.command.<serviceName>.execution.isoladtion.thread.timeoutInMilliseconds,对指定服务使用ribbon使用<serviceName>.ribbon.readTimeout/connectTimeout - 超时时间要注意:熔断设置的超时时间至少要大于(ribbon.readTimeout + ribbon.connectTimeout) * (MaxAutoRetries + 1) * (MaxAutoRetriesNextServer + 1),具体原因如下: 首先获取Hystrix的超时时间 ![](//img1.jcloudcs.com/developer.jdcloud.com/439903dc-453c-410b-8b16-38ba4c8f2e7820220505151903.png) 其次在方法getHystrixTimeout里先获取Ribbon的超时时间,然后分别获取配置文件里的默认熔断时间和针对指定服务的熔断时间,对这两个时间做简单的判断,然后设置hystrixTimeout,最后对ribbon超时时间和hystrixTimeout时间做比较,如果熔断时间小于ribbon超时时间,则打印警告信息,并提示可能的风险 ![](//img1.jcloudcs.com/developer.jdcloud.com/92036923-c7c7-4f5f-a237-64f91e91045420220505151930.png) 获取Ribbon超时时间,可以看到同样从配置文件获取相关配置,然后进行加和计算 ![](//img1.jcloudcs.com/developer.jdcloud.com/3e865d17-a512-4b68-82d0-9cc22aef094c20220505151948.png) ##### 4.2.2 限流/鉴权场景 其实就是通过不同的filter进行请求的拦截,然后根据需求在run()方法里进行处理,以下是一个限流的小例子 说明:常见的限流算法有漏桶、令牌桶、以及计数器等算法,测试的小例子采用guava 里ratelimiter组件依据令牌桶算法来实现 ```java @Component public class DemoFilter extends ZuulFilter { // 设置每秒获取一个令牌 private static final RateLimiter RATE_LIMITER = RateLimiter.create(1); @Override public String filterType() { // 前置过滤器 return PRE_TYPE; } @Override public int filterOrder() { return -4; } @Override public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); // 拦截指定请求 if ("/api/hello".equalsIgnoreCase(request.getRequestURI())){ return true; } //不拦截,放行 return false; } @Override public Object run() { //获取上下文 RequestContext requestContext = RequestContext.getCurrentContext(); //尝试获取令牌 if(!RATE_LIMITER.tryAcquire(100)){ //停止访问,并返回出错的消息 System.out.println("没有这么多令牌了"); // 后续过滤器均不起作用 requestContext.setSendZuulResponse(false); // 设置响应码 这里是429 requestContext.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value()); } //正常的话,继续向下走 return null; } } ``` 浏览器请求结果如下:发现api因为获取不到足够的令牌被限流,返回429 ![](//img1.jcloudcs.com/developer.jdcloud.com/34abda97-293c-4b6e-bf3d-3bbf856576ca20220505154008.png) ##### 4.2.3 熔断场景 因为zuul内置hystrix组件,所以我们只配置一个hystrix 仪表盘就可以通过可视化界面直观的观察到服务调用情况和断路器工作状态。 说明:需要引入hytrix dashborad 开启hystrix仪表盘,actuator监控收集数据 ```java <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-hystrix-dashboard</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> ``` 同时启动类上增加@EnableHystrixDashboard、@EnableCircuitBreaker注解 ![](//img1.jcloudcs.com/developer.jdcloud.com/508f1516-e325-450c-829a-c5fa175ece2c20220505154109.png) application.yml配置文件如下: ![](//img1.jcloudcs.com/developer.jdcloud.com/4916db54-48a1-438c-910b-a6e34c8cad0220220505154120.png) 最后访问 http://hostname:port/hystrix 即可进入到仪表盘: ![](//img1.jcloudcs.com/developer.jdcloud.com/1783b90f-e666-4118-bad0-249d25ea817020220505154134.png) 配置回退策略,继承ZuulFallbackProvider即可 ``` @Component public class Demoback implements ZuulFallbackProvider { @Override public String getRoute() { // 代表回退所有服务 return "*"; } @Override public ClientHttpResponse fallbackResponse() { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.BAD_REQUEST; } @Override public int getRawStatusCode() throws IOException { return HttpStatus.BAD_REQUEST.value(); } @Override public String getStatusText() throws IOException { return HttpStatus.BAD_REQUEST.getReasonPhrase(); } @Override public void close() { } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("services is unavailable".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); return httpHeaders; } }; ``` 当服务调用超时后,会执行回退逻辑 ![](//img1.jcloudcs.com/developer.jdcloud.com/4afffef3-4fa9-4d14-96df-096400dd08c820220505154207.png) ### 5 总结 本文从架构、工作原理和实际使用场景的角度简单描述了zuul网关的相关内容,实际上关于zuul的过滤器加载机制、内置过滤器、以及Ribbon的负载均衡机制和Hystrix限流机制的相关知识体系更加的庞大和复杂,需要我们在平时的工作中逐渐积累。本文中介绍的zuul网关基于1.x版本,由于使用多线程同步阻塞模型,所以在高并发场景下表现得不尽人意,但可喜的是zuul网关2.0版本已经开源,支持异步高并发,效率上有大幅度提升,但目前并未整合到spring cloud体系,同时spring cloud gateway 同样基于异步非阻塞模型,而且功能强大,与spring cloud体系无缝衔接,也是作为网关技术选型的好选择。 ### 6 参考资料 - spring cloud netflix 中文文档: https://www.springcloud.cc/spring-cloud-netflix.html - github: https://github.com/Netflix/zuul ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:杨威
原创文章,需联系作者,授权转载
上一篇:浅谈对敏捷的认识
下一篇:一条sql了解MYSQL的架构设计
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号