閱讀140 返回首頁    go 阿裏雲 go 技術社區[雲棲]


ASPX頁Web服務調用性能優化

摘要:本文介紹了如何通過異步方法消除使用MicrosoftASP.NET的Web服務調用的性能問題和線程池資源的消耗問題。

情況:從ASP.NET頁麵調用Web服務時的性能破壞

我們在本文中討論Web服務時,期望在各種情況下都可以享用Web服務。一個主要的情況是從中間層環境(如ASP.NETWeb頁麵)訪問Web服務。為MapPoint.NETWeb服務的用戶提供支持的人員經常收到這樣的問題,即用戶在使用其Web服務時,對MapPoint.NET的調用可能需要相當長的時間。這本身並不是什麼問題,但某些其他因素可以使之成為比表麵上要嚴重得多的大問題。

HTTP雙連接限製

HTTP規範表明,一個HTTP客戶端與任一服務器最多可以同時建立兩個TCP連接。這可以防止單個瀏覽器在瀏覽某個頁麵(例如,具有120個嵌入的縮略圖)時,由於連接請求過多而使服務器負載過重。此時,瀏覽器將僅創建2個連接,然後通過這兩個管道開始發送120個HTTP請求,而不是創建120個TCP連接並通過每個連接來發送HTTP請求。對於中間層,此方法的問題在於,中間層可能會有50個同時請求連接的用戶。如果不得不為每個用戶進行一次MapPoint.NETWeb服務調用,將會有48個用戶等待兩個管道中的一個空閑下來。

線程池限製

ASP.NET處理傳入的請求的方式是通過一個稱為進程線程池的一組線程為其提供服務。正常情況下,請求傳入後,池中某個空閑的線程將為其提供服務。這裏的問題在於,進程線程池不會創建無數個線程來處理大量的請求。具有最大線程數限製是一件好事,因為如果我們無限地創建線程,計算機上的全部資源將隻能用來管理這些線程了。通過限製所能創建的線程數,我們可以把線程管理的係統開銷保持在一個可控的水平。如果某個請求傳入時線程池中的所有線程都被占用,則該請求將排隊等候,在忙線程完成任務後,空閑出來的線程才能處理新請求。此方法實際上比切換到某個新線程更有效,因為不需要在請求之間進行線程切換。但存在的問題是,如果線程的使用效率不高(尤其是在非常忙的Web服務器上),則等候的請求隊列會變得很大。

考慮一下從ASP.NET頁麵進行Web服務調用的情況。如果進行同步調用,則正在運行的線程將被阻塞,直到Web服務調用完成為止。在調用期間,線程無法進行任何其他活動。它無法處理其他請求,隻能等待。如果某個單處理器計算機上具有默認的工作線程數20,則隻需20個同時進行的請求即可用完全部線程,以後的請求必須排隊等候。

該問題不僅限於Web服務

不僅調用Web服務的用戶會遇到從Web頁麵進行調用時的擁堵且耗時較長的問題。進行任意數量的較長的調用都會遇到同樣的問題,例如:SQLServer?請求、長文件的讀取或寫入、各種Web請求或訪問某個並發資源(其中鎖定會造成嚴重的延遲)。實際上,有許多使用Web服務的情況,其服務調用比較迅速,並不是什麼問題。但您或許會理解,如果您想通過代理服務器調用MapPoint.NETWeb服務,所使用的連接具有一定的延遲,同時相應的服務可能又要花費一些時間來處理請求,則您可能在各處位置都看到延遲的情況,並且如果站點很忙,便可能出現問題。

改善問題
 
該問題的某些方麵可以通過對環境進行某些配置設置來改善。我們看一下可用於改善該問題的某些配置設置。

maxconnections

連接到Web資源的默認雙連接限製可以通過一個名為connectionManagement的配置元素來控製。connectionManagement設置允許您添加要讓其采用非默認連接限製的站點的名稱。可以將以下內容添加到典型的Web.config文件中,將您連接的所有服務器的連接限製默認值增加到40。

<configuration>
<system.net>
<connectionManagement>
<addaddress="*"maxconnection="40"/>
</connectionManagement>
</system.net>

<system.web>
...

應當注意的是,對本地計算機的連接數量從來都沒有限製,因此,如果是連接到本地主機,則此設置無效。

maxWorkerThreads和minFreeThreads

如果收到HTTP503錯誤(“服務暫時過載”),則表明線程池中的線程已全部占用,並且請求隊列也已超出最大值(appRequestQueueLimit的默認設置為100)。對於IIS5.0安裝,可以簡單地增加線程池的大小。而對於IIS6.0安裝(與IIS5.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頁麵和ASMXWeb服務請求。這有效地使Web服務器處理的同時請求數增加了一倍。在同時進行兩個Web服務請求(使用異步Web服務調用)的情況下,我們最終使同時進行的請求數增加了兩倍。為避免在回調本地計算機時出現此類問題,您應當考慮您的應用程序的體係結構,使其簡單地直接從ASPX代碼來執行Web方法中的代碼。

WindowsXP限製

我們必須要注意,如果您在一個Windows?XP計算機上進行某項測試,則所麵臨的另一個限製是XPWeb服務器對所允許的同時連接數的人為限製。因為WindowsXP不是服務器平台,其同時連接數被限製為10。這對於開發環境中的測試通常沒問題,但是如果試圖進行任何複雜的測試,該限製問題就會比較嚴重。本地計算機的連接不受此限製影響。

真正的解決方案:異步請求處理

調整配置設置是一種改善問題的方法,而在實際設計Web應用程序時通過某種方式徹底解決問題則是另一回事。等待阻塞的調用完成的線程永遠也不會有更好的調整餘地,因此,解決的辦法是完全避免阻塞問題。異步處理請求就是一個適當的解決方案。這表現在兩個方麵:進行異步Web服務調用,以及在ASP.NETWeb應用程序中異步處理請求。

異步Web服務調用

在以前的專欄中,我寫了有關異步調用Web服務的問題。能夠使線程不用等待Web服務調用完成是創建釋放線程以便處理更多請求的異步頁麵處理模型的關鍵部分。此外,異步調用Web服務也比較簡單。

請考慮以下ASPX頁麵的VisualBasic.NET代碼:

'錯用同步Web服務調用所造成的性能極差的
'頁麵!
PublicClassSyncPage
InheritsSystem.Web.UI.Page

ProtectedWithEventsLabel1AsSystem.Web.UI.WebControls.Label
ProtectedWithEventsLabel2AsSystem.Web.UI.WebControls.Label

PrivateSubPage_Load(ByValsenderAsSystem.Object,_
ByValeAsSystem.EventArgs)HandlesMyBase.Load
'調用Web服務
DimproxyAsNewlocalhost.Service1
Label1.Text=proxy.Method1(500)
Label2.Text=proxy.Method1(200)
EndSub

EndClass

此代碼非常易懂。頁麵加載時將創建一個Web服務代理實例,然後用該實例兩次調用一個名為Method1的Web方法。Method1隻返回包含傳遞給該方法的輸入參數的字符串。為了向該係統添加一定程度的延遲,Method1在返回字符串之前還休眠了3秒鍾。從調用返回到Method1的字符串被放在ASPX頁麵上的兩個標簽的文本中。該頁麵提供的性能極差,並且像一塊海綿一樣從進程線程池中吸取線程。由於在Method1Web方法中有3秒鍾的延遲,對該頁麵的一個調用至少要6秒鍾才能完成。

以下代碼片段顯示了一個類似Web頁麵的代碼,隻不過現在進行的是異步Web服務調用。

PublicClassAsyncPage
InheritsSystem.Web.UI.Page

ProtectedWithEventsLabel1AsSystem.Web.UI.WebControls.Label
ProtectedWithEventsLabel2AsSystem.Web.UI.WebControls.Label

PrivateSubPage_Load(ByValsenderAsSystem.Object,_
ByValeAsSystem.EventArgs)HandlesMyBase.Load
'調用Web服務
DimproxyAsNewlocalhost.Service1
DimresAsIAsyncResult
=proxy.BeginMethod1(500,Nothing,Nothing)
Dimres2AsIAsyncResult
=proxy.BeginMethod1(200,Nothing,Nothing)
Label1.Text=proxy.EndMethod1(res)
Label2.Text=proxy.EndMethod1(res2)
EndSub

EndClass

同樣,該頁麵將創建一個Web服務代理,然後兩次調用Method1Web方法。不同的是,現在調用的是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代碼如下所示:

PublicFunctionBeginPreRequestHandlerExecute(
ByValsenderAsObject,_
ByValeAsEventArgs,_
ByValcbAsAsyncCallback,_
ByValextraDataAsObject)AsIAsyncResult
IfRequest.Url.AbsolutePath_
="/WebApp/PreRequestHandlerPage.aspx"Then
DimproxyAsMyProxy=NewMyProxy
proxy.Res=NewMyAsyncResult
proxy.Res.result1
=proxy.BeginMethod1(_
500,_
NewAsyncCallback(AddressOfMyCallback),_
proxy)
proxy.Res.result2
=proxy.BeginMethod1(_
300,_
NewAsyncCallback(AddressOfMyCallback),_
proxy)
proxy.Res.Callback=cb
proxy.Res.State=extraData
proxy.Res.Proxy=proxy
Returnproxy.Res
EndIf
ReturnNewMyAsyncResult
EndFunction

關於此代碼還有許多有趣的事情值得注意。首先,針對此虛擬目錄處理的每個HTTP請求都將調用此代碼。因此,我做的第一件事就是檢查請求的實際路徑,查看它是否是我要為其提供服務的頁麵的路徑。

我的函數使用了一些有趣的輸入參數來調用。cb參數是ASP.NET傳遞給我的回調函數。ASP.NET希望在我的異步工作完成後,可以調用由它提供給我的回調函數。它們就是通過這種方式知道何時調用我的EndEventHandler。同樣,如果我隻進行一個Web服務調用,則隻需將回調傳遞給BeginMethod1調用,然後Web服務調用將負責調用函數。但在本例中,我進行了兩個單獨的調用。因此,我創建了一個傳遞給兩個BeginMethod1調用的中間回調函數,並且在回調代碼中檢查兩個調用是否都已完成。如果沒完成,我將返回;如果已完成,我將調用原始的回調。另一個有趣的參數是extraData參數,它在ASP.NET調用我時為ASP.NET保存了狀態。我在調用由cb參數指定的回調函數時必須返回該狀態信息,因此,我將其存儲在所創建的IAsyncResult類中。我的回調代碼如下所示:

PublicSubMyCallback(ByValarAsIAsyncResult)
DimproxyAsMyProxy=ar.AsyncState
Ifproxy.Res.IsCompletedThen
proxy.Res.Callback.Invoke(proxy.Res)
EndIf
EndSub

還應當提到的一點是,我創建的實現IAsyncResult的類(稱為MyAsyncResult)將在查詢IsCompleted屬性時檢查兩個掛起Web服務調用的完成情況。

在EndEventHandler中,我隻是從Web服務調用獲取結果,然後將其存儲在當前的請求上下文中。該上下文與要傳遞給HttpHandler的上下文相同。在本例中,它是.aspx請求的處理程序,這樣它便可以用於我的標準代碼。我的EndEventHandler代碼如下所示:

PublicSubEndPreRequestHandlerExecute(ByValarAsIAsyncResult)
IfRequest.Url.AbsolutePath_
="/WebApp/PreRequestHandlerPage.aspx"Then
DimresAsMyAsyncResult=ar
DimproxyAsMyProxy=res.Proxy
DimretStringAsString
retString=proxy.EndMethod1(proxy.Res.result1)
Context.Items.Add("WebServiceResult1",retString)
retString=proxy.EndMethod1(proxy.Res.result2)
Context.Items.Add("WebServiceResult2",retString)
EndIf
EndSub

由於已經接收了.aspx頁麵的數據,因此實際的頁麵處理也就非常簡單了。

PublicClassPreRequestHandlerPage
InheritsSystem.Web.UI.Page

ProtectedWithEventsLabel1AsSystem.Web.UI.WebControls.Label
ProtectedWithEventsLabel2AsSystem.Web.UI.WebControls.Label

PrivateSubPage_Load(ByValsenderAsSystem.Object,_
ByValeAsSystem.EventArgs)HandlesMyBase.Load

Label1.Text=Context.Items("WebServiceResult1")
Label2.Text=Context.Items("WebServiceResult2")
EndSub
EndClass

這不僅僅是理論--它確實起作用!

如果不考慮我沒有阻塞了所有線程,至少也使得浪費的資源更少了,因而這還是有意義的。但實際的結果確實會有所不同嗎?答案是肯定的“是”!我把此專欄中介紹的三種測試情況放在了一起:從Web頁麵代碼進行2個阻塞的調用,從Web頁麵代碼進行2個異步調用,以及從PreRequestHandler代碼進行2個異步調用。我使用MicrosoftApplicationCenterTest對這三種情況進行了測試,在60秒鍾內從100個虛擬客戶端連續發送請求。下圖顯示的結果表明了在60秒鍾內完成的請求數。


圖1:100個同時進行請求的客戶端在60秒鍾內完成的請求

異步PreRequestHandler方法處理的請求數大約是排在第二位的方法處理的請求數的8倍。因此,該方法使您可以處理更多請求,但是對於單個請求,實際要多長時間才能完成呢?下圖顯示了這三種方法的平均響應時間。


圖2:100個同時進行請求的客戶端的平均完成響應時間

使用PreRequestHandler方法的平均請求響應時間僅為3.2秒。假設每個Web服務調用的內置延遲為3秒鍾,則該方法是一種非常有效的解決辦法。

我必須指出,這些並非科學的數字是在我的並非科學的辦公室中運行的並非科學的計算機上獲得的。當然,如果將空閑的線程釋放出來,讓它們做一些實際的工作確實會改善性能,因而這也很有意義。希望這些結果能夠表明性能的改善其實是非常顯著的。

PreRequestHandler方法是很必要的,因為.aspx請求的處理程序中沒有內置異步請求處理機製。但並非所有ASP.NETHTTP處理程序都是這樣。PreRequestHandler方法適用於所有ASP.NET請求類型,但使用將異步支持置於.asmx處理程序內的編程方式要比使用PreRequestHandler編程方式更容易一些。

小結

無論何時遇到任何類型的進程耗時較長的性能問題,異步執行模型都是一個很好的方法。在從.aspx頁麵調用Web服務的情況下,我們認為可以將異步Web服務調用與ASP.NET提供的異步執行模式結合起來。這解決了在處理.aspx請求的過程中缺乏異步支持的問題。使用此異步方法可以消除性能問題以及線程池資源的消耗問題。

最後更新:2017-04-02 00:06:33

  上一篇:go ASP.NET中利用cookies保持客戶端信息
  下一篇:go ASP.NET中編程殺死進程