65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (10投票s)

2005年12月9日

CPOL

7分钟阅读

viewsIcon

127686

downloadIcon

1531

介绍一种监视 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 块正确编写时,又发现了其他几个泄露。

查找 ASP.NET Web 应用程序中“泄露”的数据库连接 - CodeProject - 代码之家
© . All rights reserved.