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

EF Core and big traffic leads to max pool size was reached error

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

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.

我想知道我们使用 DbContext 和查询的模式是否正确,或者我是否遗漏了一些 using/dispose 模式并且错误是由内存泄漏引起的(经过一些研究后我读了然后我应该不使用 using 因为生命周期由框架管理)。我正在关注 documentation...

我的连接字符串:

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

的依赖注入时,这是一个糟糕的模式

您可以在 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 中(还?)是不可能的。

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

    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
                }

执行:

        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 this is tracked by dotnet/corefx#2949. Note that TransactionScope has already been ported to .NET Core but will only be available in .NET Core 1.2.

我认为问题是由将数据库上下文查询中的对象存储到 内存缓存 引起的。我对数据库上下文进行了一个大的 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() 写入我的所有子查询时它会有所帮助,因为分布式缓存需要物化对象。

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

我正在添加一个备选答案,以防有人因根本原因略有不同而来到这里,就像我的 .NET Core MVC 应用程序的情况一样。

在我的场景中,由于在同一控制器中混合使用 async/awaitTask.Result,应用程序产生了这些 "timeout expired... max pool size was reached" 错误。

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

Looks like we are making a blocking call to an Async method inside [...] constructor.

...

So, basically something is going wrong in the call to above highlighted async method and because of which all the threads listed above are blocked.

Looking at the threads which are doing same operation and blocked:

...

85.71% of threads blocked (174 threads)

We should avoid mixing async and blocking code. Mixed async and blocking code can cause deadlocks, more-complex error handling and unexpected blocking of context threads.

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/

Action Plan

Please engage your application team to revisit the application code of above mentioned method to understand what is going wrong.

Also, I would appreciate if you could update your application logic to not mix async and blocking code. You could use await Task instead of Task.Wait or Task.Result.

因此,在我们的例子中,我将 Task.Result 从构造函数中拉出并将其移至私有 async 方法中,我们可以在其中 await 它。然后,因为每次使用控制器我只希望它 运行 一次任务,所以我将结果存储到那个本地 属性,并且 运行 只有当属性 值为 null.

在我的辩护中,我希望编译器至少会在混合使用异步代码和阻塞代码时发出警告。然而,事后看来,这对我来说似乎很明显!

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