我该如何解决ASP.NET和SQL Server之间的连接池问题?

281

最近几天我们在网站上频繁看到这个错误信息:

"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."

我们的代码一段时间以来都没有改变过。我检查了未关闭的连接,但是发现一切正常。

  • 如何解决这个问题?

  • 我需要编辑连接池吗?

  • 如何编辑连接池的最大连接数?

  • 对于高流量网站,建议使用什么值作为最大连接数?


更新:

我需要在 IIS 中进行编辑吗?

更新:

我发现活动连接的数量在15到31之间,而且我发现 SQL Server 中配置的最大允许连接数超过3200个连接,31个连接是否太多了,或者我需要在 ASP.NET 配置中进行编辑?


2
如果我没记错的话,Max Pool Size 的默认值是100。在高负载下,大多数网站不会使用超过50个连接 - 这取决于查询完成所需的时间。在连接字符串中进行短期修复:尝试在连接字符串中设置更高的值:“Max Pool Size = ...” - splattne
多少钱?比如说200元? - Amr Elgarhy
3
我认为你应该真正地寻找问题的根源。你的查询(或其中一些)是否运行时间非常长? - splattne
也许那就是真正的原因,一个执行时间过长的查询,我会去搜索一下,谢谢。 - Amr Elgarhy
2
希望您能找到问题。建议:如果您正在使用SQL Server,请尝试使用“SQL Profiler”并查找长查询:http://www.sql-server-performance.com/articles/per/performance_audit_part8_p1.aspx - splattne
可能是您的统计数据需要更新。 - Alex Czarto
30个回答

278
在大多数情况下,连接池问题与“连接泄漏”有关。你的应用程序可能没有正确而一致地关闭其数据库连接。当你保持连接处于打开状态时,它们会一直被阻塞,直到.NET垃圾回收器通过调用它们的“Finalize()”方法来关闭它们。
你需要确保你真正关闭了连接。例如,以下代码将导致连接泄漏,如果在 .OpenClose 之间的代码抛出异常:
var connection = new SqlConnection(connectionString);

connection.Open();
// some code
connection.Close();                

正确的方法是这样的:

var connection = new SqlConnection(ConnectionString);

try
{
     connection.Open();
     someCall (connection);
}
finally
{
     connection.Close();                
}
或者
using (SqlConnection connection = new SqlConnection(connectionString))
{
     connection.Open();
     someCall(connection);
}

在类方法中返回连接时,请确保将其缓存到本地并调用其Close方法。例如,使用此代码会导致泄漏连接:

var command = new OleDbCommand(someUpdateQuery, getConnection());

result = command.ExecuteNonQuery();
connection().Close(); 
第一次调用 getConnection() 返回的连接没有被关闭。这行代码创建了一个新的连接并试图关闭它,而不是关闭你的连接。 如果你使用 SqlDataReaderOleDbDataReader,请关闭它们。即使关闭连接本身似乎可以解决问题,但在使用数据阅读器对象时要显式地关闭它们。 这篇文章 "Why Does a Connection Pool Overflow?" 来自 MSDN/SQL Magazine 解释了很多细节,并提供了一些调试策略:
  • 运行 sp_whosp_who2。这些系统存储过程返回来自 sysprocesses 系统表的信息,显示所有工作进程的状态和信息。通常,每个连接将看到一个服务器进程 ID (SPID)。如果您在连接字符串中使用应用程序名称参数命名连接,则可以轻松找到您的工作连接。
  • 使用带有 SQLProfiler TSQL_Replay 模板的 SQL Server Profiler 跟踪打开的连接。如果您熟悉 Profiler,则使用此方法比使用 sp_who 进行轮询更容易。
  • 使用 Performance Monitor 监视池和连接。我稍后将讨论这种方法。
  • 在代码中监视性能计数器。您可以使用例程提取计数器或使用新的 .NET PerformanceCounter 控件来监视连接池的健康状况和建立连接的数量。

5
一个小修正:GC从不调用对象的Dispose方法,只会调用它的终结器(如果有的话)。终结器可以在必要时进行“回退”调用以调用Dispose,但我不确定SqlConnection是否这样做。 - LukeH
1
当我们需要将最大池大小设置为50,而用户很少时,是否会出现任何性能下降的情况? - mahesh sharma

52
安装 .NET Framework v4.6.1 后,我们连接到远程数据库的时候立刻开始超时。为了解决这个问题,只需在连接字符串中添加参数 TransparentNetworkIPResolution,并将其设置为 false:

Server=myServerName;Database=myDataBase;Trusted_Connection=True;TransparentNetworkIPResolution=False

这是由于这个更改所导致的问题。请参考此链接获取更多信息:连接超时问题与.NET Framework 4.6.1的透明网络IP解析

在这种情况下,问题是“在从池中获取连接之前超时时间已过”。您的连接字符串修复是否解决了这个问题,还是存在其他握手问题? - FBryant87
1
据我记得,它是与问题中完全相同的错误消息。它发生在更新到.NET Framework v4.6.1之后立即出现。 - ajbeaven
这对我也是个问题,在Azure上运行的应用服务连接到Azure SQL数据库。我正在使用Dapper并正确处理连接,但仍然收到“在从池中获取连接之前超时时间已过”的错误消息。但现在不再有了,所以感谢@ajbeaven。 - Steve Kennaird
1
将连接字符串从NETBIOS名称切换到IP地址是否也可以解决此问题? - Mick
2
这篇微软文章解释了为什么这个设置可能有帮助 https://techcommunity.microsoft.com/t5/sql-server-support/connection-timeout-issue-with-net-framework-4-6-1/ba-p/318791 - Henrik Høyer

18

由于使用了async/await,在我的案例中发生了另一个原因,导致出现相同的错误信息:

System.InvalidOperationException:'超时已过期。在从池中获取连接之前已经过了超时时间。这可能是因为所有池化的连接都在使用中并且达到了最大池大小。'

简要概述一下发生了什么(以及我如何解决它),希望这能帮助未来的他人:

查找原因

这全部发生在一个带有Dapper和SQL Server的ASP.NET Core 3.1 Web项目中,但我认为它与该项目类型无关。

首先,我有一个中心函数来获取SQL连接:

internal async Task<DbConnection> GetConnection()
{
    var r = new SqlConnection(GetConnectionString());
    await r.OpenAsync().ConfigureAwait(false);
    return r;
}

我在很多方法中使用这个函数,比如像这样的一个:

public async Task<List<EmployeeDbModel>> GetAll()
{
    await using var conn = await GetConnection();
    var sql = @"SELECT * FROM Employee";

    var result = await conn.QueryAsync<EmployeeDbModel>(sql);
    return result.ToList();
}

正如您所看到的,我在使用新的using语句时没有使用大括号({, }),所以连接的销毁是在函数结束时完成的。

尽管如此,我仍然收到了关于无法使用连接池中更多连接的错误信息。

我开始调试我的应用程序,并让它在异常发生时停止。当它停止时,我首先查看了调用堆栈窗口,但这只显示了System.Data.SqlClient中的某些位置,对我没有真正的帮助:

enter image description here

接下来,我查看了任务窗口,这对我来说更有帮助:

enter image description here

我的GetConnection方法被调用了成千上万次并处于“等待”或“计划”状态。

任务窗口中双击这样一行,它会通过调用堆栈窗口显示我代码中相关的位置。

这帮助我找到了真正原因。以下是相关代码(仅供完整性参考):

[Route(nameof(LoadEmployees))]
public async Task<IActionResult> LoadEmployees(
    DataSourceLoadOptions loadOption)
{
    var data = await CentralDbRepository.EmployeeRepository.GetAll();

    var list =
        data.Select(async d =>
            {
                var values = await CentralDbRepository.EmployeeRepository.GetAllValuesForEmployee(d);
                return await d.ConvertToListItemViewModel(
                    values,
                    Config,
                    CentralDbRepository);
            })
            .ToListAsync();
    return Json(DataSourceLoader.Load(await list, loadOption));
}

在上述控制器操作中,我首先调用EmployeeRepository.GetAll()来从数据库表“Employee”中获取模型列表。

然后,对于返回的每个模型(即每个结果集中的每一行),我再次调用EmployeeRepository.GetAllValuesForEmployee(d)进行数据库调用。

虽然这在性能方面非常糟糕,在异步上下文中会以一种方式运行,即它会消耗连接池连接而不适当地释放它们。

解决方案

我通过删除外部 SQL 查询的内部循环中的 SQL 查询来解决了这个问题。

这应该通过完全省略它或将其移到外部 SQL 查询中的一个/多个JOIN中来完成,以便在一个单一的 SQL 查询中获取所有数据。

tl;dr / 经验教训

不要在短时间内执行大量的 SQL 查询,尤其是在使用async/await时。


15

除非你的使用量大幅增加,否则似乎不太可能只是工作积压。在我看来,最有可能的选项是某些东西在使用连接,但没有及时释放它们。你确定所有情况下都在使用using吗?或者(通过任何机制)释放了连接吗?


15

在关闭连接或数据读取器之前,您是否检查了未关闭的DataReaders和response.redirects?如果在重定向之前不将其关闭,连接将保持打开状态。


3
+1 - 或者返回 DataReader 的函数 - 在你创建它们的函数之外,连接将永远不会关闭... - splattne
1
如果您的函数返回SqlDataReader,最好将其转换为DataTable而不是提高最大池大小。 - live-love
然而,如果我在“ExecuteReader()”函数中使用“commandbehavior.closeconnection”,一旦读取器的关闭方法被调用,它将关闭连接。 - Tiyyob

13

我们的网站也时常遇到这个问题。在我们的情况下,罪魁祸首是我们的统计数据/索引过期了。这会导致之前运行快速的查询最终变得缓慢并且超时。

尝试更新受查询影响的表格的统计数据和/或重建索引,看看是否有帮助。


3
我认为这可以解释为什么查询会超时,但我不认为这可以解释为什么在尝试获取连接时会遇到超时。 - DrGriff
1
可能是由于索引不良,查询时间变长,同时使用更多的连接。 - LosManos
1
在更新了数据库的所有统计信息后,对我有用:EXEC sp_updatestats; - boateng

13
你也可以尝试这样做,以解决超时问题:
如果你的webconfig中没有添加httpRuntime,请在标签中添加。
<sytem.web>
     <httpRuntime maxRequestLength="20000" executionTimeout="999999"/>
</system.web>

并且

将你的连接字符串修改为这样:

 <add name="connstring" connectionString="Data Source=DSourceName;Initial Catalog=DBName;Integrated Security=True;Max Pool Size=50000;Pooling=True;" providerName="System.Data.SqlClient" />

最后使用

    try
    {...} 
    catch
    {...} 
    finaly
    {
     connection.close();
    }

7

我曾经遇到过同样的问题,想要分享一下我是如何找到问题源的:

在连接字符串中添加应用程序名称,然后监视与 SQL Server 的打开连接。

select st.text,
    es.*, 
    ec.*
from sys.dm_exec_sessions as es
    inner join sys.dm_exec_connections as ec on es.session_id = ec.session_id
    cross apply sys.dm_exec_sql_text(ec.most_recent_sql_handle) st
where es.program_name = '<your app name here>'

6
你可以通过在连接字符串中指定MinPoolSize=xyz和/或MaxPoolSize=xyz来指定池的最小和最大大小。然而,问题的原因可能是其他原因造成的。

4
建议的最大池大小是多少? - Amr Elgarhy
2
可能最好的方法是不要指定它,除非您有特殊要求并且知道适合您特定情况的良好池大小。 - Mehrdad Afshari
1
注意:我检查了一下,发现我的数据库的活动连接大约有22个,这个数量太多了吗? - Amr Elgarhy
我不这么认为。我想默认的池大小是100个连接。这取决于网络和SQL服务器上每个连接的负载。如果它们正在运行重型查询,可能会导致问题。此外,在初始化新连接时可能会出现网络问题,并可能导致该异常。 - Mehrdad Afshari

6

虽然可能稍晚了一些,但我最近遇到了这个错误,而且在搜索此问题时,没有任何结果能让我理解除了连接泄漏 - 而这并不是我的问题。

如果这可以帮助其他人,我将解释问题、解决方案以及我发现的一些有用信息,因为我在其他地方找不到它们。

在我的情况下,根本原因是由于未对键列进行模型属性注释,而这些键列是字符串。例如:

public class InventoryItem
{
    public string StateCode {get;set;}
    public string CompanyId {get;set;}
    public int Id {get;set;}
    // more properties removed for simplicity
}

在我的DbContext中,我配置了关键列为复合主键。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<InventoryItem>().HasKey(x => new { x.StateCode, x.CompanyId, x.Id });    
}

SQL表定义

CREATE TABLE [Example].[InventoryItem] (
    [Id] INT NOT NULL,
    [StateCode] VARCHAR (2) NOT NULL,
    [CompanyId] VARCHAR (50) NOT NULL,
    CONSTRAINT [PK_InventoryItems] PRIMARY KEY CLUSTERED ([StateCode] ASC, [CompanyId] ASC, [Id] ASC)
)

C#查询代码

public List<InventoryItem> GetAllCompanyInventory(string stateCode, string companyId)
{
    using (var context = ContextFactory.CreateContext())
    {
        return await context.InventoryItems.Where(x => x.StateCode == stateCode && x.CompanyId == companyId).ToListAsync().ConfigureAwait(false);
    }
}

在这种情况下,发生的事情是stateCode和companyId被参数化为,而查询正在使用CONVERT_IMPLICIT,这实际上导致系统无法正确使用索引。我的CPU和工作时间都达到了100%,不知何故,这转化为了连接池问题。在对这些属性添加注释之后,我的CPU从未超过5%,也再没有看到过连接池问题。以下是帮助我识别此问题的一些方法。
修复方法是对这些属性进行注释。
public class InventoryItem
{
    [System.ComponentModel.DataAnnotations.Schema.Column(TypeName = "varchar(2)")]
    public string StateCode {get;set;}
    [System.ComponentModel.DataAnnotations.Schema.Column(TypeName = "varchar(50)")]
    public string CompanyId {get;set;}
    public int Id {get;set;}
    // more properties removed for simplicity
}

改动后,服务器几乎处于睡眠状态。运行sp_who显示连接数更少,几乎全部都在休眠。

在此期间,有人提到使用sp_who查看活动连接。我可以看到当我开始在本地运行我的代码时,连接数从0增加到最大允许值,所有连接列表都包含我的主机名和运行状态。因此我认为我的代码有错误,但实际上我们每次创建上下文的地方都被适当地包装了using语句。我感觉快要失去理智了。然后我转变思路,认为这可能是症状而不是实际问题。这导致我找到了解决问题的途径。

我找到这篇文章,最终引出了这个SQL语句,它真正帮助我识别了发生了什么。

SELECT TOP 20
    (total_logical_reads/execution_count) AS avg_logical_reads,
    (total_logical_writes/execution_count) AS avg_logical_writes,
    (total_physical_reads/execution_count) AS avg_phys_reads,
    (total_worker_time/execution_count) AS avg_cpu_over_head,
total_logical_reads, total_logical_writes, total_physical_reads,
total_worker_time, execution_count, total_elapsed_time AS Duration,
plan_generation_num AS num_recompiles,
statement_start_offset AS stmt_start_offset,
    (SELECT SUBSTRING(text, statement_start_offset/2 + 1,
        (CASE WHEN statement_end_offset = -1
            THEN LEN(CONVERT(nvarchar(MAX),text)) * 2
                ELSE statement_end_offset
            END - statement_start_offset)/2)
     FROM sys.dm_exec_sql_text(sql_handle)) AS query_text,
      (SELECT query_plan FROM sys.dm_exec_query_plan(plan_handle)) AS query_plan
FROM sys.dm_exec_query_stats a
--JUST CHANGE THE ORDER BY TO GET THE OTHER RESOURCES
ORDER BY (total_logical_reads + total_logical_writes)/execution_count DESC

从这里,我可以获取查询文本和查询计划,发现一些输出的总工作时间超过40亿,而其他重要的查询只有大约3亿。这些超高的工作时间并不是预期的,因为它们在搜索谓词中确切地使用了索引键。查看实际查询计划,我能看到一些东西:
  1. 需要读取极高的行数(例如,估计每次执行行数仅为1,但需要读取的行数超过1000万)
  2. 使用了CONVERT_IMPLICIT
  3. 建议创建另一个索引,但实际上已经有了一个相同的索引(简化示例有点复杂,但实际索引有更多列,省略前两列以避免转换)
这篇文章帮助我理解了即使有一个使用我正在搜索的确切键的索引,为什么我的估计行数还是那么高。 这篇文章帮助我理解了我正在审查的查询计划中的CONVERT_IMPLICIT调用。
我找到了上面的文章,因为它在这篇帖子中提到。 这篇文章帮助我找出如何将默认值从nvarchar更改为varchar。
我仍然不确定为什么这会导致连接池超时问题。我有一些测试正在运行,每次只执行一个状态代码+公司ID,但几乎立即就会出现此错误。一旦我添加了注释,即使同时处理多个请求,一切都变得清晰明了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接