同事報案,有個網頁會一次發出大量 AJAX 呼叫再蒐集回應,今有某個極端案例同時丟出 600 筆資料,在 IE 發生「SCRIPT7002: XMLHttpRequest: 網路錯誤 0x2ee2, 發生錯誤,無法完成操作 00002ee2。」錯誤,但在 Chrome/Edge 則不會出錯。

先補上背景知識,瀏覽器對每個伺服器 IP 的同時 HTTP 連線是有上限的,依經驗 IE11 最大連線數為 10、Chrome 則是 6。意思即時同時對同一台網站發出 20 個非同步 XHR 請求,最多只會有 10 條連線,第 11 個請求要等前面 10 個任何一個結束釋出連線才會送出。參考:IE MaxConnectionsPerServer 參數效果實測

我設計一段測試程式可以重現這個問題。ASP.NET 程式在前端同時丟出 800 個 AJAX POST 請求,後端處理時則故意 Delay 5 秒再回應 OK 以造成塞車現象:

<%@ Page Language="C#" %>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
	if (Request.HttpMethod == "POST") 
	{
		System.Threading.Thread.Sleep(5000);
		Response.Write("OK");
		Response.End();
	}
}
</script>
<!DOCTYPE html>
<head>
	<meta charset="utf-8">
</head>
<body>
	<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
	<script>
	function getCallbackFunc(n) {
		return function() {
			console.log(new Date().toISOString().substr(14,5) + " Response-" + n);
		}
	}
	for (var i = 0; i < 800; i++) {
		$.post("MaxXhrConn.aspx").done(getCallbackFunc(i));
	}
	</script>
</body>
</html>

一如預期,由於最多同時只能有 10 條連線,800 個 AJAX POST 會以每五秒 10 個的速度消化:

然後,800 個 XHR 苦等 10 條連線,觀察等待超過 300 秒的請求便會失敗噴出「SCRIPT7002: XMLHttpRequest: 網路錯誤 0x2ee2, 發生錯誤,無法完成操作 00002ee2。」錯誤:

由 F12 開發者工具網路傳輸記錄,成功呼叫中耗時最長為 305.33 秒,其中「正在連線(TCP)」時間為 299.12 秒,即因連線數達到上線的等待時間,而「正在等侯(TTFB)」則為 ASPX 端 Delay 5 秒再回傳的時間。

同樣程式在 Chrome/Edge 執行則不會出錯(其最大連線數為 6):

總等待時間長達 11 分鐘半仍可完成:

查到 Stackoverflow 有人反映相同問題 - 在 IE11 等待 5 分鐘還沒有送出的 XHR 請求會拋出 0x2ee2 錯誤,而這個 5 分鐘等待上限與 XHR.timeout 無關,是 IE 獨有的限制,似乎也無從調整。

網路上有一派解法是放大 IE MaxConnectionsPerServer 參數增加連線數,其原理相當於加開櫃檯減少排隊時間不要超過 5 分鐘,但這畢竟是治標做法,只要伺服器端處理慢到一定程度將所有連線卡死,排隊時間仍有可能逾時。較好的做法是改變前端程式寫法,自己控制發出 AJAX 呼叫的時機及數量,我們可以比照 .NET 控制執行緒數目的做法,將所有待辦工作放進 Queue,用 setTimeout 同時跑六個 while 迴圈,每個迴圈 Queue 取出待辦項目發出 AJAX 請求,做完再處理下一個,如此即可精準控制最多只會用到 6 條連線。

又學到一些新東西。

IE11's 300s timeout for pending XHR requests waiting for available connection under max connections per sever.


Comments

# by 鳥毅

個人覺得是否能從修改規格著手? 若是老舊電腦會不會Out of memory? 我會讓每次request讀取的資料多一次,再分次分時讀取,同時用3個ajax request都有點多了,可能會有讀取順序的問題,還是打包起來一次回傳好。

# by Jeffrey

to 鳥毅,同意。文章的改法以最小改動為目標,整個流程其實還有很大改進空間,我心中最理想做法是一次把要處理的項目用一個 Request 送到後端,再即時更新處理進度到前端。

# by 小海

後端更新處理進度很不錯,若需要支援中間可取消的機制,該如何做到?

# by Jeffrey

to 小海,概念是設計一個 Manager (或借用現成的背景作業管理程式庫),由 Manager 啟動一個 Thread 或 Task 跑批次作業,設計一個 API 可呼叫 Manager 停止執行中的作業。

# by 小海

謝謝,實用

Post a comment