高性能ASP.NET应用程序(1)——优化状态管理

         今天我们来聊一下在编写ASP.NET应用程序的时候如何优化状态管理。我相信绝大多数的ASP.NET开发人员或多或少的都会对使用ASP.NET的状态有所了解。在本文中,我们不会把如何使用ASP.NET的状态作为重点,而更侧重于讨论如何更加有效率和有效果的管理ASP.NET的状态。Web应用程序为状态管理提出了一些具体的挑战,尤其是Web场/园的情形。您就状态存储的位置和方法所作出的决定对您的应用程序的性能和扩展性将会产生巨大的影响。

         总的来说,ASP.NET中有3种状态:应用程序状态、会话状态和视图状态。他们分别需要在不同的情形下使用。应用程序状态应用于所有的用户和会话。而会话状态用于存储单个用户的状态。在这里单个用户可以理解为来自同一个浏览器的请求。视图状态用于存储单个页面的状态信息,就像在某个具体网页的控件中包含的内容。归结起来有2种状态管理指南:1)在客户端上存储简单的状态;2)考虑序列化带来的开销。对于第一点,当视图状态不可用时您可以考虑利用客户端cookie、查询字串或者隐藏控件。考虑到状态的序列化,请确保只在必要时才进行存储,并且倾向于存储简单的类型(约15%到25%的性能影响)而非复杂的对象(可能会有更大的影响)。

         现在我们来分别谈一下怎样最优化应用程序状态、会话状态和视图状态管理。

应用程序状态管理技巧:

  • 1. HttpApplicationState VS 静态属性:您应该避免在应用程序对象中存储数据。取而代之的是,您可以使用应用程序类的静态成员。您可以由此获得性能提升因为访问静态成员会比访问应用程序对象快得多。一个例子是:

<%

private static string[] _states[];

private static object _lock = new object();

public static string[] States

{

  get {return _states;}

}

public static void PopulateStates()

{

  //ensure this is thread safe

  if(_states == null)

  {

    lock(_lock)

    {

        //populate the states...    }

  }

}

public void Application_OnStart(object sender, EventArgs e)

{

  PopulateStates();

}

%>

2. 应用程序状态VS ASP.NET 缓存:当您可以使用缓存来存储可读写的数据来避免服务器/进程关联问题,请倾向于在应用程序状态中存储只读日期。

3. 避免在应用程序状态中存储STA COM对象。所有访问STA COM的线程都会被序列化从而引发一些严重的性能事件。

会话状态技巧:

基本上,我们有3种选择来存储会话状态:InProc、 StateServer 和 SQL server。InProc是速度最快的选择然而状态数据有可能会在进程回收和进程崩溃。这不能应用于Web场或者Web园部署。状态服务器既不能安装在本地服务器上也不能安装在远程服务器上。这种做法很好但是因为将状态从存储处提取和传输到存储处所需要的状态序列化和反序列化而对性能造成影响。SQL Server能够在大型网络平台上为存储无数的用户会话数据提供高可扩展性和性能。序列化和反序列化的开销会比状态服务器稍微多一些。然而,带有SQL簇的SQL Server可以提供更好的鲁棒性。

会话状态管理技巧:

  • 1. 侧重于基本类型以减小序列化开销。基本类型是指Int、Byte、Decimal、String、DataTime、TimeSpan、Guid、IntPrt和UintPtr等类型。基本类型序列化的速度快于复杂类型的原因是ASP.NET使用了一种优化过的内部方法来序列化基本类型而复杂类型却使用BinaryFormatter对象。
  • 2. 关闭会话状态,如果不使用此项的话。
  • 3. 正如应用程序状态,请在会话状态中也避免存储STA COM对象。
  • 4. 尽量使用只读标签。因为内部使用会话状态的页面请求使用了一个ReaderWriterLock,这意味着它将在任何给定时间允许多线程的同时只读访问,或者可写的单线程访问。如果您的请求需要修改会话状态,这些请求将被序列化。以下是一个简单的例子。当您浏览Main.aspx页面并点击slow区域和quick区域的Refresh按钮时,您将会看到请求被序列化,这意味着快和慢的页面将会被同时呈现到客户端。

Main.aspx:

 <%@ Page Language="C#" enableSessionState="true"%>

<frameset rows="50%, 50%">

  <frame src="quickpage.aspx" />

  <frame src="slowpage.aspx" />

</frameset>

Quickpage.aspx:

<%@ Page Language="C#" enableSessionState="true"%>

<script runat="server">

 protected void Page_Load(object sender, EventArgs e)

  {

    Session["test"] = "testquickpage";

    Response.Write(DateTime.Now.ToLongTimeString());

    Response.Write(" " + Session["test"]);

  }

</script>

<form runat="server">

  <asp:button runat="server" text="refresh" />

</form>

Slowpage.aspx:

<%@ Page Language="C#" enableSessionState="true"%>

<script runat="server">

  protected void Page_Load(object sender, EventArgs e)

  {

    Session["test"] = "testslowpage";

    System.Threading.Thread.Sleep(50000);

    Response.Write(DateTime.Now.ToLongTimeString());

    Response.Write(" " + Session["test"]);

  }

</script>

<form id="Form1" runat="server">

  <asp:button runat="server" text="refresh" />

</form>

         访问Main.aspx页面将会导致3个页面出现在同一会话中。这个请求处理将会被序列化。调用堆栈显示了线程30正在处理slowpage.aspx,而quickpage.aspx将会被另外一个线程处理但却正在等待线程30来完成。查看Timeout值可以发现,它在slow和quick页面之间几乎以同样的步伐保持增长。

HttpContext    Timeout  Completed     Running  ThreadId ReturnCode   Verb RequestPath+QueryString

0x0000000155740530      110 Sec        no        10 Sec      28        200   POST /sessionRWLock/slowpage.aspx

0x000000019570b2d8      110 Sec        no       305 Sec     XXX        200   GET /sessionRWLock/main.aspx

0x00000001957af010      110 Sec        no         9 Sec     XXX        200   POST /sessionRWLock/quickpage.aspx

HttpContext    Timeout  Completed     Running  ThreadId ReturnCode   Verb RequestPath+QueryString

0x0000000155740530      110 Sec        no        89 Sec     XXX        200   POST /sessionRWLock/slowpage.aspx

0x000000019570b2d8      110 Sec        no       384 Sec     XXX        200   GET /sessionRWLock/main.aspx

0x00000001957af010      110 Sec        no        88 Sec     XXX        200   POST /sessionRWLock/quickpage.aspx

以下是线程30的托管堆栈,可以看出它停留在了Page_load状态。

0:030> !clrstack

OS Thread Id: 0x664 (30)

Child-SP         RetAddr          Call Site

000000000436de70 000007feece23ec9 ASP.slowpage_aspx.Page_Load(System.Object, System.EventArgs)

000000000436ded0 000007fee3e4800a System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr, System.Object, System.Object, System.EventArgs)

000000000436df00 000007fee3e3e7e4 System.Web.Util.CalliEventHandlerDelegateProxy.Callback(System.Object, System.EventArgs)

000000000436df30 000007fee3e3e842 System.Web.UI.Control.OnLoad(System.EventArgs)

000000000436df70 000007fee3e3adcc System.Web.UI.Control.LoadRecursive()

000000000436dfc0 000007fee3e3a2d0 System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean)

000000000436e090 000007fee3e3a1fb System.Web.UI.Page.ProcessRequest(Boolean, Boolean)

000000000436e0f0 000007fee3e3a190 System.Web.UI.Page.ProcessRequest()

000000000436e150 000007ff00190219 System.Web.UI.Page.ProcessRequest(System.Web.HttpContext)

000000000436e1b0 000007fee3e41637 ASP.slowpage_aspx.ProcessRequest(System.Web.HttpContext)

...

视图状态管理技巧:

  视图状态是被服务器端控件用来保持将数据反馈给它们自己(同一个页面)的内容。1个叫做-_VIEWSTATE的特别隐藏变量持有所有的视图状态信息。视图状态的读取发生在页面初始化之后而存储发生在网页的渲染(Render)之前。由于从视图状态存储/获取数据是如此之容易,视图状态容易被不正确的使用。视图状态实际上是一个页面非必需的开销。以下是一些技巧:

  • 1. 在可能的情形下关闭试图状态。如果页面没有提交返回(post back)、没有服务器端控件事件发生或者没有旧的数据被存储了,您不妨简单的将视图状态关闭。
  • 2. 将存储在试图状态的信息最小化。如果你需要在视图状态存储数据,请记住存储基本类型和较小的对象。
  • 3. 请随时留意视图状态的大小。开启一个跟踪或者以其他方式来监视每一个控件视图状态大小。

Reference: Understanding ASP.NET ViewStat

https://msdn.microsoft.com/en-us/library/ms972976.aspx

        以上基本上是ASP.NET状态管理中我认为比较重要的内容。希望能对您有所帮助。如果有任何问题或者担心,请告知我,我将非常高兴的和您一起探讨任何技术问题。非常感谢您花费宝贵时间阅读我们小组的博客!

Yawei