再探ASP.NET大排長龍問題

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相關效能議題時,建議可將此一特性納入考量。

歡迎推文分享:
Published 27 August 2011 11:30 AM 由 Jeffrey
Filed under: ,
Views: 28,648



意見

# MerlinJY said on 27 August, 2011 09:37 PM

有好有吾好

# Walter said on 24 November, 2011 12:48 PM

黑大您好,謝謝分享~

文章中「NoLock.aspx (EnableSessionState=”Falsey”)」最後多了一個 y 字,跟您說一下 ^^

# Jeffrey said on 24 November, 2011 05:43 PM

to Walter, 已更正,謝謝提醒~~

# huanlin said on 16 December, 2011 11:19 AM

很有幫助! Thanks!

# Anna said on 11 January, 2012 03:43 AM

This is definately good content …for sure…. it's awesome to see that you are posting some unique stuff here !

# showlife said on 24 August, 2012 07:07 AM

黑大您好,針對EnableSessionState=”False”,如果有使用masterpage的頁面,是否在masterpage指定就會生效?還是要在各頁面個別指定?謝謝。

# Jeffrey said on 24 August, 2012 10:05 PM

to showlife, 查了資料, EnableSessionState是Page層次的設定,故無法套用在MasterPage上,但你可透過web.config來指定。參考: stackoverflow.com/.../disable-session-from-master-page

# MerlinJY said on 28 October, 2012 02:03 AM

再看这个页面时做了个小试验,发现个问题想请教下老大

关于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>

参考:www.w3school.com.cn/.../ajax_xmlhttprequest_send.asp

# Jeffrey said on 28 October, 2012 08:16 PM

to MerlinJY, 看了一下,我想問題出在ajaxObj = Get_XmlHttpObject();沒宣告成區域變數: 連續呼叫兩次Async_True時,第二次時會覆寫前一次設定好的ajaxObj,所以只會得到第二次的執行結果。

要解決問題,可在ajaxObj前加上var,改為區域變數就OK了!

var ajaxObj = Get_XmlHttpObject();

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<August 2011>
SunMonTueWedThuFriSat
31123456
78910111213
14151617181920
21222324252627
28293031123
45678910
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication