ntwo

导航

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找到。

使用计数器有三个步骤:

  1. 创建自定义计数器。
  2. 集成到代码中。
  3. 在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

重启机器。

更多资源

posted on 2010-12-02 10:02  9527  阅读(977)  评论(0编辑  收藏  举报