IE下Ajax请求偶发12152连接超时错误浅析
昨天被分到一个Bug,公司某产品在IE下偶尔会随机出现请求挂起,等待30秒后弹出超时错误(错误状态码:12152)的问题,而在FireFox或Chrome则从没有这样的问题。据说这个问题已经困扰他们四年多了,一直混着。直到最近在某个大客户的新环境中频频出现,才不得不专门找人()解决。接到这个Bug,感觉就是某个经典的IERepost问题,之前一直没有机会详细了解,借这个机会翻了一些资料,在这里总结一下。
产生原因
现在的HTTP连接,特别是HTTPS连接,为了提高性能,几乎全部采用Keep-Live模式。也就是连接建立起来后会存活一段时间(数秒到数十秒不等),这段时间内的请求会重用这个连接。根据HTTP1.1(RFC2616)标准,连接路径上的代理服务器,负载均衡服务器,应用服务器等节点都可以随时中止这个连接。一般情况下如果网速较快,连接被中止时浏览器会立刻知道,下一个请求就建立新连接,不会出现明显问题。但在网速不佳的环境中,会出现一种边界场景:浏览器发出了一个请求,但在请求还没到服务器前,服务器端认为连接超时,中止了连接。这时浏览器会立刻得到一个请求超时错误,根据RFC2616标准,浏览器这时应该无须用户干预,自动重新建立新的连接并且重发之前的请求。各大浏览器都遵循了这个规范,因此在FireFox或Chrome下,用户根本不会意识到有Keep-Live连接超时这回事。
原文如下:
Aclient,server,orproxyMAYclosethetransportconnectionatanytime.Forexample,aclientmighthavestartedtosendanewrequestatthesametimethattheserverhasdecidedtoclosethe"idle"connection.Fromtheserver'spointofview,theconnectionisbeingclosedwhileitwasidle,butfromtheclient'spointofview,arequestisinprogress.
Thismeansthatclients,servers,andproxiesMUSTbeabletorecoverfromasynchronouscloseevents.ClientsoftwareSHOULDreopenthetransportconnectionandretransmittheabortedsequenceofrequestswithoutuserinteractionsolongastherequestsequenceisidempotent(seesection9.1.2).
但,IE的XMLHttpRequest实现(某COM组件)在重发请求时有个低级错误:只会重发header部分,而忘了重发原来请求的内容部分!并且,在header中,仍然包含了原来请求中的contentlength(内容大小)数据。服务器在接收到这个请求header后,就开始望眼欲穿地等待指定大小的请求内容,而IE根本不会继续发送任何内容。于是,一段时间后,用户收到了一个真正的连接超时错误。
这个问题,根据微软官方文档,确认在IE6-9中都存在,IE10和后续版本到底如何(以及IE10的兼容模式下到底如何),我暂时还没找到明确文档。
如果你已经开始惊叹IE的弱智,别急,这只是开胃菜,更无厘头的在后面。
微软在收到用户投诉后,发了一个hotfix(http://support.microsoft.com/kb/895954/en-us),题目翻译过来大概是《当你使用IE或其他程序重发post请求时,只有请求头会被发送》,全文没有任何一处提到12152错误码和上面的错误现象。最开始也不知道是哪位天才把上述问题和这个hotfix联系起来的。
在IE6下,用户需要下载安装这个hotfix。而IE7-9,则已经内置了这个hotfix。但是————这个hotfix默认是禁用的,用户需要手动在注册表中添加一个名为FEATURE_SKIP_POST_RETRY_ON_INTERNETWRITEFILE_KB895954(!!!!)的项目来开启它。具体操作可以参考http://support.microsoft.com/kb/895954/zh-cn的《如何启用此修补程序》一节,复制如下:
如何启用此修补程序
警告如果使用注册表编辑器或其他方法错误地修改了注册表,可能会出现严重问题。这些问题可能要求您重新安装操作系统。Microsoft不能保证这些问题能够得到解决。修改注册表的风险由您自己承担。
若要添加此注册表值,请执行以下步骤:
1.单击开始,单击运行,键入regedit,然后单击确定
2.找到并单击以下注册表子项之一。管理客户端计算机的相应子项使用所指定的策略。
*HKEY_CURRENT_USER\Software\Policies\Microsoft\InternetExplorer\Main\FeatureControl
*HKEY_CURRENT_USER\Software\Microsoft\InternetExplorer\Main\FeatureControl
*HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\InternetExplorer\Main\FeatureControl
*HKEY_LOCAL_MACHINE\Software\Microsoft\InternetExplorer\Main\FeatureControl
3.在编辑菜单上,单击新建,然后单击项。
键入FEATURE_SKIP_POST_RETRY_ON_INTERNETWRITEFILE_KB895954作为子项的名称,然后按enter键。
4.单击FEATURE_SKIP_POST_RETRY_ON_INTERNETWRITEFILE_KB895954子项。
5.在编辑菜单上,指向新建,然后单击DWORD值。
6.为dword值名称中,键入*,然后按enter键。
7.用鼠标右键单击*,然后单击修改。
8.在数值数据框中,键入00000001,然后单击确定。
9.退出注册表编辑器。
网上很多人摸不着头脑,这么低级且恶劣的问题,明明出来一个hotfix,后期都直接内置了,干嘛不默认打开,还搞得这么复杂。其实,微软的文档里没有说他们是如何修复这个错误的,一旦你真的开启了hotfix,就豁然开朗了。正如这个古怪的注册表项目名称所说的,微软“修复”这个错误的做法是:SkipPostRetry!!!!没错,他们不是把重发请求的问题改好,而是直接不重发了!!!!所以,你开启fix后,会偶尔直接得到一个12152错误,而不用等30秒了,问题解决,OHYEAH。
解决方案探讨
网上有一些讨论说可以通过在相应的请求头中加入Connection:close通知浏览器每次都关闭连接解决此问题,或者通过在服务器中提高Keep-Live的timeout时间来缓解问题。但一些资料说明,这类方案有一下问题:
1.某些服务器并不理会请求头中关于连接的设定,程序员需要手工添加一些关闭连接的代码。我发现多数直接加入Connection:close的成功案例都是PHP的,估计跟PHP直接架在Apache上有关。
2.如果你选择提高timeout,你需要把从用户浏览器到你服务器的物理连接路径上所有忽略请求头连接设置的节点都手工设置好。(对我们公司的实际场景来说几乎不可能)
3.每次重新创建连接的性能损耗颇大(有人说很大,有人说没感觉,具体多少我没试过,因为上面1,2已经把我踢出局了)
微软的意图很明显,要么你可以继续默认关闭hotfix,IE会非常配合地给用户一个无可挑剔的服务器超时假象,然后你可以跟客户说,你们的防火墙不稳定,自己检讨一下吧。要么你可以让客户开启hotfix,然后自己在应用中实现一个重试机制,我这两天就在干这件事。
最后一件恶心的事:这个Keep-Live连接timeout和普通timeout都使用12152错误码,拜托IE你都出hotfix了,意图这么明显是让程序员自己解决,你至少也弄个标志让我分辨出哪个是需要在应用中重发的timeout吧。目前我采用的方案是,在请求前记录时间,如果出现12152错误且等待时间小于5秒,则认为是Keep-Live连接超时,由应用主动重发请求。
也许最好的方案是,说服客户不要用IE。
==================================
2014-01-06补充
1.在压力测试中发现,同样的问题并不总是返回12152错误码(暂不确定是否跟IE版本有关),有时返回12030错误码。据网上讨论,可能的错误码应为:
switch(status){ case 12029: case 12030: case 12031: case 12152: case 12159: //检查请求等待时间,若小于5秒则重发请求并break //否则fall through到default default: originalOnError.apply(this, arguments) //执行原本的onError }
2.有人指出把发出请求的代码异步执行(放到一个延时为0的setTimeout中)能消除此问题(http://stackoverflow.com/questions/8130446/fixing-the-ie-12030-repeatability-bug-for-ajax-calls),原因未知,待测试。