Hoạt động không đồng bộ của Entity Framework mất mười lần thời gian để hoàn thành


139

Tôi đã có một trang web MVC sử dụng Entity Framework 6 để xử lý cơ sở dữ liệu và tôi đã thử nghiệm thay đổi nó để mọi thứ chạy như bộ điều khiển không đồng bộ và các cuộc gọi đến cơ sở dữ liệu được chạy dưới dạng đối tác không đồng bộ của chúng (ví dụ: ToListAsync () thay vì ToList ())

Vấn đề tôi gặp phải là việc thay đổi truy vấn của tôi thành async đã khiến chúng bị chậm một cách khó tin.

Đoạn mã sau nhận được một tập hợp các đối tượng "Album" từ ngữ cảnh dữ liệu của tôi và được dịch sang một cơ sở dữ liệu khá đơn giản:

// Get the albums
var albums = await this.context.Albums
    .Where(x => x.Artist.ID == artist.ID)
    .ToListAsync();

Đây là SQL được tạo:

exec sp_executesql N'SELECT 
[Extent1].[ID] AS [ID], 
[Extent1].[URL] AS [URL], 
[Extent1].[ASIN] AS [ASIN], 
[Extent1].[Title] AS [Title], 
[Extent1].[ReleaseDate] AS [ReleaseDate], 
[Extent1].[AccurateDay] AS [AccurateDay], 
[Extent1].[AccurateMonth] AS [AccurateMonth], 
[Extent1].[Type] AS [Type], 
[Extent1].[Tracks] AS [Tracks], 
[Extent1].[MainCredits] AS [MainCredits], 
[Extent1].[SupportingCredits] AS [SupportingCredits], 
[Extent1].[Description] AS [Description], 
[Extent1].[Image] AS [Image], 
[Extent1].[HasImage] AS [HasImage], 
[Extent1].[Created] AS [Created], 
[Extent1].[Artist_ID] AS [Artist_ID]
FROM [dbo].[Albums] AS [Extent1]
WHERE [Extent1].[Artist_ID] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=134

Theo mọi thứ, đây không phải là một truy vấn phức tạp, nhưng phải mất gần 6 giây để máy chủ SQL chạy nó. SQL Server Profiler báo cáo nó là mất 5742ms để hoàn thành.

Nếu tôi thay đổi mã của mình thành:

// Get the albums
var albums = this.context.Albums
    .Where(x => x.Artist.ID == artist.ID)
    .ToList();

Sau đó, cùng một SQL chính xác được tạo ra, nhưng điều này chỉ chạy trong 474ms theo SQL Server Profiler.

Cơ sở dữ liệu có khoảng 3500 hàng trong bảng "Album", thực sự không nhiều và có một chỉ mục trên cột "Artist_ID", do đó, nó sẽ khá nhanh.

Tôi biết rằng async có chi phí cao, nhưng làm cho mọi thứ chậm hơn mười lần có vẻ hơi dốc đối với tôi! Tôi đang sai ở đâu đây?


nó không đúng với tôi Nếu bạn thực hiện cùng một truy vấn với cùng một dữ liệu, thời gian thực hiện được báo cáo bởi SQL Server Profiler sẽ ít nhiều giống nhau vì async là những gì xảy ra trong c #, không phải Sql. Máy chủ Sql thậm chí không biết rằng mã c # của bạn không đồng bộ
Khánh đến

khi bạn chạy truy vấn đã tạo lần đầu tiên, có thể mất một chút thời gian để biên dịch truy vấn (xây dựng kế hoạch thực hiện, ...), từ lần thứ hai, cùng một truy vấn có thể nhanh hơn (máy chủ Sql lưu trữ truy vấn), nhưng không nên có quá nhiều sự khác biệt
Khánh ĐẾN

3
Bạn cần xác định những gì chậm. Chạy truy vấn trong một vòng lặp vô hạn. Tạm dừng trình gỡ lỗi 10 lần. Nơi nào nó dừng lại thường xuyên nhất? Đăng ngăn xếp bao gồm cả mã bên ngoài.
usr

1
Có vẻ như vấn đề là do thuộc tính Image mà tôi hoàn toàn quên mất. Đó là cột VARBINARY (MAX), do đó bị ràng buộc gây ra sự chậm chạp, nhưng vẫn hơi lạ khi sự chậm chạp chỉ trở thành một vấn đề khi chạy không đồng bộ. Tôi đã cấu trúc lại cơ sở dữ liệu của mình để các hình ảnh bây giờ là một phần của bảng được liên kết và mọi thứ bây giờ nhanh hơn nhiều.
Dylan Parry

1
Vấn đề có thể là EF đang phát hành hàng tấn async đọc lên ADO.NET để lấy tất cả các byte và hàng đó. Bằng cách đó, chi phí được phóng to. Vì bạn không thực hiện phép đo nên tôi hỏi chúng tôi sẽ không bao giờ biết. Vấn đề dường như được giải quyết.
usr

Câu trả lời:


286

Tôi thấy câu hỏi này rất thú vị, đặc biệt là khi tôi đang sử dụng asyncở mọi nơi với Ado.Net và EF 6. Tôi đã hy vọng ai đó đưa ra lời giải thích cho câu hỏi này, nhưng nó đã không xảy ra. Vì vậy, tôi đã cố gắng tái tạo vấn đề này về phía tôi. Tôi hy vọng một số bạn sẽ tìm thấy điều này thú vị.

Tin tốt đầu tiên: Tôi đã sao chép nó :) Và sự khác biệt là rất lớn. Với hệ số 8 ...

kết quả đầu tiên

Đầu tiên tôi đã nghi ngờ điều gì đó CommandBehavior, vì tôi đã đọc một bài viết thú vị vềasync Ado, nói điều này:

"Do chế độ truy cập không tuần tự phải lưu trữ dữ liệu cho toàn bộ hàng, nên nó có thể gây ra sự cố nếu bạn đang đọc một cột lớn từ máy chủ (chẳng hạn như varbinary (MAX), varchar (MAX), nvarchar (MAX) hoặc XML ). "

Tôi đã nghi ngờ ToList()các cuộc gọi đến CommandBehavior.SequentialAccessvà không đồng bộCommandBehavior.Default (không liên tục, có thể gây ra sự cố). Vì vậy, tôi đã tải xuống các nguồn của EF6 và đặt các điểm dừng ở mọi nơi ( CommandBehaviortất nhiên là nơi được sử dụng).

Kết quả: không có gì . Tất cả các cuộc gọi được thực hiện vớiCommandBehavior.Default .... Vì vậy, tôi đã cố gắng bước vào mã EF để hiểu điều gì xảy ra ... và .. ooouch ... Tôi chưa bao giờ thấy một mã ủy nhiệm như vậy, mọi thứ dường như lười biếng được thực thi ...

Vì vậy, tôi đã cố gắng thực hiện một số hồ sơ để hiểu những gì xảy ra ...

Và tôi nghĩ rằng tôi có một cái gì đó ...

Đây là mô hình để tạo bảng tôi đã được điểm chuẩn, với 3500 dòng bên trong và 256 Kb dữ liệu ngẫu nhiên trong mỗi bảng varbinary(MAX). (EF 6.1 - CodeFirst - CodePlex ):

public class TestContext : DbContext
{
    public TestContext()
        : base(@"Server=(localdb)\\v11.0;Integrated Security=true;Initial Catalog=BENCH") // Local instance
    {
    }
    public DbSet<TestItem> Items { get; set; }
}

public class TestItem
{
    public int ID { get; set; }
    public string Name { get; set; }
    public byte[] BinaryData { get; set; }
}

Và đây là mã tôi đã sử dụng để tạo dữ liệu thử nghiệm và điểm chuẩn EF.

using (TestContext db = new TestContext())
{
    if (!db.Items.Any())
    {
        foreach (int i in Enumerable.Range(0, 3500)) // Fill 3500 lines
        {
            byte[] dummyData = new byte[1 << 18];  // with 256 Kbyte
            new Random().NextBytes(dummyData);
            db.Items.Add(new TestItem() { Name = i.ToString(), BinaryData = dummyData });
        }
        await db.SaveChangesAsync();
    }
}

using (TestContext db = new TestContext())  // EF Warm Up
{
    var warmItUp = db.Items.FirstOrDefault();
    warmItUp = await db.Items.FirstOrDefaultAsync();
}

Stopwatch watch = new Stopwatch();
using (TestContext db = new TestContext())
{
    watch.Start();
    var testRegular = db.Items.ToList();
    watch.Stop();
    Console.WriteLine("non async : " + watch.ElapsedMilliseconds);
}

using (TestContext db = new TestContext())
{
    watch.Restart();
    var testAsync = await db.Items.ToListAsync();
    watch.Stop();
    Console.WriteLine("async : " + watch.ElapsedMilliseconds);
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess);
        while (await reader.ReadAsync())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReaderAsync SequentialAccess : " + watch.ElapsedMilliseconds);
    }
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
        while (await reader.ReadAsync())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReaderAsync Default : " + watch.ElapsedMilliseconds);
    }
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
        while (reader.Read())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReader SequentialAccess : " + watch.ElapsedMilliseconds);
    }
}

using (var connection = new SqlConnection(CS))
{
    await connection.OpenAsync();
    using (var cmd = new SqlCommand("SELECT ID, Name, BinaryData FROM dbo.TestItems", connection))
    {
        watch.Restart();
        List<TestItem> itemsWithAdo = new List<TestItem>();
        var reader = cmd.ExecuteReader(CommandBehavior.Default);
        while (reader.Read())
        {
            var item = new TestItem();
            item.ID = (int)reader[0];
            item.Name = (String)reader[1];
            item.BinaryData = (byte[])reader[2];
            itemsWithAdo.Add(item);
        }
        watch.Stop();
        Console.WriteLine("ExecuteReader Default : " + watch.ElapsedMilliseconds);
    }
}

Đối với cuộc gọi EF thông thường ( .ToList()), cấu hình có vẻ "bình thường" và dễ đọc:

Theo dõi danh sách

Ở đây chúng tôi tìm thấy 8.4 giây chúng tôi có với Đồng hồ bấm giờ (định hình làm chậm sự hoàn hảo). Chúng tôi cũng tìm thấy HitCount = 3500 dọc theo đường dẫn cuộc gọi, phù hợp với 3500 dòng trong thử nghiệm. Về phía trình phân tích cú pháp TDS, mọi thứ bắt đầu trở nên tồi tệ hơn khi chúng ta đọc 118 353 cuộc gọi trên TryReadByteArray()phương thức, đó là vòng lặp đệm xảy ra. (trung bình 33,8 cuộc gọi cho mỗi byte[]256kb)

Đối với asynctrường hợp, nó thực sự rất khác biệt .... Đầu tiên, .ToListAsync()cuộc gọi được lên lịch trên ThreadPool, và sau đó được chờ đợi. Không có gì tuyệt vời ở đây. Nhưng, bây giờ, đây là asyncđịa ngục trên ThreadPool:

Địa ngục ToListAsync

Đầu tiên, trong trường hợp đầu tiên, chúng tôi chỉ có 3500 lượt truy cập dọc theo đường dẫn cuộc gọi đầy đủ, ở đây chúng tôi có 118 371. Hơn nữa, bạn phải tưởng tượng tất cả các cuộc gọi đồng bộ hóa mà tôi đã không đặt trên màn hình ...

Thứ hai, trong trường hợp đầu tiên, chúng tôi đã có các cuộc gọi "chỉ 118 353" cho TryReadByteArray()phương thức, ở đây chúng tôi có 2 cuộc gọi 050 210! Nó gấp 17 lần ... (trong một thử nghiệm với mảng 1Mb lớn, gấp 160 lần)

Hơn nữa, có:

  • 120 000 Tasktrường hợp được tạo
  • 727 519 Interlockedcuộc gọi
  • 290 569 Monitorcuộc gọi
  • 98 283 ExecutionContexttrường hợp, với 264 481 Chụp
  • 208 733 SpinLockcuộc gọi

Tôi đoán là bộ đệm được thực hiện theo cách không đồng bộ (và không phải là tốt), với các Nhiệm vụ song song cố gắng đọc dữ liệu từ TDS. Quá nhiều tác vụ được tạo ra chỉ để phân tích dữ liệu nhị phân.

Như một kết luận sơ bộ, chúng ta có thể nói Async là tuyệt vời, EF6 là tuyệt vời, nhưng cách sử dụng không đồng bộ của EF6 trong việc triển khai hiện tại của nó thêm một chi phí lớn, về phía hiệu suất, phía Threading và phía CPU (sử dụng CPU 12% trong ToList()trường hợp và 20% trongToListAsync trường hợp cho một công việc dài hơn 8 đến 10 lần ... Tôi chạy nó trên một chiếc i7 920 cũ).

Trong khi thực hiện một số bài kiểm tra, tôi đã suy nghĩ về bài viết này một lần nữa và tôi nhận thấy một điều tôi bỏ lỡ:

"Đối với các phương thức không đồng bộ mới trong .Net 4.5, hành vi của chúng hoàn toàn giống với các phương thức đồng bộ, ngoại trừ một ngoại lệ đáng chú ý: ReadAsync ở chế độ không tuần tự."

Gì ?!!!

Vì vậy, tôi mở rộng điểm chuẩn của mình để đưa Ado.Net vào cuộc gọi thường xuyên / không đồng bộ và với CommandBehavior.SequentialAccess/ CommandBehavior.Default, và đây là một bất ngờ lớn! :

với ado

Chúng tôi có hành vi tương tự chính xác với Ado.Net !!! Khuôn mặt ...

Kết luận dứt khoát của tôi là : có lỗi trong triển khai EF 6. Nó sẽ chuyển CommandBehaviorsang SequentialAccesskhi một cuộc gọi không đồng bộ được thực hiện trên một bảng có chứa một binary(max)cột. Vấn đề tạo quá nhiều Nhiệm vụ, làm chậm quá trình, nằm ở phía Ado.Net. Vấn đề của EF là nó không sử dụng Ado.Net.

Bây giờ bạn đã biết thay vì sử dụng các phương thức không đồng bộ của EF6, tốt hơn bạn nên gọi cho EF theo cách không đồng bộ thông thường, sau đó sử dụng một TaskCompletionSource<T> để trả về kết quả theo cách không đồng bộ.

Lưu ý 1: Tôi đã chỉnh sửa bài đăng của mình vì một lỗi đáng xấu hổ .... Tôi đã thực hiện thử nghiệm đầu tiên của mình qua mạng chứ không phải cục bộ và băng thông hạn chế đã làm sai lệch kết quả. Dưới đây là kết quả cập nhật.

Lưu ý 2: Tôi đã không mở rộng thử nghiệm của mình sang các trường hợp sử dụng khác (ví dụ: nvarchar(max)có nhiều dữ liệu), nhưng có nhiều khả năng hành vi tương tự xảy ra.

Lưu ý 3: Một cái gì đó thông thường cho ToList()trường hợp, là CPU 12% (1/8 CPU của tôi = 1 lõi logic). Một cái gì đó bất thường là tối đa 20% cho ToListAsync()trường hợp, như thể Trình lập lịch biểu không thể sử dụng tất cả các Tread. Có lẽ do có quá nhiều Tác vụ được tạo hoặc có thể là nút cổ chai trong trình phân tích cú pháp TDS, tôi không biết ...


2
Tôi đã mở một vấn đề trên codeplex, hy vọng họ sẽ làm gì đó với nó. entityframework.codeplex.com/workitem/2686
bắt cóc

3
Tôi đã mở một vấn đề về repo mã EF mới được lưu trữ trên github: github.com/aspnet/EntityFramework6/issues/88
Korayem

5
Đáng buồn thay, vấn đề trên GitHub đã được đóng lại với lời khuyên không sử dụng async với varbinary. Trong lý thuyết varbinary nên là trường hợp mà async có ý nghĩa nhất vì luồng sẽ bị chặn lâu hơn trong khi tệp được truyền. Vậy chúng ta phải làm gì bây giờ nếu muốn lưu dữ liệu nhị phân trong DB?
Stilgar

8
Bất cứ ai cũng biết nếu đây vẫn là một vấn đề trong EF Core? Tôi đã không thể tìm thấy bất kỳ thông tin hoặc điểm chuẩn.
Andrew Lewis

2
@AndrewLewis Tôi không có khoa học đằng sau nó, nhưng tôi đã lặp đi lặp lại thời gian chờ kết nối với EF Core trong đó hai truy vấn gây ra sự cố .ToListAsync().CountAsync()... Đối với bất kỳ ai khác tìm thấy luồng nhận xét này , truy vấn này có thể hữu ích. Thần tốc.
Scott

2

Bởi vì tôi đã nhận được một liên kết đến câu hỏi này một vài ngày trước, tôi quyết định đăng một bản cập nhật nhỏ. Tôi đã có thể tái tạo kết quả của câu trả lời ban đầu bằng cách sử dụng phiên bản mới nhất của EF (6.4.0) và .NET Framework 4.7.2. Đáng ngạc nhiên là vấn đề này không bao giờ được cải thiện.

.NET Framework 4.7.2 | EF 6.4.0 (Values in ms. Average of 10 runs)

non async : 3016
async : 20415
ExecuteReaderAsync SequentialAccess : 2780
ExecuteReaderAsync Default : 21061
ExecuteReader SequentialAccess : 3467
ExecuteReader Default : 3074

Điều này đặt ra câu hỏi: Có sự cải thiện nào trong lõi dotnet không?

Tôi đã sao chép mã từ câu trả lời ban đầu sang dự án dotnet core 3.1.3 mới và thêm EF Core 3.1.3. Kết quả là:

dotnet core 3.1.3 | EF Core 3.1.3 (Values in ms. Average of 10 runs)

non async : 2780
async : 6563
ExecuteReaderAsync SequentialAccess : 2593
ExecuteReaderAsync Default : 6679
ExecuteReader SequentialAccess : 2668
ExecuteReader Default : 2315

Đáng ngạc nhiên là có rất nhiều cải tiến. Dường như vẫn còn một chút thời gian trễ vì luồng xử lý được gọi nhưng nó nhanh hơn khoảng 3 lần so với triển khai .NET Framework.

Tôi hy vọng câu trả lời này sẽ giúp những người khác nhận được cách này trong tương lai.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.