改进性能和样式的 25+ asp 技巧 ------------------------------- Len Cardinal - Microsoft Consulting Services 高级顾问 George V. Reilly - Microsoft IIS Performance 主管 更新时间:2000年4月 根据 Nancy Cluts 的文章(英文)改写 Nancy Cluts - 开发人员技术工程师 Microsoft Corporation
性能是一个特性。您需要预先设计性能,或是在日后重新编写应用程序。换句话说,什么是最大限度优化 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 服务器(在内存或磁盘)上缓存数据来改善性能。
<% Function GetEmploymentStatusList Dim d d = Application("EmploymentStatusList") If d = "" Then ' FetchEmploymentStatusList 函数(不显示) ' 从 DB 中取出数据,返回数组 d = FetchEmploymentStatusList() Application("EmploymentStatusList") = d End If GetEmploymentStatusList = d End Function %>
' 取记录集,以数组返回 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 作用域中存储数据,这些数据将一直保留在那儿,直到在程序中改变它、Session 过期或 Web 应用程序重新启动时为止。数据需要更新如何处理?若要用手工强制更新应用程序数据,可以调用只允许管理员访问的数据更新 ASP 页。另外,还可以通过函数,周期地自动刷新数据。下面的示例存储带缓存数据的时间戳,在指定时间间隔后刷新数据。
' 函数返回雇佣状态列表 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
有时,数据过多不能在内存中进行缓存。“过多”是一种定性的判断;它取决于打算消耗的内存量,还有缓存项的数量和这些项的检索频率。总之,如果有过多的数据要在内存中缓存,请考虑以文本或 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 最大的问题不是性能而是可伸缩性。Session 不能跨越 Web 服务器;一旦在一个服务器上创建了 Session,它的数据就保持在那里。这意味着,如果您在 Web 领域中使用 Sessions,您将不得不为每个用户的请求设计一种策略,以便始终将这些请求引向用户的 Session 所在的服务器。这被称为将用户“粘”到 Web 服务器上。术语“粘性会话”即来源于此。由于 Session 没有保持到磁盘上,所以,当 Web 服务器崩溃时,被“粘住”的用户将丢失他们的 Sessions 状态。
用于实施粘性会话的策略包括硬件和软件解决方案。如 Windows 2000 Advanced Server 中的网络负载平衡解决方案和 Cisco 公司的“本地指向器”解决方案可以实施粘性会话,但以牺牲一些可伸缩性为代价。这些解决方案并不完美。我们不主张您现在全盘推翻您的软件解决方案(我们过去常用 ISAPI 筛选器和 URL 矫直对方案进行检查)。
Application 对象也不能跨越服务器;如果您需要在 Web 领域内共享并更新 Application 数据,则需要使用后端数据库。但只读的 Application 数据在 Web 领域中仍然有用。
如果只是为了增加正常运行时间(用于处理故障转移和服务器维护),大多数执行重要任务的站点将需要部署至少两台 Web 服务器。所以,在设计执行重要任务的应用程序时,您将需要实施“粘性会话”,或者简单地避开 Sessions 以及其他任何在单个 Web 服务器上存储用户状态的状态管理技术。
如果当前没有使用 Sessions,请确保将它们关闭。可以通过“Internet 服务管理器”(请参阅 ISM 文档)来为应用程序执行该操作。如果决定使用 Sessions,可以采取几个方法来将对性能的影响降低到最小。
可以将不需要 Sessions 的内容(如“帮助”屏幕、访问者区域等)移动到关闭了 Sessions 的、单独的 ASP 应用程序中。可以逐页提示 ASP:在给定的页中您不需要 Session 对象;使用位于 ASP 页顶端的如下指令:
如果您有很多 VBScript 或 JScript,那么您可以通过把代码移动到已编译的 COM 对象来经常改进它们的性能。已编译的代码通常比被解释代码运行得更快。已编译的 COM 对象可以通过“早期绑定”访问其他 COM 对象,这种调用 COM 对象方法的手段,比脚本所使用的“后期绑定”更有效。
将代码封装在 COM 对象种有如下好处(超越性能):
COM 对象是将表达逻辑与业务逻辑分隔开来的好办法。 COM 对象启用了代码重用。 很多开发商发现,用 VB、C++ 或 Visual J++ 书写的代码,比 ASP 更容易调试。 COM 对象有一些缺点,包括初始开发时间以及需要不同的编程技巧。需要警告您的是,封装“少”量的 ASP 可能会导致性能降低,而不是提高。通常,在少量 ASP 代码封装到 COM 对象时出现这样的情况。这时候,创建和调用 COM 对象的开销,超过了已编译代码的好处。至于 ASP 脚本和 COM 对象代码怎样合并才能产生最佳性能还有待测试。注意,与 Windows NT(R) 4.0/IIS 4.0 相比,Microsoft 已经在 Windows 2000/IIS 5.0 中极大地提高了脚本和 ADO 性能。这样,已编译代码对 ASP 代码的性能优势已经随着 IIS 5.0 的引入而降低。
有关在 ASP 中使用 COM 对象的优缺点的更多讨论,请参阅 ASP 组件准则和用 COM 和 Microsoft Visual Basic 6.0 对分布式应用程序进行编程(英文)。如果您的确部署了 COM 组件,要对它们进行强度测试是非常重要的。实际上,所有 ASP 应用程序都应当作为正式过程进行强度测试。
技巧 8:晚点获取资源,早点释放资源
这是个小技巧。通常,最好晚点获取资源而要早点释放资源。这些资源包括 COM 对象、文件句柄和其他资源。
ASP 和 MTS/COM+ 都有允许您以可靠性换取性能的配置选项。当建立和部署应用程序时,应当理解这种交换。
ASP 选项
ASP 应用程序可以配置为以三种方式之一运行。在 IIS 5.0 中引入了术语“隔离级”来描述这些选项。三个隔离级值分别是低、中和高:
低级隔离。该隔离级在所有版本的 IIS 中受到支持,并且是最快的。它在主 IIS 进程 Inetinfo.exe 中执行 ASP。如果 ASP 应用程序崩溃,则 IIS 也将崩溃。(要在 IIS 4.0 下重新启动 IIS,Web 站点管理员需要使用工具,如 InetMon,来监视站点,如果服务器失败,将运行批处理文件来重新启动服务器。而 IIS 5.0 则引入了可靠的重新启动,它将自动重新启动失败的服务器。) 中级隔离。IIS 5.0 引入了这个新隔离级,它称为进程外的,这是因为 ASP 运行在 IIS 进程之外。在中级隔离中,所有被配置按“中级”运行的 ASP 应用程序,将共享单个进程空间。这将减少在一个服务器上运行多个进程外的 ASP 应用程序所需的进程数。中级是 IIS 5.0 中默认的隔离级。 高级隔离。在 IIS 4.0 和 IIS 5.0 中受到支持,高级隔离也是进程外的。如果 ASP 崩溃,则 Web 服务器并不崩溃。ASP 应用程序将在下一个 ASP 请求时自动重新启动。使用高级隔离,每个被配置为按高级运行的 ASP 应用程序,将在其自己的进程空间中运行。这样可以保护 ASP 应用程序彼此不受干扰。它的缺点是它需要为每个 ASP 应用程序建立独立的进程。当需要在一个服务器上主持十多个应用程序时,会增加很多开销。 那么,哪个选项是最好的呢?在 IIS 4.0 中,运行进程外的应用程序会极大地影响性能。在 IIS 5.0 中,做了许多工作,使得进程外运行 ASP 应用程序对性能产生的影响降到了最低。实际上,在大多数测试中,在 IIS 5.0 中的 ASP 进程外应用程序,要比 IIS 4.0 中的进程内应用程序运行得更快。无论如何,进程内(低隔离级)在两种平台上仍然产生了最好的性能。但是,如果您的命中率相对较低或最大吞吐量较低,选择低隔离级不会有太大的好处。所以,除非您需要每个 Web 服务器每秒处理数百或数千个页面,否则没有必要选择低隔离级。同样,应当测试多种配置并判断哪种情形最适合您。
注意: 当您进程外运行 ASP 应用程序(中级或高级隔离)时,则在 NT4 上它们将运行在 MTS 中,而在 Windows 2000 上它们将运行在 COM+ 中。即,在 NT4 上它们运行在 Mtx.exe 中,而在 Windows 2000 上它们运行在 DllHost.exe 中。在“任务管理器”中,您可以看见这些正在运行的进程。还可以看见 IIS 如何为进程外的 ASP 应用程序配置 MTS 程序包或 COM+ 应用程序。
COM 选项
COM 组件也有三个配置选项,虽然与 ASP 选项不完全相似。COM 组件可以被:“不配置”、配置为“库应用程序”或配置为“服务器应用程序”。“不配置”是指不向 COM+ 注册组件。组件将运行在调用者的进程空间,就是说,它们是“进程中”的。“库应用程序”也是进程中的,但受惠于 COM+ 的服务,包括安全性、事务和环境支持。“服务器应用程序”被配置为在其自己的进程空间中运行。
您可能看到,不配置的组件比库应用程序优点稍微多些。您还可能看到“库应用程序”比“服务器应用程序”有很大的性能优点。这是因为“库应用程序”与 ASP 运行在同一个进程中,而“服务器应用程序”则运行在自己的进程中。内部进程调用的开销要比进程内调用的开销大得多。而且,当在进程之间传递数据(如记录集)时,必须在两个进程之间复制所有的数据。
缺点!当使用“COM 服务器应用程序”时,如果要在 ASP 和 COM 之间传递对象,请确保对象实现“按值汇集”,即 MBV。实现 MBV 的对象将其自身从一个进程复制到另一个进程。这比另一种方式好,在另一种方式中,对象留在创建它的进程中,而其他进程则重复调用创建使用该对象的进程。被断开连接的 ADO 记录集将是按值汇集的,已连接的记录集则不是。Scripting.Dictionary 并不实现 MBV,不会在进程之间传递。最后,要另外告诉 VB 程序员的是:MBV 不是通过传递参数ByVal 获得的。MBV 是由原始组件创作者实现的。
怎么办?
如果您想要以性能与可靠性的合理交换来完成您的配置,我们的推荐如下:
在 IIS 4.0 上,使用 ASP 的低隔离级别,并使用“MTS 服务器包”。 在 IIS 5.0 上,使用 ASP 的中隔离级别,并使用“COM+ 库应用程序”。 这些是很一般的准则;通常让公司以中或高隔离级别运行 ASP,而单一目的的 Web 服务器可运行于低隔离级别。请权衡折中并自行决定满足需求的配置。
(注意,在上面 1,000 行表的示例中,许多浏览器,在看到 </table> 结束标记之前不会开始绘制表。请检查目标浏览器的支持性。要解决该问题,请将表分割为具有较少行的多个表,然后在每个表后面调用 Response.Flush。新版本的 Internet Explorer 将在表完全下载之前绘制表,特别是如果指定表的列宽则绘制速度更快;这避免强制 Internet Explorer 通过度量每个单元格的内容来计算列宽。)
VBScript 语法 <% = expression %> 将“表达式”的值写入 ASP 输出流。如果响应缓冲没有打开,则这些语句的每一句都会导致通过网络,以许多小型包的形式,向浏览器写入数据。这是非常慢的。另外,解释少量脚本和 HTML,将导致在脚本引擎和 HTML 之间切换,也降低了性能。因此,请使用下面技巧:用对 Response.Write 的一个调用,替换内嵌的密集组合表达式。例如,在下面范例中,每行每字段有一个对响应流的写入,每行都有许多 VBScript 和 HTML 之间的切换:
<table> <% For Each fld in rs.Fields %> <th><% = fld.Name %></th> <% Next While Not rs.EOF %> <tr> <% For Each fld in rs.Fields %> <td><% = fld.Value %></td> <% Next </tr> <% rs.MoveNext Wend %> </table>
下面是更有效的代码,每行中有一个对响应流的写入。所有代码均包含在一个 VBScript 块内:
<table> <% For each fld in rs.Fields Response.Write ("<th>" & fld.Name & "</th>" & vbCrLf) Next While Not rs.EOF Response.Write ("<tr>") For Each fld in rs.Fields %> Response.Write("<td>" & fld.Value & "</td>" & vbCrLf) Next Response.Write "</tr>" Wend %> </table>
流行的浏览器具有对以下功能的高级支持,例如 XML、DHTML、java 小程序以及远程数据服务。请尽量利用这些功能。所有这些技术,都可以通过执行客户端的验证和数据缓存,减少了与 Web 服务器之间的往返。如果您正在运行智能浏览器,该浏览器可以为您进行一些验证(例如,在运行 POST 之前检查信用卡的校验和否有效)。重申一次,请尽量使用这些功能。由于削减了客户端到服务器的往返路程,将减少对 Web 服务器的压力,并且削减了网络通信量(虽然发送给浏览器的初始页面可能更大),服务器访问的所有后端资源也削减了。而且用户不必经常提取新页,使用户的感受好一些。这并不减轻对服务器端验证的需要。还是应该经常进行服务器端的验证。这样能够防止由于某些原因从客户端来的坏数据,例如黑客,或者不运行客户端验证程序的浏览器。
许多站点由独立于浏览器创建的 HTML 组成。这一点经常阻碍开发人员利用可以提高性能的流行浏览器功能。对于真正高性能的、必须关心浏览器的站点,良好的策略是针对流行的浏览器优化您的页面。在 ASP 中使用“浏览器性能组件”,很容易检测到浏览器的功能。诸如 Microsoft FrontPage 等工具,能帮助您设计使用所希望的目标浏览器和 HTML 版本的代码。更详细的讨论,请查看 When is Better Worse? Weighing the Technology Trade-Offs(英文)。
技巧 20:在循环中避免字符串串联
许多人在循环中创建类似这样的字符串:
s = "<table>" & vbCrLf For Each fld in rs.Fields s = s & " <th>" & fld.Name & "</th> " Next
While Not rs.EOF s = s & vbCrLf & " <tr>" For Each fld in rs.Fields s = s & " <td>" & fld.Value & "</td> " Next s = s & " </tr>" rs.MoveNext Wend
s = s & vbCrLf & "</table>" & vbCrLf Response.Write s
特别是在将 ADO 记录集转换到 HTML 表时,请考虑使用 GetRows 或 GetString。
如果用 JScript 连接字符串,强烈建议使用 += 操作符;即用 s += "某字符串", 而不是 s = s + "某字符串"。
技巧 21:启用浏览器和代理缓存
默认情况下,ASP 禁用浏览器和代理中的缓存。这将很有意义,因为 ASP 生来就是动态的,具有潜在地对时间敏感的信息。如果有一个不需要对每次查看进行刷新的页,则应该启用浏览器和代理缓存。这使得浏览器和代理能在某一段时间内,使用某一页的缓存副本,这时间的长短可以控制。缓存能明显减轻服务器负荷,使用户的感受好一些。
Response.Redirect 通知浏览器,请求一个不同的页面。该函数经常用于重定向用户到登录或错误页面。既然重定向强制一个新页请求,浏览器就必须做两次到 Web 服务器的往返,而且 Web 服务器必须处理额外的请求。IIS 5.0 引入一个新的函数,Server.Transfer,该函数执行传送到相同服务器上的不同 ASP 页。这样避免了额外的、从浏览器到 Web 服务器的往返,从而改善了整体系统性能,同时改善了对用户的响应时间。请查看重定向中的新方向(英文),它讨论了 Server.Transfer 和 Server.Execute。
也可以查看Leveraging ASP in IIS 5.0中有关 IIS 5.0 和 ASP 3.0 新功能的完全列表。(英文)
<a href="http://msdn.microsoft.com/workshop/" title="MSDN Web Workshop">http://msdn.microsoft.com/workshop</a>
它还适用于指向在 Web 站点主页的 URL:请使用下面的: <a href="http://msdn.microsoft.com/">,不要用 <a href="http://msdn.microsoft.com">.
技巧 24:避免使用服务器变量
访问服务器变量将引起 Web 站点向服务器提出特殊的请求,然后收集所有的服务器变量,并不止是需要的那个。这好像从发霉的阁楼中的文件夹中检索某条特殊的信息一样。当想要某条信息时,在访问该信息之前必须先上阁楼取得文件夹。这与请求服务器变量时,性能访问出现第一次请求服务器变量所发生的一样。后续的对其他服务器变量的访问不会引起性能访问。
如上所述,性能是一种指标。如果您正努力改进站点的性能,请先设置性能目标,然后提高性能直到达到目标为止。请不要将所有的性能测试放在项目的最后。往往到了项目的最后,再做非做不可的体系结构改动已为时太晚,并使客户失望。性能测试是日常测试的一部分。性能测试可以针对独立组件进行,例如 ASP 页面或 COM 对象,也可以将站点作为一个整体进行。
许多人使用单一的浏览器请求页面来测试他们 Web 站点的性能。这将使您对站点的响应有很好的感觉,但对于站点在有负荷下的性能却一无所知。
通常,要准确地测量性能,需要专用的测试环境。这个环境应该由那些,在处理器速度、处理器个数、内存、硬盘、网络配置等方面,能模拟产品硬件的硬件组成。然后,需要定义客户端的工作负荷:有多少并发用户;他们提出请求的频率;他们将访问的页面类型等等。如果您无法从站点获得实际的使用数据,则需要估计它们。最后,需要一个能模拟预期客户端工作负荷的工具。在这些工具的帮助下,可以开始回答一些问题,例如,如果我有 N 个并发用户,需要多少台服务器?您还能找出瓶颈和优化的目标。
下面列出了一些好的 Web 强度测试工具。极力推荐“Microsoft Web 应用程序强度测试 (WAS)”工具包。WAS 允许记录测试脚本,然后模拟成百或上千个访问 Web 服务器的用户。WAS 报告大量统计结果,包括每秒请求数、响应时间的分布和错误计数。WAS 具有增强客户端和基于 Web 的接口;Web 接口允许进行远程测试。