EF核心和大流量导致最大池大小达到错误

时间:2016-09-29 06:50:42

标签: c# asp.net asp.net-web-api entity-framework-core .net-core

我们正在使用ASP.NET Entity Framework Core在我们的Web API应用程序中查询我们的MSSQL数据库。有时当我们有大量流量时,查询到DB会以此错误结束:

  

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

我想知道我们使用DbContext和查询的模式是否正确,或者我是否缺少某些使用/ dispose模式和错误是由于某些内存泄漏造成的(在我读过一些研究之后我不应该使用因为生命周期由框架管理)。我正在关注documentation ...

我的connectionString:

"myConnection": "Server=xxx;Database=xxx;user id=xxx;password=xxx;Max Pool Size=200;Timeout=200;"

我的Startup.cs

public void ConfigureServices(IServiceCollection services)
    {
    .....
    // scoped context            
    services.AddDbContext<MyDbContext>(
            options => options.UseSqlServer(this.Configuration.GetConnectionString("myConnection")));
    }

然后在控制器中我通过依赖注入使用了dbcontext:

public class MyController : Controller
   public MyController (MyDbContext context)
    {
        this.Context = context;            
    }

    public ActionResult Get(int id)
    {
        // querying
        return this.Context.tRealty.Where(x=>x.id == id).FirstOrDefault();
    }

我应该使用类似的东西:

using (var context = this.Context)
        {
            return this.Context.tRealty.Where(x => x.id == id).FirstOrDefault();
        }

但我认为当我使用DbContext的依赖注入时,这是一种糟糕的模式。

3 个答案:

答案 0 :(得分:6)

我认为问题是由将数据库上下文查询中的对象存储到内存缓存中引起的。我有一个大的LINQ查询到数据库上下文,里面有一些其他子查询。我在主查询结束时调用FirstOrDefault(),但不在子查询内。控制器没问题,它默认实现了查询。

 return this.Context.tRealty.AsNoTracking().Where(
                x => x.Id == id && x.RealtyProcess == RealtyProcess.Visible).Select(
                s => new
                { .....

// subquery
videos = s.TVideo.Where(video => video.RealtyId == id && video.IsPublicOnYouTube).
                        Select(video => video.YouTubeId).ToList()), // missing ToList()
.....
 }).FirstOrDefault();

存在问题 - 当子存储存储到内存缓存时,子查询与数据库上下文保持连接。当我实现 Redis分布式缓存时,它首先失败了一些奇怪的错误。当我将ToList()FirstOrDefault()写入所有子查询时,它会有所帮助,因为分布式缓存需要物化对象。

现在我明确地实现了所有查询,并且没有达到最大池大小错误。因此,当存储的对象从数据库上下文查询到内存缓存时,必须小心。需要实现所有查询以避免在内存中的某处保持连接。

答案 1 :(得分:2)

您可以在startup.cs中设置DbContext的生命周期,看看是否有帮助:

    services.AddDbContext<MyDbContext>(options => options
                                       .UseSqlServer(connection), ServiceLifetime.Scoped);

此外,如果您的查询是简单阅读,则可以使用.AsNoTracking()删除跟踪。

提高吞吐量的另一种方法是通过使用带IsolationLevel.ReadUncommitted的事务块进行简单读取来防止锁定。 如果您不想要脏读,也可以使用Snapshot隔离级别 - 这是一个更严格的限制。

TransactionOptions transactionOptions = new TransactionOptions() { IsolationLevel = IsolationLevel.ReadUncommitted};
using (TransactionScope transactionScope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
{
  // insert magic here
}

编辑:正如提到的问题的作者,上述代码在EF Core中不是(还可以?)。

可以使用显式事务找到here的解决方法:

    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();

        using (var transaction = connection.BeginTransaction())
        {
           // transaction.Commit();
           // transaction.Rollback();
        }
    }

我没有测试过这个。

编辑2:另一个未经测试的代码片段,您可以在其中执行命令来设置隔离级别:

                using (var c1= new SqlConnection(connectionString))
                {
                    c1.Open();
                    // set isolation level
                    Exec(c1, "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;");
                    Exec(c1, "BEGIN TRANSACTION;");
                    // do your magic here
                }

使用Exec:

        private static void Exec(SqlConnection c, string s)
        {
            using (var m = c.CreateCommand())
            {
                m.CommandText = s;
                m.ExecuteNonQuery();
            }
        }

编辑3:根据that线程,从.NET Core 1.2版开始支持事务。

  

@mukundabrt这是由dotnet / corefx#2949跟踪的。注意   TransactionScope已经移植到.NET Core,但只会移植到.NET Core   可在.NET Core 1.2中使用。

答案 2 :(得分:0)

我要添加一个替代答案,以防万一有人以略有不同的根本原因来到这里,就像我的.NET Core MVC应用程序一样。

在我的场景中,由于在同一环境中混合使用async / awaitTask.Result,因此应用程序产生了这些“超时已到期...已达到最大池大小”错误控制器。

我这样做是为了通过在构造函数中调用某个异步方法来设置属性来重用代码。由于构造函数不允许异步调用,因此我不得不使用Task.Result。但是,我使用async Task<IActionResult>方法在同一控制器中进行await个数据库调用。我们聘请了Microsoft支持,一名工程师帮助解释了为什么会发生这种情况:

  

好像我们正在对内部的Async方法进行阻塞调用   [...]构造函数。

     

...

     

因此,上述调用基本上出了问题   突出显示的异步方法,因此列出了所有线程   以上被阻止。

     

查看正在执行相同操作并被阻塞的线程:

     

...

     

85.71%的线程被阻止(174个线程)

     

我们应该避免混合使用异步代码和阻塞代码。混合异步和   阻塞代码可能导致死锁,更复杂的错误处理以及   意外阻止上下文线程。

     

https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

     

https://blogs.msdn.microsoft.com/jpsanders/2017/08/28/asp-net-do-not-use-task-result-in-main-context/

     

行动计划

     

请与您的应用程序团队合作,重新访问上述方法的应用程序代码,以了解发生的情况   错误的。

     

此外,如果您可以将应用程序逻辑更新为   不要混合使用异步代码和阻塞代码。您可以使用await Task代替   Task.Wait或Task.Result。

因此,在我们的案例中,我将Task.Result从构造函数中拉出,并将其移至专用的async方法中,可以在其中进行await。然后,由于我只希望它每次使用控制器都运行一次任务,因此我将结果存储到该本地属性,并且仅在属性值为null时才从该方法内部运行任务。

在我的辩护中,如果混合异步代码和阻塞代码有问题,我希望编译器至少会发出警告。但是,事后看来,这对我来说似乎很明显!

希望这对某人有帮助...