当你使用 ADO.NET 连接池来管理基于 Web 的应用程式和客户端/服务器 Web 服务应用程式的连接时,你的客户通常会获得更快的连接和更好的总体性能。不过,当你的应用程式或 Web 站点上忽然涌入了同时希望进行连接的大量客户时,会发生什么事情呢?你的应用程式会“沉没”,还是会“游泳”?就像救生员相同,你需要仔细监视连接池,以维护他的良好性能,并防止连接池发生溢出。我们首先探讨连接池可能溢出的原因,然后讨论怎么编写代码或使用 视窗系统 性能监视器来监视连接池。
正如我于 2003 年 5 月发表的 "Swimming in the .NET Connection Pool" (InstantDoc ID 38356) 一文中讨论的那样,当你使用连接池时,你需要知道许多有关可伸缩性和性能的周详信息。请记住,你需要监视和管理两个基本因素:每个池管理的连接数和连接池的数量。在一个有效的生产系统中,池的数量通常非常少(1 到 10),而且,使用中的连接的总数也非常少(少于 12 )有效的查询只用不到一秒钟的时间就能完成,并断开连接。因此,即使有数百个客户同时访问你的 Web 站点,相对较少的几个连接常常足以处理整个负载。为了使你的应用程式有效地运行,你必须使连接资源处于自己的控制之下,并要监视池的状态,这样,在监视池发生溢出及你的客户开始抱怨(或离开你的网站)之前你会收到某种警告。
为什么会发生连接池溢出?
参加电子邮件讨论组的人常常抱怨应用程式是怎么在测试中是“龙”而在形成为产品时就变成了“虫”的。有时,他们会报告说,当连接了大约 100 个客户端时,应用程式会停止或挂起。请记住,一个池中的默认连接数是 100.如果你尝试从池中打开 100 个以上的连接,ADO.NET 会使应用程式的连接请求排队等候,直到有空闲的连接。应用程式(及其用户)将这种情况视为进入 Web 页的延迟或视为应用程式死锁。让我们首先讨论一下这个问题是怎么产生的。
在 ADO.NET 中,SqlClient .NET 数据提供程式为你提供了两种打开和管理连接的方法。首先,当你需要手工管理连接时,能使用 DataReader 对象。利用这种方法,你的代码将构造一个 SqlConnection 对象,设置 ConnectionString 属性,然后使用 Open 方法来打开连接。当代码完成 DataReader 后,你要在 SqlConnection 对象停止作用之前关闭 SqlConnection.要处理行集,你能将 DataReader 传递到应用程式中的另一个例程,但仍然需要确保 DataReader 及其连接处于关闭状态。如果你不关闭 SqlConnection,代码会“泄漏”每个操作的连接,于是连接池对连接进行累积,最后便发生溢出。和 ADO 和 Visual Basic (VB) 6.0 中的情况不同,。NET 垃圾回收器不会为你关闭 SqlConnection 并进行清理。我稍后要讨论的 清单 1 显示了怎么打开连接和生成 DataReader 以从一个简单的查询返回行集,来向连接池施加压力的。
你也可能在使用 DataAdapter 对象时遇见问题。DataAdapter Fill 和 Update 方法可自动打开 DataAdapter 对象的连接,并在数据 I/O 操作完成后关闭该连接。不过,如果该连接在执行 Fill 或 Update 方法时已处于打开状态,那么,ADO.NET 在方法执行完以后不会关闭 SqlConnection.这是另一个发生连接“泄漏”的机会。
此外,你还能使用基于 COM 的 ADO 从 .NET 应用程式创建连接。ADO 利用和 ADO.NET 相同的方式将这些连接组合成池,但不能像你使用 SqlClient ADO.NET 数据提供程式时那样,提供从应用程式监视连接池的方式。
指示 DataReader
孤立连接和溢出池是严重的问题,根据有关这些问题的新闻组讨论的数量来看,他们十分常见。这些问题最有可能是由 DataReader 引起的。为了测试 DataReader 的行为,我编写了一个 视窗系统 窗体 (WinForms) 示例应用程式,该示例突出了 CommandBehavior.CloseConnection 选项。(你能在 http://www.sqlmag.com 上输入 InstantDoc ID 39031 来下载此应用程式)。你能在使用 SqlCommand 对象的 ExecuteReader 方法来执行查询并返回 DataReader 时设定此选项。我的测试应用程式显示,如果不显式关闭 DataReader(或 SqlConnection),即使使用此选项,连接池还是会溢出。当代码所请求的连接数超过连接池的容量时,该应用程式就会引发异常。
有些研发人员坚持认为,如果你设置 CommandBehavior.CloseConnection 选项,则 DataReader 及其相关联的连接会在 DataReader 完成数据读取时自动关闭。这些研发人员的看法不完全正确 ? 只有当你在 ASP.NET Web 应用程式中使用复杂的绑定控件时,该选项才以这种方式工作。在整个 DataReader 结果集中循环到其行集的末尾(也就是说,当 Dr.Read ? DataReader 的 Read 方法 ? 返回 False 时)还不足以触发连接的自动关闭。不过,如果你绑定到一个复杂的绑定控件(例如,DataGrid),该控件则会关闭 DataReader 和连接 ? 前提条件是你设置了 CommandBehavior.CloseConnection 选项。
如果你通过使用另一个 Execute 方法(例如,ExecuteScalar、ExecuteNonQuery 和 ExecuteXMLReader)执行查询,则你需要负责打开 SqlConnection 对象,而且,更重要的是,在查询结束时关闭该对象。如果你忘记了进行关闭,孤立连接会迅速地积累起来。
监视连接数
为了对孤立连接和发生溢出的连接池进行测试,我编写了一个 Web 窗体的示例应用程式。此应用程式使用的方法和你通常用于从查询返回数据的方法相同。(你能在 http://www.sqlmag.com 上下载此代码的 WinForms 版本。)
我使用了清单 1 中的代码来打开和关闭到 Web 窗体应用程式的连接。标注 A 中的例程针对 110 个新的 SqlConnection 对象创建、打开和执行查询 ? 比默认的池大小多 10 个连接。你必须在离开该例程之前关闭和放弃所有这些连接。如果不这样做,SqlConnection 对象将连同关联的池连接一起被孤立。ADO.NET 池机制 (aka the pooler) 关闭数据库连接,但不关闭池连接。我将连接池大小设置为 10,以便使该程式更快地失败 ? 如果该程式会失败的话。通常,10 个连接对于一个运行速度象这个查询相同快的查询来说已足够了。许多研发人员运行着忙碌的 Web 站点,这些 Web 站点使用不到五个连接来处理每天的几十万次点击。
标注 A 中的例程创建 SqlConnection 对象和 SqlCommand 对象,设置 CommandText,并打开连接。然后,标注 B 中的代码确定执行 DataReader 时是否使用 CommandBehavior.CloseConnection,这取决于用户在 Web 窗体上选择了哪些 CheckBox 控件。
在标注 C 的代码中,我指定是否将 DataReader 行集绑定到 DataGrid,或是否在整个行集中进行循环。标注 C 的代码测试当你到达通过 DataReader 从数据提供程式传递回来的行集的末尾时会发生什么事情。
目前,我使用标注 D 中的代码来指定是手工关闭连接还是让某个其他操作(例如,数据绑定)来完成这项工作。坦白地说,以手工方式关闭连接通常是最安全的,因此,你能肯定连接不会被孤立。
如果代码成功地运行到这一步,说明我已成功地打开和关闭了 110 个连接。不过,如果出了问题,标注 E 的代码中的异常处理程式会将异常(通常是 Timeout)作为 InvalidOperationException 捕捉,该异常是连接池已满时 ADO.NET 的响应方式。
表 1 汇总了各个选项使例程成功运行或失败的方式。请注意,如果你不设置 CommandBehavior.CloseConnection 选项,你的操作最终会失败 ? 即使在使用绑定控件的情况下也是如此。即使你使用该选项,但如果你没有使用复杂的绑定控件,或没有手工关闭 SqlDataAdapter 或 SqlConnection,该进程仍然会失败。
当我结束了这些示例应用程式的运行后,我已生成了 1000 多个以上的池连接 ? 所有连接均处于孤立状态。虽然“SQL Server 用户连接”计数为 0,但留下大约 40 个连接池。在我重新引导系统之前,孤立的池不会消失。
我用于此测试的示例应用程式包括使用 DataAdapter 来返回行的例程。除非你手工管理连接,否则,DataAdapter 将正确地打开和关闭 SqlConnection 对象,因此,你不太可能遇见孤立的池连接。不过,如果你的应用程式同时使用 DataReader 和 DataAdapter,你可能会发现,如果某个连接和一个未关闭的 DataReader 相关联,则 DataAdapter 无法针对该连接运行查询。
确定连接池何时达到最大连接数
正如我在 "Swimming in the .NET Connection Pool" 一文中讨论的那样,当连接池达到你通过 "Max Pool Size ConnectionString" 选项指定的最大连接数时,ADO.NET 将阻止所有随后打开额外连接的尝试。如果某个连接在你在 "ConnectionTimeout 选项中指定的时间之前变为可用,。NET 数据提供程式将向你的应用程式传递一个指向该连接的指针,以便将控件返回给应用程式。不过,如果没有及时释放所有连接,连接请求将引发 InvalidOperationException 异常。
目前你必须决定要采取的措施,我不建议你告诉用户你已用完了所有连接。有些应用程式会通知用户系统正忙于帮助其他客户,并建议用户稍后进行访问。其他应用程式则播放一段动画,通知用户系统尚未死锁,而是正在忙于处理他们的请求。同时,你的代码重新尝试操作。在所有情况下,你应该记录这些故障,以便帮助诊断问题的症结所在,并记录你已耗尽了资源。
监视连接池
你已打开和关闭了一个连接,目前你希望知道该连接是否仍然处于打开状态。你能使用几种方法来确定有多少连接仍然处于打开状态,及他们正在执行何种操作:
? 运行 sp_who 或 sp_who2.这些系统存储过程从 sysprocess 系统表返回信息,该系统表显示所有工作进程的状态及其有关信息。通常,你会看到每个连接有一个服务器进程 ID (SPID)。如果你是通过在连接字符串中使用 Application Name 参数来命名你的连接的,那么,你将非常容易找到工作的连接。
? 使用带有 SQLProfiler TSQL_Replay 模板的 SQL Server 事件探查器来跟踪打开的连接。如果你非常熟悉事件探查器,此方法比通过使用 sp_who 进行轮询要更容易。
? 使用性能监视器来监视池和连接。我稍后再讨论此方法。
? 在代码中监视性能计数器。你能通过使用例程来提取计数器或通过使用新的 .NET PerformanceCounter 控件来监视连接池的状况和已建立的连接的数量。这两种方法都包括在你能从 http://www.sqlmag.com 进行下载的示例应用程式中。
目前我们将讨论怎么查找连接池计数器,及怎么使用这些监视方法。
连接池计数器在哪里?要监视连接池计数器,你必须监视 ADO.NET 在其中创建和增加这些计数器的系统。如果你从远程系统进行连接,ADO.NET 并不总是在 Microsoft IIS 服务器或 SQL Server 上创建池;他在 ADO.NET 代码运行的系统上创建池。此系统能是运行 IIS、Web 应用程式或 Web 服务的远程 视窗系统 或中间层系统。相反,SQL Server 性能计数器位于 SQL Server 系统上 ? 而不是客户端上。
使用性能监视器来监视池。如果你使用 Microsoft 管理控制台 (MMC) 视窗系统 2000 系统监视器管理单元,则你能通过从 Performance 对象下拉列表中选择 ".NET CLR Data" 来用图像表示 SqlClient 计数器,如 图 1所示。请注意,你能通过选择 _global_ 计数器实例来监视所有进程,或,你能查看某个特定实例 ? 每个池生成自己的一组监视器。性能监视器可列出这些计数器,并将他们作为所选定的性能对象的实例提供。但性能监视器不会公开这些计数器,除非有实例需要他们进行监视。例如,图 1 显示了 .NET CLR Data 性能对象,但没有列出特定实例。这意味着你必须至少创建一个连接,以便使 _global_ 实例连同每个进程的特定实例一起出现。这种行为对于你的代码来说是个问题;你将无法使用 PerformanceCounter 控件来返回其中的所有计数器,直到 ADO.NET 在打开连接时创建这些计数器。所以说,这个规定真有点令人左右为难。当你使用此方法时,因为缺少有效计数器实例,所以会引发异常 ? 此时要准备好捕捉异常。
你还能通过使用 SQL Server 性能计数器 "User Connections" 来监视打开的连接的数量。该计数器被列在 Performance 对象下拉列表中的 sql Server: General Statistics 下。我喜欢监视 "User Connections" 值和一些所选定的 .NET CLR Data SqlClient 计数器(我稍后将讨论此内容),因为我能获得我需要的信息,而不必担心实例。
使用代码来监视性能计数器。当你需要以编程方式监视连接池时,你能编写代码来监视由 SqlClient 管理的性能计数器 ? 这些计数器和 MMC 视窗系统 NT 性能监视器管理单元所提供的计数器是相同的。编写执行监视的代码似乎是一件有些令人畏惧的事情。但我已提供了从 SqlClient 提供程式的内部工作提取这些计数器的例程的快照(作为本文提供的可下载程式之一)。
你能编写检查 表 2显示的五个计数器的代码。通过利用这五个计数器,你能实时监视连接池。。NET 预期你会在性能监视器中提供一个类别 ? 复制的 performance Object ? 并从那些注册到系统的计数器中选择适当的计数器。要访问 SqlClient 计数器,请将该类别设置为 ".NET CLR Data".
使用 PerformanceCounter 控件。你可能会发现,在设计时向你的应用程式窗体添加 PerformanceCounter 要比手工编写代码来访问性能计数器更加容易。要使用 PerformanceCounter 控件,请从“Visual Studio .NET 工具箱组件”菜单中选择一个 PerformanceCounter,将他拖到你的应用程式窗体,然后设置属性,如 图 2 所示。这些控件工作在 Web 窗体和 WinForms 应用程式中。
因为 PerformanceCounter 控件提供了方便的下拉列表,所以,你能在设计时看到所有一种性能计数器类别、计数器名称和特定实例 ? 你将要运行的实例除外。这意味着你必须使用图 2 显示的方法来捕捉应用程式正在使用的池的适当实例。为了回避这个问题,我选择 _global_ 实例。再次说明一下,此方法假设某个应用程式已至少创建了一个池,因此你需要做好不存在计数器实例时 ADO.NET 引发异常的准备,就像他在不存在池连接时也会引发异常相同。
注意不准确的池计数。因为 SqlClient .NET 数据提供程式中存在 .NET 框架 1.1 尚未解决的错误,所以,性能计数器会在池实际上已删除时错误地指示池“仍然存在”。我能通过结束 MMC 性能监视器管理单元、然后结束 Visual Studio .NET 来验证池已不再存在。这些步骤说明,。NET 数据提供程式在创建连接池的进程结束时会正确地删除连接池。显然,这种不准确性降低了性能计数器在监视池方面的有效性,所以我希望 Microsoft 将来能解决这个问题。
计数器不显示的内容
你可能会面临的一个问题是无法从计数器或 SqlClient 属性看到每个池的设置。每个 SqlConnection 对象的 ConnectionString 保存着这些池设置的密钥。因为你不能依赖于默认设置,所以非常难确定池几乎已满或非常难使用。这会成为未来版本的 ADO.NET 的另一个方便功能。
不过,假设你知道各个连接池 ConnectionString 参数的值,则利用清单 1 中的代码,你能非常容易地设置一个计时器来检查你创建的特定池并报告使用百分比。然后,监视应用程式会向你发出警报,以便你能解决问题并防止溢出。
最后,请记住,ado.net 采用的方法和基于 COM 的 ADO 有所不同。Visual Basic .NET 完全改动了放弃对象的方式,并且不再确保 Connection 对象在停止作用时被关闭。请确保 SqlConnection 对象(或所有 Connection 对象)在停止作用之前被关闭。
连接池是一种非常强大的功能,他能提高应用程式的性能。但如果你不是个出色的救生员,你的连接池会成为一个危害而不是个好处。我希望本文讨论的方法有助于你有效地监视连接池并满足用户的需要。