转:通过避免下列 10 个常见 ASP.NET 缺陷使网站平稳运行

中文页面: http://blog.163.com/chuan_zheng/blog/static/856478720074155351773/
英文原文: http://msdn.microsoft.com/msdnmag/issues/06/07/WebAppFollies/default.aspx
示例源码页: http://msdn.microsoft.com/msdnmag/issues/06/07/WebAppFollies/default.aspx?fig=true#fig4

我看过后印象比较深刻的片断:

LoadControl 和输出缓存

极少有不使用用户控件的 ASP.NET 应用程序。在出现母版页之前,开发人员使用用户控件来提取公用内容,如页眉和页脚。即使在 ASP.NET 2.0 中,用户控件也提供了有效的方法来封装内容和行为以及将页面分为多个区域,这些区域的缓存能力可以独立于作为整体的页面进行控制(一种称为段缓存的特殊输出缓存形式)。

用户控件可以采用声明的方式加载,也可以强制加载。强制加载依赖于 Page.LoadControl,它实例化用户控件并返回控件引用。如果用户控件包含自定义类型的成员(例如,公共属性),则您可以转换该引用并从您的代码访问自定义成员。图 1 中的用户控件实现名为 BackColor 的属性。以下代码加载用户控件并向 BackColor 分配一个值:

protected void Page_Load(object sender, EventArgs e)
{
// 加载用户控件并将其添加到页面中
Control control = LoadControl("~/MyUserControl.ascx");
PlaceHolder1.Controls.Add(control);

// 设置其背景色
((MyUserControl)control).BackColor = Color.Yellow;
}

以上代码实际上很简单,但却是一个等待粗心的开发人员掉进去的陷阱。您能找出其中的破绽吗?

如果您猜到该问题与输出缓存有关,那么您是正确的。正如您所看到的一样,上述代码示例编译和运行都正常,但是如果尝试将以下语句(完全合法)添加到 MyUserControl.ascx 中:

<%@ OutputCache Duration="5" VaryByParam="None" %>

则当您下一次运行该页面时,您将看到 InvalidCastException (oh joy!) 和以下错误消息:

“无法将类型为‘System.Web.UI.PartialCachingControl’的对象转换为类型‘MyUserControl’。”

因此,此代码在没有 OutputCache 指令时运行正常,但如果添加了 OutputCache 指令就会出错。ASP.NET 不应该以这种方式运行。页面(和控件)对于输出缓存应该是不可知的。那么,这代表什么意思?

问题在于为用户控件启用输出缓存时,LoadControl 不再返回对控件实例的引用;相反,它返回对 PartialCachingControl 实例的引用,而 PartialCachingControl 可能会也可能不会包装控件实例,具体取决于控件的输出是否被缓存。因此,如果开发人员调用 LoadControl 以动态加载用户控件并且为了访问控件特定的方法和属性而转换控件引用,他们必须注意进行该操作的方式,以便不管是否具有 OutputCache 指令,代码都可以运行。

图 2 说明动态加载用户控件以及转换返回的控件引用的正确方法。以下是其工作原理概要:

如果 ASCX 文件缺少 OutputCache 指令,则 LoadControl 返回一个 MyUserControl 引用。Page_Load 将该引用转换为 MyUserControl 并设置控件的 BackColor 属性。

如果 ASCX 文件包括一个 OutputCache 指令并且控件的输出没有被缓存,则 LoadControl 返回一个对 PartialCachingControl 的引用,此 PartialCachingControl 的 CachedControl 属性包含对基础 MyUserControl 的引用。Page_Load 将 PartialCachingControl.CachedControl 转换为 MyUserControl 并设置该控件的 BackColor 属性。

如果 ASCX 文件包括一个 OutputCache 指令并且控件的输出被缓存,则 LoadControl 返回一个对 PartialCachingControl(其 CachedControl 属性为空)的引用。注意,Page_Load 不再继续执行操作。无法设置控件的 BackColor 属性,因为该控件的输出来源于输出缓存。换句话说,根本没有要设置属性的 MyUserControl。

不管 .ascx 文件中是否具有 OutputCache 指令,图 2中的代码都将运行。虽然看起来复杂一点,但它会避免烦人的错误。简单并不总是代表易于维护。

 

视图状态:无声的性能杀手

从某种意义上说,视图状态是有史以来最伟大的事情。毕竟,视图状态使得页面和控件能够在回发之间保持状态。因此,您不必像在传统的 ASP 中那样编写代码,以防止在单击按钮时文本框中的文本消失,或在回发后重新查询数据库和重新绑定 DataGrid。

但是视图状态也有缺点:当它增长得过大时,它便成为一个无声的性能杀手。某些控件(例如文本框)会根据视图状态作出相应判断。其他控件(特别是 DataGrid 和 GridView)则根据显示的信息量确定视图状态。如果 GridView 显示 200 或 300 行数据,我会望而生畏。即使 ASP.NET 2.0 视图状态大致是 ASP.NET 1 x 视图状态的一半大小,一个糟糕的 GridView 也可以容易地将浏览器和 Web 服务器之间的连接的有效带宽减少 50% 或更多。

您可以通过将 EnableViewState 设置为 false 来关闭单个控件的视图状态,但某些控件(特别是 DataGrid)在不能使用视图状态时会失去某些功能。控制视图状态的更佳解决方案是将其保留在服务器上。在 ASP.NET 1.x 中,您可以重写页面的 LoadPageStateFromPersistenceMedium 和 SavePageStateToPersistenceMedium 方法并按您喜欢的方式处理视图状态。图 4 中的代码显示的重写可防止视图状态保留在隐藏字段中,而将其保留在会话状态中。当与默认会话状态进程模型一起使用时(即,会话状态存储在内存中的 ASP.NET 辅助进程中时),在会话状态中存储视图状态尤其有效。相反,如果会话状态存储在数据库中,则只有测试才能显示在会话状态中保留视图状态会提高还是降低性能。

在 ASP.NET 2.0 中使用相同的方法,但是 ASP.NET 2.0 能够提供更简单的方法将视图状态保留在会话状态中。首先,定义一个自定义页适配器,其 GetStatePersister 方法返回 .NET Framework SessionPageStatePersister 类的一个实例:

public class SessionPageStateAdapter :
System.Web.UI.Adapters.PageAdapter
{
public override PageStatePersister GetStatePersister ()
{
return new SessionPageStatePersister(this.Page);
}
}

然后,通过将 App.browsers 文件按以下方式放入应用程序的 App_Browsers 文件夹,将自定义页适配器注册为默认页适配器:

<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.Page"
adapterType="SessionPageStateAdapter" />
</controlAdapters>
</browser>
</browsers>

(您可以将文件命名为您喜欢的任何名称,只要它的扩展名为 .browsers 即可。)此后,ASP.NET 将加载页适配器并使用返回的 SessionPageStatePersister 以保留所有页面状态,包括视图状态。

使用自定义页适配器的一个缺点是它全局性地作用于应用程序中的每一页。如果您更愿意将其中一些页面的视图状态保留在会话状态中而不保留其他页面的视图状态,请使用图 4 中显示的方法。另外,如果用户在同一会话中创建多个浏览器窗口,您使用该方法可能会遇到问题。

未缓存的角色

以下语句经常出现于 ASP.NET 2.0 应用程序的 web.config 文件以及介绍 ASP.NET 2.0 角色管理器的示例中:

<roleManager enabled="true" />

但正如以上所示,该语句确实会对性能产生明显的负面影响。您知道为什么吗?

默认情况下,ASP.NET 2.0 角色管理器不会缓存角色数据。相反,它会在每次需要确定用户属于哪个角色(如果有)时参考角色数据存储。这意味着一旦用户经过了身份验证,任何利用角色数据的页(例如,使用启用了安全裁减设置的网站图的页,以及使用 web.config 中基于角色的 URL 指令进行访问受到限制的页)将导致角色管理器查询角色数据存储。如果角色存储在数据库中,那么对于每个请求需要访问多个数据库的情况,您可以轻松地免除访问多个数据库。解决方案是配置角色管理器以在 Cookie 中缓存角色数据:

<roleManager enabled="true" cacheRolesInCookie="true" />

您可以使用其他<roleManager> 属性控制角色 Cookie 的特征 — 例如,Cookie 应保持有效的期限(以及角色管理器因此返回角色数据库的频率)。角色 Cookie 默认情况下是经过签名和加密的,因此安全风险虽然不为零,但也有所缓解。

返回页首 返回页首

线程池饱和

在执行数据库查询并等待 15 秒或更长时间来获得返回的查询结果时,我经常对看到的实际的 ASP.NET 页数感到非常惊讶。(我也等待了 15 分钟才看到查询结果!)有时,延迟是由于返回的数据量很大而导致的不可避免的无奈结果;而有时,延迟则是由于数据库的设计不佳导致的。但不管是什么原因,长时间的数据库查询或任何类型的长时间 I/O 操作在 ASP.NET 应用程序中都会导致吞吐量的下降。

关于这个问题我以前已经详细地描述过,所以在此就不再作过多的说明了。我只说一点就够了,ASP.NET 依赖于有限的线程池处理请求,如果所有线程都被占用来等待数据库查询、Web 服务调用或其他 I/O 操作完成,则在某个操作完成并且释放出一个线程之前,其他请求都必须排队等待。当请求排队时,性能会急剧下降。如果队列已满,则 ASP.NET 会使随后的请求失败并出现 HTTP 503 错误。这种情况不是我们希望在 Web 生产服务器的生产应用程序上所乐见的。

解决方案非异步页面莫属,这是 ASP.NET 2.0 中最佳却鲜为人知的功能之一。对异步页面的请求从一个线程上开始,但是当它开始一个 I/O 操作时,它将返回该线程以及 ASP.NET 的 IAsyncResult 接口。操作完成后,请求通过 IAsyncResult 通知 ASP.NET,ASP.NET 从池中提取另一个线程并完成对请求的处理。值得注意的是,当 I/O 操作发生时,没有占用线程池线程。这样可以通过阻止其他页面(不执行较长的 I/O 操作的页面)的请求在队列中等待,从而显著地提高吞吐量。

您可以在 MSDN®Magazine 的 2005 年 10 月刊中阅读有关异步页面的所有信息。I/O 绑定而不是计算机绑定且需要很长时间执行的任何页面很有可能成为异步页面。

当我将关于异步页面的信息告知开发人员时,他们经常回答“那真是太棒了,但是我的应用程序中并不需要它们。”对此我回答说:“你们的任何页面需要查询数据库吗?它们调用 Web 服务吗?您是否已经检查 ASP.NET 性能计数器中关于排队请求和平均等待时间的统计信息?即使您的应用程序至今运行正常,但是随着您的客户规模的增长,应用程序的负载可能会增加。”

实际上,绝大多数实际的 ASP.NET 应用程序都需要异步页面。请切记这一点!

你可能感兴趣的:(asp.net)