问题背景描述
现有系统,存在技术网关A,因集团安全架构升级,需要引入技术网关B,需要使用兼容的方式,同时共存两个网关,通过切流的方式逐步切到网关B,期间需要对线上业务无影响,用户无感切换。
系统架构
- 物理网关,最外层,可以理解为nginx,负责负载均衡,解析等内容
- 技术网关,统一接入层,做权限控制,转发内网等操作
- 应用服务,实际的服务端
问题描述
现象
- 通过A网关访问接口,可以正常访问(携带cookie信息)
- 通过B网关访问接口,偶尔正常,偶尔302跳转到taobao.com/error页面(携带cookie信息)
- 跳过网关直接访问接口,正常(携带网关A的登录token,因为网关B的token无法模拟)
分析
通过现象,基本可以确定是网关B的问题,请求不稳定导致的。但是经过对方的排查定位,他们已经转发到了应用服务器,302转发是应用服务器抛出的。
但是目前应用服务器内部逻辑是没有抛出302异常的能力的,如果真的是应用服务器抛出的,那么也是框架层抛出的。框架抛出的异常的话,那就是异常流,我认为依然是网关的问题,因为集团框架的兼容性本就要同为框架的网关来做兼容。设计开闭原则,不能因为引入一个框架,而去修改自己的基座。
处理过程
第一阶段,基本定位
基本可以定位是他们的问题,找他们排查,由于是接入的去中心化网关,他们那边定位比较困难是一方面,另一方面因为网关在集团内接入的比较多,我们系统的问题是个例,所以他们始终坚持是我们系统的内部原因,也没有积极配合来处理问题。
由于调用接口并不稳定,所以不能输出稳定的测试用例,而且也不能稳定复现问题。但这个问题就是存在,存在问题就一定不能发到线上,用户肯定无法允许接口存在成功率这一回事。
第二阶段,多元排查
既然只能依靠自己排查,就需要将case范围扩大,多元化的排查问题。
通过测试,请求方式,get,post两种,环境测试项目环境,日常环境,预发环境三套。请求网关A接口,网关B接口,直接请求接口。得出的结论就是网关A各种环境各种方式均正常,直接请求的接口因为需要mock网关A的token,所以也都正常。网关B日常环境,项目环境的get和post均正常。预发环境,get和post均有成功率问题。
这样定位到的点依然是网关的,无从下手,网关那块转发异常也没有日志可查。通过对应的链路id,也只能查到网关侧的日志,没有应用日志。
第三阶段,大胆设想
转发的时候,直接报错302,应用没有相关日志。那么就定位302到底是哪爆出来的。通过转发的链路id,只能查到转发前的链路,转发后的链路也没有。
- 转发的时候网关B携带了什么信息?
- 没有链路信息,是物理网关直接给拒掉了吗?
- 如何确定是不是物理网关抛出的异常?
无法确认物理网关数据,那就把思路拐个弯,我先去定位查看一下,请求成功的时候,都携带了哪些信息,然后再想办法去获取失败的时候的信息。然后就在入口处,将请求的url,cookie,header全部打印出来了。
通过打印成功的日志,发现了一个问题,核心问题,转发后的链路id被更改了。日志是不同的。因为成功的id中,header中会放一个,通过id查询的时候,查到了一条不是这个id的日志,通过定位,发现确实是同一个链路的,但是id却不同。
那么,链路id不同,之前的所有假设都需要推翻了。
第四阶段,朝着预想方向排查
由于id不同,那么就可能不是物理网关的问题,抛出错误的地方可能也不是物理网关。再次重试异常链路,通过异常链路中的转发异常返回值中的链路id排查,果然查到了异常的日志。
异常的调用链中,发现已经到了应用服务器,应用服务器的链路抛出的异常。难道真的不是网关的问题,而是应用中的bug导致的??
通过排查详细日志,跟踪调用链路。发现了一个特别奇怪的问题,在业务处理流程中,有一个未登录的异常抛出。
未登录的话,在上层就会被return掉,并且返回not login的字样,不会抛出异常。由于这个异常,filter中的异常直接抛出到tomcat层面了,然后整个应用都是错误的,这个信息就层层往外,直接抛到了物理网关。物理网关针对异常信息,会统一转发到taobao.com/error页面。那么我想的前半段是对的,确实是物理网关抛出的302异常,但不是直接抛出的,而是由于应用服务器的异常导致的。
接着就去定位应用服务器的异常信息,通过日志追踪,详细去观察那个奇怪的问题,发现上层的filter执行到了一半,突然就中断了,没有继续执行了,而是去执行了下层的filter。
我理解中的filter是有顺序控制的,只有上层filter执行完,并且执行了doFilter之后,才可以到下层的filter。这个明显不合理,甚至怀疑自己排查方向是错的。但是日志上面记录的明明确确的,就是这么个链路。排除掉所有的可能性后,剩下的那个即使再不可能,也是真相!
于是我就顺着中断的地方,往下走,硬撸代码。发现网关的一个threadLocal的代码中,有一块是直接get,然后强转,最后再get强转后的对象中的属性。这块是特别容易发生空指针的,一般不允许这么链式调用,需要对null的情况进行兼容的。但是这是他们自己的网关内部方法,这块理应不应该有null的情况,但是没有其他的异常点了,所以需要验证一下。
我在外层捕捉了异常,并且打印堆栈信息,再次调用了异常链路,发现了这里确实有npe异常,并且这里npe异常后,上层filter结束,并没有结束整个调用链,而是继续执行了下层filter,导致下层filter又抛出了问题,最终的302转发就是由于下层filter抛出的异常导致的。
第五阶段,问题总结
由于网关B转发请求的时候,有时携带了登录信息,有时没有携带。导致filter中的执行链路异常,网关的threadLocal没有做npe的兼容,导致应用服务器抛出异常,触发物理网关的302转发。
排查问题困难,由于网关侧没有做好链路id的转发和继承,导致将问题引向了一个错误的方向。
总结
- 写代码时,一定要注意代码习惯,不要去写那种明显有npe的情况,尽量去避免系统因npe导致的异常
- 做好异常控制,不要任由异常肆无忌惮的往上抛
- 链路id管控好,即使没有办法统一,也要建立一个关联关系,不要引导错误的指向
- filter为什么会中断后还继续执行下一个呢??这块问题后面专门分析