查找 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
块正确编写时,又发现了其他几个泄露。