对于ASP程序来说,性能非常重要;必须在最初设计程序时就要考虑到性能,否则以后恐怕就要因为性能问题而重写代码。怎样才能使ASP程序性能达到最大呢?本文就将介绍一些提高ASP程序性能的技巧。
技巧1:将常用数据在Web服务器端缓存起来
大部分的ASP页面都要从后台数据库中提取数据,然后将数据用HTML方式表现出来。
不管你的数据库多么快,从内存中提取数据总比从后台数据库中提取快;从本地硬盘中读取数据通常也比从数据库中快。因此,你可以通过在Web服务器端缓存数据来提高性能。
缓存是个典型的以空间换取时间的交易。如果你正确的缓存了数据,性能可能会突飞猛进。要想一个缓存能真正发挥效益,必须缓冲那些常用和计算复杂的数据。装满过期数据的缓冲区只能浪费内存。
不经常变化的数据也是缓存的一个良好候选者,因为你可以不用关心同数据库中的数据保持同步。下拉列表框、引用表、小段DHTML代码,XML字符串、菜单项和站点配置变量(包括数据源名字(DSN),IP地址和Web路径)都是很好的缓存候选者。注意,不仅仅可以缓存数据本身,还可以缓存数据的表现。如果一个ASP页面很少变化,并且缓存代价比较高(比如,产品列表),可以考虑用静态HTML页面。
技巧2:用Application对象或Session对象缓存常用数据
ASP的Application和Session对象是一个极其方便的在内存中缓存数据的容器。你可以把数据放到Application或Session对象中,这些数据就会在整个HTTP调用中一直存在。每个用户有自己的Session对象中的数据,而Application对象中的数据可以在所有用户中共享。
应该在什么时候将数据装入Application或Session中呢?通常,数据在Application或Session启动的时候装入。要想在Application或Session启动的时候装入数据,需要分别在Global.asa的Application_OnStart()或Session_OnStart()中添加适当的代码;如果Global.asa中没有这两个函数,你可以手工添加。也可以在数据第一次使用的时候将其装入。要想这样,应该在ASP页面中写一些代码(或是写一个可重用的脚本函数)来检查数据是否存在并且如果数据不存在则将其装入内存。下面是一个经典的性能调整技术--Lazy Evaluation:
<%
Function GetEmploymentStatusList
Dim d
d = Application("EmploymentStatusList")
If d = "" Then
' FetchEmploymentStatusList function (not shown)
' fetches data from DB, returns an Array
d = FetchEmploymentStatusList()
Application("EmploymentStatusList") = d
End If
GetEmploymentStatusList = d
End Function
%>
Similar functions could be written for each chunk of data needed.
In what format should the data be stored? Any variant type can be
stored, since all script variables are variants. For instance, you
can store strings, integers, or arrays. Often, you’ll be storing the
contents of an ADO recordset in one of these variable types. To get
data out of an ADO recordset, you can manually copy the data into
VBScript variables, one field at a time. It’s faster and easier to
use one of the ADO recordset persistence functions GetRows(),GetString
() or Save() (ADO 2.5). Full details are beyond the scope of this
article, but here’s a function that demonstrates using GetRows() to
return an array of recordset data:
' 获取记录集,返回数组
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
A further refinement of the above might be to cache the HTML for the
list, rather than the array. Here’s a simple sample:
' 获取记录集,返回HTML Option列表
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 ' 释放rs
FetchEmploymentStatusList = s ' 用字符串方式返回数据
End Function
在正确情况下,你可以将ADO记录集本身缓存在Application或Session范围,但必须满足下面两个条件: .ADO必须被标记为自由线程模型(Free-threaded) .必须使用无连接记录集
如果不能满足上面两个条件,一定不要缓存记录集。在下面的“不灵活的组件”和“不要缓存Connection”两个技巧中,我们将讨论在Application和Session中保存COM对象的危险性。
当你在Application或Session中存储数据后,数据将一直保存,知道你的程序改变它,或是Session过期,或是Web服务重新启动。What if the data needs to be updated?手工刷新Application数据,可以调用只有管理员才可访问的用来刷新数据的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
' Note: two or more calls might get in here. This is okay and
will simply
' result in a few unnecessary fetches (there is a workaround
for this)
' FetchEmploymentStatusList function (not shown)
' fetches data from DB, returns an Array
d = FetchEmploymentStatusList()
' 更新Application对象时用Application.Lock()来保持数据一致性
Application.Lock
Application("EmploymentStatusList") = Events
Application("LastUpdate") = CStr(Now)
Application.Unlock
End If
End Sub
要知道在Session或Application中缓存大数组并不是一个太好的方法。在访问数组中的任何元素之前,脚本解释器都需要生成一个临时的整个数组的副本。例如,如果你缓存了一个100,000个字符串元素的数组,用来将邮政编码和当地的天气对应一一起来,在访问数组中任何一个字符串之前,ASP解释器首先必须复制所有的100,000个天气情况数据到一个临时数组中。在这种情况下,开发一个组件来储存天气情况数据或是使用词典(Dictioary)对象更为合适一点。不过,也不要因小失大,数组对象的的查找速度更快。索引一个词典比索引一个数组慢。你可以因你的情况而宜,选择合适的数据结构。
技巧3:在硬盘上缓存数据和HTML页面
有时,可能有太多的数据缓存在内存中。“太多”是个模糊的说法,它取决与Web服务器的内存大小、缓存项的数目和这些缓存项被访问的频度。无论如何,如果太多的数据在内存中缓存,可以考虑将数据用文本或XML文件缓存到Web服务器的硬盘上。可以将缓存到硬盘上和到内存中结合起来,针对你的站点,找到最优化的策略。
注意,当我们测量单一ASP页面的性能时,从硬盘上读取数据可能比从数据库中读取慢。但是,缓存能够减少数据库和网络的负载。在高负载的情况下,这将大大提高总体吞吐量。当被缓存的数据是非常复杂的查询,比如多表连接或是一个复杂的查询过程或一个非常大的记录集,缓存的效果将非常明显。
ASP和COM提供了一些工具来建立基于硬盘的缓存方案。ADO Recordset对象的Save和Open方法可以保存和装入到磁盘上。还有一些用来访问文件的组件: .Scripting.FileSystemObject允许你创建、读取和写入文件。 .MSXML,同IE捆绑的微软的XML解释器,支持保存和装入XML文档。 .LookupTable对象是一个用来从磁盘装入简单列表的非常好的选择。
最后,将数据表现缓存在硬盘上,比缓存数据本身要好。生成的HTML可以一个.htm或.asp文件保存在硬盘上;超连可以直接指向那些文件。你也可以用一些商业工具,如XBuilder和SQL Server互连网发布特性,来生成和处理HTML文件。另外,也可以用#include将HTML片段包含到ASP文件中;还可以用FileSystemObject来读取HTML文件。
技巧4:避免在Application或Session对象中缓存COM对象
虽然在Application或Session对象中缓存数据是一个好注意,但缓存COM对象可能带来严重的后果。在Application或Session对象中缓存常用COM对象非常诱人,但非常不幸,很多COM对象,包括那些用VB 6.0或早期版本写的组件,如果被缓存到Application或Session对象中将会导致严重的性能瓶颈。
特别地,所有非Agile的组件被缓存到Session或Application中时,都将产生性能瓶颈。Agile组件是指聚合了Free-threaded marshaler(FTM)并且线程模型是Both
(ThreadingModel=Both),或线程模型是Neutral(Netural新出现在Windows 2000
和COM+中)的组件。下面的组件都是非Agile的:
自由线程模型组件(除非他们聚合了FTM)
Apartment线程模型组件
单线程组件
Configured组件(MTS/COM+库和服务包/应用)是非Agile的,除非它们是Neutral线程模型的。Apartment线程模型组件和其他非Agile组件最好是工作在页面范围内(就是说,他们在一个单一ASP页面中创建和销毁)。
在IIS 4.0中,线程模型是Both的组件被看作是Agile的,但在IIS 5.0中,他们不再满足Agile的条件。组件线程模型必须是Both的,并且聚合了FTM,才被看作Agile的。如果试图将一个用Server.CreateObject创建的非Agile组件存储到Application对象中时,IIS 5.0将会抛出一个错误。
当ADO组件被标记为自由线程模型时,ADO记录集对象可以安全地存储。可以用Makfre15.bat,一般是放在\\Program Files\Common\System\ADO这个文件夹里,将ADO组件标记为自由线程模型。有一点要注意:当用Access作后台数据库时,ADO不能被标记为自由线程模型。词典(Dictionary)组件也是Agile对象。
技巧5:不要缓存数据库连接
缓存ADO Connection对象是一个不好的策略。如果一个Connection对象被存储在Application对象中并被所有页面使用,所有页面就会争着使用这个连接。如果Connection对象被存储在Session对象中,就要为每个用户创建一个数据库连接,这就消减了连接池的作用,并且增大了Web服务器和数据库服务器的压力。可以用在每个使用ADO的ASP页创建和释放ADO对象来替代缓存数据库连接;因为IIS内建了数据库连接池,所以这种方法非常有效。
既然有连接的记录集保存了一个数据库连接的引用,因此也不应该在Application或Session对象中保存有连接的记录集。但是,你可以安全的缓存无连接的记录集,因为它并不包含到数据连接的引用。要想挂断一个记录集,可以采取如下两个步骤:
Set rs = Server.CreateObject("ADODB.RecordSet")
rs.CursorLocation = adUseClient ' 第一步
rs.Open strQuery, strProv
' 将记录集同数据提供者和数据源挂断
rs.ActiveConnection = Nothing '第二步
技巧6:正确地使用Session对象
我们已经提到了在Application和Session中缓存数据的好处,下面我们将说一些Session对象的缺点。在繁忙的站点上使用Session有一些不利的地方。繁忙是指这个站点每秒钟要处理数以百计的页面请求或同时连接数以千计的并发用户。这个技巧对那些必须要水平伸缩的站点--就是说,这些站点用多个服务器来实现负载平衡或容错--非常重要。对小的站点,如公司内网,Session相对与他消耗的资源来说,还是值得一用的。
ASP自动为每个访问Web服务器的拥护创建一个Session对象。每个Session大约消耗10K的资源,并使所有的请求都慢了一点。这个Session在超时周期内一直存在,这个周期一般是20分钟。对于Session来说最大的问题不是性能而是伸缩能力。Session不是跨Web服务器的;一旦一个Session在某个服务器上创建,它的数据都保存在那儿。
这意味着如果你要在多个Web服务器环境中使用Session,你必须设计一套能使用户总是访问它的Session对象所在的Web服务器的策略;即将一个用户粘到一个Web服务器上。如果Web服务器崩溃,因为Session不是永久保存在磁盘上的饿,所以全部“粘”
在其上的用户的Session状态都将丢失。实现“粘Session(sticky session)”的策略包括硬件和软件方案,如Windows 2000 Advanced Server中的Network Load Balancing和Cisco的Local Director。当然,这些方案并不完美,都要损失一些可伸缩性。Application对象也不是跨服务器的,如果你想在多服务器间共享和更新Application数据,你必须使用一个后台数据库。但无论如何,只读Application数据在多服务器环境中还是十分有用的。
绝大多数任务优先(mission-critical)的站点都想在至少两台Web服务器上发布--如果没有比延长正常运行时间更重要的理由的话。因此,在设计阶段,你就要实现“粘Session”,或是简单地避免Session和其他将用户状态保存在一个独立Web服务器上的状态管理技术。
如果不使用Session,就将它们关闭;可以通过Internet Service Manager(参看ISM文档)关闭你的应用的Session功能。如果决定使用Session,就要用一些方法将他们对性能的影响减到最小。可以将不需要Session的内容(如帮助窗口等)移到一个的关闭了Session的ASP应用中。如果某个单一页面不需要Session,可以将下面的语句放在页面的顶部来禁止Session功能:
<% @EnableSessionState=False %>
使用该语句还有一个原因是Session在帧中会产生一个有趣的问题。ASP保证任何时候一个会话只有一个请求,这就导致如果浏览器同时请求多个页面,同一时刻将只有一个ASP请求能够访问Session;这避免了访问Session对象时产生的多线程问题;但很不幸,一个帧中的多个页面只能顺序的生成,一个接着一个,而不是兵法。用户可能会为多个帧等待较长时间。所以如果帧中的某个页面没有使用Session,就在页面顶部放置<% @EnableSessionState=False %>语句。
作为使用Session对象的替代,还有很多其他的方法来管理会话状态。对小规模的状态(小于4KB),推荐使用Cookies,QueryString变量和隐藏表单变量。对大量数据,如购物信息,一个后台数据库可能是很好的选择。
技巧7:将代码封装到COM对象中
如果有很多VBScript或JScript代码,可以通过将代码封装到COM对象中来提高性能。编译过的代码通常比解释代码运行得快。COM对象可以通过“前期绑定”来访问其他COM对象,这比脚本使用的“后期绑定”更高效。
下面是将代码码封装到COM对象中的优点(不仅仅是性能):
COM对象可以很好地将商业逻辑同表现逻辑分离
COM对象使代码可重用
用VB,C或VJ写的代码比ASP代码更易调试
COM对象也有不足,包括开发周期长和需要不同的编程经验等。有一点尤需注意,封装少量ASP代码可能在性能上适得其反;这种情况下,创建和调用COM对象的代价超过了编译代码性能上的好处。如何组合ASP代码和COM组件代码来产生最佳性能,往往是个令人头疼的问题。注意,同Windows NT 4.0/IIS 4.0相比,Windows 2000/IIS 5.0在脚本和ADO性能上已经大大提高。
技巧8:对资源晚获取,早释放
通常情况下,晚获取和早释放资源是最好的。这不仅适用于COM对象,也适用于文件句柄和其他资源。ADO连接和记录集是这项优化策略的主要对象。当使用完一个Recordset对象,应该立即将它释放,而不应等到页面结束。将VBScript变量设成Nothing是最好的方法。同时,释放相关的Command和Connection对象(别忘了在将Connection对象设成Nothing之前调用Close()方法)。
技巧9:进程外执行以性能换取可靠性
ASP和MTS/COM+都有选项让你来用可靠性换取性能。当建立和发布你的应用时,你应该理解这项交易的内幕。
ASP选项
ASP应用有三种运行方法可选择。在IIS 5.0中,引入“分离级别(isolation
level)”这个术语来描述这些选项。三种分离级别分别是:低(Low),中
(Medium)和高(High)。
低分离级 这种级别被所有版本的IIS支持,并且速度也是最快的。它在
Inetinfo.exe--主要的IIS进程--中运行ASP。如果ASP应用崩溃,IIS也将崩溃。
(在IIS 4.0中,网管必须用诸如InetMon之类的工具来监视IIS,一旦IIS停止,运行批处理文件。IIS 5.0引入了“可靠的重启(reliable restart)”,会自动重新启动失败的服务器。
中分离级 从IIS 5.0开始引入的新级别,指进程外运行,即ASP运行在IIS进程之外。
在中分离级中,所有的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进程外应用比II4 4.0中的进程内运行都快。但无论如何,在任何平台上,还是进程内(低分离级)运行能带来最佳的性能。然而,在相对低点击率或低最大吞吐量的情况下,低分离级不会带来任何益处;因此,除非每个Web服务器需要应付成千上百的页面请求,不然你不会需要用低分离级。通常,需要在多个配置下进行测试,才能决定使用哪种配置。
注意:当在进程外运行ASP应用时(中或低分离级),ASP应用运行在NT 4上的MTS中或Windows 2000的COM+中;就是说,在NT4中,ASP应用运行在Mtx.exe中;在Windows 2000中,ASP应用运行在DllHost.exe中。你可以在任务管理器中看到这些进程在运行。
COM选项
COM组件也有三种配置选项,但不完全对应于ASP的选项。COM组件可以是“无配置的(Unconfigured)”、作为一个库应用(Library Application)或是作为一个服务应用(Server Application)。“无配置的”意味着组件不注册到COM+中,组件将在调用者进程空间中运行;即“进程内”。库应用也是进程内的,但可以从COM+的服务,如安全、事务和上下文支持,中获益。服务应用则被配置成运行在自己的进程空间内。
“无配置”比库应用有一点优越性;而库应用比服务应用在性能上更优越。这是因为库应用和ASP是在同一个进程内的,而服务应用是运行在自己的进程空间里的。进程间调用比进程内调用的代价高。同样,在进程间传递如记录集这样的数据,需要在两个进程间复制所有的数据。
缺陷!当使用COM服务应用时,要想在ASP和COM间传送数据,必须保证对象实现了“按值排列(marshall-by-valu)”,或者说MBV。实现了MBV的对象将自身从一个进程复制到另一个进程。这比下面的方法好:对象留在创建者进程,其他进程重复调用创建进程来使用对象。无连接ADO记录集是MBV,有连接记录集就不是。
Scripting.Dictonary对象没有实现MBV,不能在进程之间传递。最后,对VB程序员的一个提示:MBV不是通过用ByVal来传递参数。MBV是原始组件作者实现的。
怎样做?
推荐的用可靠性换取性能的配置:
在IIS 4.0上,用ASP的低分离级,并使用MTS服务包。
在IIS 5.0SHANG,用ASP的中分离级,使用COM+的库应用。
技巧10:使用Option Explicit
在.asp文件中使用Option Explicit。该指示放在.asp文件的顶部,强制开发者在使用任何变量之前必须定义它。许多程序员认为这有助于调试程序,因为它消除了打字错误的可能(如将MyXMLString=敲成MyXLMString=)
另外一点可能更加重要:已定义变量比未定义的变量快。ASP每次是用名字来引用未定义变量的;而另一方面,每个已定义变量有一个序号,ASP用这个序号来引用已定义变量。既然Option Explicit强制变量定义,就保证了所有的变量都是已定义的,访问速度就更快了。
技巧11:在子过程和函数中使用本地变量
本地变量是那些在子过程和函数中定义的变量。在函数和子过程中,访问本地变量比访问全局变量更快。使用本地变量也使代码更干净,因此尽量使用本地变量吧。
技巧12:将常用数据复制到脚本变量中
当访问ASP中的COM对象时,应该将常用对象数据复制到脚本变量中。着将减少COM方法调用。而COM方法调用代价相对比访问脚本数据更高。当访问Collection和Dictonary对象时,这项技术也能消减高昂的查询代价。
通常,当准备不止一次访问一个对象数据时,应该将这个数据放当一个脚本对象中。
这项优化的主要目标是Request变量(Form和QueryString变量)。例如,你的站点传递一个叫UserID的QueryString变量,假定在一个特定页UserID被引用十次。在ASP页面的顶部,将UserID的值赋给一个变量,来替代十次的调用Request("UserID"),将接生9次COM调用。
在实际中,访问COM属性或方法的昂贵代价可能比较隐蔽。下面是一个例子,显示一段普通的代码:
Foo.bar.blah.baz = Foo.bar.blah.qaz(1)
If Foo.bar.blah.zaq = Foo.bar.blah.abc Then ' ...
下面是这段代码运行的步骤:
1. 变量Foo被解析为一个全局对象
2. 变量bar被解析为Foo的一个成员。这触发一次COM方法调用
3. 变量blash被解析为Foo.bar的一个成员。同样,这也触发一次COM方法调用
4. 变量qaz被解析为Foo.bar.blash的一个成员。对,这也触发一次COM方法调用
5. 调用 Foo.bar.blah.qaz(1)。一个或多个COM方法调用。获取图片?
6. 重复步骤1到步骤3来解析baz。系统不知道调用qaz是否会改变对象模型,所以步骤1到步骤3又执行了一次,来解析baz
7. 解析出baz是Foo.bar.blah的一个成员,执行属性put.
8. 重复步骤1到步骤3来解析zaq
9. 重复步骤1到步骤3来解析abc
正如你所看到的,这是多么低效(并且慢)。快速的方法是按如下代码写VBScript:
Set myobj = Foo.bar.blah ' do the resolution of blah ONCE
Myobj.baz = myobj.qaz(1)
If Myobj.zaq = Myobj.abc Then '...
如果你用的是VBScript 5.0或更后的版本,可以使用With语句:
With Foo.bar.blah
.baz = .qaz(1)
If .zaq = .abc Then '...
...
End With