再探ASP.NET大排長龍問題
9 | 38,245 |
在MaxConnectionsPerServer實驗中,發現一個過去被忽略的問題: 原來同一個Session下,啟用Session的ASP.NET網頁,因鎖定限制有可能出現單一時間內只能有一個Request被處理的情況。換句話說,即便我們使用非同步方式同時發出10個對ASP.NET網頁的Request,若該ASP.NET網頁涉及Session,這10個Request將不會同步執行,而是10個Request排成一列,一個執行完再執行下一個。
回到上回MaxConnectionsPerServer文章最後一段的案例,IE端同時丟出16個Request:
window.maxConnectionsPerServer = 16
18:43:44.316 -> 0 (234ms)
18:43:44.535 -> 16 (437ms)
18:43:44.769 -> 17 (672ms)
18:43:45.003 -> 18 (906ms)
18:43:45.237 -> 19 (1140ms)
18:43:45.877 -> 1 (1779ms)
18:43:46.423 -> 3 (2341ms)
18:43:46.938 -> 2 (2840ms)
18:43:47.484 -> 4 (3402ms)
18:43:47.999 -> 5 (3917ms)
18:43:48.514 -> 6 (4479ms)
18:43:49.029 -> 8 (5010ms)
18:43:49.544 -> 10 (5462ms)
18:43:50.059 -> 11 (5962ms)
18:43:50.574 -> 7 (6508ms)
18:43:51.090 -> 9 (7007ms)
18:43:51.605 -> 12 (7538ms)
18:43:52.120 -> 13 (8007ms)
18:43:52.635 -> 14 (8522ms)
18:43:53.150 -> 15 (9037ms)
由任兩個回應時間都至少相差0.2秒以上(這是程式故意產生的延遲),足可推論沒有任何兩個Request是同時被執行的。
MSDN Blog裡有篇文章做了詳細的探討: 當我們在一個Session中同時發出多個ASP.NET網頁Request,它們會被同步執行嗎? 答案是,可能會,可能不會。
文章提到,在ASP.NET存取任何Session物件前,它們是可以被同步執行的;但只要任何ASP.NET存取過Session物件,就會演變成每次只執行一個ASP.NET Request的局面。而這個現象源於對Session的Reader Lock/Writer Lock機制,ASP.NET網頁宣告中有個EnableSessionState Attribute,可以指定"true”、"ReadOnly"或"false"。當宣告為"true”或"ReadOnly”時,ASP.NET會在AcquireRequestState階段對Session放上Writer Lock(ReadOnly則是放Reader Lock),直到ReleaseRequestState階段才解除,幾乎涵蓋了整過ASP.NET執行周期。一旦某個ASP.NET對Session放上了Writer Lock,同一個Session的其他ASP.NET Request就必須等待放Writer Lock的網頁Request執行完解除Lock後,才有機會對Session放上自己的Reader/Writer Lock,才輪得到它執行。而Reader Lock與Writer Lock有一點不同,Reader Lock不會阻擋其他Reader Lock,但會阻擋Writer Lock;而Writer Lock則具有獨佔性,一旦Session被Writer Lock,所有的Reader Lock/Writer Lock都得乖乖在旁等待鎖定解除。
預設ASP.NET EnableSessionState="true”,所以一旦ASP.NET動用了Session,之後在每個Request執行時,都必須對Session放上獨佔性的Writer Lock,而必須獨佔Session的前題,解釋了為什麼同一時間內,永遠只有一個Request被執行。
這裡用兩個範例來觀察:
1) 為什麼存取Session物件與否會影響同步執行?
2) Reader Lock與Writer Lock間的相互影響
首先寫一支簡單ASP.NET程式:
<%@ Page Language="C#"%>
<%@ Import Namespace="System.Threading" %>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
if (Request["usesession"] == "Y")
Session["Boo"] = "Foo";
Response.Write("UseSession.aspx " +
DateTime.Now.ToString("HH:mm:ss.fff"));
Response.End();
}
</script>
程式會在傳入?usesession=Y時隨意存取Session["Boo”],我們前後發出Request 4次,只有在第三次加上?usesession=Y,使用HttpWatch觀察(點圖檔可以檢視原圖):
在第3個Request時送出的Stream檢視左方可以看到我們首次加上usesession=Y,而當時還沒夾帶任何Cookie,而右方的回應中出現了Set-Cookie: ASP.NET_Session_Id=…,換句話說,ASP.NET的Session Cookie是在程式執行過Session[“…”]才會產生。
而如預期,之後的Request在送出時,就都會加上ASP.NET Session Cookie用以識別自己所屬的Session。一旦有了Session Cookie,ASP.NET才會衍生對Session做Writer Lock/Reader Lock的動作,這就是MSDN Blog文章中所說: 一旦你用了Session,便無法再同時執行多個Request。
接著我們寫三個使用不同Session鎖定的ASP.NET網頁:
WriterLock.aspx (EnableSessionState=”True”)
<%@ Page Language="C#" EnableSessionState="True" %>
<%@ Import Namespace="System.Threading" %>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
Thread.Sleep(2000);
Response.Write("WriterLock.aspx " +
DateTime.Now.ToString("HH:mm:ss.fff"));
Response.End();
}
</script>
ReaderLock.aspx (EnableSessionState=”ReadOnly”)
<%@ Page Language="C#" EnableSessionState="ReadOnly" %>
<%@ Import Namespace="System.Threading" %>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
Thread.Sleep(2000);
Response.Write("ReaderLock.aspx " +
DateTime.Now.ToString("HH:mm:ss.fff"));
Response.End();
}
</script>
NoLock.aspx (EnableSessionState=”False”)
<%@ Page Language="C#" EnableSessionState="False" %>
<%@ Import Namespace="System.Threading" %>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
Thread.Sleep(2000);
Response.Write("NoLock.aspx " +
DateTime.Now.ToString("HH:mm:ss.fff"));
Response.End();
}
</script>
以及測試用的網頁Test.htm:
<!DOCTYPE html>
<html>
<head>
<title>Session Lock Test</title>
<style>
iframe
{
width: 250px; height: 30px; margin: 10px;
}
</style>
<script type="text/javascript"
src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.2.js"> </script>
<script>
$(function () {
var h = [];
for (var i = 0; i < 4; i++) {
h.push("<select id='s" + i + "'>");
$.each(["NoLock", "ReaderLock", "WriterLock"],
function (index, value) {
h.push("<option value='" + value + ".aspx'>" +
value + "</option>");
});
h.push("</select>");
}
h.push("<input type='button' id='go' value='GO!' />");
$("#controller").html(h.join('\n'));
$("#go").click(function() {
$("select").each(function() {
$("#" + this.id.replace('s', 'f'))
.attr("src", $(this).find(":selected").val());
});
});
});
</script>
</head>
<body>
<div id='controller'></div>
<div><iframe id='f0'></iframe></div>
<div><iframe id='f1'></iframe></div>
<div><iframe id='f2'></iframe></div>
<div><iframe id='f3'></iframe></div>
</body>
</html>
由於WriterLock.aspx、ReaderLock.aspx、NoLock.aspx都會延遲兩秒後再顯示目前時間,Test.htm則可使用下拉選項選取,一次在四個IFrame中分別載入不同的ASP.NET網頁(可視為同步發出4個Request),由其顯示時間先後即可看出不同的ASP.NET網頁是同時執行或彼此阻擋。有了這個玩具,就不難驗證Reader Lock與Writer Lock理論。(註: 在執行以下測試前,我已執行過UserSession.aspx?usesession=Y,以確保Session機制被啟動)
測試1 NoLock不會彼此阻擋:
測試2 ReaderLock也不會彼此阻擋:
測試3 WriterLock擋住大家2秒,才輪到其他三個ReaderLock同步執行:
測試4 WriterLock具有獨佔性,四個WriterLock排成一列,一個執行完才跑另外一個(注意: 同時發出Request的先後,不保證其在IIS執行的順序)
最後,再證明一件事,WriterLock一定會阻止ASP.NET同時執行嗎? 不一定,前面提過,WriterLock會造成影響是因為動用到Session物件,此時會有Session Cookie指向其所屬Session物件,所以若我們將Cookie清除,就可以看到四個WriterLock同時執行囉~~~
【結論】
Session Writer Lock有可能阻礙同一Session ASP.NET網頁的同時執行,針對同Session下同步發出多個Request的情境(例如: AutoComplete輸入自動完成隨按鍵連續觸發Request、網頁內嵌多個IFrame指向多個同Session ASP.NET網頁...),可考慮透過EnableSessionState=”False”(或ReadOnly,但唯讀時仍有可能被其他WriterLock阻擋)避免鎖定效應,提升效能。而在偵察ASP.NET相關效能議題時,建議可將此一特性納入考量。
Comments
# by MerlinJY
有好有吾好
# by Walter
黑大您好,謝謝分享~ 文章中「NoLock.aspx (EnableSessionState=”Falsey”)」最後多了一個 y 字,跟您說一下 ^^
# by Jeffrey
to Walter, 已更正,謝謝提醒~~
# by huanlin
很有幫助! Thanks!
# by Anna
This is definately good content …for sure…. it's awesome to see that you are posting some unique stuff here !
# by showlife
黑大您好,針對EnableSessionState=”False”,如果有使用masterpage的頁面,是否在masterpage指定就會生效?還是要在各頁面個別指定?謝謝。
# by Jeffrey
to showlife, 查了資料, EnableSessionState是Page層次的設定,故無法套用在MasterPage上,但你可透過web.config來指定。參考: http://stackoverflow.com/questions/958687/disable-session-from-master-page
# by MerlinJY
再看这个页面时做了个小试验,发现个问题想请教下老大 关于Ajax中"Async = true"时的一个奇怪表现 连续发出两人Ajax请求后,只有一个返回了现果? Async.aspx代码: <html> <head> <title></title> <script src="../script/jquery.min.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function () { //setTimeout("Async_True('text_1', 0)", 0); //setTimeout("Async_True('text_2', 1)", 100); Async_True('text_1', 0); Async_True('text_2', 1); //setTimeout("Async_False('text_1', 0)", 0); //setTimeout("Async_False('text_2', 1)", 100); //Async_False('text_1', 0); //Async_False('text_2', 1); }); // function Async_True(text_id, num) { ajaxObj = Get_XmlHttpObject(); if (ajaxObj == null) { alert('您的浏览器不支持AJAX!'); return; } ajaxObj.onreadystatechange = function () { if (ajaxObj.readyState == 4) { if (ajaxObj.status == 200) { $('#' + text_id).text(ajaxObj.responseText); } else { alert(ajaxObj.status); } } } ajaxObj.open("POST", "Ajax.aspx?Num=" + num + "&Rand=" + Math.random(), true); ajaxObj.setRequestHeader("Content-Type", "text/xml;charset=UTF-8"); ajaxObj.send(''); } // function Async_False(text_id, num) { ajaxObj = Get_XmlHttpObject(); if (ajaxObj == null) { alert('您的浏览器不支持AJAX!'); return; } ajaxObj.open("POST", "Ajax.aspx?Num=" + num + "&Rand=" + Math.random(), false); ajaxObj.setRequestHeader("Content-Type", "text/xml;charset=UTF-8"); ajaxObj.send(''); $('#' + text_id).text(ajaxObj.responseText); } // function _Create_StandardXHR() { try { return new window.XMLHttpRequest(); } catch (e) { } } function _Create_ActiveXHR() { try { return new window.ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { } } function Get_XmlHttpObject() { var xmlHttp = null; xmlHttp = window.ActiveXObject ? _Create_ActiveXHR() : _Create_StandardXHR(); return xmlHttp; } </script> </head> <body> <div><span>1:</span><span id='text_1'></span></div> <div><span>2:</span><span id='text_2'></span></div> </body> </html> Ajax.aspx代码: <%@ Page Language="C#" %> <script type="text/C#" runat="server"> protected void Page_Load(object sender, EventArgs e) { string result = string.Empty; int i = 0; int.TryParse(Request.QueryString["Num"].ToString(), out i); DateTime t = DateTime.Now; result += t.ToLongTimeString() + " " + t.Millisecond.ToString(); if (i % 2 == 1) { System.Threading.Thread.Sleep(500); } result += "=>" + (DateTime.Now - t).Milliseconds.ToString(); //返回结果 Response.ContentType = "text/plain;charset=UTF-8"; Response.Write(result); } </script> 参考:http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp
# by Jeffrey
to MerlinJY, 看了一下,我想問題出在ajaxObj = Get_XmlHttpObject();沒宣告成區域變數: 連續呼叫兩次Async_True時,第二次時會覆寫前一次設定好的ajaxObj,所以只會得到第二次的執行結果。 要解決問題,可在ajaxObj前加上var,改為區域變數就OK了! var ajaxObj = Get_XmlHttpObject();