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
参考资料:

转载于:https://www.cnblogs.com/alby/archive/2012/10/18/orchard-WarmupStarter.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值