澳门新葡萄京官网注册 ,防止可淹没应用程序的池溢出
  大多数 ADO.NET 数据提供程序使用连接池,以提高围绕 Microsoft
断开连接的 .NET
结构构建的应用程序的性能。应用程序首先打开一个连接(或从连接池获得一个连接句柄),接着运行一个或多个查询,然后处理行集,最后将连接释放回连接池。如果没有连接池,这些应用程序将花费许多额外时间来打开和关闭连接。

如何有效地监视连接池并满足用户的需要   

2008-04-26
10:49:57|  分类:
默认分类
|  标签: |字号大中小 订阅

NET 连接池救生员

防止可淹没应用程序的池溢出

William Vaughn

大多数 ADO.NET 数据提供程序使用连接池,以提高围绕 Microsoft 断开连接的
.NET
结构构建的应用程序的性能。应用程序首先打开一个连接(或从连接池获得一个连接句柄),接着运行一个或多个查询,然后处理行集,最后将连接释放回连接池。如果没有连接池,这些应用程序将花费许多额外时间来打开和关闭连接。

当您使用 ADO.NET 连接池来管理基于 Web 的应用程序和客户端/服务器 Web
服务应用程序的连接时,您的客户通常会获得更快的连接和更好的总体性能。但是,当您的应用程序或
Web
站点上突然涌入了同时希望进行连接的大量客户时,会发生什么事情呢?您的应用程序会“沉没”,还是会“游泳”?就像救生员一样,您需要仔细监视连接池,以维护它的良好性能,并防止连接池发生溢出。我们首先探讨连接池可能溢出的原因,然后讨论如何编写代码或使用
Windows 性能监视器来监视连接池。

正如我于 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 的行为,我编写了一个 Windows 窗体
(WinForms) 示例应用程序,该示例突出了 CommandBehavior.CloseConnection
选项。(您可以在 上输入 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
窗体的示例应用程序。此应用程序使用的方法与您通常用于从查询返回数据的方法相同。(您可以在
上下载此代码的 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

© Microsoft Corporation. All rights reserved.

TABLE 1: Web Application CommandBehavior

Close Connection

Cn.Close

Dr.Close

Bind to DataGrid

Result

Connections Attempted/Made

(default)

 

 

 

Fail

17/10

(default)

 

X

 

Fail

26/10

(default)

 

X

X

Fail

3/10

X

 

 

 

Fail

27/10

X

X

 

 

OK

110/10

X

 

X

 

OK

110/10

X

 

X

 

OK

110/10

X

X

 

X

OK

110/10

X

 

X

X

OK

110/10

X

X

X

 

OK

110/10

X

X

X

X

OK

110/10

汇总了各个选项使例程成功运行或失败的方式。请注意,如果您不设置
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
服务的远程 Windows 或中间层系统。相反,SQL Server 性能计数器位于 SQL
Server 系统上 — 而不是客户端上。

使用性能监视器来监视池。如果您使用 Microsoft 管理控制台 (MMC) Windows
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 Windows NT
性能监视器管理单元所提供的计数器是相同的。编写执行监视的代码似乎是一件有些令人畏惧的事情。但我已经提供了从
SqlClient
提供程序的内部工作提取这些计数器的例程的快照(作为本文提供的可下载程序之一)。

您可以编写检查 表
2

© Microsoft Corporation. All rights reserved.

TABLE 2: SqlClient .NET CLR Data Performance Counters

Counter

Description

SqlClient: Current # of pooled and nonpooled connections

The current number of connections, pooled or not.

SqlClient: Current # pooled connections

The current number of connections in all pools associated with the
process.

SqlClient: Current # connection pools

The current number of pools associated with the process.

SqlClient: Peak # pooled connections

The highest number of connections in all pools since the process
started. Note that this counter is available only when associated with a
specific process instance. The _global_ instance always returns 0.

SqlClient: Total # failed connects

The total number of connection-open attempts that have failed for any
reason.

显示的五个计数器的代码。通过利用这五个计数器,您可以实时监视连接池。.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
对象)在停止作用之前被关闭。

连接池是一种非常强大的功能,它可以提高应用程序的性能。但如果您不是一个出色的救生员,您的连接池会成为一个危害而不是一个优点。我希望本文讨论的方法有助于您有效地监视连接池并满足用户的需要。

设计错误、缺陷及文档错误等导致正确使用.NET
HttpClient变得出奇地困难。所以,即使是生产环境中看似运行正常的应用程序,在负荷不满的情况下,也遭受着性能问题和运行时故障。

  当您使用 ADO.NET 连接池来管理基于 Web 的应用程序和客户端/服务器 Web
服务应用程序的连接时,您的客户通常会获得更快的连接和更好的总体性能。但是,当您的应用程序或
Web
站点上突然涌入了同时希望进行连接的大量客户时,会发生什么事情呢?您的应用程序会“沉没”,还是会“游泳”?就像救生员一样,您需要仔细监视连接池,以维护它的良好性能,并防止连接池发生溢出。我们首先探讨连接池可能溢出的原因,然后讨论如何编写代码或使用
Windows 性能监视器来监视连接池。

来自ASP.NET Monsters的Simon
Timms就通过一篇题为“你正在错误地使用HttpClient,它会破坏软件的稳定性”的文章揭示了这个事实。

  正如我于 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 以从一个简单的查询返回行集,来向连接池施加压力的。

–Voltrondemort

  您也可能在使用 DataAdapter 对象时遇到问题。DataAdapter Fill 和
Update 方法可自动打开 DataAdapter 对象的连接,并在数据 I/O
操作完成后关闭该连接。不过,如果该连接在执行 Fill 或 Update
方法时已经处于打开状态,那么,ADO.NET 在方法执行完以后不会关闭
SqlConnection。这是另一个发生连接“泄漏”的机会。

我想说,这表明,HttpClient要么Bug多,要么架构差。无法确定是哪一种。如果是第二种则会很有趣,就需要使用另外一种方法代替它发送Http请求。

  此外,您还可以使用基于 COM 的 ADO 从 .NET 应用程序创建连接。ADO
利用与 ADO.NET 相同的方式将这些连接组合成池,但不能像您使用 SqlClient
ADO.NET 数据提供程序时那样,提供从应用程序监视连接池的方式。

— Eirenarch

  指示 DataReader
  孤立连接和溢出池是严重的问题,根据有关这些问题的新闻组讨论的数量来看,它们十分常见。这些问题最有可能是由
DataReader 引起的。为了测试 DataReader 的行为,我编写了一个 Windows 窗体
(WinForms) 示例应用程序,该示例突出了 CommandBehavior.CloseConnection
选项。(您可以在 上输入 InstantDoc ID 39031
来下载此应用程序)。您可以在使用 SqlCommand 对象的 ExecuteReader
方法来执行查询并返回 DataReader
时设定此选项。我的测试应用程序显示,如果不显式关闭 DataReader(或
SqlConnection),即使使用此选项,连接池还是会溢出。当代码所请求的连接数超过连接池的容量时,该应用程序就会引发异常。

C#开发人员所受到的培训

  有些开发人员坚持认为,如果您设置 CommandBehavior.CloseConnection
选项,则 DataReader 及其相关联的连接会在 DataReader
完成数据读取时自动关闭。这些开发人员的看法不完全正确 — 只有当您在
ASP.NET Web
应用程序中使用复杂的绑定控件时,该选项才以这种方式工作。在整个
DataReader 结果集中循环到其行集的末尾(也就是说,当 Dr.Read — DataReader
的 Read 方法 — 返回 False
时)还不足以触发连接的自动关闭。不过,如果您绑定到一个复杂的绑定控件(例如,DataGrid),该控件则会关闭
DataReader 和连接 — 前提条件是您设置了 CommandBehavior.CloseConnection
选项。

为了理解我们如何陷入了这种境地,我们首先需要看下另外一个面向连接的类SqlConnection。在第一次接受如何使用IDisposable和using语句的培训时,绝大多数开发人员看到的都是类似下面这样的例子:

  如果您通过使用另一个 Execute
方法(例如,ExecuteScalar、ExecuteNonQuery 和
ExecuteXMLReader)执行查询,则您需要负责打开 SqlConnection
对象,而且,更重要的是,在查询结束时关闭该对象。如果您忘记了进行关闭,孤立连接会迅速地积累起来。

using (var con = new SqlConnection(connectionString)) {
    con.open();
    //这里使用连接
} //这里关闭连接

  监视连接数
  为了对孤立连接和发生溢出的连接池进行测试,我编写了一个 Web
窗体的示例应用程序。此应用程序使用的方法与您通常用于从查询返回数据的方法相同。(您可以在
上下载此代码的 WinForms 版本。)

虽然针对这个示例的说明并不完善,但这个模式是正确的,而且多年来很好地服务了开发人员。然而,如果你试图将这个模式应用到另一个IDisposable类HttpClient上,则会遇到一些始料未及的问题。

  我使用了清单 1 中的代码来打开和关闭到 Web 窗体应用程序的连接。标注 A
中的例程针对 110 个新的 SqlConnection 对象创建、打开和执行查询 —
比默认的池大小多 10
个连接。您必须在离开该例程之前关闭和放弃所有这些连接。如果不这样做,SqlConnection
对象将连同关联的池连接一起被孤立。ADO.NET 池机制 (aka the pooler)
关闭数据库连接,但不关闭池连接。我将连接池大小设置为
10,以便使该程序更快地失败 — 如果该程序会失败的话。通常,10
个连接对于一个运行速度象这个查询一样快的查询来说已经足够了。许多开发人员运行着忙碌的
Web 站点,这些 Web 站点使用不到五个连接来处理每天的几十万次点击。

具体来说,它会打开许多套接字,比你实际的需求多许多,这极大地增加了服务器的负载。而且,这些套接字实际上不会被using语句关闭。相反,它们是在应用程序停止使用它们几分钟之后才会关闭。

  标注 A 中的例程创建 SqlConnection 对象和 SqlCommand 对象,设置
CommandText,并打开连接。然后,标注 B 中的代码确定执行 DataReader
时是否使用 CommandBehavior.CloseConnection,这取决于用户在 Web
窗体上选择了哪些 CheckBox 控件。

连接池

  在标注 C 的代码中,我指定是否将 DataReader 行集绑定到
DataGrid,或者是否在整个行集中进行循环。标注 C 的代码测试当您到达通过
DataReader 从数据提供程序传递回来的行集的末尾时会发生什么事情。

回到SqlConnection的例子,多数面向连接的资源都会放入连接池。当你“打开”一个数据库连接时,它首先会检查连接池中是否存在未使用的连接。如果找到了,就重用它,而不是创建一个新的连接。

  现在,我使用标注 D
中的代码来指定是手工关闭连接还是让某个其他操作(例如,数据绑定)来完成这项工作。坦白地说,以手工方式关闭连接通常是最安全的,因此,您可以肯定连接不会被孤立。

同样,当你“关闭”一个SqlConnection连接时,它只是简单地将连接放回连接池。最后,一个单独的进程可以关闭长期未使用的连接,但通常来说,你可以认为它会正确地执行操作,实现性能和服务器负载的平衡。

  如果代码成功地运行到这一步,说明我已经成功地打开和关闭了 110
个连接。不过,如果出了问题,标注 E
的代码中的异常处理程序会将异常(通常是 Timeout)作为
InvalidOperationException 捕获,该异常是连接池已满时 ADO.NET
的响应方式。

HttpClient的工作机制并非如此。当你销毁它时,它就启动一个进程,关闭在它控制之下的套接字。也就是说,你下次请求连接时,必须重复整个连接新建过程。如果网络延迟很高,或者连接是受保护的(需要新一轮的SSL/TLS协商),就会非常痛苦。

  表 1 汇总了各个选项使例程成功运行或失败的方式。请注意,如果您不设置
CommandBehavior.CloseConnection 选项,您的操作最终会失败 —
即使在使用绑定控件的情况下也是如此。即使您使用该选项,但如果您没有使用复杂的绑定控件,或者没有手工关闭
SqlDataAdapter 或 SqlConnection,该进程仍然会失败。

关闭一个套接字需要花费4分钟

  当我结束了这些示例应用程序的运行后,我已经生成了 1000
多个以上的池连接 — 所有连接均处于孤立状态。虽然“SQL Server
用户连接”计数为 0,但留下大约 40
个连接池。在我重新引导系统之前,孤立的池不会消失。

如上所述,关闭套接字的过程并不快。当“关闭”套接字时,你真正做的是将其状态置为TIME_WAIT。在一个预先配置好的时间窗口内,Windows将保持该套接字的状态不变,默认情况下是4分钟。这是为了防止有任何剩余的数据包仍在传输。

  我用于此测试的示例应用程序包括使用 DataAdapter
来返回行的例程。除非您手工管理连接,否则,DataAdapter 将正确地打开和关闭
SqlConnection
对象,因此,您不太可能遇到孤立的池连接。不过,如果您的应用程序同时使用
DataReader 和 DataAdapter,您可能会发现,如果某个连接与一个未关闭的
DataReader 相关联,则 DataAdapter 无法针对该连接运行查询。

这大大增加了可用套接字耗尽的可能,导致运行时错误,比如“无法连接到远程服务器。System.Net.Sockets.SocketException:每个套接字地址(协议/网络地址/端口)通常只允许使用一次”。Simon
Timms写到:

  确定连接池何时达到最大连接数

“通过谷歌搜索那个错误会得出一些有关缩短连接超时时间的糟糕建议。事实上,当服务器上运行的应用程序恰当地使用了HttpClient或者类似的结构,缩短超时时间会导致其他不利的结果。我们需要理解“恰当”是指什么,并修复底层的问题,而不是修改机器层的变量”。

  正如我在 “Swimming in the .NET Connection Pool”
一文中讨论的那样,当连接池达到您通过 “Max Pool Size ConnectionString”
选项指定的最大连接数时,ADO.NET
将阻止任何随后打开额外连接的尝试。如果某个连接在您在 “ConnectionTimeout
选项中指定的时间之前变为可用,.NET
数据提供程序将向您的应用程序传递一个指向该连接的指针,以便将控件返回给应用程序。不过,如果没有及时释放任何连接,连接请求将引发
InvalidOperationException 异常。

.NET Core的性能影响

  现在您必须决定要采取的措施,我不建议您告诉用户您已经用完了所有连接。有些应用程序会通知用户系统正忙于帮助其他客户,并建议用户稍后进行访问。其他应用程序则播放一段动画,通知用户系统尚未死锁,而是正在忙于处理他们的请求。同时,您的代码重新尝试操作。在所有情况下,您应该记录这些故障,以便帮助诊断问题的症结所在,并记录您已经耗尽了资源。

大多数仅仅使用.NET
Framework完整版的开发人员不会注意到这些问题。不过,那些使用.NET
Core的开发人员会有一个额外的问题,使得整个问题更加明显。

  监视连接池
  您已经打开和关闭了一个连接,现在您希望知道该连接是否仍然处于打开状态。您可以使用几种方法来确定有多少连接仍然处于打开状态,以及它们正在执行何种操作:

在.NET
Core的RC1和RC2版本之间,引入了一个Bug,导致HttpClient.Dispose调用会产生一个介于1010毫秒和1030毫秒之间的延迟。在.NET
Core 1.2之前,这个问题预计不会得到修复。

  • 运行 sp_who 或 sp_who2。这些系统存储过程从 sysprocess
系统表返回信息,该系统表显示所有工作进程的状态及其有关信息。通常,您会看到每个连接有一个服务器进程
ID (SPID)。如果您是通过在连接字符串中使用 Application Name
参数来命名您的连接的,那么,您将很容易找到工作的连接。
  • 使用带有 SQLProfiler TSQL_Replay 模板的 SQL Server
事件探查器来跟踪打开的连接。如果您很熟悉事件探查器,此方法比通过使用
sp_who 进行轮询要更容易。
  • 使用性能监视器来监视池和连接。我稍后再讨论此方法。
  •
在代码中监视性能计数器。您可以通过使用例程来提取计数器或通过使用新的
.NET PerformanceCounter
控件来监视连接池的状况和已建立的连接的数量。这两种方法都包括在您可以从
进行下载的示例应用程序中。

使用代理类作为解决方案

  现在我们将讨论如何查找连接池计数器,以及如何使用这些监视方法。

虽然HttpClient的文档没有提及,但微软模式&实践的GitHub站点介绍了一种模式。他们把HttpClient称为“代理类”,并作了如下描述:

  连接池计数器在哪里?要监视连接池计数器,您必须监视 ADO.NET
在其中创建和增加这些计数器的系统。如果您从远程系统进行连接,ADO.NET
并不总是在 Microsoft IIS 服务器或 SQL Server 上创建池;它在 ADO.NET
代码运行的系统上创建池。此系统可以是运行 IIS、Web 应用程序或 Web
服务的远程 Windows 或中间层系统。相反,SQL Server 性能计数器位于 SQL
Server 系统上 — 而不是客户端上。

那些代理类的创建成本很高。因此,它们应该只初始化一次,并在应用程序的整个生存期内重用。然而,这些类的使用方式经常会被误解,开发人员把它们当作资源对待,认为只能根据需要请求并快速释放[……]

  使用性能监视器来监视池。如果您使用 Microsoft 管理控制台 (MMC)
Windows 2000 系统监视器管理单元,则您可以通过从 Performance
对象下拉列表中选择 “.NET CLR Data” 来用图形表示 SqlClient 计数器,如 图
1所示。请注意,您可以通过选择 _global_
计数器实例来监视所有进程,或者,您可以查看某个特定实例 —
每个池生成自己的一组监视器。性能监视器可列出这些计数器,并将它们作为所选定的性能对象的实例提供。但性能监视器不会公开这些计数器,除非有实例需要它们进行监视。例如,图
1 显示了 .NET CLR Data
性能对象,但没有列出特定实例。这意味着您必须至少创建一个连接,以便使
_global_
实例连同每个进程的特定实例一起出现。这种行为对于您的代码来说是个问题;您将无法使用
PerformanceCounter 控件来返回其中的任何计数器,直到 ADO.NET
在打开连接时创建这些计数器。所以说,这个规定真有点令人左右为难。当您使用此方法时,因为缺少有效计数器实例,所以会引发异常
— 此时要准备好捕获异常。

Microsoft
P&P建议创建一个HttpClient实例,把它存储在一个静态字段中,并在应用程序的生存期内共享该实例,而不是根据需求创建和销毁。

  您还可以通过使用 SQL Server 性能计数器 “User Connections”
来监视打开的连接的数量。该计数器被列在 Performance 对象下拉列表中的 sql
Server: General Statistics 下。我喜欢监视 “User Connections”
值和一些所选定的 .NET CLR Data SqlClient
计数器(我稍后将讨论此内容),因为我可以获得我需要的信息,而不必担心实例。

存在误导的文档

  使用代码来监视性能计数器。当您需要以编程方式监视连接池时,您可以编写代码来监视由
SqlClient 管理的性能计数器 — 这些计数器与 MMC Windows NT
性能监视器管理单元所提供的计数器是相同的。编写执行监视的代码似乎是一件有些令人畏惧的事情。但我已经提供了从
SqlClient
提供程序的内部工作提取这些计数器的例程的快照(作为本文提供的可下载程序之一)。

这将我们带回到了文档存在误导的问题。虽然是基本的样本文件,但官方文档v118(当前谷歌和必应搜索返回的结果)指出,HttpClient不支持跨线程共享。

  您可以编写检查 表
2显示的五个计数器的代码。通过利用这五个计数器,您可以实时监视连接池。.NET
预期您会在性能监视器中提供一个类别 — 复制的 performance Object —
并从那些注册到系统的计数器中选择适当的计数器。要访问 SqlClient
计数器,请将该类别设置为 “.NET CLR Data”。

该类型的任何公有静态(在Visual
Basic中为Shared)成员都是线程安全的,而任何实例成员都不保证线程安全。

  使用 PerformanceCounter
控件。您可能会发现,在设计时向您的应用程序窗体添加 PerformanceCounter
要比手工编写代码来访问性能计数器更加容易。要使用 PerformanceCounter
控件,请从“Visual Studio .NET 工具箱组件”菜单中选择一个
PerformanceCounter,将它拖到您的应用程序窗体,然后设置属性,如 图 2
所示。这些控件工作在 Web 窗体和 WinForms 应用程序中。

差不多就是这样。当然,如果你看一下官方文档v110,就会发现下面这段有用的描述。

  因为 PerformanceCounter
控件提供了方便的下拉列表,所以,您可以在设计时看到任何一种性能计数器类别、计数器名称和特定实例
— 您将要运行的实例除外。这意味着您必须使用图 2
显示的方法来捕获应用程序正在使用的池的适当实例。为了回避这个问题,我选择
_global_
实例。再次说明一下,此方法假设某个应用程序已经至少创建了一个池,因此您需要做好不存在计数器实例时
ADO.NET 引发异常的准备,就像它在不存在池连接时也会引发异常一样。

HttpClient应该只初始化一次,并在应用程序的整个生存期内重用。在负载很高的情况下,为每个请求初始化一个HttpClient类会耗尽可用的套接字数量。这会导致SocketException错误。下面的例子展示了HttpClient的正确用法:

public class GoodController : ApiController
{
    // OK
    private static readonly HttpClient HttpClient;
    static GoodController()
    {
        HttpClient = new HttpClient();
    }
}

  注意不准确的池计数。因为 SqlClient .NET 数据提供程序中存在 .NET 框架
1.1
尚未解决的错误,所以,性能计数器会在池实际上已经删除时错误地指示池“仍然存在”。我能通过结束
MMC 性能监视器管理单元、然后结束 Visual Studio .NET
来验证池已经不再存在。这些步骤说明,.NET
数据提供程序在创建连接池的进程结束时会正确地删除连接池。显然,这种不准确性降低了性能计数器在监视池方面的有效性,所以我希望
Microsoft 将来能解决这个问题。

根据这份文档,以下方法是线程安全的。

  计数器不显示的内容

  1. CancelPendingRequests

  2. DeleteAsync

  3. GetAsync

  4. GetByteArrayAsync

  5. GetStreamAsync

  6. GetStringAsync

  7. PostAsync

  8. PutAsync

  9. SendAsync

  您可能会面临的一个问题是无法从计数器或 SqlClient
属性看到每个池的配置。每个 SqlConnection 对象的 ConnectionString
保存着这些池设置的密钥。因为您不能依赖于默认设置,所以很难确定池几乎已满或很难使用。这会成为未来版本的
ADO.NET 的另一个方便功能。

这似乎是MSDN文档一直存在的问题。要了解任何类的演进过程,都必须检查每个版本的文档,才能了解到新增或删除的重要段落。

  不过,假设您知道各个连接池 ConnectionString 参数的值,则利用清单 1
中的代码,您可以很容易地设置一个计时器来检查您创建的特定池并报告使用百分比。然后,监视应用程序会向您发出警报,以便您可以解决问题并防止溢出。

DNS Bug

  最后,请记住,ado.net 采用的方法与基于 COM 的 ADO 有所不同。Visual
Basic .NET 完全改变了放弃对象的方式,并且不再确保 Connection
对象在停止作用时被关闭。请确保 SqlConnection 对象(或任何 Connection
对象)在停止作用之前被关闭。

如果我们遵循目前为止的建议,则会出现其他的问题。Ali
Kheyrollahi写道:

  连接池是一种非常强大的功能,它可以提高应用程序的性能。但如果您不是一个出色的救生员,您的连接池会成为一个危害而不是一个优点。我希望本文讨论的方法有助于您有效地监视连接池并满足用户的需要

但事实证明,有一个更严重的问题:HttpClient不遵循DNS变化,它会(通过
HttpClientHandler)独占连接,直到套接字关闭。没有时间限制!那么,DNS什么时候会发生变化呢?每次你进行蓝绿部署的时候(在
Azure云服务中,当你部署到过渡槽,然后切换生产/过渡槽);每次你改变Azure流量管理器的设置;故障转移场景;许多PaaS服务的内部。

在被报道出来之前,这种情况已经存在了两年多……我在想,我们到底使用.NET构建了怎样的应用程序?

现在,如果DNS变化的原因是故障转移,则连接应该是出现了某种形式的故障,因此,这时会打开一个到新服务器的连接。但是,如果变化的原因是蓝
绿部署,你切换了过渡环境和生产环境,而调用仍然会转到过渡环境——这是我们见过的一种行为,但已经通过重启从属服务器修复,我们认为这可能是Azure
的一个怪象。我真是个傻瓜——它就在代码里!谁的代码?好吧,起争执了……

这个问题并不是无法修复。理论上讲,HttpClient会遵循DNS
TTL(生存期)值,默认为1小时。每次过期后,HttpClient会验证该DNS记录是否仍然有效,并在必要时新建一个连接指向更新后的IP地址。

但是,由于那种情况可能不会出现,所以Kheyrollahi为我们提供了一个更简单的变通方案。借助ServicePointManager,你可以告诉HttpClient自动回收连接。

var sp = ServicePointManager.FindServicePoint(new Uri("http://foo.bar"));
sp.ConnectionLeaseTimeout = 60*1000; // 1分钟

因此,你会希望只在应用程序启动时做这件事,只做一次,并且是针对应用程序将来会访问的所有端点(如果端点是运行时确定的,就需要在发现那个端点时设置那个值)。记住,路径和查询字符串会被忽略,只有主机、端口和模式是重要的。根据场景的不同,可以将该值设为1到5分钟

(作者:Jonathan
Allen,出处:InfoQ,译者:谢丽 

查看英语原文:Bugs and Documentation Errors in .NET’s HttpClient
Frustrate Developers