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

设计错误、缺陷及文档错误等导致正确使用.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的开发人员会有一个额外的问题,使得整个问题更加明显。

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

网站地图xml地图