spring cloud zuul使用记录(2)路由接入流程以及并发刷新问题
最近在看spring cloud zuul(版本Finchley.SR1)的源代码,一不小心还看到了个bug(我认为是哈),更神奇的是,这个bug一年前已经有人提了issue,并提交了PR(竟然抢在我之前了)。但是现在还没有合并进来,7天前被管理员放进了icebox,这是什么操作?我不太清楚?是说会拿出来合并么?还是啥?哪位有经验的同学知道麻烦告诉我。那这整个事情是怎么样的呢?这都得从Zuul路由管理与SpringMVC的请求接入说起
起源
我们知道在配置zuul property的时候,当配置了一个route的path之后,zuul就会自动读取这些路由规则并进行配置。那这一切是怎么做到的呢?我们首先看我们启用@EnableZuulProxy启动zuul之后spring具体做了什么:
EnableZuulProxy配置类
这个配置类引入了一个ZuulProxyMarkerConfiguration
ZuulProxyMarkerConfiguration类
而这个类只是引入了一个Marker Bean,通过find usage我们看到
ZuulProxyAutoConfiguration类
这个autoConfiguration类是通过spring.factories来注入的自动配置。而这个类他继承了ZuulServerAutoConfiguration:
ZuulServerAutoConfiguration类
这个类中我们注意的是一个SimpleRouteLocator,这个类注入了zuulProperties:
ZuulProperties类
这个类就是读取的配置文件中的zuul相关的properties,那注入这个properties的SimpleRouteLocator就很有可能是生成route的地方。
SimpleRouteLocator类
SimpleRouteLocator类中的代码也表明了我的判断。到这里我们知道了配置是怎么映射到route规则的,当然仅仅这一点远远不够,我们知道,当我们定义了一个route规则之后,我们可以直接请求访问这个route的path来达到我们想要到达的serviceId或者url而不需要定义任何controller,这个spring是如何做到的呢?
引路人
针对上面的问题,我重新回到之前提到的几个配置类,发现了如下信息:
ZuulController和Mapping配置
我们知道本身Zuul是通过servlet来做的入口,而我们上图看到的这个ZuulController
ZuulController实现
我们可以发现他就是一个Servlet的包装,通过将请求代理给ZuulServlet来实现zuul的功能,可见在这个Controller前面肯定需要一个组件去把请求forward给它,这个组件很有可能就是之前看到的ZuulHandlerMapping,因为它的初始化使用到了zuulController
zuulHandlerMapping继承关系与注释
通过查看ZuulHandlerMapping继承关系和注释我们看到了他继承了AbstractUrlHandlerMapping抽象类,熟悉SpringMVC的同学知道,对于请求入口或者我们自己编写的Controller方法,SpringMVC会生成HandlerMapping示例,DispatcherServlet通过遍历spring上下文中已经存在的HandlerMapping来进行http请求的查找匹配,执行链路的组建和请求的执行,我给大家列出来DispatcherServlet中的代码,具体的调用链路和原理有兴趣的同学可以下来看看:
DispatcherServlet调用入口
针对ZuulHandlerMapping中的代码,我们目前只需要知道,对于每次request请求进来,dispatcherServlet都会调用ZuulHandlerMapping的lookupHandler方法,来查找是否有合适的zuul route规则,如果有就将请求导入给ZuulController,那么我们再来仔细看看具体的方法:
lookupHandler实现
之前一通常规操作,然后通过一个volatile变量dirty判断目前的route是否有变更,如果有就重新注册路由信息并且重置dirty变量为false,最后调用父类的loopupHandler。dirty变量默认为true,就保证在请求进来的时候肯定会有一个初始化的过程,那我们进入registerHandlers方法看看
registerHandlers方法
这里我们就明白了,这个方法对当前所有的routes信息都调用父类的registerHandler来注册能处理的path。从而完成了整个调用链路的匹配与搭建。ZuulHandlerMapping就像是一个引路人一样指引每一个能被zuul处理的request到ZuulController中。一切看起来都非常美好对吧。但是善于思考的同学又会有新的问题了,dirty只会在初始化的时候使用么?routes可以中途刷新么?答案是可以的。
Bad Smell
我注意到ZuulHandlerMapping类中有这样一个方法:
setDirty
通过find usage我们知道这个方法会在一些EventListener中被调用
更新设置dirty调用
通过上述两份代码和查阅Spring Cloud Zuul的文档我知道,如果你想要使得你的RouteLocator能够可以更新,那就让你的RouteLocator类实现RefreshableRouteLocator接口并实现refresh方法,然后在每次需要更新的时候向spring 上下文发布RoutesRefreshedEvent就行了,剩下的一切就交给刚刚看到的代码和spring做就行了。这一切看上去也很perfect。但是我总觉得哪里不对,感觉闻到了怪怪的味道。这边再给大家仔细列一下代码:
怪怪的味道
当一个更新事件发起的时候,setDirty方法会先设置dirty为true,然后调用routeLocator的refresh方法,这没问题。在一个请求进来的时候,会检查dirty是否为true,如果有,则重新注册path和handler,这似乎也没问题,还用了double check。但是这两个加在一起,是否存在这样的场景,当一次routeLocator refresh的时间比较长而这时候zuul的请求load比较高的时候,一个请求进来发现此时需要重新注册handler,但这是routes信息并没有完成刷新,或者说根本没有开始刷新,那这时候注册的,还是刷新前的老的数据,也就是说,更新之后的路由信息完完全全没有被注册到springmvc的处理链路中,整个网关并不会处理新增加的path,或者还会接入已经删除的path,这是个bug!归纳起来,就是当registerHandler的调用线程优先于routeLocator的refresh的调用,那么路由数据的更新就会失效并且这是不可恢复的!我在github上查找有关dirty的issue,也发现了下面的记录
https://github.com/spring-cloud/spring-cloud-netflix/pull/2259
这个issue跟我描述的基本上一毛一样,并且也提了PR,也就是本文最开始所提到(有木有哪位老铁告诉我啥叫icebox啊)。
当然BB是不够的,我下面会通过一个例子来阐述这个bug:
证据
下面所讲的代码都已经提交的github:
https://github.com/ro9er/zuul-dirty-bug-sample
首先我们定义一个RefreshableRouteLocator
RefreshableRouteLocator实现
这个实现其实很简单,用Entiry抽象了路由信息,并且在每次刷新的时候重新完成Entiry到Route的映射工作,并且实现了SimpleRouteLocator的getRoutes和getMatchingRoute方法,getRoutes在我们之前看到的ZuulHandlerMapping中registerHandlers中被调用,用来注册handler信息。getMatchingRoute方法在Spring Cloud Zuul实现的PreDecorationFilter中被调用,用来确定一个具体的route,并设置到跳转规则中,这里就不具体展开了。这里我在refresh方法中注释了线程sleep10秒的操作,后面我会打开它。当这个routeLocator初始化的时候只有一个/baidu路由规则跳转到百度
然后我实现了一个Controller:
刷新controller方法
这个controller暴露一个刷新路由信息,这里我们看到它会向我之前定义的routeLocator增加一条/163跳转网易的规则,并且发出一个RoutesRefreshedEvent,从而触发路由规则触发流程。然后我们来看看整个调用场景:
调用百度场景
调用163报404
调用刷新路由接口
再次调用163
通过上面整个场景流程我们知道,在在开始启动的时候,只有/baidu规则有效,/163会直接404,在我们刷新路由之后,在此访问/163,成功跳转到网易,证明我们的刷新机制是生效了。那么我们现在来复现bug,为了让这个bug比较容易的复现,我在refresh方法中打开了线程sleep 10s的操作,使得我们的刷新路由操作会延迟执行:
打开线程sleep
再次重复之前的流程,重复的步骤我就不贴图了,我们知道在增加了这个线程sleep的情况下,我们的refreshRoutes接口会变慢,当我们在这个接口执行的过程中我们调用一个/163,会因为dirty重新出发regsiterHandler,并且返回404(显然的,因为现在根本没有增加163这个规则),然后我们在refreshRoutes返回之后再次执行/163:
调用refresh
第一次调用163
第二次调用163
从上述的调用可以发现,刷新之后新的路由并没有生效,而且这个除非你重新调用一次refresh,不然不可能恢复。然鹅,就算你调用refresh,也不一定能够恢复,因为有可能下次request进来又把你冲掉了。
解决方案
问题明确了,怎么解决呢?如之前PR中所说的,可以把setDirty中的dirty赋值操作放到最后:
dirty放到最后
这个修改应该就能解决这个问题,但是现在并没有合并进来,还有没有其他办法呢?
我的办法是增加一个Listener,并且通过Ordered接口保证第一个执行,在消息处理里面手动触发refresh,不过弊端就是refresh会调用两次
增加消息处理
亲测可用。
结语
到此整个bug的出现我大概已经说明清楚了,并且顺带把zuul的handler mapping流程也梳理了一遍,大家有什么问题欢迎留言,希望能跟大家一起交流,共同进步。
作者:ro9er
链接:https://www.jianshu.com/p/eab9e2f9fbb6
共同学习,写下你的评论
评论加载中...
作者其他优质文章