Len Cardinal - Microsoft Consulting Services 高级顾问
George V. Reilly - Microsoft IIS Performance 主管
更新时间:2000年4月
根据 Nancy Cluts 的文章(英文)改写
Nancy Cluts - 开发人员技术工程师
Microsoft Corporation
摘要:本文提供了优化 ASP 应用程序和 VBScript 的技巧。
目录
简介
技巧 1:在 Web 服务器上缓存常用数据
技巧 2:在 Application 或 Session 对象中缓存常用数据
技巧 3:在 Web 服务器磁盘上缓存数据和 HTML
技巧 4:避免在 Application 或 Session 对象中缓存非灵活组件
技巧 5:不要在 Application 或 Session 对象中缓存数据库连接
技巧 6:妙用 Session 对象
技巧 7:在 COM 对象中封装代码
技巧 8:晚点获取资源,早点释放资源
技巧 9:进程外的执行将牺牲可靠性
技巧 10:显式使用选项
技巧 11:在子例程和函数中使用局部变量
技巧 12:将常用数据复制到脚本变量
技巧 13:避免重新定义数组
技巧 14:使用响应缓冲
技巧 15:批处理内嵌脚本和 Response.Write 语句
技巧 16:在开始长时间的任务之前先使用 Response.IsClientConnected
技巧 17:使用 <OBJECT> 标记实例化对象
技巧 18:使用 ADO 对象和其他组件的 TypeLib 绑定
技巧 19:利用浏览器的验证能力
技巧 20:在循环中避免字符串串联
技巧 21:启用浏览器和代理缓存
技巧 22:尽可能使用 Server.Transfer 替代 Response.Redirect
技巧 23:在目录 URL 尾部加斜线
技巧 24:避免使用服务器变量
技巧 25:升级为最新的和最好的版本
技巧 26:调整 Web 服务器
技巧 27:进行性能测试
技巧 28:读取资源链接
--------------------------------------------------------------------------------
简介
性能是一个特性。您需要预先设计性能,或是在日后重新编写应用程序。换句话说,什么是最大限度优化 Active Server Pages (ASP) 应用程序性能的好策略?
本文为优化 ASP 应用程序和“Visual Basic(R) 脚本编辑器 (VBScript)”提供了许多技巧。对许多陷阱和缺陷进行了讨论。本文所列的建议均在 http://www.microsoft.com 及其他站点上进行了测试,而且工作正常。本文假定您对 ASP 开发有基本的理解,包括对 VBScript 和/或 JScript、ASP Application、ASP Session 和其他 ASP 内部对象(请求、响应和服务器)。
ASP 的性能,通常不止取决于 ASP 代码本身。我们并不想在一篇文章中囊括所有的至理名言,只在最后列出与性能相关的资源。这些链接包括 ASP 和非 ASP 主题,包括“ActiveX(R) 数据对象 (ADO)”、“部件对象模型 (COM)”、数据库和“Internet 信息服务器 (IIS)”配置。这些是我们喜欢的链接 - 务请关注它们。
技巧 1:在 Web 服务器上缓存常用数据
典型的 ASP 页从后端数据库检索数据,然后将结果转换为超文本标记语言 (HTML)。无论数据库的速度如何,从内存检索数据要比从后端数据库检索数据快得多。从本地硬盘读取数据通常也要比从数据库检索数据快得多。因此,通常可以通过在 Web 服务器(在内存或磁盘)上缓存数据来改善性能。
缓存是典型的空间与时间的折衷。如果恰当地缓存数据,您将看到性能会有惊人的提高。为使缓存发挥效力,它必须保持经常重用的数据,而且重新计算这些数据的代价是昂贵的或比较昂贵的。如果缓存充满了垃圾数据,则是对存储器的浪费。
不经常变化的数据也是缓存的候选数据,因为您无须担心数据与数据库的同步问题。组合框、引用表、DHTML 碎片、可扩展标记语言 (XML) 字符串、菜单项和站点配置变量(包括数据源名称 (DSN)、Internet 协议 (IP) 地址和 Web 路径)都是缓存的候选数据。注意,您可以缓存数据的表示而不是数据本身。如果 ASP 页不经常更改,而且缓存的成本也非常高(例如,整个产品目录),请考虑预先生成 HTML,而不是在每次请求时重新绘制。
数据应缓存在何处,有哪些缓存策略?数据经常缓存在 Web 服务器内存或 Web 服务器磁盘上。下面两个技巧讨论这些选项。
技巧 2:在 Application 或 Session 对象中缓存常用数据
ASP Application 和 Session 对象为在内存中缓存数据提供了方便的容器。既可以将数据赋予 Application 对象,也可将数据赋予 Session 对象,这些数据在 HTTP 调用中将保留在内存中。Session 数据按用户存储,而 Application 数据在所有用户间共享。
何时将数据载入 Application 或 Session?通常,在 Application 或 Session 启动时加载数据。要在 Application 或 Session 启动时加载数据,请在下面两函数中添加相应的代码:Application_OnStart() 或 Session_OnStart()。这两个函数应该位于 Global.asa;如果没有,可以添加这些函数。也可以在第一次需要数据时加载数据。要进行上述操作,请在 ASP 页中添加一些代码(或编写可重用的脚本函数),这些代码检查数据是否存在,并在数据不存在时加载数据。这是称为迟缓计算的经典性能技术的例子 - 在您的确需要它之前,不进行计算。请看例子:
<%
Function GetEmploymentStatusList
Dim d
d = Application("EmploymentStatusList")
If d = "" Then
' FetchEmploymentStatusList 函数(不显示)
' 从 DB 中取出数据,返回数组
d = FetchEmploymentStatusList()
Application("EmploymentStatusList") = d
End If
GetEmploymentStatusList = d
End Function
%>
可以为每一块所需的数据编写类似的函数。
数据应该以什么格式存储?任何变量类型均可存储,因为所有脚本变量是各不相同的。例如,可以存储字符串、整型或数组。通常,您将以这些变量类型之一存储 ADO 记录集的内容。若要获取 ADO 记录集衍生的数据,可以手工将数据复制到 VBScript 变量中,每次一个字段。使用一个 ADO 记录集保留函数 GetRows()、GetString() 或 Save() (ADO 2.5),会更快更简便。完整而详细的内容已超出了本文的范围。下面的演示函数使用了 GetRows() 来返回记录集数据的数组:
' 取记录集,以数组返回
Function FetchEmploymentStatusList
Dim rs
Set rs = CreateObject("ADODB.Recordset")
rs.Open "select StatusName, StatusID from EmployeeStatus", _
"dsn=employees;uid=sa;pwd=;"
FetchEmploymentStatusList = rs.GetRows() ' 以数组返回数据
rs.Close
Set rs = Nothing
End Function
对上面示例的进一步改进应当是缓存该列表的 HTML,而不是缓存数组。下面是一个简单的范例:
' 取记录集,以“HTML 选项”列表返回
Function FetchEmploymentStatusList
Dim rs, fldName, s
Set rs = CreateObject("ADODB.Recordset")
rs.Open "select StatusName, StatusID from EmployeeStatus", _
"dsn=employees;uid=sa;pwd=;"
s = "<select name=""EmploymentStatus">" & vbCrLf
Set fldName = rs.Fields("StatusName") ' ADO 字段绑定
Do Until rs.EOF
' 下面一行违背了不要进行字符串连接,
' 但这是可以的,因为我们正在建立高速缓存
s = s & " <option>" & fldName & "</option>" & vbCrLf
rs.MoveNext
Loop
s = s & "</select>" & vbCrLf
rs.Close
Set rs = Nothing ' 参见尽早释放
FetchEmploymentStatusList = s ' 以字符串返回数据
End Function
在正常的情况下,可以在 Application 或 Session 作用域中缓存 ADO 记录集本身。有两个警告:
ADO 必须为标记的自由线程
必须使用断开连接的记录集。
如果不能保证满足这两个要求,请不要缓存 ADO 记录集。在下面的非灵活组件和不要缓存连接技巧中,我们将讨论在 Application 或 Session 作用域中存储 COM 对象的危险。
如果在 Application 或 Session 作用域中存储数据,这些数据将一直保留在那儿,直到在程序中改变它、Session 过期或 Web 应用程序重新启动时为止。数据需要更新如何处理?若要用手工强制更新应用程序数据,可以调用只允许管理员访问的数据更新 ASP 页。另外,还可以通过函数,周期地自动刷新数据。下面的示例存储带缓存数据的时间戳,在指定时间间隔后刷新数据。
<%
' 未显示错误处理...
Const UPDATE_INTERVAL = 300 ' 刷新时间间隔,以秒计
' 函数返回雇佣状态列表
Function GetEmploymentStatusList
UpdateEmploymentStatus
GetEmploymentStatusList = Application("EmploymentStatusList")
End Function
' 定期更新缓存的数据
Sub UpdateEmploymentStatusList
Dim d, strLastUpdate
strLastUpdate = Application("LastUpdate")
If (strLastUpdate = "") Or _
(UPDATE_INTERVAL DateDiff("s", strLastUpdate, Now)) Then
' 注意:此处可能有两个或多个调用。这是可以的,只不过
' 产生几个不必要的取指令罢了(就此有一个工作区)
' FetchEmploymentStatusList 函数(不显示)
' 从 DB 中取数据,返回一个数组
d = FetchEmploymentStatusList()
' 更新 Application 对象。用 Application.Lock()
' 来确保一致的数据
Application.Lock
Application("EmploymentStatusList") = d
Application("LastUpdate") = CStr(Now)
Application.Unlock
End If
End Sub
其他示例,请参阅具有 Application 数据的最快列表框(英文)。
请注意,在 Session 或 Application 对象中缓存大型数组并非上策。在访问数组元素之前,脚本语言的语法要求建立整个数组的临时副本。例如,如果在 Application 对象中缓存了将美国邮政编码映射到本地气象站的字符串数组,该字符串数组有 100,000 个元素,ASP 在找出一个字符串之前,必须将所有 100,000 个气象站复制到临时数组中。在这种情况下,建立带自定义方法的自定义组件,来存储气象站 - 或使用一个字典组件,也许更好。
请不要在倒洗澡水时把孩子一同倒掉,对这种观点的一个新的注解是:数组提供了对内存中相邻关键-数据对的快速查找和存储。索引字典比索引数组要慢。您应该根据具体情况选择能够提供最佳性能的数据结构。
技巧 3:在 Web 服务器磁盘上缓存数据和 HTML
有时,数据过多不能在内存中进行缓存。“过多”是一种定性的判断;它取决于打算消耗的内存量,还有缓存项的数量和这些项的检索频率。总之,如果有过多的数据要在内存中缓存,请考虑以文本或 XML 文件的形式,在 Web 服务器的硬盘上缓存数据。可以将在磁盘上缓存数据和在内存中缓存数据组合起来,为站点建立最优的缓存策略。
注意,在度量单个 ASP 页的性能时,在磁盘上检索数据不一定比从数据库中检索数据快。但是,缓存减轻了数据库和网络的负荷。在高负荷情况下,这将明显提高总体通信量。在查询成本很高时缓存查询的结果,缓存便非常有效,例如多表联合或复杂的存储过程,或缓存大型的结果集。按照惯例,测试竞争方案。
ASP 和 COM 提供了几种构建磁盘缓存方案的工具。ADO 记录集的 Save() 和 Open() 函数,保存和加载磁盘上的记录集。您可以使用这些方法重写上面 Application 数据缓存技巧中的范例代码,用 Save() 文件替换向 Application 对象写入数据的代码。
还有其他一些处理文件的组件:
Scripting.FileSystemObject 使您能够创建、读取和写入文件。
MSXML 是随 Internet Explorer 提供的 Microsoft(R) XML 解析器,它支持保存和加载 XML 文档。
LookupTable 对象(在 MSN 上使用的范例)是从磁盘加载简单列表的良好选择。
最后,请考虑在磁盘上缓存数据的表示,而不是数据本身。预制的 HTML 可以作为 .htm 或 .asp 文件存储在磁盘上;超级链接可以直接指向这些文件。可以使用商业工具,如 XBuilder 或 Microsoft(R) SQL Server 的 Internet 发行功能来自动化 HTML 生成过程。另外,可以将 HTML 片段 #include 到 .asp 文件。还可以使用 FileSystemObject 从磁盘读取 HTML 文件或使用 XML 进行早期调整(英文)。
技巧 4:避免在 Application 或 Session 对象中缓存非灵活组件
虽然在 Application 或 Session 对象中缓存数据是个好主意,但是缓存 COM 对象可能有严重缺陷。将常用 COM 对象嵌入 Application 或 Session 对象通常具有吸引力。遗憾的是,很多 COM 对象,包括用 Visual Basic 6.0 或更早版本编写的 COM 对象,在 Application 或 Session 对象中存储时将导致严重的瓶颈。
特别是任何非灵活组件,在 Session 或 Application 对象中缓存时将导致性能瓶颈。灵活组件是标记为 ThreadingModel=Both 的组件(它聚集了自由线程汇集器 (FTM))或标记为 ThreadingModel=Neutral 的组件(Windows(R) 2000 和 COM+ 中新增的“中性”模型。)下列组件是非灵活的:
自由线程组件(除非它们聚集了 FTM)。
单元线程组件。
单线程组件。
已配置组件(Microsoft Transaction Server (MTS)/COM+ 库和服务器包/应用程序)为非灵活组件,除非它们是“中性”线程的。单元线程组件和其他非灵活组件最适于在页作用域工作(也就是说,它们在单个 ASP 页上创建和销毁)。
在 IIS 4.0 中,标记为 ThreadingModel=Both 的组件被视为灵活的。在 IIS 5.0 中,这已经不够了。组件不仅必须标记为 Both,而且还必须聚集 FTM。灵活性文章说明了如何使得用“活动模板库”编写的 C++ 组件聚集 FTM。请注意,如果组件缓存接口指针,这些指针本身必须为灵活的、或者必须存储在“COM 全局接口表 (GIT)”中。如果不能重新编译 Both 线程组件,使它聚集 FTM,则可以将该组件标记为 ThreadingModel=Neutral。另外,如果不希望 IIS 进行灵活性检查(这样,希望非灵活组件能够存储在 Application 或 Session 作用域中),可以在 metabase 中设置 AspTrackThreadingModel 为 True。不主张更改 AspTrackThreadingModel。
如果试图在 Application 对象中存储用 Server.CreateObject 创建的非灵活组件,IIS 5.0 将产生错误。可以通过在 Global.asa 中使用 <object runat=server scope=application ...> 解决该问题,但是不主张这样做,因为这将导致汇集和串行化,说明如下。
如果缓存非灵活组件,会发生什么错误呢?缓存在 Session 对象中的非灵活组件,将把会话“锁定”到某个 ASP 工作器线程。ASP 维护着一个工作器线程池,它向请求提供服务。通常,新的请求由第一个可用的工作器线程来处理。如果 Session 被锁定到某个线程,则该请求将不得不等待它所关联的线程变为可用。打个比方:您进入一个超市,挑选了一些食品,然后在第 3 号收款台交款。从这以后,每当您在这个超市购买食品,都不得不始终在第 3 号收款台交款,即使是在其他收款台人少或没人时。
将非灵活组件存储在 Applicaton 作用域甚至会对性能产生更严重的影响。ASP 将不得不创建专用的线程来运行非灵活的、Applicaton 作用域内的组件。这将导致两种后果:所有调用不得不被汇集到该线程,而且所有调用被串行化。汇集意味着:参数不得不存储在内存的共享区;对该专用线程执行昂贵的上下文切换;组件的方法被执行;结果汇集到共享区域;以及经过另一个昂贵的上下文切换,使控制权返回原来的线程。串行化意味着所有方法必须一个挨一个地运行(同一时刻只能运行一个方法)。两个不同的 ASP 工作器线程不可能同时执行共享组件上的方法。这将扼杀并行机制,尤其是在多处理器计算机上。更坏的是,所有非灵活的、Application 作用域内的组件都将共享一个线程(“Host STA”),所以串行化的影响更加严重。
是否感到困惑?下面我们提出几个通用规则。如果您正在用 Visual Basic (6.0) 或更早版本编写对象,请不要将它们缓存在 Application 或 Session 对象中。如果您不知道对象的线程模型,就不要缓存它。不要缓存非灵活对象,而应当在每页上创建并释放它们。对象将直接运行在 ASP 工作器线程上,这样,将不会发生汇集或串行化。如果 COM 对象正运行在 IIS 框中,而且如果它们没有花很长时间来初始化和取消,性能将是足够的。注意,不要用该方法使用单线程对象。小心:VB 可以创建单线程的对象!如果您必须以该方式使用单线程的对象(如 Microsoft Excel 电子表格),则不要期望有很高的吞吐量。
当 ADO 被标记为自由线程时,则缓存 ADO 记录集是安全的。要将 ADO 标记为自由线程,请使用 Makfre15.bat 文件,该文件通常位于如下目录中:\\Program Files\Common\System\ADO。
警告: 如果您正在用 Microsoft Access 作为数据库,则不应当将 ADO 标记为自由线程。通常,ADO 记录集还必须是断开连接的,如果您不能控制站点的 ADO 配置(例如,您是独立的软件厂商 [ISV],将 Web 应用程序卖给客户,然后由他们来管理他们自己的配置),那么不缓存记录集可能会更好。
词典组件也是灵活对象。LookupTable 从数据文件加载它的数据,并且它对组合框数据和配置信息是有用的。来自 Duwamish Books 的 PageCache 对象提供了目录语义,和 Caprock Dictionary 的表现一样。这些对象或它们的派生对象可以构成有效缓存策略的基础。注意,Scripting.Dictionary 对象不是灵活的,所以不应当存储在 Application 或 Session 作用域。
技巧 5:不要在 Application 或 Session 对象中缓存数据库连接
缓存 ADO 连接通常是不好的策略。如果一个 Connection 对象存储在 Application 中,并在所有页上使用,那么所有页将竞争使用该连接。如果 Connection 对象存储在 ASP Session 对象中,那么将为每个用户创建数据库连接。这将连接池的好处毁于一旦,并对 Web 服务器和数据库产生不必要的压力。
取代缓存数据库连接的方法是,在每个使用 ADO 的 ASP 页上创建并取消 ADO 对象。这是个有效的方法,因为 IIS 具有内置的数据库连接池。更准确的说,IIS 自动启用 OLEDB 和 ODBC 连接池。这确保了创建并取消每个页上的连接将是有效的。
由于被连接的记录集中存储有对数据库连接的引用,所以,不应当在 Application 或 Session 对象中缓存被连接的记录集。但是,可以安全地缓存断开连接的记录集,因为它不包含对其数据连接的引用。要断开记录集的连接,请执行如下两个步骤:
Set rs = Server.CreateObject("ADODB.RecordSet")
rs.CursorLocation = adUseClient ' 第 1 步
' 植入带数据的记录集
rs.Open strQuery, strProv
' 现在断开记录集同数据提供者和数据源的连接
rs.ActiveConnection = Nothing ' 第 2 步
有关连接池的详细信息,请参阅 ADO 和 SQL Server(英文)引用。
……