【问题标题】:Entity Framework Core - Query ran simultaneously behaves differently on different environmentEntity Framework Core - 同时运行的查询在不同的环境中表现不同
【发布时间】:2020-12-17 13:38:45
【问题描述】:

问题: 我注意到同时开始的实体框架调用的处理时间越来越长。该程序是 .NET Core 2.2,带有 EF 2.2

以下是演示该行为的日志示例:

在第 30 次查询时持续长达 10 秒。

调查:

为了重现和测试这一点,我创建了一个简单的可执行文件,它将并行调用方法 Test() 20 次,Test() 从 ServiceProvider 获取作用域上下文,并对数据库进行查询。我已经执行了几次,并显示了每个设置的 3 个结果的屏幕截图。注意:Query() 方法在 20 次测试之前运行一次,允许 EF 生成查询缓存

 private async Task Test()
    {
        List<Task> tasks = new List<Task>();
        // Populate the queue.
        for (int i = 0; i < 10000; i++)
        {
            cq.Enqueue(i);
        }
        Query();
        Thread.Sleep(1000);
        Query();
        Thread.Sleep(1000);
        for (int i = 0; i < 20; i++)
        {
            tasks.Add(Task.Run(() => Query()));
        }


        await Task.WhenAll(tasks);

    }



    private async Task Query()
    {
        cq.TryDequeue(out int id);

        Log.Debug($"[{id}] {DateTime.Now.ToString("mm:ss.fff")}: started");

        using (var serviceScope = _serviceProvider.CreateScope())
        {
            var watch = new Stopwatch();
            var ctx = serviceScope.ServiceProvider.GetService<Core.Models.MyContext>();

            watch.Start();

            var quoteLineJob = await ctx.QuoteLineJobs
                .FirstOrDefaultAsync(o => o.Id == id);

            watch.Stop();

            Log.Debug($"[{id}] {DateTime.Now.ToString("mm:ss.fff")}: Quote line job loaded in {watch.ElapsedMilliseconds}ms");
            
        }
    }

我无法理解结果,希望有人能提出一个假设:

  • 从生产客户端计算机到生产数据库

加载时间越来越长

  • 从开发人员机器到生产数据库

但是,访问相同的数据库,结果要慢得多。

  • 随机服务器到生产数据库

结果与开发人员的机器非常相似,但是,它有 50% 的可能性是随机行为(参见第三个结果)而不是增量

我想补充一点,查看 SQL Server Profiler,对于所有设置,所有同时查询的读取查询需要 0 毫秒。

好像这并不令人困惑,当我在 localhost 上尝试相同的测试时:

  • 开发者的机器到开发者的数据库(生产中的数据库副本,相同的数据)

这里的结果是随机的,不递增

  • 随机服务器到随机服务器数据库

同样,加载时间是随机的,不会增加

有没有人看到一些模式,以及为什么 Entity Framework 会有这样的行为?

【问题讨论】:

  • 请提供代码块中的代码,而不是图片,这样会更方便其他人提供帮助
  • 可能您的 DbContext 存在时间过长,并且一直跟踪通过它的所有实体。使用代码而不是图像提供minimal reproducible example
  • 让我猜可能不是你的情况。在运行查询之前,它应该被翻译成 SQL、生成的映射器等。它在 EF 中可能是一个缓慢的过程,所以 EF 正在缓存已翻译的查询。如果很多线程开始执行相同的查询 - 他们错过了缓存。这意味着许多线程开始翻译相同的 LINQ 查询。一旦一个线程完成翻译 - 查询存储在缓存中,尚未开始运行的新线程重用已翻译的 LINQ 查询 - 并更快地开始工作。
  • @SvyatoslavDanyliv,这是一个很好的观点,我实际上忘了说我在 20 个测试开始之前运行了一次查询,这是一个需要 3-7 秒的慢速查询,所以所有的它们从缓存中运行。我已经编辑了我的帖子,请查看代码
  • 那么,如果您使用new Your context(),它是否可以在没有 DI 的情况下工作? DI 长什么样子?

标签: c# entity-framework .net-core entity-framework-core


【解决方案1】:

我的第一个建议是确定原因是 EF 还是您的服务包装器,具体取决于实现方式。将您的测试改为此并检查不同环境的性能:

var watch = new Stopwatch();
watch.Start();
using (var ctx = new YourAppDbContext(ConnectionString))
{
    var quoteLineJob = await ctx.QuoteLineJobs
        .FirstOrDefaultAsync(o => o.Id == id);
}
watch.Stop();

Log.Debug($"[{id}] {DateTime.Now.ToString("mm:ss.fff")}: Quote line job loaded in {watch.ElapsedMilliseconds}ms");
        

如果这表现得更可预测,那么您的上下文范围或 DbContext 的提供者有问题。 (不太可能,但没看代码,有可能)

如果它仍然有些变化,这可能是并行化的症状。在执行异步代码时,等待的方法将设置恢复执行点并释放正在执行的线程。 .Net 将响应恢复线程操作的请求。鉴于这些操作已经被并行化到任务中,一项测试是同步进行 EF 调用以评估是否恢复了更可预测的性能。服务器的内核数量和整体负载情况可能会极大地影响 20 个或更多并行化的执行方式。

var watch = new Stopwatch();
watch.Start();
using (var ctx = new YourAppDbContext(ConnectionString))
{
    var quoteLineJob = ctx.QuoteLineJobs
        .FirstOrDefault(o => o.Id == id);
}
watch.Stop();

Log.Debug($"[{id}] {DateTime.Now.ToString("mm:ss.fff")}: Quote line job loaded in {watch.ElapsedMilliseconds}ms");

同样,这只是为了帮助确定性能难题的潜在原因。 async/await 不等于更好的性能,它们使单个操作变慢,但使服务器作为一个整体响应处理更多请求,而无需等待较慢或高频任务完成后再接收请求。线程之间的上下文切换将意味着每个任务的运行速度都会变慢,并且可能会在服务器处于负载状态时发生变化。我会将async 操作保留给预计会花费大量时间或预计会极其频繁地调用的任务。

【讨论】:

  • 感谢您的建议。异步/等待是麻烦制造者,通过删除它,我没有注意到每个实例的增量加载时间,一些序列有它但只有几毫秒,EF 查询要快得多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-10
  • 1970-01-01
  • 1970-01-01
  • 2018-11-28
  • 2014-03-10
  • 1970-01-01
相关资源
最近更新 更多