.NET连接池的问题详解
NET连接池救生员
防止可淹没应用程序的池溢出
WilliamVaughn
大多数ADO.NET数据提供程序使用连接池,以提高围绕Microsoft断开连接的.NET结构构建的应用程序的性能。应用程序首先打开一个连接(或从连接池获得一个连接句柄),接着运行一个或多个查询,然后处理行集,最后将连接释放回连接池。如果没有连接池,这些应用程序将花费许多额外时间来打开和关闭连接。
当您使用ADO.NET连接池来管理基于Web的应用程序和客户端/服务器Web服务应用程序的连接时,您的客户通常会获得更快的连接和更好的总体性能。但是,当您的应用程序或Web站点上突然涌入了同时希望进行连接的大量客户时,会发生什么事情呢?您的应用程序会“沉没”,还是会“游泳”?就像救生员一样,您需要仔细监视连接池,以维护它的良好性能,并防止连接池发生溢出。我们首先探讨连接池可能溢出的原因,然后讨论如何编写代码或使用Windows性能监视器来监视连接池。
正如我于2003年5月发表的“Swimminginthe.NETConnectionPool”(InstantDocID38356)一文中讨论的那样,当您使用连接池时,您需要知道许多有关可伸缩性和性能的详细信息。请记住,您需要监视和管理两个基本因素:每个池管理的连接数和连接池的数量。在一个有效的生产系统中,池的数量通常很少(1到10),而且,使用中的连接的总数也很少(少于12)有效的查询只用不到一秒钟的时间就可以完成,并断开连接。因此,即使有数百个客户同时访问您的Web站点,相对较少的几个连接常常足以处理整个负载。为了使您的应用程序有效地运行,您必须使连接资源处于自己的控制之下,并要监视池的状态,这样,在监视池发生溢出以及您的客户开始抱怨(或离开您的网站)之前您会收到某种警告。
为什么会发生连接池溢出?
GPS平台、网站建设、软件开发、系统运维,找森大网络科技!
参加电子邮件讨论组的人常常抱怨应用程序是如何在测试中是“龙”而在形成为产品时就变成了“虫”的。有时,他们会报告说,当连接了大约100个客户端时,应用程序会停止或挂起。请记住,一个池中的默认连接数是100。如果您尝试从池中打开100个以上的连接,ADO.NET会使应用程序的连接请求排队等候,直到有空闲的连接。应用程序(及其用户)将这种情况视为进入Web页的延迟或视为应用程序死锁。让我们首先讨论一下这个问题是如何产生的。
在ADO.NET中,SqlClient.NET数据提供程序为您提供了两种打开和管理连接的方法。首先,当您需要手工管理连接时,可以使用DataReader对象。利用这种方法,您的代码将构造一个SqlConnection对象,设置ConnectionString属性,然后使用Open方法来打开连接。当代码完成DataReader后,您要在SqlConnection对象停止作用之前关闭SqlConnection。要处理行集,您可以将DataReader传递到应用程序中的另一个例程,但仍然需要确保DataReader及其连接处于关闭状态。如果您不关闭SqlConnection,代码会“泄漏”每个操作的连接,于是连接池对连接进行累积,最后便发生溢出。与ADO和VisualBasic(VB)6.0中的情况不同,.NET垃圾回收器不会为您关闭SqlConnection并进行清理。我稍后要讨论的清单1显示了如何打开连接和生成DataReader以从一个简单的查询返回行集,来向连接池施加压力的。
您也可能在使用DataAdapter对象时遇到问题。DataAdapterFill和Update方法可自动打开DataAdapter对象的连接,并在数据I/O操作完成后关闭该连接。不过,如果该连接在执行Fill或Update方法时已经处于打开状态,那么,ADO.NET在方法执行完以后不会关闭SqlConnection。这是另一个发生连接“泄漏”的机会。
此外,您还可以使用基于COM的ADO从.NET应用程序创建连接。ADO利用与ADO.NET相同的方式将这些连接组合成池,但不能像您使用SqlClientADO.NET数据提供程序时那样,提供从应用程序监视连接池的方式。
指示DataReader
孤立连接和溢出池是严重的问题,根据有关这些问题的新闻组讨论的数量来看,它们十分常见。这些问题最有可能是由DataReader引起的。为了测试DataReader的行为,我编写了一个Windows窗体(WinForms)示例应用程序,该示例突出了CommandBehavior.CloseConnection选项。(您可以在http://www.sqlmag.com上输入InstantDocID39031来下载此应用程序)。您可以在使用SqlCommand对象的ExecuteReader方法来执行查询并返回DataReader时设定此选项。我的测试应用程序显示,如果不显式关闭DataReader(或SqlConnection),即使使用此选项,连接池还是会溢出。当代码所请求的连接数超过连接池的容量时,该应用程序就会引发异常。
有些开发人员坚持认为,如果您设置CommandBehavior.CloseConnection选项,则DataReader及其相关联的连接会在DataReader完成数据读取时自动关闭。这些开发人员的看法不完全正确—只有当您在ASP.NETWeb应用程序中使用复杂的绑定控件时,该选项才以这种方式工作。在整个DataReader结果集中循环到其行集的末尾(也就是说,当Dr.Read—DataReader的Read方法—返回False时)还不足以触发连接的自动关闭。不过,如果您绑定到一个复杂的绑定控件(例如,DataGrid),该控件则会关闭DataReader和连接—前提条件是您设置了CommandBehavior.CloseConnection选项。
如果您通过使用另一个Execute方法(例如,ExecuteScalar、ExecuteNonQuery和ExecuteReader)执行查询,则您需要负责打开SqlConnection对象,而且,更重要的是,在查询结束时关闭该对象。如果您忘记了进行关闭,孤立连接会迅速地积累起来。
监视连接数
为了对孤立连接和发生溢出的连接池进行测试,我编写了一个Web窗体的示例应用程序。此应用程序使用的方法与您通常用于从查询返回数据的方法相同。(您可以在http://www.sqlmag.com上下载此代码的WinForms版本。)
我使用了清单1中的代码来打开和关闭到Web窗体应用程序的连接。标注A中的例程针对110个新的SqlConnection对象创建、打开和执行查询—比默认的池大小多10个连接。您必须在离开该例程之前关闭和放弃所有这些连接。如果不这样做,SqlConnection对象将连同关联的池连接一起被孤立。ADO.NET池机制(akathePooler)关闭数据库连接,但不关闭池连接。我将连接池大小设置为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多个以上的池连接—所有连接均处于孤立状态。虽然“SQLServer用户连接”计数为0,但留下大约40个连接池。在我重新引导系统之前,孤立的池不会消失。
我用于此测试的示例应用程序包括使用DataAdapter来返回行的例程。除非您手工管理连接,否则,DataAdapter将正确地打开和关闭SqlConnection对象,因此,您不太可能遇到孤立的池连接。不过,如果您的应用程序同时使用DataReader和DataAdapter,您可能会发现,如果某个连接与一个未关闭的DataReader相关联,则DataAdapter无法针对该连接运行查询。
确定连接池何时达到最大连接数
正如我在“Swimminginthe.NETConnectionPool”一文中讨论的那样,当连接池达到您通过“MaxPoolSizeConnectionString”选项指定的最大连接数时,ADO.NET将阻止任何随后打开额外连接的尝试。如果某个连接在您在"ConnectionTimeout选项中指定的时间之前变为可用,.NET数据提供程序将向您的应用程序传递一个指向该连接的指针,以便将控件返回给应用程序。不过,如果没有及时释放任何连接,连接请求将引发InvalidOperationException异常。
现在您必须决定要采取的措施,我不建议您告诉用户您已经用完了所有连接。有些应用程序会通知用户系统正忙于帮助其他客户,并建议用户稍后进行访问。其他应用程序则播放一段动画,通知用户系统尚未死锁,而是正在忙于处理他们的请求。同时,您的代码重新尝试操作。在所有情况下,您应该记录这些故障,以便帮助诊断问题的症结所在,并记录您已经耗尽了资源。
监视连接池
您已经打开和关闭了一个连接,现在您希望知道该连接是否仍然处于打开状态。您可以使用几种方法来确定有多少连接仍然处于打开状态,以及它们正在执行何种操作:
- 运行sp_who或sp_who2。这些系统存储过程从sysprocess系统表返回信息,该系统表显示所有工作进程的状态及其有关信息。通常,您会看到每个连接有一个服务器进程ID(SPID)。如果您是通过在连接字符串中使用ApplicationName参数来命名您的连接的,那么,您将很容易找到工作的连接。
- 使用带有SQLProfilerTSQL_Replay模板的SQLServer事件探查器来跟踪打开的连接。如果您很熟悉事件探查器,此方法比通过使用sp_who进行轮询要更容易。
- 使用性能监视器来监视池和连接。我稍后再讨论此方法。
- 在代码中监视性能计数器。您可以通过使用例程来提取计数器或通过使用新的.NETPerformanceCounter控件来监视连接池的状况和已建立的连接的数量。这两种方法都包括在您可以从http://www.sqlmag.com进行下载的示例应用程序中。
现在我们将讨论如何查找连接池计数器,以及如何使用这些监视方法。
连接池计数器在哪里?要监视连接池计数器,您必须监视ADO.NET在其中创建和增加这些计数器的系统。如果您从远程系统进行连接,ADO.NET并不总是在MicrosoftIIS服务器或SQLServer上创建池;它在ADO.NET代码运行的系统上创建池。此系统可以是运行IIS、Web应用程序或Web服务的远程Windows或中间层系统。相反,SQLServer性能计数器位于SQLServer系统上—而不是客户端上。
使用性能监视器来监视池。如果您使用Microsoft管理控制台(MMC)Windows2000系统监视器管理单元,则您可以通过从Performance对象下拉列表中选择“.NETCLRData”来用图形表示SqlClient计数器,如图1所示。请注意,您可以通过选择global计数器实例来监视所有进程,或者,您可以查看某个特定实例—每个池生成自己的一组监视器。性能监视器可列出这些计数器,并将它们作为所选定的性能对象的实例提供。但性能监视器不会公开这些计数器,除非有实例需要它们进行监视。例如,图1显示了.NETCLRData性能对象,但没有列出特定实例。这意味着您必须至少创建一个连接,以便使global实例连同每个进程的特定实例一起出现。这种行为对于您的代码来说是个问题;您将无法使用PerformanceCounter控件来返回其中的任何计数器,直到ADO.NET在打开连接时创建这些计数器。所以说,这个规定真有点令人左右为难。当您使用此方法时,因为缺少有效计数器实例,所以会引发异常—此时要准备好捕获异常。
您还可以通过使用SQLServer性能计数器“UserConnections”来监视打开的连接的数量。该计数器被列在Performance对象下拉列表中的SQLServer:GeneralStatistics下。我喜欢监视“UserConnections”值和一些所选定的.NETCLRDataSqlClient计数器(我稍后将讨论此内容),因为我可以获得我需要的信息,而不必担心实例。
使用代码来监视性能计数器。当您需要以编程方式监视连接池时,您可以编写代码来监视由SqlClient管理的性能计数器—这些计数器与MMCWindowsNT性能监视器管理单元所提供的计数器是相同的。编写执行监视的代码似乎是一件有些令人畏惧的事情。但我已经提供了从SqlClient提供程序的内部工作提取这些计数器的例程的快照(作为本文提供的可下载程序之一)。
您可以编写检查表2显示的五个计数器的代码。通过利用这五个计数器,您可以实时监视连接池。.NET预期您会在性能监视器中提供一个类别—复制的Performance—并从那些注册到系统的计数器中选择适当的计数器。要访问SqlClient计数器,请将该类别设置为“.NETCLRData”。
使用PerformanceCounter控件。您可能会发现,在设计时向您的应用程序窗体添加PerformanceCounter要比手工编写代码来访问性能计数器更加容易。要使用PerformanceCounter控件,请从“VisualStudio.NET工具箱组件”菜单中选择一个PerformanceCounter,将它拖到您的应用程序窗体,然后设置属性,如图2所示。这些控件工作在Web窗体和WinForms应用程序中。
因为PerformanceCounter控件提供了方便的下拉列表,所以,您可以在设计时看到任何一种性能计数器类别、计数器名称和特定实例—您将要运行的实例除外。这意味着您必须使用图2显示的方法来捕获应用程序正在使用的池的适当实例。为了回避这个问题,我选择global实例。再次说明一下,此方法假设某个应用程序已经至少创建了一个池,因此您需要做好不存在计数器实例时ADO.NET引发异常的准备,就像它在不存在池连接时也会引发异常一样。
注意不准确的池计数。因为SqlClient.NET数据提供程序中存在.NET框架1.1尚未解决的错误,所以,性能计数器会在池实际上已经删除时错误地指示池“仍然存在”。我能通过结束MMC性能监视器管理单元、然后结束VisualStudio.NET来验证池已经不再存在。这些步骤说明,.NET数据提供程序在创建连接池的进程结束时会正确地删除连接池。显然,这种不准确性降低了性能计数器在监视池方面的有效性,所以我希望Microsoft将来能解决这个问题。
计数器不显示的内容
您可能会面临的一个问题是无法从计数器或SqlClient属性看到每个池的配置。每个SqlConnection对象的ConnectionString保存着这些池设置的密钥。因为您不能依赖于默认设置,所以很难确定池几乎已满或很难使用。这会成为未来版本的ADO.NET的另一个方便功能。
不过,假设您知道各个连接池ConnectionString参数的值,则利用清单1中的代码,您可以很容易地设置一个计时器来检查您创建的特定池并报告使用百分比。然后,监视应用程序会向您发出警报,以便您可以解决问题并防止溢出。
最后,请记住,ADO.NET采用的方法与基于COM的ADO有所不同。VisualBasic.NET完全改变了放弃对象的方式,并且不再确保Connection对象在停止作用时被关闭。请确保SqlConnection对象(或任何Connection对象)在停止作用之前被关闭。
连接池是一种非常强大的功能,它可以提高应用程序的性能。但如果您不是一个出色的救生员,您的连接池会成为一个危害而不是一个优点。我希望本文讨论的方法有助于您有效地监视连接池并满足用户的需要。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。