การดำเนินการ async Framework Entity ใช้เวลาสิบเท่าในการดำเนินการให้เสร็จสมบูรณ์


139

ฉันมีไซต์ MVC ที่ใช้ Entity Framework 6 เพื่อจัดการฐานข้อมูลและฉันได้ทำการทดลองกับการเปลี่ยนแปลงเพื่อให้ทุกอย่างทำงานเป็นตัวควบคุม async และการเรียกไปยังฐานข้อมูลนั้นจะทำงานเหมือน async counterparts (เช่น ToListAsync () แทน ToList ())

ปัญหาที่ฉันมีคือเพียงแค่เปลี่ยนการสืบค้นของฉันเป็น async ทำให้พวกเขาช้าอย่างไม่น่าเชื่อ

รหัสต่อไปนี้จะได้รับการรวบรวมวัตถุ "Album" จากบริบทข้อมูลของฉันและถูกแปลเป็นฐานข้อมูลที่ค่อนข้างง่าย:

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

นี่คือ SQL ที่สร้างขึ้น:

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

ทุกอย่างดำเนินไปได้ไม่ใช่แบบสอบถามที่ซับซ้อนมาก แต่ใช้เวลาเกือบ 6 วินาทีเพื่อให้เซิร์ฟเวอร์ SQL เรียกใช้ SQL Server Profiler รายงานว่ามันใช้เวลา 5742 มิลลิวินาทีในการทำให้เสร็จสมบูรณ์

ถ้าฉันเปลี่ยนรหัสเป็น:

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

จากนั้นจะมีการสร้าง SQL ที่เหมือนกัน แต่สิ่งนี้จะทำงานในเวลาเพียง 474 มิลลิวินาทีตาม SQL Server Profiler

ฐานข้อมูลมีแถวประมาณ 3,500 แถวในตาราง "อัลบั้ม" ซึ่งมีไม่มากนักและมีดัชนีในคอลัมน์ "Artist_ID" ดังนั้นจึงควรรวดเร็ว

ฉันรู้ว่า async มีค่าโสหุ้ย แต่การทำให้ทุกอย่างช้าลงสิบเท่าดูเหมือนว่าจะสูงชันสำหรับฉัน! ฉันจะไปผิดที่นี่ที่ไหน


มันดูไม่ถูกต้องสำหรับฉัน ถ้าคุณดำเนินการแบบสอบถามเดียวกันกับข้อมูลเดียวกันเวลาดำเนินการรายงานโดย SQL Server Profiler ควรมากหรือน้อยเหมือนกันเพราะ async เป็นสิ่งที่เกิดขึ้นใน c # ไม่ใช่ SQL เซิร์ฟเวอร์ Sql ไม่ทราบว่ารหัส c # ของคุณเป็น async
Khanh ถึง

เมื่อคุณเรียกใช้แบบสอบถามที่สร้างขึ้นในครั้งแรกอาจใช้เวลานานในการรวบรวมแบบสอบถาม (แผนการดำเนินการสร้าง, ... ) จากครั้งที่สองแบบสอบถามเดียวกันอาจเร็วกว่า (เซิร์ฟเวอร์ Sql แคชแบบสอบถาม) ไม่ควรมีความแตกต่างมากเกินไป
Khanh ถึง

3
คุณต้องพิจารณาว่าอะไรช้า เรียกใช้แบบสอบถามในการวนซ้ำไม่สิ้นสุด หยุดการดีบัก 10 ครั้ง หยุดที่ไหนบ่อยที่สุด โพสต์สแต็กรวมถึงรหัสภายนอก
usr

1
ดูเหมือนว่าปัญหาจะเกิดขึ้นกับคุณสมบัติอิมเมจซึ่งฉันลืมไปโดยสิ้นเชิง มันเป็นคอลัมน์ VARBINARY (สูงสุด) ดังนั้นจึงถูกผูกไว้เพื่อทำให้เกิดความเชื่องช้า แต่ก็ยังแปลกอยู่เล็กน้อยที่ความช้าจะกลายเป็นปัญหาที่ใช้ async ฉันปรับโครงสร้างฐานข้อมูลของฉันใหม่เพื่อให้รูปภาพเป็นส่วนหนึ่งของตารางที่เชื่อมโยงและตอนนี้ทุกอย่างเร็วขึ้นมาก
Dylan Parry

1
ปัญหาอาจเป็นได้ว่า EF กำลังออก async จำนวนมากที่อ่านไปยัง ADO.NET เพื่อดึงข้อมูลไบต์และแถวเหล่านั้นทั้งหมด วิธีนี้จะทำให้ค่าใช้จ่ายขยาย เนื่องจากคุณไม่ได้ทำการวัดฉันถามว่าเราจะไม่มีทางรู้ ดูเหมือนว่าปัญหาจะได้รับการแก้ไข
usr

คำตอบ:


286

ฉันพบคำถามนี้น่าสนใจมากโดยเฉพาะอย่างยิ่งเมื่อฉันใช้asyncทุกที่กับ Ado.Net และ EF 6 ฉันหวังว่าจะมีคนให้คำอธิบายสำหรับคำถามนี้ แต่ก็ไม่ได้เกิดขึ้น ดังนั้นฉันจึงพยายามทำซ้ำปัญหานี้ที่ด้านข้างของฉัน ฉันหวังว่าบางท่านจะพบว่าสิ่งนี้น่าสนใจ

ข่าวดีแรก: ฉันทำซ้ำ :) และความแตกต่างนั้นมหาศาล ด้วยปัจจัย 8 ...

ผลลัพธ์แรก

ครั้งแรกที่ฉันสงสัยว่ามีบางอย่างเกี่ยวข้องกับCommandBehaviorเนื่องจากฉันอ่านบทความที่น่าสนใจเกี่ยวasyncกับ Ado พูดนี้:

"เนื่องจากโหมดการเข้าถึงที่ไม่ต่อเนื่องจำเป็นต้องเก็บข้อมูลสำหรับทั้งแถวจึงอาจทำให้เกิดปัญหาหากคุณอ่านคอลัมน์ขนาดใหญ่จากเซิร์ฟเวอร์ (เช่น varbinary (MAX), varchar (MAX), nvarchar (MAX) หรือ XML )."

ฉันสงสัยว่ามีการToList()โทรเป็นCommandBehavior.SequentialAccessและแบบอะซิงโครนัสเป็นแบบCommandBehavior.Defaultไม่ต่อเนื่องซึ่งอาจทำให้เกิดปัญหา ดังนั้นฉันจึงดาวน์โหลดแหล่งที่มาของ EF6 และใส่จุดพักทุกที่ ( CommandBehaviorแน่นอนว่าใช้ที่ไหน)

ส่งผลให้เกิด: ไม่มีอะไร ทุกการโทรทำด้วยCommandBehavior.Default.... ดังนั้นฉันจึงพยายามที่จะก้าวไปสู่โค้ดของ EF เพื่อทำความเข้าใจว่าเกิดอะไรขึ้น ... และ .. ooouch ... ฉันไม่เคยเห็นรหัสผู้แทนเช่นนั้นทุกอย่างดูเหมือนจะถูกดำเนินการขี้เกียจ ...

ดังนั้นฉันจึงพยายามทำโปรไฟล์เพื่อทำความเข้าใจว่าเกิดอะไรขึ้น ...

และฉันคิดว่าฉันมีบางสิ่ง ...

นี่คือรูปแบบการสร้างตารางที่ฉันสร้างเกณฑ์มาตรฐานโดยมี 3,500 บรรทัดอยู่ในนั้นและ 256 Kb ข้อมูลสุ่มในแต่ละ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; }
}

และนี่คือรหัสที่ฉันใช้ในการสร้างข้อมูลการทดสอบและเกณฑ์มาตรฐานของ 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);
    }
}

สำหรับการโทรแบบปกติของ EF ( .ToList()) การทำโปรไฟล์ดูเหมือนว่า "ปกติ" และอ่านง่าย:

ToList trace

ที่นี่เราพบ 8.4 วินาทีที่เรามีกับนาฬิกาจับเวลา (การทำโปรไฟล์ช้าลง perfs) นอกจากนี้เรายังพบ HitCount = 3500 ตามเส้นทางการโทรซึ่งสอดคล้องกับ 3500 สายในการทดสอบ ในด้านตัวแยกวิเคราะห์ TDS สิ่งต่าง ๆ เริ่มแย่ลงเนื่องจากเราอ่านTryReadByteArray()วิธีการโทร 118 353 ซึ่งเกิดจากการวนรอบบัฟเฟอร์ (เฉลี่ย 33.8 สายสำหรับแต่ละbyte[]256kb)

สำหรับasyncกรณีนี้มันแตกต่างกันจริงๆ .... ก่อนอื่นการ.ToListAsync()โทรถูกกำหนดเวลาไว้ใน ThreadPool จากนั้นรอ ไม่มีอะไรน่าอัศจรรย์ที่นี่ แต่ตอนนี้นี่คือasyncนรกบน ThreadPool:

ToListAsync นรก

ครั้งแรกในกรณีแรกเรามีจำนวนการเข้าชม 3,500 ครั้งตลอดเส้นทางการโทรเต็มรูปแบบที่นี่เรามี 118 371 นอกจากนี้คุณต้องจินตนาการถึงการซิงโครไนซ์สายทั้งหมดที่ฉันไม่ได้วางบนหน้าจอ ...

ประการที่สองในกรณีแรกเราได้รับ "เพียง 118 353" TryReadByteArray()วิธีการที่นี่เรามี 2 050 210 สาย! มันมากกว่า 17 เท่า ... (ในการทดสอบกับอาร์เรย์ 1Mb ขนาดใหญ่มันเพิ่มขึ้น 160 เท่า)

นอกจากนี้ยังมี:

  • Taskสร้างอินสแตนซ์120,000 รายการแล้ว
  • Interlockedโทร727 519
  • 290 569 การMonitorโทร
  • ExecutionContextอินสแตนซ์98 283 กับ 264 481 จับ
  • การSpinLockโทร208 733

ฉันเดาว่าการบัฟเฟอร์จะทำในลักษณะ async (และไม่ใช่แบบที่ดี) ด้วย Tasks แบบขนานที่พยายามอ่านข้อมูลจาก TDS มีการสร้าง Task มากเกินไปเพื่อแยกวิเคราะห์ข้อมูลไบนารี

จากบทสรุปเบื้องต้นเราสามารถพูดได้ว่า Async นั้นยอดเยี่ยม EF6 นั้นยอดเยี่ยม แต่การใช้ async ของ EF6 ในการใช้งานในปัจจุบันนั้นเพิ่มค่าใช้จ่ายที่สำคัญด้านประสิทธิภาพการทำงานด้าน Threading และด้าน CPU (การใช้งาน CPU 12% ในToList()เคสและ 20% ในToListAsyncเคสสำหรับการทำงานนานกว่า 8 ถึง 10 เท่า ... ฉันรันบน i7 920 รุ่นเก่า)

ในขณะที่ทำการทดสอบบางอย่างฉันกำลังคิดถึงบทความนี้อีกครั้งและฉันสังเกตเห็นบางสิ่งที่ฉันพลาด:

"สำหรับวิธีการแบบอะซิงโครนัสใหม่ใน. Net 4.5 พฤติกรรมของพวกเขาเหมือนกับวิธีการแบบซิงโครนัสยกเว้นสำหรับข้อยกเว้นที่โดดเด่นอย่างหนึ่ง: ReadAsync ในโหมดที่ไม่ต่อเนื่อง"

อะไร ?!!!

ดังนั้นฉันจึงขยายมาตรฐานเพื่อรวม Ado.Net ในการโทรปกติ / async และด้วยCommandBehavior.SequentialAccess/ CommandBehavior.Defaultและนี่เป็นเรื่องที่น่าประหลาดใจมาก! :

ด้วยความกังวลใจ

เรามีพฤติกรรมเดียวกันกับ Ado.Net !!! facepalm ...

ข้อสรุปที่ชัดเจนของฉันคือมีข้อบกพร่องในการใช้งาน EF 6 มันควรจะสลับCommandBehaviorไปSequentialAccessเมื่อมีสายเรียก async จะทำมากกว่าตารางที่มีbinary(max)คอลัมน์ ปัญหาของการสร้างงานมากเกินไปทำให้กระบวนการช้าลงคือด้าน Ado.Net ปัญหาของ EF คือไม่ใช้ Ado.Net เท่าที่ควร

ตอนนี้คุณรู้แทนที่จะใช้วิธี async ของ EF6 คุณจะต้องโทรหา EF ในแบบที่ไม่ใช่ async ตามปกติจากนั้นใช้ a TaskCompletionSource<T>เพื่อส่งคืนผลลัพธ์ในรูปแบบ async

หมายเหตุ 1: ฉันแก้ไขโพสต์ของฉันเนื่องจากมีข้อผิดพลาดน่าอับอาย .... ฉันได้ทำการทดสอบครั้งแรกผ่านเครือข่ายไม่ใช่ในพื้นที่และแบนด์วิดท์ จำกัด มีการบิดเบือนผลลัพธ์ นี่คือผลการอัพเดท

หมายเหตุ 2: ฉันไม่ได้ขยายการทดสอบของฉันไปยังกรณีการใช้งานอื่น (เช่น: nvarchar(max)มีข้อมูลจำนวนมาก) แต่มีโอกาสที่พฤติกรรมเดียวกันจะเกิดขึ้น

หมายเหตุ 3: สิ่งปกติสำหรับToList()กรณีนี้คือ CPU 12% (1/8 ของ CPU = 1 ตรรกะหลัก) สิ่งผิดปกติคือสูงสุด 20% สำหรับToListAsync()กรณีเช่นเดียวกับ Scheduler ไม่สามารถใช้ Treads ทั้งหมดได้ อาจเป็นเพราะงานที่สร้างขึ้นมากเกินไปหรืออาจเป็นปัญหาคอขวดในตัวแยกวิเคราะห์ TDS ฉันไม่รู้ ...


2
ฉันเปิดปัญหา codeplex หวังว่าพวกเขาจะทำอะไรเกี่ยวกับมัน entityframework.codeplex.com/workitem/2686
rducom

3
ฉันเปิดปัญหาเกี่ยวกับ repo โค้ดใหม่ของ EF ที่โฮสต์ไว้บน github: github.com/aspnet/EntityFramework6/issues/88
Korayem

5
น่าเศร้าปัญหาใน GitHub ถูกปิดด้วยคำแนะนำที่จะไม่ใช้ async กับ varbinary ในทฤษฎี varbinary ควรเป็นกรณีที่ async เหมาะสมที่สุดเนื่องจากเธรดจะถูกบล็อกอีกต่อไปในขณะที่ส่งไฟล์ ดังนั้นเราจะทำอย่างไรถ้าเราต้องการบันทึกข้อมูลไบนารีในฐานข้อมูล
Stilgar

8
มีใครรู้บ้างว่านี่ยังคงเป็นปัญหาใน EF Core หรือไม่? ฉันไม่พบข้อมูลหรือการวัดประสิทธิภาพ
Andrew Lewis

2
@AndrewLewis ฉันไม่มีวิทยาศาสตร์อยู่เบื้องหลัง แต่ฉันมีเวลาในการเชื่อมต่อซ้ำกับ EF Core ที่ทั้งสองข้อความค้นหาเป็นสาเหตุของปัญหา.ToListAsync()และ.CountAsync()... สำหรับใครก็ตามที่ค้นหาเธรดความคิดเห็นนี้ข้อความค้นหานี้อาจช่วยได้ โชคดี
สกอตต์

2

เพราะฉันมีลิงค์ไปยังคำถามนี้สองสามวันที่ผ่านมาฉันตัดสินใจที่จะโพสต์การปรับปรุงเล็กน้อย ฉันสามารถสร้างผลลัพธ์ของคำตอบเดิมโดยใช้รุ่นล่าสุดของ EF (6.4.0) และ. NET Framework 4.7.2 น่าแปลกที่ปัญหานี้ไม่เคยดีขึ้น

.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

คำถามนี้ขอร้อง: มีการปรับปรุงใน dotnet core หรือไม่?

ฉันคัดลอกรหัสจากคำตอบดั้งเดิมไปยังโครงการ dotnet core 3.1.3 ใหม่และเพิ่ม EF Core 3.1.3 ผลลัพธ์ที่ได้คือ:

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

น่าแปลกใจที่มีการปรับปรุงมากมาย ยังคงมีบางช่วงเวลาที่ล่าช้าเนื่องจาก threadpool ถูกเรียกใช้ แต่เร็วกว่าการใช้. NET Framework ประมาณ 3 เท่า

ฉันหวังว่าคำตอบนี้จะช่วยให้คนอื่น ๆ ที่ได้รับการส่งแบบนี้ในอนาคต

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.