ASP.NET站点性能提升-减少长等待时间
如果服务器没有用尽内存、CPU或线程,请求依然花了很长时间才完成,就有可能是服务器等待外部资源,例如,数据库等外部资源的时间很长。
在这一章中,讨论这些问题:
- 如何使用自定义计数器度量长等待时间。
- 并行而不是串行等待。
- 提高会话状态性能。
- 减少线程锁延迟。
度量等待时间
度量等待外部资源响应的频率和时长有几种方法:
- 在调试器中运行代码,在请求外部资源的地方设置断点。但是,不能在生产环境中使用这个方法,并且这个方法只能提供少量请求的信息。
- 使用Trace类(在System.Diagnostics使用空间中)跟踪每个请求花费的时间。这个方法会提供详细的信息。但是,如果用在生产环境,处理跟踪消息的成本就太高了,必须合并跟踪数据,查找哪些请求的频率最后,花费的时间最长。
- 在代码中使用性能计数器,记录每个请求的频率和平均等待时间。这些计数器是轻量级的,所以可能用在生产环境中。
Windows提供了28种类型的性能计数器。增加自定义计数器也很容易,可以在perfmon中查看实时值,和内建的计数器一样。
计数器的运行开销很小。ASP.NET,SQL Server和Window已经发布了上百个计数器。即使增加了很多的计数器,CPU开销也会保持在1%以下。
本章只使用三种常用计数器:简单数字、每秒比率和时间。所有类型计数器和它们的使用方法可以在http://msdn.microsoft.com/en-us/library/system.diagnostics.performancecountertype.aspx?ppud=4找到。
使用计数器有三个步骤:
- 创建自定义计数器。
- 集成到代码中。
- 在perfmon中查看值。
创建自定义计数器
在这个例子中,有一个页面简单地等待一秒钟,模拟等待外部资源。我们会放置计数器在这个页面上。
Windows允许对计数器进行分类。我们为新计数器创建一个”Test Counters”新分类。
这里有一些我们将放到页面上计数器。它们是经常用到三种类型的计数器。
| 计数器名 | 计数器类型 | 描述 |
| Nbr Page Hits | NumberOfItem64 | 64位计数器,记录网站启动后页面的访问次数。 |
| Hits/second | RateOfCountsPerSecond32 | 每秒访问次数 |
| Average Wait | AverageTime32 | 等待资源的间隔时间。 |
| Average Wait Base | AverageBase | Average Wait需要的工具计数器 |
创建计数器和”Test Counters”分类有两种方法:
- 使用Visual Studio:这个方法比较快,但是如果希望在多个环境,如开发环境和生产环境应用同一个计数器,必须在多个环境中都要加入这个计数器。
- 编程:在多个环境中应用同一个计数器比较容易。
使用Visual Studio创建计数器
略
编程创建计数器
从维护的角度,最好在Global.asax文件中,当应用程序启动时创建计数器最好。但是,这样就必须考虑性能监视器用户组运行在哪个应用程序池下。
另一个方法在一个单独的控制台程序中创建计数器。管理员可以在服务器上运行这个程序创建计数器。代码如下:
using System;
using System.Diagnostics;
namespace CreateCounters
{
class Program
{
static void Main(string[] args)
{
CounterCreationDataCollection ccdc =
new CounterCreationDataCollection();
CounterCreationData ccd = new CounterCreationData
("Nbr Page Hits", "Total number of page hits",
PerformanceCounterType.NumberOfItems64);
ccdc.Add(ccd);
ccd = new CounterCreationData("Hits / second",
"Total number of page hits /sec",
PerformanceCounterType.RateOfCountsPerSecond32);
ccdc.Add(ccd);
ccd = new CounterCreationData("Average Wait",
"Average wait in seconds",
PerformanceCounterType.AverageTimer32);
ccdc.Add(ccd);
ccd = new CounterCreationData("Average Wait Base", "",
PerformanceCounterType.AverageBase);
ccdc.Add(ccd);
if (PerformanceCounterCategory.Exists("Test Counters"))
{
PerformanceCounterCategory.Delete("Test Counters");
}
PerformanceCounterCategory.Create("Test Counters",
"Counters for test site",PerformanceCounterCategoryType.
SingleInstance,ccdc);
}
}
}
在代码中使用计数器
using System;
using System.Diagnostics;
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
PerformanceCounter nbrPageHitsCounter =
new PerformanceCounter("Test Counters", "Nbr Page Hits", false);
nbrPageHitsCounter.Increment();
PerformanceCounter nbrPageHitsPerSecCounter =
new PerformanceCounter("Test Counters", "Hits / second", false);
nbrPageHitsPerSecCounter.Increment();
Stopwatch sw = new Stopwatch();
sw.Start();
// Simulate actual operation
System.Threading.Thread.Sleep(1000);
sw.Stop();
PerformanceCounter waitTimeCounter = new
PerformanceCounter("Test Counters", "Average Wait", false);
waitTimeCounter.IncrementBy(sw.ElapsedTicks);
PerformanceCounter waitTimeBaseCounter = new
PerformanceCounter("Test Counters", "Average Wait Base",
false);
waitTimeBaseCounter.Increment();
}
}
在Perfmon中查看自定义计数器
略。参考以前的文章。
并行等待
如果网站需要等待多个外部资源等待响应,并且这些请求间没有相互依赖,可以同时初始化这些请求,并行等待,而不是串行等待。如果需要三个web service的信息,每个需要5秒,使用并行等待只需要5秒,而不是15秒。
可以使用异步代码很容易实现并行等待。当注册每个异步任务时,传递true给PageAsyncTask构造器的executeInParallel参数:
bool executeInParallel = true;
PageAsyncTask pageAsyncTask =
new PageAsyncTask(BeginAsync, EndAsync, null, null, executeInParallel);
RegisterAsyncTask(pageAsyncTask);
从数据库中获取多个结果集
参考以前的文章。
减少使用外部会话模式的开销
如果使用在服务器园上使用会话状态,可能使用StateServer或SqlServer状态,而不是InProc模式,因为来自同一个访问者的请求可能由不同的服务器处理。
当ASP.NET开始处理一个请求时,从StateServer或SQL Sever获取当前会话状态,然后反序列化。然后,在页面生命周期结束时,再序列化会话状态,存储在StateServer或SQL Server数据库中。在这个过程中,ASP.NET更新会话的最后访问时间,这样会可能清除超时的会话。如果使用SqlServer模式,就意味着每次请求有两个来回。
减少数据库访问
可以通过设置Page指令为False或ReadOnly,减少数据库访问:
<%@ Page EnableSessionState="True" ... %>
EnableSessionState可以取这些值:
- True:默认值。一个访问两次数据库。
- False:不读取Session,但为了防止Session过期,页面结束时,需要更新Session,所以只需要访问一次数据库。
- ReadOnly:当页面初始话时,获取并反序列化会话状态。但页面结束时,不更新会话状态。只需要访问一次数据库。另外,这种模式使用读锁,使得多个只读请求可以同时访问会话状态。因此,当来处理自同一个用户的请求时,就避免了锁竞争。
设置EnableSessionState
可以在Page指令中设置EnableSessionState:
<%@ Page EnableSessionState="ReadOnly" %>
也可以web.config中设置整个网站的会话模式:
<configuration>
<system.web>
<pages enableSessionState="ReadOnly" />
</system.web>
</configuration>
在每个页面中的Page指令中可以重载默认值。
減少序列化和传输开销
不要在会话中存在有多个字段的对象,单独存储每个字段。这样有以下好处:
- 序列化。.NET简单类型,例如String、Boolean、DateTime、TimeSpan、Int16、Int32、Int64、Byte、Char、Single、Double、Decimal、SByte、UInt16、UInt32、UInt64、Guid和InPtr是非常快速和高效的。序列化对象类型,会使用BinaryFormatter,则非常慢。
- 允许只访问需要访问的单个字段。没有访问的字段不会更新,节省了序列化和传输时间。
假设有一个类:
[Serializable]
private class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
在Session中这样获取和存储:
// Get object from session Person myPerson = (Person)Session["MyPerson"]; // Make changes myPerson.LastName = "Jones"; // Store object in session Session["MyPerson"] = myPerson;
这会使用BinaryFormatter反序列化/序列化整个myPerson对象并整个在会话存储服务器间传输。
另一种方式:
private class SessionBackedPerson
{
private string _id;
public SessionBackedPerson(string id)
{
_id = id;
}
private string _firstName;
public string FirstName
{
get
{
_firstName = HttpContext.Current.Session[_id +
"_firstName"].ToString();
return _firstName;
}
set
{
if (value != _firstName)
{
_firstName = value;
HttpContext.Current.Session[_id + "_firstName"] = value;
}
}
}
private string _lastName;
public string LastName
{
get
{
_lastName = HttpContext.Current.Session[_id +
"_lastName"].ToString();
return _lastName;
}
set
{
if (value != _lastName)
{
_lastName = value;
HttpContext.Current.Session[_id + "_lastName"] = value;
}
}
}
}
这个类在会话中存储每个属性。它在创建时,必须知道它的ID,这样它才能构造一个唯一的session键。当设置属性时,只有当新值和旧值不同时,都会访问session对象。
这个方法的结果是只会存储单独的基本类型,它们比整个对象序列化更快。也只有当这些字段真正更新时,才会被更新session中的字段值。
页面中使用:
protected void Page_Load(object sender, EventArgs e)
{
SessionBackedPerson myPerson = new SessionBackedPerson("myPers
on");
// Update object, and session, in one go.
// Only touch LastName, not FirstName.
myPerson.LastName = "Jones";
}
完全消除对会话的依赖
会话的最大好处是,会话状态存储在服务器上,未授权的用户很难访问和修改它。然后,如果这不是个问题,这里有一些其它选择去除会话状态和它的开销:
- 如果不是需要保存很多会议数据,使用cookie。
- 在ViewState中存储会话,这需要更大的带宽,但減少了数据库流量。
- 使用AJAX异步调用取代下整个页面刷新,这样就可以在页面上保存会话信息。
线程锁
如果使用锁保证只有一个线程可以访问资源,其它线程就必须等待这个锁释放。
使用.NET CLR LocksAndThreads分类中的以下计数器查看锁的相关情况:
分类:.NET CLR LocksAndThreads
| Contention Rate/sec | 运行时试图获得托管锁,并失败的比率。 |
| Current Queue Length | 上一次记录的正在等待托管锁的线程数 |
连续有线程申请锁失败,是造成延迟的一个原因。可以考虑使用以下方法减少这些延迟:
- 减少锁的持续时间
- 使用granular锁
- 使用System.Threading.Interlocked
- 使用ReaderWriterLock
减少锁的持续时间
在访问共享资源前申请锁,访问结束后,立即释放。
使用granular锁
如果使用C# lock语句,或Monitor对象,锁尽可能小的对象。
lock (protectedObject)
{
// protected code
}
这时以下代码的简写:
try
{
Monitor.Enter(protectedObject);
// protected code ...
}
finally
{
Monitor.Exit(protectedObject);
}
这样写没有问题,只要锁住的对象只与被保护代码相关。只锁私有的或内部对象。否则,一些不相关的代码可能会锁相同的对象,以保护其它一段代码,这会导致不必须的延迟。例如,不要锁this:
lock (this)
{
// protected code ...
}
锁一个私有对象:
private readonly object privateObject = new object();
public void MyMethod()
{
lock (privateObject)
{
// protected code ...
}
}
如果是保护静态代码,不要锁类:
lock (typeof(MyClass))
{
// protected code ...
}
锁静态对象:
private static readonly object privateStaticObject = new object();
public void MyMethod()
{
lock (privateStaticObject)
{
// protected code ...
}
}
使用System.Threading.Interlocked
如果被保护的代码只是增加或减少一个整数,将一个整数加到另一个整数,或者交换两个值,考虑使用System.Threading.Interlocked类代替锁。Interlocked执行速度要比锁快。
例如,不要
lock (privateObject)
{
counter++;
}
使用:
Interlocked.Increment(ref counter);
使用ReaderWriterLock
如果大多数访问受保护对象线程只读那个对象,而相对少的线程更新对象,考虑使用ReadWriterLock。这允许多个只读线程访问受保护代码,而只有一个写线程访问。
请求读锁
当使用ReaderWriterLock时,在类级别声明ReaderWriterLock:
static ReaderWriterLock readerWriterLock = new ReaderWriterLock();
在一个方法中请求读锁,调用AcquireReaderLock。可以指定超时时间。当超时发生时,抛出一个ApplicationException。调用ReleaseReaderLock释放锁。只有在真正拥有锁时,才能释放锁,也就是锁还没超时,否则会抛出一个ApplicationException。
try
{
readerWriterLock.AcquireReaderLock(millisecondsTimeout);
// Read the protected object
}
catch (ApplicationException)
{
// The reader lock request timed out.
}
finally
{
// Ensure that the lock is released, provided there was no
// timeout.
if (readerWriterLock.IsReaderLockHeld)
{
readerWriterLock.ReleaseReaderLock();
}
}
请求写锁
调用AcquireWriterLock请求读锁。调用RealeaseWriterLock释放锁:
try
{
readerWriterLock.AcquireWriterLock(millisecondsTimeout);
// Update the protected object
}
catch (ApplicationException)
{
// The writer lock request timed out.
}
finally
{
// Ensure that the lock is released, provided there was no
// timeout.
if (readerWriterLock.IsWriterLockHeld)
{
readerWriterLock.ReleaseWriterLock();
}
}
如果代码持有一个读锁,然后需要更新受保护对象,可以先释放读锁,再请求写锁,或者调用UpdateToWriterLock方法。也可以调用DowngradeFromWriterLock从写锁降级到读锁,以允许其它读锁开始读。
读锁和写锁交替
虽然多个线程可以同时读受保护对象,但是更新受保护对象需要排他的访问。当一个线程更新对象时,其它线程不能读或更新同一个对象。等待读锁和等待写锁的线程分配在不同的队列中。当写锁释放锁时,所有等待读锁的线程都可以访问对象。当它们都释放了读锁后,下一个等待写锁的线程可能得到写锁。
为了保证在不断的有线程请求读锁时,写线程能够得到写锁,如果一个新线程请求读锁,而此时其它读线程已经在执行代码了,新线程必须等待,直到下一个写线程完成操作。当然,如果没有线程等待写锁,新读线程可以立刻得到读锁。
优化磁盘写
如果网站在磁盘上创建了很多新文件,例如访问者上传的文件,考虑这些性能提高措施:
- 避免磁盘头查找
- 使用FileStream.SetLength避免碎片
- 使用64K缓冲区
- 禁用8.3文件名
避免磁盘头查找
顺序写字节时不用移动读/头,要比随机访问快得多。如果只是写文件,不读它们,使用专用线程将它们写到专用驱动器上。这样,其它进程就不会移动那个驱动器的读/写头。
使用FileStream.SetLength避免碎片
如果多个线程在同一时间写文件,这些文件占用的空间是交错是,会导致碎片。
为了避免这种情况,使用FileStream.SetLength方法在开始写前预留足够的空间。
如果使用ASP.NET FileUpload控件接收文件,可以使用以下代码得到文件的长度:
int bufferLength = FileUpload1.PostedFile.ContentLength;
使用64K缓冲区
NTFS文件系统使用64KB的内部缓冲区。FileStream构造器允许设置文件写的缓冲区大小。通过设置FileStream缓存区大小为64KB,可能获得更高的性能。
禁用8.3文件名
为了保持与MS-DOS的向后兼容性,NTFS文件系统为每个文件或目录维护了一个8.3文件名。这会有一些额外开销,因为系统必须保证8.3文件名必须是唯一 的,所以必须检查目录中的其它文件名。如果一个目录中的文件超过20000个文件,这个开销就很显著了。
在禁用8.3文件名前,确保系统中没有程序依赖这些名称。在相同的操作系统上先进行测试。
在命令行中执行这个命令(先备份注册表):
fsutil behavior set disable8dot3
重启机器。
更多资源
- Why Disabling the Creation of 8.3 DOS File Names Will Not Improve Performance. Or Will It?
http://blogs.sepago.de/helge/2008/09/22/why-disabling-the-creation-of-83-dos-file-names-will-not-improve-performance-or-will-it/. - Fast, Scalable, and Secure Session State Management for Your Web Applications
http://msdn.microsoft.com/en-us/magazine/cc163730.aspx. - Performance Counter Type Reference
http://www.informit.com/guides/content.aspx?g=dotnet&seqNum=253.
浙公网安备 33010602011771号