【问题标题】:"Timeout expired" exception on code exclusively using using statements仅使用 using 语句的代码出现“超时已过期”异常
【发布时间】:2014-05-14 15:13:07
【问题描述】:

我有一个通过 Linq to Sql 与 SQL 服务器对话的多线程应用程序。当线程数人为地保持在 8 时,应用程序在四核 (Intel I-7) 机器上运行良好:

            Parallel.ForEach(allIds, 
                    new ParallelOptions { MaxDegreeOfParallelism = 8 },
                    x => DoTheWork(x));

什么时候线程的数量留给系统决定:

                Parallel.ForEach(allIds, x => DoTheWork(x));

运行一段时间后,我得到以下异常:

超时。在获得一个之前的超时时间 来自池的连接。这可能是因为所有汇集的 正在使用连接并且已达到最大池大小。

我的应用中只有两种调用 SQL 的模式:

第一:

    using (var dc = new MyDataContext())
    {
        //do stuff
        dc.SafeSubmitChanges();
    }

秒:

        using (var dc = new MyDataContext())
        {
            //do some other stuff
            DoStuff(dc);
        }

.....
    private void DoStuff(DataContext dc)
    {
       //do stuff
       dc.SafeSubmitChanges();
    }

我决定通过这种形式的逻辑来限制调用:

public static class DataContextExtention
{
    public const int SQL_WAIT_PERIOD = 5000;
    public static void SafeSubmitChanges(this DataContext dc)
    {
        try
        {
            dc.SubmitChanges();
        }
        catch (Exception e)
        {
            if (e.Message ==
                "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.")
            {
                System.Data.SqlClient.SqlConnection.ClearAllPools();
                System.Threading.Thread.Sleep(SQL_WAIT_PERIOD);
                dc.SafeSubmitChanges();
            }
            else
            {
                throw;
            }
        }
    }
}

这完全没有区别。一旦应用程序抛出第一个此类异常,应用程序中的各种随机位置(甚至与 SQL Server 无关的代码行)都会开始抛出此异常。

Q1:难道不是虔诚地使用 using 语句来防范这种情况吗?

Q2:出了什么问题,我该如何解决?

注意:大约有 250,000 个 ID。我还在MaxDegreeOfParallelism = 16 进行了测试,我得到了同样的异常。

【问题讨论】:

  • 我不知道答案,但是你为什么要将e.Message 与一个常量字符串进行比较而不是检查e 的类型呢?这是不可接受的,尤其是任何语言都可以抛出异常。
  • 平行foreach 中进行了多少次迭代?很可能由于某种原因您正在处理DataContext,但连接没有关闭。看看你能不能把dc.Connection.Close() 放在using 块中。
  • 我认为连接的最大池大小为 100。尝试将 MaxDegreeOfParallelism 设置为 100,看看是否可行,然后尝试 101 看看是否失败。即使您的DataContexts 被正确处理(我认为是这样),一旦您用尽了连接池,其他上下文也必须等待连接释放。由于数据库操作的等待时间相对较长,Parallel.ForEach 很有可能正在启动超过 100 个并发操作。
  • @hatchet,它在 MaxDegreeOfParallelism = 16 时失败。在它失败之前我们甚至没有接近 100
  • 多线程应用程序中“各种随机位置”的异常几乎总是由于未能正确同步对多线程之间共享资源的访问。

标签: c# linq-to-sql task-parallel-library using-statement timeoutexception


【解决方案1】:

我想这取决于allIds 中有多少项目。如果Parallel.ForEach 创建了太多并行并发任务,则可能是每个都尝试打开与数据库的连接(并行),从而耗尽连接池,使其无法为所有请求新连接的并发任务提供连接。

如果满足连接池请求的时间比超时时间长,则该错误消息是有意义的。因此,当您设置MaxDegreeOfParallelism = 8 时,您的并发任务不超过 8 个,因此从池中“签出”的连接不超过 8 个。在任务完成之前(并且Parallel.ForEach 现在有一个可用槽来运行新任务)连接被返回到池中,这样当Parallel.ForEach 运行下一个项目时,连接池可以满足下一个连接请求,因此,当您人为限制并发时,您不会遇到此问题。

编辑 1

@hatched 上面的建议是正确的——增加池的大小。但是,有一个警告。您的瓶颈可能实际上并不在于计算能力,而在于数据库活动。我怀疑(推测,诚然)正在发生的是,在与数据库交谈时,线程不能做太多事情并被阻塞(或切换到另一个任务)。所以线程池看到有更多的任务挂起,但是 CPU 没有被利用(因为未完成的 IO 操作),因此决定为可用的 CPU slack 承担更多的任务。当然,这只会使瓶颈更加饱和,然后回到正题。因此,即使您增加了连接池大小,您也可能会一直碰壁,直到您的池大小与您的任务列表一样大。因此,您实际上可能希望具有有限的并行性,使其永远不会耗尽线程池(并根据数据库负载等通过使线程池更大/更小来进行微调)。

尝试找出上述是否属实的一种方法是查看为什么连接需要这么长时间并且没有返回到池中。 IE。分析以查看是否存在降低所有连接速度的数据库争用。如果是这样,更多的并行化对你没有任何好处(事实上,这会让事情变得更糟)。

【讨论】:

  • 是的,但是在 MaxDegreeOfParallelism = 8 时,我没有利用硬件的全部计算能力。我需要以不同的方式进行节流,以免使节流成为瓶颈。这就是为什么我在上面添加了 thottle 代码,但这没有用。问题依然存在。
  • @user277498 请参阅上述帖子中的EDIT 1
【解决方案2】:

我认为以下可能会有所帮助,根据我使用 Oracle 的经验,数据库连接池之前曾给我带来过问题。所以我认为 SQL Server 连接池可能存在类似的问题。有时了解默认连接设置并查看数据库上的连接活动是很好的信息。

如果您使用的是 Sql Server 8,则默认 SQL 连接池为 100。默认超时为 15 秒。我想让 SQL 管理员跟踪您在运行应用程序时建立的连接数,并查看您是否在数据库服务器上施加了负载。也许还可以添加一些性能计数器。因为这看起来像一个 SQL Server 异常,所以我会得到一些指标来看看发生了什么。您还可以使用 Intellitrace 来帮助查看 DB 活动。

Intellitrace 链接:http://www.dotnetcurry.com/showarticle.aspx?ID=943

Sql Server 2008 连接池链接:http://msdn.microsoft.com/en-us/library/8xx3tyca(v=vs.110).aspx

性能计数器链接:http://msdn.microsoft.com/en-us/library/ms254503(v=vs.110).aspx

【讨论】:

    【解决方案3】:

    我可能在这里偏离目标,但我想知道问题是否不是由于连接池这一事实的副作用引起的(取自here,强调我的):

    启用连接池后,如果发生超时错误或其他登录错误,将抛出异常随后的连接尝试将在接下来的 5 秒内失败,即“阻塞期”。如果应用程序在阻塞期内尝试连接,将再次抛出第一个异常。 阻止期结束后的后续失败将导致新的阻止期是前一个阻止期的两倍,最长为一分钟。

    • 所以换句话说,这并不是说您本身的连接用完了,而是一个或多个并行操作出现故障,可能是因为可怜的表在并行写入的压力下崩溃了- 您是否分析了数据库端发生的情况,以查看操作期间是否存在表争用问题?

    • 由于上述“惩罚”,这可能会导致其他连接请求开始备份;因此例外,一旦你开始得到一个,你的SafeSubmit方法只会让事情变得更糟,因为它不断重试已经被banjaxed的操作。

    • 这种解释也极大地支持了这样一种观点,即这里的真正瓶颈是数据库,并且尝试用无限并行 IO 锤击表可能不是一个好主意;最好根据数据库可以承受的特性来衡量并得出最大 DOP(对于不同的硬件可能会有所不同)

    另外,关于您的第一个问题,using 仅保证您的 DataContext 对象在超出范围时将自动神奇地变为 Dispose()d,因此在这种情况下它根本不是为了保护而设计的 -它只是语法糖

    try
    {
        var dc = new DataContext();
        //do stuff with dc
    }
    finally
    {
        dc.Dispose();
    }
    

    在这种情况下,这并不能防止当前有(太多)DataContexts 尝试同时连接到数据库。

    【讨论】:

      【解决方案4】:

      您确定您没有遇到连接泄漏吗?请在this link查看接受的答案

      另外,你已经设置MultipleActiveResultSets = true了吗?

      来自 MSDN:

      当为 true 时,应用程序可以维护多个活动结果集 (火星)。如果为 false,应用程序必须处理或取消所有结果 从一个批次中设置,然后才能对其执行任何其他批次 联系。公认的值是真假。

      如需了解更多信息,请参阅Multiple Active Result Sets (MARS)

      【讨论】:

      • 谢谢。因为我专门使用“使用语句”,所以我不应该有连接泄漏。正确的?我会打开 MARS 看看它是否有影响,然后通知你。
      猜你喜欢
      • 2019-03-06
      • 2016-02-29
      • 1970-01-01
      • 2013-08-29
      • 1970-01-01
      • 2013-11-23
      • 2012-03-31
      • 1970-01-01
      • 2014-01-12
      相关资源
      最近更新 更多