摘要:本文介紹了如何通過異步方法消除使用 Microsoft ASP.Net 的 Web 服務調用的性能問題和線程池資源的消耗問題。
情況:從 ASP.Net 頁面調用 Web 服務時的性能破壞
我們在本文中討論 Web 服務時,期望在各種情況下都可以享用 Web 服務。一個主要的情況是從中間層環境(如 ASP.NET Web 頁面)訪問 Web 服務。為 MapPoint .NET Web 服務的用戶提供支持的人員經常收到這樣的問題,即用戶在使用其 Web 服務時,對 MapPoint .Net 的調用可能需要相當長的時間。這本身並不是什麼問題,但某些其他因素可以使之成為比表面上要嚴重得多的大問題。
HTTP 雙連接限制
HTTP 規范表明,一個 HTTP 客戶端與任一服務器最多可以同時建立兩個 TCP 連接。這可以防止單個浏覽器在浏覽某個頁面(例如,具有 120 個嵌入的縮略圖)時,由於連接請求過多而使服務器負載過重。此時,浏覽器將僅創建 2 個連接,然後通過這兩個管道開始發送 120 個 HTTP 請求,而不是創建 120 個 TCP 連接並通過每個連接來發送 HTTP 請求。對於中間層,此方法的問題在於,中間層可能會有 50 個同時請求連接的用戶。如果不得不為每個用戶進行一次 MapPoint .Net Web 服務調用,將會有 48 個用戶等待兩個管道中的一個空閒下來。
線程池限制
ASP.Net 處理傳入的請求的方式是通過一個稱為進程線程池的一組線程為其提供服務。正常情況下,請求傳入後,池中某個空閒的線程將為其提供服務。這裡的問題在於,進程線程池不會創建無數個線程來處理大量的請求。具有最大線程數限制是一件好事,因為如果我們無限地創建線程,計算機上的全部資源將只能用來管理這些線程了。通過限制所能創建的線程數,我們可以把線程管理的系統開銷保持在一個可控的水平。如果某個請求傳入時線程池中的所有線程都被占用,則該請求將排隊等候,在忙線程完成任務後,空閒出來的線程才能處理新請求。此方法實際上比切換到某個新線程更有效,因為不需要在請求之間進行線程切換。但存在的問題是,如果線程的使用效率不高(尤其是在非常忙的 Web 服務器上),則等候的請求隊列會變得很大。
考慮一下從 ASP.Net 頁面進行 Web 服務調用的情況。如果進行同步調用,則正在運行的線程將被阻塞,直到 Web 服務調用完成為止。在調用期間,線程無法進行任何其他活動。它無法處理其他請求,只能等待。如果某個單處理器計算機上具有默認的工作線程數 20,則只需 20 個同時進行的請求即可用完全部線程,以後的請求必須排隊等候。
該問題不僅限於 Web 服務
不僅調用 Web 服務的用戶會遇到從 Web 頁面進行調用時的擁堵且耗時較長的問題。進行任意數量的較長的調用都會遇到同樣的問題,例如:SQL Server? 請求、長文件的讀取或寫入、各種 Web 請求或訪問某個並發資源(其中鎖定會造成嚴重的延遲)。實際上,有許多使用 Web 服務的情況,其服務調用比較迅速,並不是什麼問題。但您或許會理解,如果您想通過代理服務器調用 MapPoint .Net Web 服務,所使用的連接具有一定的延遲,同時相應的服務可能又要花費一些時間來處理請求,則您可能在各處位置都看到延遲的情況,並且如果站點很忙,便可能出現問題。
改善問題
該問題的某些方面可以通過對環境進行某些配置設置來改善。我們看一下可用於改善該問題的某些配置設置。
maxconnections
連接到 Web 資源的默認雙連接限制可以通過一個名為 connectionManagement 的配置元素來控制。connectionManagement 設置允許您添加要讓其采用非默認連接限制的站點的名稱。可以將以下內容添加到典型的 Web.config 文件中,將您連接的所有服務器的連接限制默認值增加到 40。
<configuration>
<system.Net>
<connectionManagement>
<add address="*" maxconnection="40" />
</connectionManagement>
</system.Net>
<system.web>
...
應當注意的是,對本地計算機的連接數量從來都沒有限制,因此,如果是連接到本地主機,則此設置無效。
maxWorkerThreads 和 minFreeThreads
如果收到 HTTP 503 錯誤(“服務暫時過載”),則表明線程池中的線程已全部占用,並且請求隊列也已超出最大值(appRequestQueueLimit 的默認設置為 100)。對於 IIS 5.0 安裝,可以簡單地增加線程池的大小。而對於 IIS 6.0 安裝(與 IIS 5.0 不兼容),這些設置將無效。
maxWorkerThreads 和 maxIoThreads 分別控制工作線程數以及處理新提交的 ASP.Net 請求的線程數。這些設置需要在您的 Machine.config 中進行配置,它們將影響您計算機上運行的所有 Web 應用程序。maxWorkerThreads 是 Machine.config 中的 processModel 元素的一部分,並且您在查看後會發現,該設置的默認值為每個處理器 20 個線程。
minFreeThreads 設置可以在 Machine.config 中進行配置,或者在您的應用程序的 Web.config 文件中的 httpRuntime 元素下進行配置。該設置的作用是,當空閒的線程數低於所設置的限制時,將禁止使用線程池中的線程來處理傳入的 HTTP 請求。如果您需要某個進程線程池線程完成掛起的請求,這會很有用。如果所有的線程都被用來處理傳入的 HTTP 請求,並且這些請求在等待另一個線程完成其處理,那麼就會進入死鎖狀態。例如,如果您正在從 ASP.Net 應用程序進行對某個 Web 服務的異步 Web 服務調用,並且在等待回調函數完成該請求,就會出現這種情況。因為回調必須在進程線程池中的空閒線程上進行。如果查看一下您的 Machine.config,將會注意到 minFreeThreads 設置的默認值為 8,如果工作線程池的限制為 20,則該默認值還可以滿足需要,但是,如果線程池的大小增加到 100,該默認值就太小了。
應當注意的是,如果您的 ASP.Net 應用程序對本地計算機進行 Web 服務調用,則線程池限制的問題將被激化。例如,我為此專欄創建的測試應用程序調用與 ASPX 頁面同處一台計算機上的 Web 服務。因而,對於阻
塞的調用,一個線程被同時用於 ASPX 頁面和 ASMX Web 服務請求。這有效地使 Web 服務器處理的同時請求數增加了一倍。在同時進行兩個 Web 服務請求(使用異步 Web 服務調用)的情況下,我們最終使同時進行的請求數增加了兩倍。為避免在回調本地計算機時出現此類問題,您應當考慮您的應用程序的體系結構,使其簡單地直接從 ASPX 代碼來執行 Web 方法中的代碼。
Windows XP 限制
我們必須要注意,如果您在一個 Windows? XP 計算機上進行某項測試,則所面臨的另一個限制是 XP Web 服務器對所允許的同時連接數的人為限制。因為 Windows XP 不是服務器平台,其同時連接數被限制為 10。這對於開發環境中的測試通常沒問題,但是如果試圖進行任何復雜的測試,該限制問題就會比較嚴重。本地計算機的連接不受此限制影響。
真正的解決方案:異步請求處理
調整配置設置是一種改善問題的方法,而在實際設計 Web 應用程序時通過某種方式徹底解決問題則是另一回事。等待阻塞的調用完成的線程永遠也不會有更好的調整余地,因此,解決的辦法是完全避免阻塞問題。異步處理請求就是一個適當的解決方案。這表現在兩個方面:進行異步 Web 服務調用,以及在 ASP.Net Web 應用程序中異步處理請求。
異步 Web 服務調用
在以前的專欄中,我寫了有關異步調用 Web 服務的問題。能夠使線程不用等待 Web 服務調用完成是創建釋放線程以便處理更多請求的異步頁面處理模型的關鍵部分。此外,異步調用 Web 服務也比較簡單。
請考慮以下 ASPX 頁面的 Visual Basic.Net 代碼:
' 錯用同步 Web 服務調用所造成的性能極差的
' 頁面!
Public Class SyncPage
Inherits System.Web.UI.Page
Protected WithEvents Label1 As System.Web.UI.WebControls.Label
Protected WithEvents Label2 As System.Web.UI.WebControls.Label
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
'調用 Web 服務
Dim proxy As New localhost.Service1
Label1.Text = proxy.Method1(500)
Label2.Text = proxy.Method1(200)
End Sub
End Class
此代碼非常易懂。頁面加載時將創建一個 Web 服務代理實例,然後用該實例兩次調用一個名為 Method1 的 Web 方法。Method1 只返回包含傳遞給該方法的輸入參數的字符串。為了向該系統添加一定程度的延遲,Method1 在返回字符串之前還休眠了 3 秒鐘。從調用返回到 Method1 的字符串被放在 ASPX 頁面上的兩個標簽的文本中。該頁面提供的性能極差,並且像一塊海綿一樣從進程線程池中吸取線程。由於在 Method1 Web 方法中有 3 秒鐘的延遲,對該頁面的一個調用至少要 6 秒鐘才能完成。
以下代碼片段顯示了一個類似 Web 頁面的代碼,只不過現在進行的是異步 Web 服務調用。
Public Class AsyncPage
Inherits System.Web.UI.Page
Protected WithEvents Label1 As System.Web.UI.WebControls.Label
Protected WithEvents Label2 As System.Web.UI.WebControls.Label
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
'調用 Web 服務
Dim proxy As New localhost.Service1
Dim res As IAsyncResult
= proxy.BeginMethod1(500, Nothing, Nothing)
Dim res2 As IAsyncResult
= proxy.BeginMethod1(200, Nothing, Nothing)
Label1.Text = proxy.EndMethod1(res)
Label2.Text = proxy.EndMethod1(res2)
End Sub
End Class
同樣,該頁面將創建一個 Web 服務代理,然後兩次調用 Method1 Web 方法。不同的是,現在調用的是 BeginMethod1,而不是直接調用 Method1。BeginMethod1 調用將立即返回,這樣我們就可以開始第二次調用該方法。與第一個示例中等待第一個 Web 服務調用完成不同,現在我們可以同時開始這兩個調用。對 EndMethod1 的調用只是在特定的調用完成前會造成阻塞。
值得注意的是,當我們從 ASPX 頁面返回後,響應將發送給客戶端。因此,在獲得所需的數據之前,我們無法從 Page_Load 方法返回。這就是我們要阻塞 Web 服務調用直至其完成的原因。好的方面是兩個調用可以同時執行,因此先前 6 秒鐘的延遲現在將降到 3 秒鐘左右。這雖然好一些,但仍然創建了阻塞的線程。我們真正需要的是在完成 Web 服務調用的同時,能夠釋放線程以便其處理 HTTP 請求。問題在於,ASPX 頁面的處理模型沒有一個異步執行模式。不過,ASP.Net 確實提供了一個解決此問題的方法。
異步 PreRequestHandler 執行
ASP.NET 支持稱為 HttpHandlers 的類。HttpHandlers 是實現 IHttpHandler 接口的類,用於為帶有特定擴展名的文件的 HTTP 請求提供服務。例如,如果查看一下 Machine.config 文件,您將注意到,有許多 HttpHandlers 服務於帶有擴展名(如 .asmx、.aspx、.ashx 甚至 .config)的文件的請求。對於帶有特定擴展名的文件的請求,ASP.Net 將查看其配置信息,然後調用與其相關聯的 HttpHandler 為該請求提供服務。
ASP.Net 還支持寫事件處理程序,在處理 Http 請求過程中的各個時候都可以發生這類事件。其中一個事件是 PreRequestHandlerExecute 事件,它恰好發生在某個特定請求的 HttpHandler 被調用之前。還有一個對 PreRequestHandlerExecute 通知的異步支持,可以注冊這些通知以使用 HttpApplication 類的 AddOnPreRequestHandlerExecuteAsync 方法。HttpApplication 類源自基於 Global.asax 文件創建的事件處理程序。我們將使用異步 PreRequestHandler 選項為 Web 服務調用提供異步執行模式。
在調用 AddOnPreRequestHandlerExecuteAsync 之前要做的第一件事是創建一個 BeginEventHandler 和一個 EndEventHandler 函數。請求傳入後,將調用 BeginEventHandler 函數。我們將在此時開始異步 Web 服務調用。BeginEventHandler 必須返回一個 IAsyncResult 接口。如果您正在進行一個 Web 服務調用,則可以只返回由 Web 服務 begin 函數返回的 IAsyncResult 接口(在我們的示例中,將由 BeginMethod1 方法返回一個 IAsyncResult 接口)。在我創建的示例中,我想執行與前面的 Web 頁面示例(其中揭示了同步和異步 Web 服務調用)相同的操作。這就意味著我必須創建自己的 IAsyncResult 接口。我的 BeginEventHandler 代碼如下所示:
Public Function BeginPreRequestHandlerExecute(
ByVal sender As Object, _
ByVal e As EventArgs, _
ByVal cb As AsyncCallback, _
ByVal extraData As Object) As IAsyncResult
If Request.Url.AbsolutePath _
= "/WebApp/PreRequestHandlerPage.ASPx" Then
Dim proxy As MyProxy = New MyProxy
proxy.Res = New MyAsyncResult
proxy.Res.result1
= proxy.BeginMethod1( _
500, _
New AsyncCallback(AddressOf MyCallback), _
proxy)
proxy.Res.result2
= proxy.BeginMethod1( _
300, _
New AsyncCallback(AddressOf MyCallback), _
proxy)
proxy.Res.Callback = cb
proxy.Res.State = extraData
proxy.Res.Proxy = proxy
Return proxy.Res
End If
Return New MyAsyncResult
End Function
關於此代碼還有許多有趣的事情值得注意。首先,針對此虛擬目錄處理的每個 HTTP 請求都將調用此代碼。因此,我做的第一件事就是檢查請求的實際路徑,查看它是否是我要為其提供服務的頁面的路徑。
我的函數使用了一些有趣的輸入參數來調用。cb 參數是 ASP.NET 傳遞給我的回調函數。ASP.NET 希望在我的異步工作完成後,可以調用由它提供給我的回調函數。它們就是通過這種方式知道何時調用我的 EndEventHandler。同樣,如果我只進行一個 Web 服務調用,則只需將回調傳遞給 BeginMethod1 調用,然後 Web 服務調用將負責調用函數。但在本例中,我進行了兩個單獨的調用。因此,我創建了一個傳遞給兩個 BeginMethod1 調用的中間回調函數,並且在回調代碼中檢查兩個調用是否都已完成。如果沒完成,我將返回;如果已完成,我將調用原始的回調。另一個有趣的參數是 extraData 參數,它在 ASP.NET 調用我時為 ASP.Net 保存了狀態。我在調用由 cb 參數指定的回調函數時必須返回該狀態信息,因此,我將其存儲在所創建的 IAsyncResult 類中。我的回調代碼如下所示:
Public Sub MyCallback(ByVal ar As IAsyncResult)
Dim proxy As MyProxy = ar.AsyncState
If proxy.Res.IsCompleted Then
proxy.Res.Callback.Invoke(proxy.Res)
End If
End Sub
還應當提到的一點是,我創建的實現 IAsyncResult 的類(稱為 MyAsyncResult)將在查詢 IsCompleted 屬性時檢查兩個掛起 Web 服務調用的完成情況。
在 EndEventHandler 中,我只是從 Web 服務調用獲取結果,然後將其存儲在當前的請求上下文中。該上下文與要傳遞給 HttpHandler 的上下文相同。在本例中,它是 .ASPx 請求的處理程序,這樣它便可以用於我的標准代碼。我的 EndEventHandler 代碼如下所示:
Public Sub EndPreRequestHandlerExecute(ByVal ar As IAsyncResult)
If Request.Url.AbsolutePath _
= "/WebApp/PreRequestHandlerPage.ASPx" Then
Dim res As MyAsyncResult = ar
Dim proxy As MyProxy = res.Proxy
Dim retString As String
retString = proxy.EndMethod1(proxy.Res.result1)
Context.Items.Add("WebServiceResult1", retString)
retString = proxy.EndMethod1(proxy.Res.result2)
Context.Items.Add("WebServiceResult2", retString)
End If
End Sub
由於已經接收了 .ASPx 頁面的數據,因此實際的頁面處理也就非常簡單了。
Public C
lass PreRequestHandlerPage
Inherits System.Web.UI.Page
Protected WithEvents Label1 As System.Web.UI.WebControls.Label
Protected WithEvents Label2 As System.Web.UI.WebControls.Label
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Label1.Text = Context.Items("WebServiceResult1")
Label2.Text = Context.Items("WebServiceResult2")
End Sub
End Class
這不僅僅是理論 -- 它確實起作用!
如果不考慮我沒有阻塞了所有線程,至少也使得浪費的資源更少了,因而這還是有意義的。但實際的結果確實會有所不同嗎?答案是肯定的“是”!我把此專欄中介紹的三種測試情況放在了一起:從 Web 頁面代碼進行 2 個阻塞的調用,從 Web 頁面代碼進行 2 個異步調用,以及從 PreRequestHandler 代碼進行 2 個異步調用。我使用 Microsoft Application Center Test 對這三種情況進行了測試,在 60 秒鐘內從 100 個虛擬客戶端連續發送請求。下圖顯示的結果表明了在 60 秒鐘內完成的請求數。
圖 1:100 個同時進行請求的客戶端在 60 秒鐘內完成的請求
異步 PreRequestHandler 方法處理的請求數大約是排在第二位的方法處理的請求數的 8 倍。因此,該方法使您可以處理更多請求,但是對於單個請求,實際要多長時間才能完成呢?下圖顯示了這三種方法的平均響應時間。
圖 2:100 個同時進行請求的客戶端的平均完成響應時間
使用 PreRequestHandler 方法的平均請求響應時間僅為 3.2 秒。假設每個 Web 服務調用的內置延遲為 3 秒鐘,則該方法是一種非常有效的解決辦法。
我必須指出,這些並非科學的數字是在我的並非科學的辦公室中運行的並非科學的計算機上獲得的。當然,如果將空閒的線程釋放出來,讓它們做一些實際的工作確實會改善性能,因而這也很有意義。希望這些結果能夠表明性能的改善其實是非常顯著的。
PreRequestHandler 方法是很必要的,因為 .aspx 請求的處理程序中沒有內置異步請求處理機制。但並非所有 ASP.NET HTTP 處理程序都是這樣。PreRequestHandler 方法適用於所有 ASP.Net 請求類型,但使用將異步支持置於 .asmx 處理程序內的編程方式要比使用 PreRequestHandler 編程方式更容易一些。
小結
無論何時遇到任何類型的進程耗時較長的性能問題,異步執行模型都是一個很好的方法。在從 .aspx 頁面調用 Web 服務的情況下,我們認為可以將異步 Web 服務調用與 ASP.Net 提供的異步執行模式結合起來。這解決了在處理 .ASPx 請求的過程中缺乏異步支持的問題。使用此異步方法可以消除性能問題以及線程池資源的消耗問題。
下載本文相關源代碼