我们的平台网关日常流量比较大,一周前突然发现有少量正常执行的请求,到后台被报出丢失参数的错误。这种是非常不正常的,而且低概率出现,毫无规律可循,而且平台测试环境和本地环境均无法复现。由于接口经过平台传递到真正的目标接口,中间的执行链相对较长,我们首先进行了各种排除性测试,经过几天的测试,目标最终指向网关层。
网关层的流量其实比较大,负责所有正常和不正常请求的过滤、校验和转发,其中一步就是校验请求参数是否符合要求。由于低概率偶发性POST请求参数丢失,导致部分请求被打回,经过测试,实际请求参数的确是到达了服务器。我们在过滤器链的每一步都加了日志,发现在过滤器链的第一步执行的时候,参数就丢失了。于是目标精确地指向了spring boot或spring boot内置tomcat对请求的包装过程中。 经过排查,我们发现正常的请求是这样的:
不正常的请求却把didQueryParameters置为true:
由此可见,关键在与这个参数的变化。测试中,spring boot对请求的各种操作,使用的对象均是同一个,例如上面两个图的parameters对象变量,一级serverCookies对象变量,地址都是一样的。事实上,干这个活儿的是spring boot内置的tomcat,位于tomcat-embed-core-xxxx.jar里,request和response对象都是复用的同一个,里面的各个对象变量也不出意外的都是复用的;对请求的处理步骤,是位于该jar中的org.apache.coyote.http11.Http11Processor.java里的service()方法发起的,每个请求的初始处理都在这里,正常情况下,上一个请求结束之后,对org.apache.coyote.request里Paremeters参数下的didQueryParameters参数,tomcat会设置为false:
这里的recycle()方法包括了对Parameters类中的recycle()的调用:
这样,下一个请求执行的时候,该参数直接就是false,也就是queryString请求内容未读取状态,下一个请求就可以正常被读取请求内容。 但是,转折还是存在。Parameter类中,还有一个对didQueryParameters变量进行操作的方法,handleQueryParameters():
该方法被一些其他方法调用,比如几个很有名的方法:
一旦系统中存在异步线程,在服务器高负荷状态下,异步线程可能发生延迟,导致响应已经返回了,但是异步线程仍然可以读取活动中的请求参数。但请求对象是同一个对象复用的,在调用getParameter(String)之类的方法后,现存的下一个请求(或者正在执行的请求)中的didQueryParameters变量就会被置为true,如果该请求正在被包装处理,那么就会在后续的处理过程中,因为变量值发生变化而被认为请求参数已经无法读取,从而发生异常。 解决方法也很简单,从异步线程中去掉getParameter的直接调用即可恢复正常。