來看一個有趣實驗。

以下是個簡單的 ASP.NET MVC Controller,在 Index View 透過 AJAX 呼叫向 Server 讀取資料,SimuAjaxCall 則模擬 AJAX 呼叫動作,使用 Thread.Delay() 延遲指定秒數後傳回字串結果:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace LabWeb.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
        public ActionResult SimuAjaxCall(int seqNo, int delay)
        {
            System.Threading.Thread.Sleep(delay * 1000);
            return Content($"AjaxCall-{seqNo}");
        }
    }
}

Index.cshtml 網頁內容如下。有個測試按鈕觸發同步發出 7 個 AJAX 呼叫 SimuAjaxCall,並將每次呼叫取得內容顯示在網頁上。先聲明,這並非良好的設計方式,依據 HTTP 規範,瀏覽器對同一網站來源的同時連線數有其上限,預設為 6 條,故第 7 個 AJAX 請求必須等待前 6 個請求有人執行完畢後才會送出,故設計時應盡可能透過合併或其他技巧,減少 AJAX Request 數目。(關於連線數上限議題,可參考這篇文章)網頁上還有另一顆「變蝸牛」按鈕,背後呼叫 /Magic/Snail 取得字串顯示,至於它背後做了什麼事,在此先賣個關子。

@{
    Layout = null;
}
 
<!DOCTYPE html>
 
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Multiple AJAX Call Test</title>
</head>
<body>
    <div> 
        <button id="btnAjax">測試 AJAX 呼叫</button>
        <button id="btnSnail">變蝸牛</button>
        <ul>
        </ul>
    </div>
    <script src="https://code.jquery.com/jquery-3.2.1.js"></script>
    <script>
        $("#btnAjax").click(function () {
            //發出7個耗時1秒的AJAX呼叫
            for (var i = 0; i < 7; i++) {
                $.post("/Home/SimuAjaxCall", { seqNo: i, delay: 1 })
                    .done(function (res) {
                        $("ul").append("<li>" + res + "</li>");
                    });
            }
        });
        $("#btnSnail").click(function () {
            $.post("/Magic/Snail")
                .done(function (res) {
                    $("ul").append("<li>" + res + "</li>");
                });
        });
    </script>
</body>
</html>

我們的測試方法是先按「測試AJAX呼叫」,用 F12 開發者工具觀察 7 個 AJAX Request 的執行時間,接著使用「變蝸牛」魔法捲軸,之後再按一次「測試AJAX呼叫」觀察結果差異。實測結果如下:

第一次 7 個 AJAX Request 齊發測試(黃色部分)一如預期,前六個同步執行,第七個等了 1 秒才執行(1 秒綠色長條前方有 1 秒的灰色細長條為等待時間),驗證了瀏覽器對同一站台同時連線上限數為 6。

呼叫 /Magic/Snail 後再做一次相同測試,結果卻截然不同,七個 AJAX Request 分別花了 1 到 8 秒才執行完(紅色部分)!若使用者必須等待全部 AJAX Request 完成,等待時間也由 2 秒拉長到 8 秒。

這情境似曾相識,對吧?(感覺陌生的同學可參考 再探ASP.NET大排長龍問題

是的,揭曉 /Magic/Snail 裡的魔法,就是 Session!

using System.Web.Mvc;
 
namespace LabWeb.Controllers
{
    public class MagicController : Controller
    {
        public ActionResult Snail()
        {
            Session["A"] = 123;
            return Content("變蝸牛!");
        }
    }
}

有趣的實驗,但發生在真實環境我可笑不出來… (補聲暗)

當時我遇到的狀況是網頁同時發出十多個 AJAX Request,前六個 AJAX 呼叫每個耗時 5-12 秒,但個別執行明明只要 1-3 秒。尤其某個應該瞬間完成的 Action,我在 Action 第一行跟最後一行寫 Log 記錄執行時間,發現 Action 從開始到結束花不到 0.1 秒,但 IIS Log 記錄跟瀏覽器端觀察到執行時間都在 4 秒以上,推測時間耗消在呼叫 MVC Action 之前或 Action 完成之後,卻又無從調查。同事 J 提醒可能與 Session 有關,這才恍然大悟。萬萬沒想到,原本以為不用 WebForm 就再也不用擔心 Session 阻塞交通,但事實不然…

一旦你在網站應用程式的某個角落用了 Session,MVC Action 也會大排長龍,一秒變蝸牛!

追究原因,程式用了某個 WebForm 時代的古老元件,其中使用 Session 保存狀態。傳統 WebForm 以 PostBack 為主,Session 的鎖定行為影響有限,當應用在會同時發出多個 AJAX Request 的場景,便導致了可怕的後遺症。 

解決方法很簡單,有同步 AJAX 執行需求且要避免被 Session 摧毁效能的 Controller 上請加註[SessionSate()],設成 Disabled 或 ReadOnly:(前題是這個 Controller 未使用 Session 或對 Session 只讀不寫)

    [SessionState(System.Web.SessionState.SessionStateBehavior.Disabled)]
    public class HomeController : Controller

修正後,Action 同步呼叫不再變蝸牛。

狠狠地被上了一課!如果你的網站採取 AJAX 方式設計,Session 這種活化石,就別再用了。


Comments

# by Spider

請問大大不用session, 如何記住當前登入的user?我用的是webform

# by Jeffrey

to Spider, 這是這兩天詢問度頗高的疑問,我準備另寫一篇較完整的解釋,敬請期待。

# by Anonymous

大大您好: 請問 "完整的解釋" 是哪篇文章?

# by Jeffrey

to Anonymous: 傳送門在此:http://blog.darkthread.net/post-2017-06-12-session-alternative.aspx (歡迎訂閱FB專頁或RSS就不會錯過文章囉)

# by 9ing

To 黑大: 我目前在.net core 3.1測試,不會遇到這個情況了。 https://imgur.com/a/pVvQJCr 是.net core有針對這個問題處理過了嗎?

# by Jeffrey

to 9ing, 依據找到的資料 https://github.com/dotnet/AspNetCore.Docs/issues/12110 ,https://andrewlock.net/an-introduction-to-session-storage-in-asp-net-core/ ,ASP.NET Core Session 不再全程鎖定,若同時更新就會出現後面覆寫前面,換言之,如果要防止需自行處理鎖定。

# by 9ing

謝謝黑大 獲益良多。

# by JACKY

DEAR Jeffrey 我開發的WEBAPI 也碰到相同情況 但是找不到正確解法 WEBAPI 不能參考 MVC.DLL //[System.Web.Mvc.SessionState (System.Web.SessionState.SessionStateBehavior.Disabled)] public class JSONWSController : ApiController { [System.Web.Http.HttpGet] [System.Web.Http.Route (XXXX)] public IHttpActionResult XXX( string guid, string dynamic = "N" ) {} 另外 我在 protected void Application_BeginRequest ( object sender, EventArgs e ) { System.Web.HttpContext.Current.SetSessionStateBehavior (System.Web.SessionState.SessionStateBehavior.ReadOnly);) 這樣設定也無效 只要我併發200個REQUEST 他就開始塞車了 單一REQUEST都正常的速度

# by Jeffrey

to JACKY,WebAPI 一般不會用到 ASP.NET Session,應該不會踩到這個雷才對。 建議先用 20 個 Request,有觀察到一個接一個排隊執行的狀況,確定是 Session 問題再朝這個方向解。

# by Jacky

DEAR Jeffrey 今日測試結果 20 Requset 一樣會發生 某幾個開始便慢 不過是跳著發生 不是從某一個之後就開始都變慢 但是後來 經過測試 我每併發五個 停頓0.4秒 整這運作就變正常了 好像是我的站台無發同時接受同一個IP進行高併發 我發200個也都正常

Post a comment