概述
Orchard.WarmupStarter程序集包含三個類:WarmupUtility、WarmupHttpModule和Starter<T>。該程序集主要為Orchard應用啟動初始化服務。
一、WarmupUtility類
該類是一個靜態工具類,包含一個靜態只讀String型字段WarmupFilesPath,以及三個方法EncodeUrl、ToUrlString和DoBeginRequest。
1、WarmupFilesPath其值為"~/App_Data/Warmup/"。
public
static
readonly
string
WarmupFilesPath =
"~/App_Data/Warmup/"
;
2、 EncodeUrl方法與HttpServerUtility.UrlEncode或HttpUtility.UrlEncode等方法執行結果是不相同 的,它將一個url字符串轉換成另一個只包含數字、字母和下滑線的字符串,使之能夠作為友好文件名。比如調用 Encodeurl("http://localhost:30320/OrchardLocal/".Trim('/'))將返回字符 串"http_3A_2F_2Flocalhost_3A30320_2Forchardlocal"。
public
static
string
EncodeUrl(
string
url) {
if
(
String
.IsNullOrWhiteSpace(url)) {
throw
new
ArgumentException
(
"url can't be empty"
);
}
var
sb =
new
StringBuilder
();
foreach
(
var
c
in
url.ToLowerInvariant()) {
// only accept alphanumeric chars
if
((c >=
'a'
&& c <=
'z'
) || (c >=
'0'
&& c <=
'9'
)) {
sb.Append(c);
}
// otherwise encode them in UTF8
else
{
sb.Append(
"_"
);
foreach
(
var
b
in
Encoding
.UTF8.GetBytes(
new
[] { c })) {
sb.Append(b.ToString(
"X"
));
}
}
}
return
sb.ToString();
}
3、ToUrlString方法一般情況下等效於Request.Url.AbsoluteUri屬性的值,源碼注釋說如果使用了代理請求、負載平衡等可能獲取不了真實的絕對Url地址,關於這方面我不太熟悉。
public
static
string
ToUrlString(
HttpRequest
request) {
return
string
.Format(
"{0}://{1}{2}"
, request.Url.Scheme, request.Headers[
"Host"
], request.RawUrl);
}
4、DoBeginRequest方法接收一個HttpApplication型參數,返回一個bool值表示是否已經在該方法內處理了BeginRequest事件。
首 先它通過上面提到的兩個方法根據當前請求的絕對Url地址生成一個文件名,然后與WarmupFilesPath字段組合成一個物理文件系統路徑。比如對 於請求"http://localhost:30320/OrchardLocal/",生成的物理文件系統路徑可能是 @"E:\Orchard.Source.1.4.1.0\src\Orchard.Web\App_Data\Warmup \http_3A_2F_2Flocalhost_3A30320_2Forchardlocal"。DoBeginRequest方法會檢查該文件是否 存在。如果存在,則直接Response.WriteFile輸出並返回true。如果不存在,則繼續檢測請求Url對應的物理文件系統路徑下是否存在文 件。比如對於請求"http://localhost:30320/OrchardLocal/Default.aspx",將會檢測網站目錄下是否存在 Default.aspx。如果存在,將不會進入ASP.NET MVC管道處理(當然,MVC也可以在物理文件存在的情況下繼續進行路由),直接返回true。這兩種情況之外,方法將返回false。該方法會在 WarmupHttpModule類中被使用,如果返回值是false將會把請求放入一個請求隊列中——后面將詳述。
public
static
bool
DoBeginRequest(
HttpApplication
httpApplication) {
// use the url as it was requested by the client
// the real url might be different if it has been translated (proxy, load balancing, ...)
var
url = ToUrlString(httpApplication.Request);
var
virtualFileCopy =
WarmupUtility
.EncodeUrl(url.Trim(
'/'
));
var
localCopy =
Path
.Combine(
HostingEnvironment
.MapPath(WarmupFilesPath), virtualFileCopy);
if
(
File
.Exists(localCopy)) {
// result should not be cached, even on proxies
httpApplication.Response.Cache.SetExpires(
DateTime
.UtcNow.AddDays(-1));
httpApplication.Response.Cache.SetValidUntilExpires(
false
);
httpApplication.Response.Cache.SetRevalidation(
HttpCacheRevalidation
.AllCaches);
httpApplication.Response.Cache.SetCacheability(
HttpCacheability
.NoCache);
httpApplication.Response.Cache.SetNoStore();
httpApplication.Response.WriteFile(localCopy);
httpApplication.Response.End();
return
true
;
}
// there is no local copy and the file exists
// serve the static file
if
(
File
.Exists(httpApplication.Request.PhysicalPath)) {
return
true
;
}
return
false
;
}
二、WarmupHttpModule類和Starter<T>類
這兩個類是本文將重點分析的類,這里放在一起來分析。WarmupHttpModule類是一個HttpModule,處理 異步BeginRequest事件。WarmupHttpModule已經在~/Web.config進行過注冊。Starter<T>類則處理初始化相關事宜。
這兩個類是本文將重點分析的類,這里放在一起來分析。WarmupHttpModule類是一個HttpModule,處理 異步BeginRequest事件。WarmupHttpModule已經在~/Web.config進行過注冊。Starter<T>類則處理初始化相關事宜。
首先我們來重現兩類異常,然后看Orchard中是如何利用這兩個類來解決的。
1、模擬初始化異常
通過項目模板新建立一個ASP.NET MVC的項目,在Global.asax.cs文件Application_Start方法開頭throw一個異常出來:
throw
new
Exception();
然后啟動調試。在第一次請求發生時,拋出初始化異常:

但第二次(刷新頁面)及以后的請求將可能導致請求如果找不到合適的路由,將會顯示404錯誤頁:

這 種情況下,就不得不重新啟動站點了。這對於調試、用戶使用上來說非常不友好。當然ASP.NET WebForm程序初始化的時候也存在着類似的問題。你可能會想,在初始化有異常的時候可以繼續初始化——如果初始化一直報錯就可能導致一個死循環(可通 過初始化計數避免,但畢竟不是好的方式)。另外,如果初始化太耗時,則可能會導致將要模擬的第二種異常。
2、模擬"服務器太忙"異常
通過項目模板新建立一個ASP.NET MVC的項目,在Global.asax.cs文件Application_Start方法中Sleep 10分鍾模擬耗時初始化操作:
Thread
.Sleep(1000 * 60 * 10);
然后再創建一個控制台程序對網站進行10000次並發請求,核心代碼:
static
void
Main(
string
[] args)
{
//網址根據實際情況調整
String url =
"http://localhost:10670/"
;
for
(
int
i = 0; i < 10000; i++)
{
WebRequest request = WebRequest.Create(url);
request.BeginGetResponse(ar=>
{
using
(WebResponse response = request.EndGetResponse(ar))
{
Console.WriteLine(
"Length:{0} {1}"
,response.ContentLength,DateTime.Now.ToString())
}
},
null
);
}
Console.ReadKey();
}
在VS中運行網站,打開任務管理器,WebDev.WebServer40.EXE的線程數為17(可能會有差異)

然后啟動控制台應用程序,運行一段時間后再看任務管理器,WebDev.WebServer40.EXE的線程數達到129個。在我的電腦上,這時候控制台程序已經報異常"遠程服務器返回錯誤: (503) 服務器不可用。"

退出控制台程序,馬上在瀏覽器打開:http://localhost:10670/,出現錯誤:

注意,要確保運行控制台測試程序的時候網站沒有初始化過,否則看不到效果。
WebDev.WebServer 或IIS等Web服務器的線程池中能創建的線程數畢竟是有限的,能並發處理的請求任務也是有限的。當Web服務器發現有太多的請求任務來不及處理的時候, 將會導致該錯誤。像Orchard這類網站,在啟動並初始化的時候會進行大量耗時的操作,如果這時候有大量的請求進入,如果不采用合適的處理方式,很快線 程將被耗光並且導致未處理的請求任務過多。
總結問題:ASP.NET初始化操作時,初始化異常不能方便的重現,並且不能繼續嘗試初始化操作,除非重啟應用程序;由於Web服務器能處理並發請求任務數的限制,有可能導致“服務器太忙”的錯誤。
Orchard.WarmupStarter程序集正是為了解決這些問題。Orchard的初始化步驟如下:
1、在第1次請求發生時,Application_Start方法得以執行,創建 Orchard.WarmupStarter.Starter<T>對象,並調用該對象的OnApplicationStart方法。 OnApplicationStart方法又調用LaunchStartupThread方法。
public
void
OnApplicationStart(
HttpApplication
application)
{
LaunchStartupThread(application);
}
2、LaunchStartupThread方法會通過線程池
啟動一個新的線程進行異步初始化操作。
public
void
LaunchStartupThread(
HttpApplication
application)
{
// Make sure incoming requests are queued
WarmupHttpModule
.SignalWarmupStart();
ThreadPool
.QueueUserWorkItem(
state =>
{
try
{
var
result = _initialization(application);
_initializationResult = result;
}
catch
(
Exception
e)
{
lock
(_synLock)
{
_error = e;
_previousError =
null
;
}
}
finally
{
// Execute pending requests as the initialization is over
WarmupHttpModule
.SignalWarmupDone();
}
});
}
由 於是新開線程進行初始化操作,在初始化過程中,第一次請求會在管道中繼續進行,這時候也有可能會有新的請求進入。如果在初始化操作完成之前,任由這些請求 進行下去,很可能得不到要想的結果。所以Orchard提供了一個異步HttpMoudle,即 Orchard.WarmupStarter.WarmupHttpModule。在初始化正在進行時,將請求的異步BeginRequest處理"暫 停"在那兒,等初始化完成后(不管失敗與否),讓異步BeginRequest處理完成。在初始化的過程中如果有異常發生,則會將異常記錄下來。
LaunchStartupThread方法首先調用WarmupHttpModule的靜態方法SignalWarmupStart,用於初始化一個靜 態List<Action>列表,該列表保存異步結果WarmupAsyncResult類的Completed方法。在初始化完成后,不管 成功與否,Completed方法都將會得到調用以保證"暫停"在那的異步BeginRequest處理完成,即WarmupHttpModule的靜態 方法SignalWarmupDone。
3、在異步BeginRequest事件處理完成后,將處理同步BeginRequest事件。事件處理程序將檢查上一次初始化請求是否有異常發生;如果 檢查到有異常發生,則會再次執行LaunchStartupThread方法嘗試新的初始化操作;如果新的初始化沒有異常發生,就"忘記"上次初始化出現 過異常,否則將本次異常進行記錄,拋出上次初始化異常。
注意:在再次執行LaunchStartupThread方法時,如果有新的請求進入,也會將請求的異步BeginRequest處理"暫停"在那里,直到初始化完成。請查看Starter<T>的OnBeginRequest方法的代碼:
public
void
OnBeginRequest(
HttpApplication
application)
{
// Initialization resulted in an error
if
(_error !=
null
)
{
// Save error for next requests and restart async initialization.
// Note: The reason we have to retry the initialization is that the
// application environment may change between requests,
// e.g. App_Data is made read-write for the AppPool.
bool
restartInitialization =
false
;
lock
(_synLock)
{
if
(_error !=
null
)
{
_previousError = _error;
_error =
null
;
restartInitialization =
true
;
}
}
if
(restartInitialization)
{
LaunchStartupThread(application);
}
}
// Previous initialization resulted in an error (and another initialization is running)
if
(_previousError !=
null
)
{
throw
new
ApplicationException
(
"Error during application initialization"
, _previousError);
}
// Only notify if the initialization has successfully completed
if
(_initializationResult !=
null
)
{
_beginRequest(application, _initializationResult);
}
}
可以放心,如果上次初始化出現異常,不會導致多個同步BeginRequest事件處理程序嘗試都去執行LaunchStartupThread方法,Orchard加了個lock以保證線程安全。
下面看看Orchard是不是解決了上述提到的兩個問題。
首先,修改LaunchStartupThread方法,使初始化線程總是拋出異常:
public
void
LaunchStartupThread(
HttpApplication
application)
{
// ...
try
{
throw
new
Exception
(
DateTime
.Now.ToString());
var
result = _initialization(application);
_initializationResult = result;
}
// ...
}
啟動站點,隔幾秒刷新一次頁面,根據輸出的時間可以看到總是拋出上一次初始化的異常。
最后,我們來模擬一次在初始化時大量請求涌入的情況。修改LaunchStartupThread方法,讓初始化看起來更耗時:
public
void
LaunchStartupThread(
HttpApplication
application)
{
// ...
try
{
Thread
.Sleep(1000 * 60 * 2);
var
result = _initialization(application);
_initializationResult = result;
}
// ...
}
啟動Orchard,接着啟動控制台測試程序(請求URL要修改成Orchard站點的),查看任務管理器,可以看到WebDev.WebServer40.EXE的線程始終保持在比較低的范圍:

初始化完畢后,WebDev.WebServer40.EXE的線程數有所下降,但是CPU卻消耗很多,這是因為它現在正在處理10000個等待的請求,所以是正常的:

別忘記了最初我們分析的WarmupUtility類,為了在初始化的同時能夠響應用戶請求,我們在"~/App_Data/Warmup/"對應目錄下 手工新建一個名為"http_3A_2F_2Flocalhost_3A30320_2Forchardlocal"的文件,當然具體文件名要根據實際情 況來定。再次初始化網站並運行控制台測試程序:

可以看到,瀏覽器(發送第一個請求)、控制台程序(模擬初始化過程中的若干請求)能得到及時響應,而Web服務器的的性能保持在一個比較穩定的水平。
這里我們只是手工制作了個首頁的WarmUp文件,但是在初始化過程中用戶可能訪問任何一個可以訪問的頁面。Orchard提供了一個Orchare.WarmUp模塊,可以用來生成對應的靜態文件。
另外,雖然我們上面只考慮了初始化的情況,實際上Orchare.WarmUp模塊對於運行時提高服務器吞吐能力也是大有好處的。
相關類型:
Orchard.WarmupStarter.WarmupUtility
Orchard.WarmupStarter.WarmupHttpModule: IHttpModule
Orchard.WarmupStarter.Starter<T>
Orchard.Environment.OrchardStarter
Orchard.Environment.DefaultOrchardHost : IOrchartHost
Orchard.Web.MvcApplication
參考資料: