一【BUG描述】
最近开发一个Web系统的过程中遇到了一个诡异的BUG,花了2天时间解决,感觉如释重负。
这个BUG的现象是这样的:一个很普通的JSP网页,本来显示很正常,后来我在这个html页面上加了一些控件元素,并修改了下CSS, JavaScript代码,并加入了些中文字符。本来是觉得没什么问题,可是在服务器上跑起来就发现浏览器上这个页面显示一片空白,同时其他页面却显示正常。另外,我用的Tomcat版本是8.0.17。
那么我第一反应就是这个页面代码哪里改错了,可是我仔细看了下觉得又没什么问题,然后页面字符编码和Content-type都是显示设置UTF-8编码。看来前端好像没什么问题,然后我就在浏览器上查看页面源码,结果发现浏览器上显示的HTML源码居然不完整,html页面尾部的4~5行代码全部不见了。这就是导致页面无法正常显示的直接原因。
这个无非就两种可能:浏览器数据没有全部接收,或者服务器数据没有全部发送。这时,我就用WireShark抓包观察,结果显示tomcat服务器在发送数据时最后几行数据根本就没有发送过来。而且这儿非常巧合,因为html数据以chunked的方式发送,最后那几行数据恰好应该全部在最后的一个chunked包里面,换句话说,就是最后一个chunked包丢了。
为啥偏偏这个页面的最后一个chunked包丢了,而其他页面好好的呢?
这时没别的办法啦,只能一步一步调试JSP页面代码咯(这个耗费了我1天多的时间)。通过观察其他正常显示的JSP页面与这个出问题的JSP页面的执行流程,我发现问题出在:JSP页面在tomcat中输出的时候,最后一个chunked数据包在缓存中没有被flush。
二【定位】
为啥不flush?这个多种原因造成的:
- JSP页面在通过JspWriter输出之后,其实还留了点在缓存里面。以前一般是在JSP执行完,销毁PageContext的时候将缓存中的数据flush到网络流上,可现在作者为了提高网络性能,只是将数据flush到了较低一层的CoyoteWriter的缓存中,等到整个请求-响应过程结束,再flush到网络流上。如下图:
- 作者想法是不错,可惜少算了一步。CoyoteWriter对象是通过一个org.apache.catalina.connector.OutputBuffer来完成数据输出的。这个OutputBuffer对象有个suspended(暂停)标志位,用来随时暂停这个输出对象,处于暂停状态的OutputBuffer对象是不会进行数据输出的。tomcat在完成JspWriter输出的工作后,提前将OutputBuffer的状态设置为suspended状态,这一步的意思防止响应数据输出之后又输出其他不该输出的数据。这个本意是好的,可是等到整个过程结束,关闭OutputBuffer对象的时候,就因为这个suspended状态导致close这个函数还没来得及执行flush就直接退出了。如下图:
- 当页面通过OutputBuffer输出第1个chunked包的数据的时候,这个对象偷偷将这个chunked包的字符数据转换成二进制数据缓存了起来,并没有真正输出到网络流中。只有等到要输出第二个chunked包的时候,才flush缓存,将第一个包的数据输出到网络中,为第二个chunked包腾出足够的缓存空间,并缓存第二个chunked包。
- 当OutputBuffer输出第一个chunked数据包到网络中的时候,会抢先输出的Http 响应头,并将response对象的commited标志设置为true(这就表示响应头已经发送)。
- 在ErrorReportValve.invoke函数中,处理完http请求后,会判断response对象的commited标志是否为true,如果不是true,则将OutputBuffer的suspended标志设置为false,保证响应数据能够输出。
- 好了,原因找到了:因为其他web网页比较小,导致jsp页面处理结束了,所有的chunked数据还在缓冲中,完全并没有被输出到网络中(一部分在OutputBuffer缓存中,一部分在JspWriter缓存中),commited标志此时为false,等到ErrorReportValve处理时,根据commited标志,将suspended标志重置为false,这样在最后关闭OutputBuffer的时候,就将所有这些缓存中的数据一个个都flush到了网络上。相反,我修改的那个网页比较大,导致OutputBuffer中途就将部分数据flush到了网络上,是的commited标志位true,那么ErrorReportValve也就不会将suspended标志重置为false,导致OutputBuffer在关闭时,并不会flush残留在缓存中的数据。
三【解决】
- 在JSP页面最后加<% out.flush %>强制刷新;
- 升级或者回退Tomcat 服务器版本;