Entity Framework zaman uyumsuz işleminin tamamlanması on kat daha uzun sürer


139

Veritabanını işlemek için Entity Framework 6 kullanan bir MVC sitem var ve her şeyi zaman uyumsuz denetleyiciler olarak çalıştırıyor ve veritabanına çağrılar zaman uyumsuz muadilleri olarak çalıştırılıyor (ör. ToListAsync () ToList () yerine

Yaşadığım sorun, sorgularımı zaman uyumsuz olarak değiştirmenin inanılmaz derecede yavaş olmasına neden oldu.

Aşağıdaki kod, veri bağlamımdan "Albüm" nesnelerinin bir koleksiyonunu alır ve oldukça basit bir veritabanı birleştirmesine çevrilir:

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

Oluşturulan SQL şöyledir:

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

İşler ilerledikçe, oldukça karmaşık bir sorgu değil, ancak SQL sunucusunun çalıştırması neredeyse 6 saniye sürüyor. SQL Server Profiler, tamamlanması 5742 ms sürdüğünü bildiriyor.

Kodumu şu şekilde değiştirirsem:

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

Daha sonra aynı SQL üretilir, ancak SQL Server Profiler'e göre bu sadece 474ms'de çalışır.

Veritabanının "Albümler" tablosunda çok fazla olmayan yaklaşık 3500 satırı vardır ve "Artist_ID" sütununda bir dizine sahiptir, bu nedenle oldukça hızlı olmalıdır.

Async'in ek yükleri olduğunu biliyorum, ancak işleri on kat daha yavaş yapmak bana biraz dik görünüyor! Burada nerede yanlış gidiyorum?


bana doğru gelmiyor. Aynı sorguyu aynı verilerle yürütürseniz, SQL Server Profiler tarafından bildirilen yürütme süresi aşağı yukarı aynı olmalıdır, çünkü async, Sql'de değil, c # 'da olan şeydir. Sql sunucusu bile c # kodu zaman uyumsuz farkında değildir
Khanh TO

oluşturulan sorgunuzu ilk kez çalıştırdığınızda, sorguyu derlemek biraz daha uzun sürebilir (derleme yürütme planı, ...), ikinci kez aynı sorgu daha hızlı olabilir (Sql sunucusu sorguyu önbelleğe alır), ancak çok fazla farklı olmamalı.
Khanh TO

3
Neyin yavaş olduğunu belirlemelisin. Sorguyu sonsuz bir döngüde çalıştırın. Hata ayıklayıcıyı 10 kez duraklatın. En sık nerede durur? Harici kod içeren yığını kaydedin.
usr

1
Sorun, tamamen unutmuş olduğum Image özelliğiyle ilgili gibi görünüyor. Bu bir VARBINARY (MAX) sütunudır, bu nedenle yavaşlığa neden olmak zorundadır, ancak yavaşlığın yalnızca zaman uyumsuzluğu çalıştıran bir sorun haline gelmesi biraz gariptir. Görüntülerimi bağlantılı bir tablonun parçası olacak ve her şey şimdi çok daha hızlı olacak şekilde veritabanımı yeniden yapılandırdım.
Dylan Parry

1
Sorun, EF'in tüm bu bayt ve satırları almak için ADO.NET'e tonlarca asenkron okuma yayınlaması olabilir. Bu şekilde ek yük büyütülür. Ölçümü yapmadığınız için asla bilemeyeceğimizi sordum. Sorun çözülmüş gibi görünüyor.
usr

Yanıtlar:


286

Özellikle asyncAdo.Net ve EF 6 ile her yerde kullandığım için bu soruyu çok ilginç buldum, birisinin bu soru için bir açıklama yapmasını umuyordum, ama olmadı. Bu sorunu kendi tarafımda yeniden oluşturmaya çalıştım. Umarım bazılarınız bunu ilginç bulur.

İlk iyi haber: Ben çoğalttım :) Ve fark çok büyük. Faktör 8 ile ...

ilk sonuçlar

İlk önce bir şeyle uğraştığından şüpheleniyordum CommandBehavior beri ben ilginç bir yazı okudum hakkında asyncbu söyleyerek, Ado ile:

"Sıralı olmayan erişim modu, tüm satırın verilerini saklamak zorunda olduğundan, sunucudan büyük bir sütun (varbinary (MAX), varchar (MAX), nvarchar (MAX) veya XML gibi) okuyorsanız sorunlara neden olabilir. )."

Şüpheleniyordum ToList()CommandBehavior.SequentialAccess ve async olanlar CommandBehavior.Default( sorunlara neden olabilir) olmak için çağrı . Bu yüzden EF6'nın kaynaklarını indirdim ve her yere kesme noktaları koydum ( CommandBehaviortabii ki nerede kullanılırsa).

Sonuç: hiçbir şey . Tüm aramalar yapılıyor CommandBehavior.Default.... Bu yüzden neler olduğunu anlamak için EF koduna adım atmaya çalıştım ... ve .. ooouch ... Asla böyle bir temsilci kodu görmüyorum, her şey tembel görünüyor ...

Bu yüzden ne olduğunu anlamak için biraz profil yapmaya çalıştım ...

Sanırım bir şeyim var ...

İşte içinde 3500 satır ve her birinde 256 Kb rasgele veri bulunan, kıyasladığım tabloyu oluşturmak için model 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; }
}

İşte test verilerini oluşturmak için kullandığım kod ve EF'i karşılaştırmak.

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);
    }
}

Normal EF çağrısı ( .ToList()) için profil oluşturma "normal" görünür ve okunması kolaydır:

ToList izleme

Burada Kronometre ile sahip olduğumuz 8.4 saniyeyi görüyoruz (perfs'de yavaşlama profili). Ayrıca test yolundaki 3500 hat ile tutarlı olan çağrı yolu boyunca HitCount = 3500 buluyoruz. TDS ayrıştırıcı tarafında, TryReadByteArray()tamponlama döngüsünün meydana geldiği yöntemin 118 353 çağrısını okuduğumuz için işler daha da kötüleşmeye başladı . ( byte[]256 kb'nin her biri için ortalama 33,8 çağrı )

İçin asyncsöz, o, Birinci .... gerçekten çok farklı .ToListAsync()çağrı ThreadPool'da planlanan ve ardından beklenmektedir. Burada inanılmaz bir şey yok. Ama, şimdi, asyncThreadPool'daki cehennem:

ToListAsync cehennem

Birincisi, ilk durumda tam çağrı yolu boyunca sadece 3500 isabet sayımı yapıyorduk, burada 118 371 var.

İkincisi, ilk durumda, TryReadByteArray()metoda "sadece 118 353" çağrısı yapıyorduk , burada 2 050 210 çağrımız var! 17 kat daha fazla ... (büyük 1Mb dizisi ile yapılan bir testte 160 kat daha fazla)

Ayrıca:

  • 120.000 Taskörnek oluşturuldu
  • 727 519 Interlockedçağrı
  • 290569 Monitorçağrı
  • 98 283 ExecutionContextbulut sunucusu, 264481 Yakalama ile
  • 208733 SpinLockçağrı

Tahminime göre, tamponlama, paralel Görevler TDS'den veri okumaya çalışırken, asenkron bir şekilde (iyi bir değil) yapılır. İkili verileri ayrıştırmak için çok fazla Görev oluşturulur.

Bir ön sonuç olarak, Async'in harika olduğunu, EF6'nın harika olduğunu söyleyebiliriz, ancak EF6'nın mevcut uygulamasında async kullanımları, performans tarafında, İş parçacığı tarafında ve CPU tarafında (% 12 CPU kullanımı) ToList()durumda ve% 20ToListAsync 8 ila 10 kat daha fazla çalışma için ... ve eski i7 920 ile çalıştırıyorum).

Bazı testler yaparken, yine bu makaleyi düşünüyordum ve özlediğim bir şey fark ettim:

".Net 4.5'teki yeni eşzamansız yöntemler için, davranışları, dikkate değer bir istisna dışında, eşzamanlı yöntemlerle tamamen aynıdır: Sıralı olmayan modda ReadAsync."

Ne ?!!!

Ben Ado.Net düzenli / zaman uyumsuz çağrısında ve dahil etmek benim kriterler uzatmak Yani CommandBehavior.SequentialAccess/ CommandBehavior.Defaultve burada büyük bir sürpriz! :

ado ile

Ado.Net ile aynı davranışa sahibiz !!! Facepalm ...

Kesin sonuç : EF 6 uygulamasında bir hata var. Bu geçiş olmalıdır CommandBehavioriçin SequentialAccessbir zaman uyumsuz çağrı bir içeren bir tablo üzerinde yapıldığında binary(max)sütunu. Çok fazla Görev yaratma, süreci yavaşlatma sorunu Ado.Net tarafında. EF sorunu Ado.Net'i gerektiği gibi kullanmamasıdır.

Artık EF6 zaman uyumsuz yöntemlerini kullanmak yerine EF'yi zaman uyumsuz bir şekilde çağırmanız ve ardından bir TaskCompletionSource<T> sonucu zaman uyumsuz bir şekilde döndürmek için .

Not 1: Yazımı utanç verici bir hata nedeniyle düzenledim .... İlk testimi yerel olarak değil ağ üzerinden yaptım ve sınırlı bant genişliği sonuçları bozdu. İşte güncellenen sonuçlar.

Not 2: Testimi diğer kullanımlar için genişletmedim (ör: nvarchar(max) çok fazla veri ile), ancak aynı davranışın gerçekleşme olasılığı vardır.

Not 3: ToList()Durum için olağan bir şey ,% 12 CPU'dur (CPU'mun 1 / 8'i = 1 mantıksal çekirdek). ToListAsync()Programlayıcı tüm Adımları kullanamıyormuş gibi olağandışı bir şey, vaka için maksimum% 20'dir . Muhtemelen çok fazla Görev yaratıldı, ya da belki TDS ayrıştırıcısında bir darboğaz, bilmiyorum ...


2
Codeplex ile ilgili bir konu açtım, umarım bu konuda bir şeyler yaparlar. entityframework.codeplex.com/workitem/2686
rducom


5
Ne yazık ki GitHub'daki sorun, varbinary ile async kullanılmaması önerisiyle kapatıldı. Teoride varbinary, dosya iletilirken iş parçacığı daha uzun süre engelleneceğinden asenkronun en mantıklı olduğu durumda olmalıdır. Peki ikili verileri DB'ye kaydetmek istiyorsak şimdi ne yapacağız?
Stilgar

8
EF Core'da bunun hala bir sorun olup olmadığını bilen var mı? Herhangi bir bilgi veya kriter bulamadım.
Andrew Lewis

2
@AndrewLewis Arkasında bilim yok, ancak sorunlara neden olan iki sorgunun olduğu EF Core ile tekrarlanan bağlantı havuzu zaman aşımları yaşıyorum .ToListAsync()ve .CountAsync()... Bu yorum dizisini bulan başkalarına bu sorgu yardımcı olabilir. Yolunuz açık olsun.
Scott

2

Birkaç gün önce bu soruya bir bağlantım olduğu için küçük bir güncelleme yapmaya karar verdim. Orijinal yanıtın sonuçlarını şu anda en yeni EF (6.4.0) ve .NET Framework 4.7.2 sürümünü kullanarak yeniden üretebildim . Şaşırtıcı bir şekilde, bu sorun asla düzelmedi.

.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

Bu şu soruyu yalvardı: Dotnet çekirdeğinde bir gelişme var mı?

Kodu orijinal yanıttan yeni bir dotnet core 3.1.3 projesine kopyaladım ve EF Core 3.1.3'ü ekledim. Sonuçlar:

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

Şaşırtıcı bir şekilde çok fazla gelişme var. Threadpool çağrıldığı için hala biraz gecikme var gibi görünüyor, ancak .NET Framework uygulamasından yaklaşık 3 kat daha hızlı.

Umarım bu cevap gelecekte bu şekilde gönderilecek diğer insanlara yardımcı olur.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.