ASP.NET 效能問題排除筆記

上回有提到正在協助研判一個 ASP.NET 應用程式的效能問題,該問題的癥結已經找到,這裡記錄一下始末。

問題描述/症狀

ASP.NET 2.0 應用程式,部署於 IIS 6 或 IIS 7。此應用程式有個現象:在某個大量使用 AJAX 查詢的網頁上,查詢鈕按下之後,會有數十個 request 以非同步的方式向 server 端送出請求。這些請求是由同一個 server 端網頁程式處理,而該程式又會再去呼叫別的 web services。

問題症狀是,明明是非同步請求,可是網頁上很明顯看得出來,總是要等到某個欄位出現查詢結果(該 AJAX 請求已返回)之後,其他欄位才會陸續完成顯示。為了確認這點,該程式的負責人修改了最終被呼叫到的 web service 程式碼,在裡面加了一些判斷條件,讓固定某個欄位的查詢 sleep 五秒鐘,以便讓效能問題在 UI 端能夠重複出現同樣的症狀。初步觀察,許多非同步的 request 確實會等到那個 sleep 五秒鐘的 request 完成後才陸續完成,固可確認有排隊處理的情況。

然而,同樣的程式(包括 web service 和 dll 組件),如果部署到不同的網站(仍在同一台機器上),卻不會有上述情形。

該程式的負責人說,這個效能問題已經困擾他一個多月了。

診斷步驟 1

請對方調整用來凸顯效能問題的 sleep 時間,由 5 秒加到 10 秒。再進行測試,問題竟然消失了。

暫時想不出原因。

目前都還沒有看程式碼,我想先透過調整組態的方式來觀察效能變化。

診斷步驟 2

遠端登入 app server,調整兩個網站的 app pool 設定。發現有效能問題的那個網站,如果啟用 web garden,效能問題就消失了,速度非常快。可是 session 資料卻會不見,導致網站的其他功能出問題。

跟對方詢問之後,得知另一個完全沒效能問題的網站並不需要登入,而有問題的網站必須登入。

於是建議對方:將此狀況轉達撰寫此程式的原始作者,想想看他的程式裡面是否有哪些要存取 session 的地方會造成排隊等候。

此時我覺得該程式的作者應該要知道哪裡有問題了,畢竟範圍已經縮小很多了。所以我還是沒有要對方寄程式碼給我。

處理回報

經過一些時間,對方回覆他找到問題在哪裡了。原來他那用來處理前端 AJAX 請求的程式是自己寫的 HTTP handler,且該 handler 有實作 IRequiresSessionState:

public class HandlerService : IHttpHandler, IRequiresSessionState

如果把那個 IRequiresSessionState 拿掉,程式就不會有卡住的現象。

問題根源

有實作 IRequireSessionState 的 HTTP handler,在寫入 Session 物件時會令 ASP.NET 獨佔鎖定 Session,這是造成前端大量非同步請求,到了後端卻變成排隊處理的真正原因。從程式的行為來看,這個獨佔鎖定似乎是要等到 HTTP handler 結束時才會釋放。這需要寫點程式進一步確認。

解決方法

如果你的 HTTP hanlder 不需要讀寫 Session,就不要實作 IRequireSessionState。如果需要讀取 Session 但不需要寫入,則可以實作 IReadOnlySessionState。因為 Session 物件的鎖定機制是採用 multi-reader single-writer 的模式,只讀取的話,並不會造成獨佔鎖定 Session 的情形。ASP.NET 網頁(.aspx)預設為實作 IRequireSessionState(亦即 EnableSessionState = "true")。

Case closed!

延伸閱讀

黑暗執行緒再探ASP.NET大排長龍問題有很詳細的實驗和討論,很值得參考。

如果有時間,還可以看一下 MSDN 網站的 Session Provider 說明文件:

ASP.NET applications are inherently multithreaded. Because requests that arrive in parallel are processed on concurrent threads drawn from a thread pool, it's possible that two or more requests targeting the same session will execute at the same time. (The classic example is when a page contains two frames, each targeting a different ASPX in the same application, causing the browser to submit overlapping requests for the two pages.) To avoid data collisions and erratic behavior, the provider "locks" the session when it begins processing the first request, causing other requests targeting the same session to wait for the lock to come free.

Because there's no harm in allowing concurrent requests to perform overlapping reads, the lock is typically implemented as a reader/writer lock-that is, one that allows any number of threads to read a session but that prevents overlapping reads and writes as well as overlapping writes.

Which brings up two very important questions:
  1. How does a session state provider know when to apply a lock?
  2. How does the provider know whether to treat a request as a reader or a writer?
If the requested page implements the IRequiresSessionState interface (by default, all pages implement IRequiresSessionState), ASP.NET assumes that the page requires read/write access to session state. In response to the AcquireRequestState event fired from the pipeline, SessionStateModule calls the session state provider's GetItemExclusive method. If the targeted session isn't already locked, GetItemExclusive applies a write lock and returns the requested data along with a lock ID (a value that uniquely identifies the lock). However, if the session is locked when GetItemExclusive is called, indicating that another request targeting the same session is currently executing, GetItemExclusive returns Nothing and uses the ByRef parameters passed to it to return the lock ID and the lock's age (how long, in seconds, the session has been locked).

If the requested page implements the IReadOnlySessionState interface instead, ASP.NET assumes that the page reads but does not write session state. (The most common way to implement IReadOnlySessionState is to include an EnableSessionState="ReadOnly" attribute in the page's @ Page directive.) Rather than call the provider's GetItemExclusive method to retrieve the requestor's session state, ASP.NET calls GetItem instead. If the targeted session isn't locked by a writer when GetItem is called, GetItem applies a read lock and returns the requested data. (The read lock ensures that if a read/write request arrives while the current request is executing, it waits for the lock to come free. This prevents read/write requests from overlapping with read requests that are already executing.) Otherwise, GetItem returns Nothing and uses the ByRef parameters passed to it to return the lock ID and the lock's age-just like GetItemExclusive.

The third possiblity—that the page implements neither IRequiresSessionState nor IReadOnlySessionState—tells ASP.NET that the page doesn't use session state, in which case SessionStateModule calls neither GetItem nor GetItemExclusive. The most common way to indicate that a page should implement neither interface is to include an EnableSessionState="false" attribute in the page's @ Page directive.

If SessionStateModule encounters a locked session when it calls GetItem or GetItemExclusive (that is, if either method returns null), it rerequests the data at half-second intervals until the lock is released or the request times out. If a time-out occurs, SessionStateModule calls the provider's ReleaseItemExclusive method to release the lock and allow the session to be accessed.

SessionStateModule identifies locks using the lock IDs returned by GetItem and GetItemExclusive. When SessionStateModule calls SetAndReleaseItemExclusive or ReleaseItemExclusive, it passes in a lock ID. Due to the possibility that a call to ReleaseItemExclusive on one thread could free a lock just before another thread calls SetAndReleaseItemExclusive, the SetAndReleaseItemExclusive method of a provider that supports multiple locks IDs per session should only write the session to the data source if the lock ID input to it matches the lock ID in the data source.

Post Comments

技術提供:Blogger.