开发高性能的 ASP.NET 应用程序
使用本主题中的准则所列出的方法有助于最大程度提高 ASP.NET Web 应用程序的吞吐量。这些准则分为以下部分:
-
页面和服务器控件处理
-
状态管理
-
数据访问
-
Web 应用程序
-
编码实践
页面和服务器控件处理
下列准则提供了有效使用 ASP.NET 页面和控件的建议。
-
避免到服务器的不必要的往返行程 在某些情况下不必使用 ASP.NET 服务器控件和执行回发事件处理。例如,在 ASP.NET 网页中验证用户输入经常可在数据提交到服务器之前在客户端进行。通常,如果不需要将信息传递到服务器以进行验证或将其写入数据存储区,请避免使用导致到服务器的往返行程的代码,这样可以提高页的性能和并改善用户体验。您也可以不执行整个往返行程,而是使用客户端回调从服务器中读取数据。有关详细信息,请参见在 ASP.NET 网页中不经过回发而实现客户端回调。
如果您开发自定义服务器控件,请考虑让它们为支持 ECMAScript (JavaScript) 的浏览器呈现客户端代码。通过以这种方式使用服务器控件,您可以显著地减少信息被发送到 Web 服务器的次数。有关更多信息,请参见开发自定义 ASP.NET 服务器控件。
-
使用 Page 对象的 IsPostBack 属性来避免对往返行程执行不必要的处理 如果您编写处理服务器控件回发处理的代码,有时可能需要代码仅在首次请求页时执行,而不是每次回发时都执行。根据该页是否是响应服务器控件事件生成的,使用 IsPostBack 属性有条件地执行代码。
-
只在必要时保存服务器控件视图状态 自动视图状态管理使服务器控件可以在往返行程中重新填充它们的属性值,而您不需要编写任何代码。但是,因为服务器控件的视图状态在隐藏的窗体字段中往返于服务器,所以该功能影响性能。了解在哪些情况下视图状态会有所帮助,在哪些情况下它影响页的性能,这样是有帮助的。例如,如果您将服务器控件绑定到每个往返行程上的数据,因为控件的值会在数据绑定期间用新值替换,所以保存的视图状态没有用处。在这种情况下,禁用视图状态可以节省处理时间并减少页的大小。
默认情况下,为所有服务器控件启用视图状态。若要禁用它,请将控件的 EnableViewState 属性设置为 false,如下面的 DataGrid 服务器控件示例所示:
<asp:datagrid EnableViewState="false" datasource="..." runat="server"/>
您还可以使用 @ Page 指令禁用整个页的视图状态。当您不从页回发到服务器时,这将十分有用:
<%@ Page EnableViewState="false" %>
注意 @ Control 指令中还支持 EnableViewState 属性以指定是否为用户控件启用视图状态。
若要分析服务器控件在页中使用的视图状态的大小,请通过将 trace="true" 属性包含在 @ Page 指令中启用对该页的跟踪。然后在跟踪输出中,查看“控件层次结构”表的“Viewstate”列。有关跟踪和如何启用它的信息,请参见 ASP.NET 跟踪。
-
除非有特殊的原因要关闭缓冲,否则使其保持打开状态 禁用 ASP.NET 网页的缓冲会导致大量的性能开销。有关更多信息,请参见 Buffer 属性。
-
使用 Transfer Server 对象或跨页发送的方法在同一个应用程序中的不同 ASP.NET 页之间重定向 有关详细信息,请参见将用户重定向到另一页。
状态管理
下列准则提供了有效进行状态管理的建议。
-
当不使用会话状态时禁用它 并不是所有的应用程序或页都需要具体用户的会话状态;您应该在不需要时禁用会话状态。若要禁用页的会话状态,请将 @ Page 指令中的 EnableSessionState 属性设置为 false,如下面的示例所示:
<%@ Page EnableSessionState="false" %>
注意 如果页需要访问会话变量,但不会创建或修改它们,则将 @ Page 指令中的 EnableSessionState 属性设置为 ReadOnly。
还可以禁用 XML Web services 方法的会话状态。有关更多信息,请参见 使用 ASP.NET 和 XML Web 服务客户端创建的 XML Web 服务。
若要禁用应用程序的会话状态,请在应用程序的 Web.config 文件的 SessionState 节中将 Mode 属性设置为 Off,如下面的示例所示:
<sessionState mode="Off" />
-
针对应用程序需要,选择适当的会话状态提供程序 ASP.NET 为存储应用程序的会话数据提供了多种方法:进程内会话状态、作为 Windows 服务的进程外会话状态和 SQL Server 数据库中的进程外会话状态。(您还可以创建自定义会话状态提供程序,以在所选数据存储区中存储会话数据。)每种方法都有自己的优点,但进程内会话状态是迄今为止速度最快的解决方案。如果只在会话状态中存储少量易失数据,则建议您使用进程内提供程序。进程外会话状态选项用于跨多个处理器或多个计算机缩放应用程序,或者用于您希望在服务器或进程重新启动时保留会话数据的情况。有关更多信息,请参见 ASP.NET 会话状态。
数据访问
下列准则提供了在应用程序中有效进行数据访问的建议。
-
将 SQL Server 和存储过程用于数据访问 在 .NET Framework 提供的所有数据访问方法中,使用 SQL Server 进行数据访问是生成高性能、可缩放 Web 应用程序的推荐选择。使用托管 SQL Server 提供程序时,可通过尽可能使用编译的存储过程而不是 SQL 命令获得额外的性能提高。有关使用 SQL Server 存储过程的信息,请参见将存储过程用于命令。
-
将 SqlDataReader 类用于快速只进数据游标 SqlDataReader 类提供了从 SQL Server 数据库检索的只进数据流。如果您可以在 ASP.NET 应用程序中使用只读流,则 SqlDataReader 类提供比 DataSet 类更高的性能。SqlDataReader 类使用 SQL Server 的本机网络数据传输格式从数据库连接直接读取数据。例如,当绑定到 SqlDataSource 控件时,通过将 DataSourceMode 属性设置为 DataReader,您将获得更好的性能。(使用数据读取器会导致某些功能的丢失。)另外,SqlDataReader 类实现 IEnumerable 接口,该接口也使您可以将数据绑定到服务器控件。有关更多信息,请参见 SqlDataReader 类。有关 ASP.NET 如何访问数据的信息,请参见通过 ASP.NET 访问数据。
-
尽可能缓存数据和页输出 ASP.NET 提供了一些机制,它们会在不需要为每个页请求动态计算页输出或数据时缓存这些页输出或数据。另外,通过设计要进行缓存的页和数据请求(特别是在站点中预期将有较大通讯量的区域),可以优化这些页的性能。与使用 .NET Framework 的任何其他功能相比,适当地使用缓存可以更好地提高站点的性能。
在使用 ASP.NET 缓存时,应注意以下事项。首先,不要缓存太多项。缓存每个项都有内存开销。不要缓存容易重新计算和很少使用的项。其次,给缓存项分配的有效期不要太短。很快到期的项会导致缓存中不必要的周转,并且会导致额外的代码清除和垃圾回收工作。使用与“ASP.NET Applications”性能对象关联的“Cache Total Turnover Rate”(缓存总流通率)性能计数器,您可以监视缓存中由于项到期而导致的周转。高周转率可能说明存在问题,特别是当项在到期前被移除时。(这种情况有时称作内存压力。)
有关如何缓存页输出和数据请求的信息,请参见 ASP.NET 缓存概述。
-
适当地使用 SQL 缓存依赖项 ASP.NET 同时支持基于表的轮询和查询通知,具体取决于所使用的 SQL Server 的版本。所有 SQL Server 版本都支持基于表的轮询。在基于表的轮询中,如果表中的任何内容发生更改,所有侦听器都会失效。这可能导致应用程序中不必要的改动。建议不要将基于表的轮询用于具有许多频繁更改的表。例如,建议将基于表的轮询用于很少更改的目录表。建议不要将基于表的轮询用于订单表,订单表具有更频繁的更新。SQL Server 2005 支持查询通知。查询通知支持特定查询,从而减少在表更改时发送的通知数量。虽然它比基于表的轮询提供更好的性能,但是它无法扩展到适应数千个查询。
有关 SQL 缓存依赖项的更多信息,请参见演练:将 ASP.NET 输出缓存与 SQL Server 结合使用或使用 SqlCacheDependency 类在 ASP.NET 中缓存。
-
使用数据源分页和排序而不是 UI(用户界面)分页和排序 DetailsView 和 GridView 等数据控件的 UI 分页功能可用于支持 ICollection 接口的任何数据源对象。对于每个分页操作,数据控件查询数据源的整个数据集并选择要显示的行,并放弃其余的数据。如果数据源实现 DataSourceView 并且 CanPage 属性返回 true,则数据控件将使用数据源分页而不是 UI 分页。在这种情况下,数据控件仅查询每个分页操作需要的行。因此,数据源分页比 UI 分页更高效。只有 ObjectDataSource 数据源控件才支持数据源分页。若要在其他数据源控件上启用数据源分页,必须从该数据源控件继承并修改其行为。
-
平衡事件验证的安全性受益和性能开销 从 System.Web.UI.WebControls 和 System.Web.UI.HtmlControls 类派生的控件可以验证事件是否源自该控件所呈现的用户界面。这样有助于防止控件响应伪造的事件通知。例如,DetailsView 控件可以防止 Delete(删除)调用(控件中本质上不支持该调用)的处理以及被操纵而删除数据。此验证会带来一定的性能开销。可以使用 EnableEventValidation 配置元素和 RegisterForEventValidation 方法控制此行为。验证的开销取决于页上的控件数量,并在几个百分点范围内。
安全注意 强烈建议不要禁用事件验证。在禁用事件验证之前,应该确保不会构造任何可能对应用程序具有意外影响的回发。
-
除非必要,否则避免使用视图状态加密 视图状态加密会阻止用户能够读取隐藏视图状态字段中的值。典型情况是在 DataKeyNames 属性中带有一个标识符字段的 GridView 控件。标识符字段是协调对记录的更新所必需的。由于不想要标识符对用户可见,可以加密视图状态。但是,加密对于初始化具有恒定的性能开销,并具有取决于被加密的视图状态大小的附加开销。加密为每次页加载而设置,因此在每次页加载时都会发生相同的性能影响。
-
使用 SqlDataSource 缓存、排序和筛选 如果 SqlDataSource 控件的 DataSourceMode 属性设置为 DataSet,则 SqlDataSource 能够缓存查询产生的结果集。如果以这种方式缓存数据,则控件的筛选和排序操作(使用 FilterExpression 和 SortParameterName 属性进行配置)将使用缓存的数据。在许多情况下,如果缓存整个数据集,并使用 FilterExpression 和 SortParameterName 属性进行排序和筛选,而不是使用带“WHERE”和“SORT BY”子句的 SQL 查询(对于这些查询,每个选择操作都要访问数据库),应用程序会运行得更快。
Web 应用程序
下列准则提供了使整个 Web 应用程序有效工作的建议。
-
如果有大型 Web 应用程序,请预编译它 在第一次对应用程序资源(如页)的请求中,Web 应用程序是批编译的。如果应用程序中的页都没有编译,批编译功能会成批编译目录中的所有页,以便更好地利用磁盘和内存。批编译功能为 ASP.NET 带来性能上的好处,因为它将许多页面编译为单个程序集。从已加载的程序集访问一页比每页加载新的程序集要快。请注意,如果将多个页面批编译到一个目录超出 BatchTimeout 属性指定的秒数,则将分析并编译单个页面,以便请求能被快速处理。
批编译的缺点在于:如果服务器接收到许多对尚未编译的页面的请求,那么当 Web 服务器分析并编译它们时,性能可能较差。要解决此问题,可以预编译应用程序。有关详细信息,请参见 ASP.NET 网站预编译。
-
在 Internet 信息服务 5.0 上,在进程外运行 Web 应用程序 默认情况下,IIS 5.0 上的 ASP.NET 将使用进程外辅助进程为请求提供服务。此功能已被优化以提高吞吐量。由于在进程外的辅助进程中运行 ASP.NET 有其功能和优点,建议在生产站点上使用它。
-
定期回收进程 为了同时保证稳定性和性能,应该定期回收进程。经过较长的时间,有内存泄漏和 bug 的资源可以影响 Web 服务器的吞吐量,而回收进程可以清理内存避免这类问题。但是,应当平衡定期回收的需求和过频的回收,因为停止辅助进程、重新加载页面并重新获取资源和数据的开销可能会超过回收的好处。
在使用 IIS 6.0 的 Windows Server 2003 上运行的 ASP.NET Web 应用程序不需要调整进程模型设置,因为 ASP.NET 将使用 IIS 6.0 进程模型设置。
-
必要时调整应用程序每个辅助进程的线程数 ASP.NET 的请求结构试图在执行请求的线程数和可用资源之间达到一种平衡。该结构将根据可用于请求的 CPU 功率,来决定允许同时执行的请求数。这项技术称作线程门控。但是在某些条件下,线程门控算法不是很有效。通过使用与“ASP.NET Applications”性能对象关联的“Pipeline Instance Count”(管线实例计数)性能计数器,可以在 Windows 性能监视器中监视线程门控。
当 ASP.NET 网页调用外部资源,如执行数据库访问或 XML Web services 请求时,页面请求通常停止并释放 CPU 以处理其他线程,直到外部资源响应为止。如果另一个请求正在等待处理,并且线程池中有一个线程释放,则开始处理这个正在等待的请求。这可能导致 ASP.NET 辅助进程或应用程序池中存在大量同时执行的请求和许多正在等待的线程,而它们会影响 Web 服务器的吞吐量,从而对性能产生不利的影响。
为缓解这种情况,可以通过更改 Machine.config 配置文件的 processModel 节中的 MaxWorkerThreads 和 MaxIOThreads 属性,手动设置对进程中的线程数的限制。
注意 辅助线程是用来处理 ASP.NET 请求的,而 IO 线程则是用于为来自文件、数据库或 XML Web services 的数据提供服务的。
分配给进程模型属性的值是进程中每个 CPU 每类线程的最大数目。对于双处理器计算机,最大数是设置值的两倍。对于四处理器计算机,最大值是设置值的四倍。对于有一个或两个处理器的计算机,默认值就可以,但对于有两个以上处理器的计算机的性能,进程中有一百或两百个线程则弊大于利。因为额外的上下文交换导致操作系统将 CPU 周期花在维护线程而不是处理请求上,所以进程中有太多线程往往会降低服务器的速度。线程适当的数目最好通过应用程序的性能测试来确定。
-
对于广泛依赖外部资源的应用程序,请考虑在多处理器计算机上启用网络园艺 ASP.NET 进程模型帮助启用多处理器计算机上的可伸缩性,方法是将工作分发给多个进程(每个 CPU 一个),并且每个进程都将处理器关联设置为一个 CPU。此技术称为网络园艺。如果应用程序使用较慢的数据库服务器或调用具有外部依赖项的 COM 对象(这里只是提及两种可能性),则为您的应用程序启用网络园艺是有益的。但是,在决定对生产网站启用网络园艺之前,您应该测试应用程序在网络园中的执行情况。
-
禁用调试模式 在部署生产应用程序或进行任何性能测量之前,始终禁用调试模式。如果启用了调试模式,应用程序的性能可能受到影响。有关设置调试模式的语法信息,请参见编辑 ASP.NET 配置文件。
-
优化 Web 服务器计算机和特定应用程序的配置文件以符合您的需要 默认情况下,ASP.NET 配置被设置成启用最广泛的功能集并尽量适应最常见的情况。可更改某些默认配置设置以提高应用程序的性能,具体取决于您使用的功能。下面的列表包含您应考虑的配置设置:
-
仅对需要的应用程序启用身份验证 默认情况下,ASP.NET 应用程序的身份验证模式为 Windows 或集成的 NTLM。大多数情况下,最好仅对需要身份验证的应用程序在 Machine.config 文件中禁用身份验证,并在 Web.config 文件中启用身份验证。
-
根据适当的请求和响应编码设置来配置应用程序 ASP.NET 默认编码格式为 UTF-8。如果您的应用程序仅使用 ASCII 字符,请配置您的 ASCII 应用程序以获得稍许的性能提高。
-
考虑对应用程序禁用 AutoEventWireup 在 Machine.config 文件中将 AutoEventWireup 属性设置为 false,意味着页面不会将页事件绑定到基于名称匹配的方法(例如 Page_Load)。如果禁用 AutoEventWireup,页面将通过将事件连接留给您而不是自动执行它,获得稍许的性能提升。
如果想要处理页事件,可以使用两种策略之一。第一种策略是重写基类中的方法。例如,可以为页加载事件重写 Page 对象的 OnLoad 方法,而不是使用 Page_Load 方法。(务必调用基方法以确保引发所有事件。)第二种策略是使用 Visual Basic 中的 Handles 关键字或 C# 中的委托连接来绑定到事件。
-
从请求处理管线中移除不用的模块 默认情况下,服务器计算机的 Machine.config 文件中 HttpModules 节点的所有功能均保留为活动状态。根据应用程序所使用的功能,您可以从请求管线中移除不用的模块以获得稍许的性能提升。检查每个模块及其功能,并按您的需要自定义它。例如,如果您在应用程序中不使用会话状态和输出缓存,则可以从 HttpModules 列表中移除它们,以便请求在不执行其他有意义的处理时,不必调用这些模块。
-
编码实践
下列准则提供了编写有效代码的建议。
-
不要依赖代码中的异常 异常会大大地降低性能,所以您应该避免将它们用作控制正常程序流的方式。如果有可能检测到代码中可能导致异常的状态,请执行这种操作,而不要捕捉异常本身和处理该状态。常见的代码检测方案包括:检查 null,将一个值分配给将分析为数值的 String,或在应用数学运算前检查特定值。下面的示例演示可能导致异常的代码以及测试是否存在某种状态的代码。两者产生相同的结果。
C#// This is not recommended. try { result = 100 / num; } catch (Exception e) { result = 0; } // This is preferred. if (num != 0) result = 100 / num; else result = 0;
Visual Basic' This is not recommended. Try result = 100 / num Catch (e As Exception) result = 0 End Try ' This is preferred. If Not (num = 0) result = 100 / num Else result = 0 End If
-
适当地使用 自动内存管理 注意不要对每个请求使用过多内存(如在内存中存储大型对象或数据集),因为这样垃圾回收器将必须更频繁地做更多的工作。同样,当不再需要对象时,请不要在代码中保留不必要的对象引用,因为在它们仍然被引用的情况下,垃圾回收器将无法释放资源。
避免使用含 Finalize 方法的对象,因为它们在后面会导致更多的垃圾回收器工作。特别是在对 Finalize 的调用中永远不要释放资源,因为资源在垃圾回收器调用其 Finalize 方法之前可能一直消耗着内存。最后这个问题经常会对 Web 服务器环境的性能造成毁灭性的打击,因为在等待 Finalize 运行时,很容易耗尽某个给定资源的可用性。
有关垃圾回收器和自动内存管理的更多信息,请参见自动内存管理。
-
在托管代码中重写调用密集型的 COM 组件 .NET Framework 提供了一个简单的方法与传统的 COM 组件进行交互。其优点是可以在保留现有 COM 组件投资的同时利用 .NET 的功能。但是在某些情况下,保留旧组件的性能开销使得将组件迁移到托管代码是值得的。每一情况都是不一样的,决定是否需要迁移组件的最好方法是对网站运行性能测量。建议研究一下如何将经常调用的任何 COM 组件迁移到托管代码。
许多情况下不可能将旧式组件迁移到托管代码,特别是在最初迁移 Web 应用程序时。在这种情况下,最大的性能障碍之一是将数据从非托管环境封送到托管环境。因此,在交互操作中,请在任何一端执行尽可能多的任务,然后进行单个调用而不是一系列小调用。例如,公共语言运行库中的所有字符串都是 Unicode 的,所以应在调用托管代码之前将组件中的所有字符串转换成 Unicode 格式。
一旦处理完任何 COM 对象或本机资源就释放它们。这样,其他请求就能够使用它们,并且最大限度地减少了因稍后请求垃圾回收器释放它们所引起的性能问题。
-
避免单线程单元 (STA) COM 组件 默认情况下,ASP.NET 不允许 STA COM 组件在页内运行。若要运行它们,必须在 .aspx 文件内将 ASPCompat=true 属性包含在 @ Page 指令中。这样就将页执行用的线程池切换到 STA 线程池,而且使 HttpContext 和其他内置对象可用于 COM 对象。避免使用 STA COM 组件是一种性能优化,因为它避免了将多线程单元 (MTA) 封送到 STA 线程的任何调用。
如果必须使用 STA COM 组件,则应避免在执行期间进行大量调用,并尝试在每次调用期间发送尽可能多的信息。另外,避免在构造页面期间创建 STA COM 组件。例如在下面的代码中,在页面构造时将实例化由某个线程创建的 SampleSTAComponent,而该线程并不是运行页面的 STA 线程。这可能对性能有不利影响,因为要构造页面就需要在 MTA 和 STA 线程之间进行封送处理。
<%@ Page Language="VB" ASPCompat="true" %> <script runat=server> Dim myComp as new SampleSTAComponent() Public Sub Page_Load() myComp.Name = "Sample" End Sub </script> <html> <% Response.Write(Server.HtmlEncode(myComp.SayHello)) %> </html>
首选机制是推迟对象的创建,直到在 STA 线程下执行上述代码,如下面的例子所示。
<%@ Page Language="VB" ASPCompat="true" %> <script runat=server> Dim myComp Public Sub Page_Load() myComp = new SampleSTAComponent() myComp.Name = "Sample" End Sub </script> <html> <% Response.Write(Server.HtmlEncode(myComp.SayHello)) %> </html>
推荐的做法是仅在需要时或者在 Page_Load 方法中构造 COM 组件和外部资源。
永远不要将 STA COM 组件存储在可以由构造它们的线程以外的其他线程访问的共享资源(如缓存或会话状态)里。即使 STA 线程调用 STA COM 组件,也只有构造此 STA COM 组件的线程能够为该调用服务,而这要求封送处理对创建者线程的调用。此封送处理可能产生重大的性能损失和可伸缩性问题。在这种情况下,请考虑使 COM 组件成为 MTA COM 组件或在托管代码中重写该组件。