查找 ASP.NET Web 应用程序中“泄露”的数据库连接






4.62/5 (10投票s)
介绍一种监视 ASP.NET 应用程序中数据库连接打开时间的方法。

引言
在本文中,我介绍了一个 ConnectionMonitor 类,该类监视 ASP.NET Web 应用程序中数据库连接的打开时间。该类还可以选择性地将所有打开时间超过给定长度的连接条目写入 Windows 事件日志,包括每个连接首次创建位置的堆栈跟踪。
ConnectionMonitor 类是为 SqlConnection 创建的,但没有理由认为该代码对任何其他类型的连接不起作用,只需将 SqlConnection 替换为您的连接类型即可。(如果您使用的是 .NET 2.0 版本,可以将 SqlConnection 替换为 DbConnection,它是所有连接类派生的基类。)
背景
当我看到 Web 应用程序事件日志中出现以下错误时,我萌生了创建此实用工具的想法。
System.InvalidOperationException: Timeout expired. 
The timeout period elapsed prior to obtaining a connection 
from the pool. This may have occurred because all pooled 
connections were in use and max pool size was reached.
服务器重启后,错误会持续数周不出现,然后开始以逐渐增加的频率出现。
这可能发生有多种原因,但最可能的原因是连接被“泄露”了。也就是说,连接已打开但从未关闭。(有关此问题的精彩讨论,请参见 此处[^])。
“泄露”连接的一种方式(显然)是根本忘记关闭它。另一种不太明显的方式:如果在打开和关闭连接之间抛出异常,连接就会被泄露。
SqlConnection cnn = new SqlConnection(myConnectionString); 
cnn.Open();
// ...
// ...do something with the connection
// ... 
// if an exception is thrown while 'doing something' 
// connection will never be closed
// and is "leaked"
cnn.Close();
当然,解决方法如下:
SqlConnection cnn = new SqlConnection(myConnectionString); 
try
{
    cnn.Open();
// ...
// ...do something with the connection
// ... 
}
finally
{
    cnn.Close();
}
与其对每个正在创建的连接进行视觉代码审查(我有三个大型 ASP.NET Web 应用程序在运行!),我决定找到一种方法来监视我应用程序中打开的所有连接。
设置监视
要启用应用程序中的连接监视,您必须构造一个 ConnectionMonitor 对象并将其存储在 Web 应用程序的全局 HttpApplicationState 对象中。最简单的方法是使用随附的示例项目提供的 ConnectionUtility 类。
ConnectionMonitor monitor = ConnectionUtility.Monitor
此 static 属性会检查 HttpApplicationState 对象中是否已存储 ConnectionMonitor。如果没有,它会创建一个。很简单,对吧?
监视连接的真正难点在于,每次创建连接时,您都必须像这样将它添加到 ConnectionMonitor 中:
SqlConnection cnn = new SqlConnection();
cnn.ConnectionString= myConnectionString;
ConnectionUtility.Monitor.Add(new ConnectionInfo(cnn));
在我的应用程序中,这不成问题,因为我所有的数据库访问代码都位于一个小程序集中。我只需要更改几行代码。如果您不那么幸运,那就别无选择,只能找到所有连接,并在创建时将它们添加到监视器。这不可否认是使用此工具的一个潜在障碍。为了稍微容易一些,ConnectionUtility 类提供了一个 static 帮助方法,该方法负责创建连接并将其添加到 ConnectionMonitor。
SqlConnection cnn = 
   ConnectionUtility.CreateConnection( myConnectionString )
将连接添加到监视器后,您就完成了。您不必担心连接何时打开和/或关闭,因为 ConnectionMonitor 会自动管理这些。
使用 ConnectionMonitor
当您使用 ConnectionUtility.Monitor 类创建监视器时,它会自动构造一个 System.Diagnostics.EventLog 对象,该对象使用源名称“ConnectionMonitoring”将条目记录到应用程序事件日志中。您可以使用 Windows 中包含的事件查看器查看这些日志条目。典型的条目包括连接打开的时间长度。
但这当然对您没有太大帮助,如果您不知道连接是在应用程序的哪个位置创建的。因此,日志条目还包括从连接首次添加到 ConnectionMonitor 时开始的代码堆栈跟踪。
默认情况下,对于打开时间超过 180 秒的任何连接,每小时会将条目写入日志。您可以通过更改 ConnectionUtility 类顶部的常量来修改此行为。
public class ConnectionUtility
{
  // change this name to use a different event source
  public const string EventLogSource = "ConnectionMonitoring";
  // Repeat seconds is how often the event log is written to:
  // the default is 1 hour.
  public const int RepeatSeconds = 3600;
  // this is how many seconds the connection must be open
  // before it is written to the EventLog.
  // the default is 3 minutes
  public const int OpenSeconds = 180;
  // change this to false to disable automatic logging
  public const bool UseLogging = true;
}
ConnectionMonitor 的工作原理
如果您曾调试过 System.Exception,您可能会注意到它包含一个非常方便的 StackTrace 属性。此字符串包含在抛出异常时调用的方法的当前堆栈。我的第一个想法是,.NET 公共语言运行时使用了某种未公开的魔法来创建跟踪。但并非如此!它只是使用了 System.Diagnostics.StackTrace 类。
要为添加到 ConnectionMonitor 类的每个连接创建堆栈跟踪,我使用了以下代码:
return string GetStackTrace()
{
  StackTrace trace = new StackTrace( true );
  StringBuilder sb = new StringBuilder();
  for ( int i = 1; i < trace.FrameCount; i++ )
  {
    StackFrame frame = trace.GetFrame( i );
    sb.AppendFormat("at {0}.{1}\r\n", 
      frame.GetMethod().DeclaringType, frame.GetMethod().Name );
  }
    
  return sb.ToString();
}
当我最初构思这段代码时,我还担心另一件事:“我怎么才能知道连接何时打开和关闭?”。手动告知 ConnectionMonitor 类每次发生这种情况会很麻烦,而且很可能容易出错,以至于使这项工作变得徒劳。幸运的是,.NET 中的连接类提供了 StateChange 事件,该事件在连接打开或关闭时发出信号。这使得监视这些事件变得容易。
//..
// When a connection is added to the ConnectionMonitor
// it hooks into the connection's state change event
connection.StateChange += 
    new StateChangeEventHandler( Connection_StateChange );
//..
//..
void Connection_StateChange(object sender, 
                            StateChangeEventArgs e)
{
  if ( ( e.CurrentState & ConnectionState.Open ) != 0 )
 {
   DoOpen();
 }
 if ( e.CurrentState == ConnectionState.Closed  )
 {
   DoClose();
 }
}
请注意,StateChange 事件直到 .NET 1.1 版本才添加。如果您使用的是 1.0 版本,此方法对您不起作用。
处理 Application_End 事件
如果您的 Web 应用程序被重新启动(由您或 IIS 重新启动),存储在 HttpApplicationState 中的 ConnectionMonitor 对象将会丢失。您可以在 Application_End 事件处理程序(位于您项目 Global.asax.cs 文件中)中添加一些代码,以便在监视器消失之前强制其写入事件日志。如果需要,您也可以在该时间关闭任何过时的连接。不幸的是,在 Application_End 事件期间,您无法使用 ConnectionUtility 来获取监视器。ConnectionUtility 使用 HttpContext.Current 来获取 HttpApplicationState。但是,在 Application_End 期间,HttpContext.Current 返回 null。
protected void Application_End(Object sender, EventArgs e)
{
 ConnectionMonitor monitor = 
   Application[ConnectionMonitor.CacheName] as ConnectionMonitor;
  if ( monitor != null )
  {    
      EventLog logger = monitor.Logger;
      if ( logger != null )
      {
          monitor.ForceLogging();
          logger.WriteEntry("---Application Ending---", 
                                EventLogEntryType.Information);
      }
      
      //Uncomment out the following statement to close connections
      //that have been opened for longer than 180 seconds
      //monitor.Close( 180 );
  }
}
我不清楚 Web 应用程序重新启动时,打开的连接会发生什么。我不知道它们是否会被系统回收或完全丢失。如果您担心,最好取消注释 monitor.Close 语句,并将秒数从 180 更改为 0。
事件日志权限
如果您目前没有在 Web 应用程序中写入事件日志,设置权限可能会很棘手(特别是如果您必须等待管理员为您提供访问权限)。事件日志存储在 Windows 注册表中 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog 下。您可以使用注册表编辑器查看或更改权限。
如果您无法正确设置此功能,我建议如下。首先,通过将 ConnectionUtility 类中的常量 UseLogging(如上所示)从 true 更改为 false 来禁用事件日志记录。然后定期浏览 ShowConnectionStatus.aspx 页面(在示例代码中提供),以查看打开连接的状态。
线程安全
HttpApplicationState 可以被多个线程同时访问。因此,为了防止无效数据,ConnectionMonitor 的设计必须考虑到这一点。这主要涉及大量使用 lock 语句。例如:
// Adds a new connection to the list
public void Add( ConnectionInfo item )
{
    lock ( this )
    {
        mList.Add( item );
    }
}
我其实不是多线程访问方面的专家。虽然我没有遇到任何问题,但也许一些更了解这方面知识的热心读者在需要时可以提供建议。
限制
由于 ConnectionUtility 使用 HttpApplicationState 类,因此它不适用于 Web 场或 Web 园。ConnectionMonitor 本身没有此类限制。但您需要找到一种方法来存储可以跨不同服务器检索的 ConnectionMonitor 对象。这是否可行,我不知道。
摘要
ConnectionMonitor 类提供了一个方法来定位 ASP.NET Web 应用程序中的泄露连接。它还可以选择性地启用自动事件日志记录(带有堆栈跟踪),以便轻松定位和诊断连接问题。如前所述,使用此工具的一个潜在缺点是,您必须手动将创建的每个连接添加到 ConnectionMonitor。但如果您有连接泄露问题,这将是值得付出的努力。当我将连接监视添加到我的生产 Web 站点时,在事件日志第一次写入时就发现了一个泄露。堆栈跟踪将我指向了我只是忘记关闭连接的代码!在接下来的几周内,当异常被抛出并且有问题的代码没有使用 try/finally 块正确编写时,又发现了其他几个泄露。


