Orchard源碼分析(3):Orchard.WarmupStarter程序集


概述
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>類則處理初始化相關事宜。
首先我們來重現兩類異常,然后看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
參考資料:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM