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

参考资料:

通过 ASP.NET 异步编程实现可扩展的应用程序 HTTP 处理程序和 HTTP 模块概述 演练:创建和注册自定义 HTTP 模块

时间: 2024-10-14 06:38:17

Orchard源码分析(3):Orchard.WarmupStarter程序集的相关文章

Orchard源码分析(5):Host相关(Orchard.Environment.DefaultOrchardHost类)

概述 Host 是应用程序域级的单例,代表了Orchard应用程序.其处理应用程序生命周期中的初始化.BeginRequest事件.EndRequest事件等. 可以简单理解为HttpApplication的功能转移到了Host身上.从源码角度上看,Host对应的是实现了IOrchardHost接口的 DefaultOrchardHost类. 回顾一下之前对Orchard.Web.MvcApplication类的分析.在Orchard启动时,会创建一个DefaultOrchardHost对象:

Orchard源码分析(1):Orchard架构

本文主要参考官方文档"How Orchard works"以及Orchardch上的翻译. 源码分析应该做到庖丁解牛,而不是以管窥豹或瞎子摸象.所以先对Orchard架构有个整体的了解,以及对一些基本概念有所认识. 创建一个基于Web的CMS(内容管理系统)不同于创建一个普通的Web应用程序:它更像是建立一个应用程序容器. 这样一个系统,必须拥有优良的开放性.可扩展性.但是作为一个可扩展系统,它可能会面临应用程序"可用性"的挑战:在系统中的核心模块与未知的未来模块的

Orchard源码分析(1):Orchard架构 (转)

源码分析应该做到庖丁解牛,而不是以管窥豹或瞎子摸象.所以先对Orchard架构有个整体的了解,以及对一些基本概念有所认识. 创建一个基于Web的CMS(内容管理系统)不同于创建一个普通的Web应用程序:它更像是建立一个应用程序容器. 这样一个系统,必须拥有优秀的开放性.可扩展性.但是作为一个可扩展系统,它可能会面临应用程序"可用性"的挑战:在系统中的核心模块与未知的未来模块的组合,包括用户界面级别的整合.编排所有这些小零件,让互不知道的彼此的模块成一个连贯的整体,是Orchard是关键

Orchard源码分析(2):Orchard.Web.MvcApplication类(Global)

概述 分析一个的ASP.NET项目源码,首先可以浏览其项目结构,大致一窥项目其全貌,了解项目之间的依赖关系.其次可以浏览Web.config和Global.asax文件,找到应用程序的入口点. 本 文主要分析Orchard项目的Global.asax文件,而真正的分析入口点在Global.asax的CodeBehind文件 Global.asax.cs中,即Orchard.Web.MvcApplication类(以下简称MvcApplication类). MvcApplication类处理了三个

Orchard源码分析(4.3):Orchard.Events.EventsModule类(Event Bus)

概述 采用Event Bus模式(事件总线),可以使观察者模式中的观察者和被观察者实现解耦. 在.Net 中使用观察者模式,可以使用事件(委托)和接口(类).Orchard Event  Bus使用的是接口的形式,这样方便将“观察者”注册到Autofac容器中.EventsModule模块是构成Orchard Event  Bus的一部分.这里先分开分析Orchard Event Bus涉及的类型和知识点,然后在将他们组合起来分析Orchard Event  Bus的机制. 一.Registra

Orchard源码分析(5.1):Host初始化(DefaultOrchardHost.Initialize方法)

概述 Orchard作为一个可扩展的CMS系统,是由一系列的模块(Modules)或主题(Themes)组成,这些模块或主题统称为扩展(Extensions).在初始化或运行时需要对扩展进行安装:DefaultOrchardHost.SetupExtensions方法. 当添加新的扩展.删除扩展或修改扩展源码后,需要通知扩展加载器(Extension Loader)重新加载或完成一些清理工作,所以需要进行监视:DefaultOrchardHost.MonitorExtensions方法. Orc

Orchard源码分析(7.1):Routing(路由)相关

概述 关于ASP.NET MVC中路由有两个基本核心作用,一是通过Http请求中的Url参数等信息获取路由数据(RouteData),路由数据包含了area.controller.action的名称等信息.只有获取了匹配的路由数据,才有可能转入ASP.NET MVC管道:二是根据由规则生成Url,比如要根据某些数据生成View上显示的链接. Orchard对路由进行扩展主要基于如下原因: (1).路由定义在各个模块中.在Orchard应用程序初始化时将分散在各个模块的路由定义收集起来统一注册.

Orchard源码分析(4.4):Orchard.Caching.CacheModule类

概述 CacheModule也是一个Autofac模块. 一.CacheModule类 CacheModule将DefaultCacheManager注册为ICacheManager: public class CacheModule : Module { protected override void Load( ContainerBuilder builder) { builder.RegisterType<DefaultCacheManager>() .As< ICacheMana

Orchard源码分析(4.1):Orchard.Environment.CollectionOrderModule类

CollectionOrderModule类是一个Autofac模块(Module,将一系列组件和相关的功能包装在一起),而非Orchard模块.其作用是保证多个注册到容器的组件能按FIFO(First In First Out)的顺序提取.下面举例说明:1.创建ICustomerService接口: public interface ICustomerService { } 2.创建两个实现ICustomerService接口的类: public class DefaultCustomerSe