用 ASP.NET 编写 Web 应用程序其轻松程度令人难以置信。它是如此的容易,以至于许多开发人员不用花费多少时间来构筑其应用便能获得非常好的性能。在本文中,我将给出10个编写高性能 Web 应用的技巧。我的评论不仅仅局限与 ASP.NET 应用,因为它们只是 Web 应用的一个子集。本文也不是 Web 应用性能调整的权威指南――这方面的内容可以写成一本书。相反,本文可以被视作一个好的起点。
在废寝忘食地工作之前,我常常要去攀岩。在攀岩之前,我总是要看一下指南手册中的线路并阅读以前来此一游的人留下的建议和忠告。但是,不管指南手册有多磨好,在尝试一次特定的具有挑战性的攀爬之前,你都必须付诸实际的行动。同样,在你面临解决的性能问题或者营运一个高吞吐量的站点之前,你只能想方设法编写高性能 Web 应用程序。
我们个人经验来自在微软 ASP.NET 团队从事底层架构程序经理,运行和管理 www.asp.net ,并协助架构 Community Server 过程中的经历,Community Server 是几个有名的 ASP.NET 应用程序的下一个版本(它将 ASP.NET Forums,.Text 和 nGallery 整合到一个平台)。我确信这些帮助过我的技巧也会对你有所裨益。
你应该考虑将应用程序分离成几个逻辑层。你可能听说过术语3-层(或n-层)物理体系结构。它们通常是跨进程和/或硬件对功能进行物理划分的规定的体系结构模式。当系统需要伸缩时,更多的硬件能被添加。然而,总是应该避免与进程和机器忙碌程度相关的性能问题。所以,不管什么时候,只要可能,都要在相同的应用中一起运行 ASP.NET 页面及其相关的组件。
由于代码和层之间的边界分离,使用 Web 服务或远程调用将降低20%以上的性能。
数据层则稍微有些不同,因为数据库通常都用专门的硬件。但是,数据库的处理成本仍然很高,因此最优化代码时,数据层的性能应该是首当其充要关注的地方。
在着手解决你的应用程序的性能问题之前,一定要剖析应用程序,确定问题之所在。获取关键的性能计数器值(如实现垃圾收集所花时间之百分比的性能计数器的值)对于查找应用程序在何处最耗时也是非常重要的。凭借直觉常常也能找到耗时所在。
本文所描述的性能改进有两种类型:大型优化,如使用 ASP.NET Cache,以及不断重复进行的微型优化。这些微型优化有时很有意思。对代码的小小改动便会引起很大的动静,产生成千次的调用。对于大型优化,你可能会看到整体性能的大跳跃。而对微型优化,给定请求可能只是毫秒级的调整,但按每天的请求总数计算,其结果的改进可能是巨大的。
数据层的性能
当调整某个应用程序的性能时,有一个简单的试金石,你可以用它按先后次序:检查代码是否存取数据库?如果是,多长时间存取一次?注意相同的测试也可以被应用于使用 Web 服务或远程调用的代码,但我们本文中不涉及这方面内容。
如果在特定的代码流程中必须具有对数据库的请求以及要考察其它方面,如:想对字符串处理进行优先优化,那么暂且把它放一放,先按照上面定好的优先次序来做。除非你有异乎寻常的性能问题,否则你的时间应该用在尝试最优化与数据库的连接所花的时间,返回的数据量以及多长时间往返一次和数据库的通讯上。
有了这些概括信息,下面就让我们来看看能帮助你改善应用程序性能的十个技巧。我将从能获得最显著效果的改变开始。
技巧 1 ―― 返回多个结果集
复审你的数据库代码,看看是否有多于一次的对数据库的访问请求。这样每次往返数据库都降低你的应用程序能处理的每秒请求数。通过在单个数据库请求中返回多结果集,你能降低与数据库通信的总体时间。同时你也将使系统更具伸缩性,因为你减少了数据库服务器处理请求的负担。
虽然你可以用动态 SQL 返回多结果集,我更喜欢使用存储过过程。是否将业务逻辑驻留在存储过程当中是个有待争论的问题,但我认为,如果存储过程中的逻辑能约束返回的数据(降低数据集的尺寸,在网络上传输的时间以及逻辑层不必过虑数据),这是一件好事情。
使用 SqlCommand 命令实例及其 ExecuteReader 方法来处理强类型的各个业务类,你通过调用 NextResult 可以向前移动结果集指针。Figure 1 示范了处理几个带类型的 ArrayLists 例子会话。从数据库只返回你需要的数据还会降低服务器上内存的分配。
技巧 2 ―― 分页数据存取
ASP.NET DataGrid 提供了非常好的能力:数据分页支持。当启用 DataGrid 中的分页功能,则每次只显示固定数量的记录。此外,分页用户界面也会显示在 DataGrid 底部用于导航记录。分页用户界面允许你向前向后导航所显示的记录,一次显示固定数量的记录。
有一个美中不足的是用 DataGrid 分页需要将所有数据邦定到此栅格控件(gird)。例如,你的数据层必须返回所有数据,然后 DataGrid 将根据当前页过滤掉所有显示的记录。当你通过 DataGrid 进行分页时,如果有 100,000 条记录被返回,那么每个请求有 99,975 条记录将被废弃掉(假设页尺寸为 25)。当记录数不断增加,此应用程序的性能便会遭受痛苦,因为每次请求所要发送的数据会越来越多。
编写较好的分页代码的一个好的方法是用存储过程。Figure 2 示范了一个用 Northwind 数据库中 Orders 表通过存储过程分页的例子。很简单,只要你在页面中传递索引以及页尺寸即可。相应的结果集先被计算然后被返回。
在 Community Server 中,我们编写了几个分页控件来完成数据分页。你将会看到,我使用了技巧 1 中讨论的思路,从一个存储过程中返回连个结果集:总记录数和请求的数据。
返回的总记录数依赖于所执行的查询不同而不同。例如,某个 WHERE 子句可被用于约束返回的数据。为了计算在分页用户界面显示的总页数,返回的总记录数必须是已知的。例如,如果有 1,000,000 条记录,用一个 WHERE 子句对之过滤后为 1,000 条记录,则分页逻辑必须要知道总记录数以便在分页用户界面中正确呈现。
技巧 3 ―― 连接池
建立 Web 应用程序与 SQL Server 之间的 TCP 连接是一项昂贵的操作。微软的开发人员利用连接池技术已经有好长一段时间了,这个技术使他们能重用到数据库的连接。而不是每次请求都建立新的 TCP 连接,新连接仅在连接池中得不到连接时才建立。当连接被关闭时,它被返回到连接池中,在那里它仍然保持与数据库的连接,与完全断开 TCP 连接相反。
当然,你需要提防泄漏的连接。当你处理完毕,一定要关闭连接。重申一次:不管人们怎么吹嘘微软 .NET 框架中的垃圾收集特性,每当你处理完毕,一定要显式地调用连接对象的 Close 或 Dispose 方法。不要指望公共语言运行时(CLR)来为你定时清除和关闭连接。CLR 最终将销毁类并强行关闭连接,但你无法保证该对象的垃圾收集届时会起作用。
为了充分用好连接池,有几条规则必须了然于心。首先,打开连接,进行处理,然后关闭连接。宁愿每个请求的连接打开和关闭多次,也不要保持连接打开状态以及在不同的方法间将它传来传去。其次,使用相同的连接串(如果你使用集成身份检查,那么也要用相同的线程身份)。如果你不用相同的连接串,例如,根据登录用户来定制连接串,你将无法得到连接池所提供的相同的最优化值。当模拟大用户量情形时,如果你使用集成身份检查,那么你的连接池将效力大减。.NET CLR 数据性能计数器在试图跟踪任何与连接池有关的性能问题时是非常有用的。
不管什么时候,只要你的应用程序连接到运行在其它进程中的资源,比如某个数据库,你都应该针对连接到资源所耗时间,发送和接收数据所耗时间以及往返次数进行优化。为了实现较好的性能,应该首当其充优化应用程序中任何种类的忙碌进程。
应用层包含到数据层的连接以及将数据转换成有意义的类实例和业务处理的逻辑。以 Community Server 为例,你要在其中处理 Forums 和 Threads 集合;以及应用许可这样的业务规则;尤其重要的是缓冲(Caching)逻辑也实现其中。
技巧 4 ―― ASP.NET Cache API
在编写代码之前要做的头等大事之一是最大限度地构建应用层并发掘 ASP.NET 的 Cache 特性。
如果你的组件在 ASP.NET 应用程序内运行,那么你只需要在应用程序工程中引用 System.Web.dll 即可。当你需要访问 Cache 时,用 HttpRuntime.Cache 属性(相同的对象也可以通过 Page.Cache 和 HttpContext.Cache 访问)。
缓冲数据有几个准则。首先,如果数据能被使用多次,缓冲是个好的后选方案。其次,如果数据对给定请求或用户是一般的数据而非专用数据,那么最好是选择缓冲。如果数据用户或请求专用,如果需要保存期很长但可能不被经常使用,那么仍然要用缓冲。第三,常常被忽略的一个准则是有时缓冲太多的东西。一般来说,在x86机器上,为了降低内存不足错误的几率,运行某个进程不要超过800MB私有字节。因此,缓冲应该有个上限。换句话说,你也许能重用某个计算的结果,但如果该计算有10个参数,你可能试图针对10个置换进行缓冲,这样做可能会给你带来麻烦。ASP.NET 提供的最常见的容错是由覆盖缓冲导致的内存不足错误,尤其是大型数据集。
Cache 有几个重要特性是必须要了解的。第一个是 Cache 实现了最近最少使用(least-recently-used)算法,允许 ASP.NET 强制 Cache 清除操作 ―― 如果可用内存下降到低水平 ―― 则自动从 Cache 中删除不使用的项目。第二个是 Cache 支持依赖性到期特性,它能强制包括时间,键值,文件失效。时间常常被使用,但 ASP.NET 2.0 引入了具有更强大的失效类型:数据库缓冲失效。也就是当数据库中的数据改变时,缓冲中的条目会自动删除。有关