Datenpunkte

Reduzieren der Netzwerklatenz für SQL Azure mithilfe von Entity Framework

Julie Lerman

Julie LermanAuf den ersten Blick scheint der Wechsel von einer lokal verwalteten SQL Server-Datenbank zur cloudbasierten Microsoft SQL Azure-Datenbank schwierig zu sein. In Wirklichkeit ist es sehr einfach: Ändern Sie einfach die Verbindungszeichenfolge – das war es schon! Das ist es, was wir Entwickler lieben – es funktioniert einfach wie vorgesehen.

Der Wechsel verursacht jedoch Netzwerklatenzen, die erhebliche Auswirkungen auf die Gesamtleistung der Anwendung haben können. Ein gutes Verständnis der Auswirkungen von Netzwerklatenzen kann Ihnen jedoch helfen, mittels des Entity Framework die Auswirkungen im Zusammenhang mit SQL Azure zu reduzieren.

Profilieren des Datenzugriffscodes

Ich verwende die Visual Studio 2010-Profilierungstools (msdn.microsoft.com/library/ms182372) für den Vergleich verschiedener Datenzugriffsaktivitäten mit der AdventureWorksDW-Datenbank, die im lokalen Netzwerk und in meinem SQL Azure-Konto gespeichert ist. Ich verwende den Profilierer, um Aufrufe für das Laden einiger Kunden aus der Datenbank zu untersuchen, wobei ich Entity Framework verwende. Mit einem Test werden nur die Kunden abgefragt und anschließend die entsprechenden Vertriebsinformationen abgerufen. Hierfür verwende ich die Lazy Loading-Funktion von Entity Framework 4.0. In einem anschließenden Test werden die Kundenumsätze zusammen mit den Kunden mittels Eager Load geladen. Hierfür verwende ich die Include-Methode. Abbildung 1 zeigt die Konsolenanwendung, die ich für die Ausführung dieser Abfrage und die Enumeration der Ergebnisse verwendet habe.

Abbildung 1 – Abfragen zur Untersuchung der Leistung

{

  using (var context = new AdventureWorksDWEntities())

  {

    var warmupquery = context.Accounts.FirstOrDefault();

  }

  DoTheRealQuery();

}



private static void DoTheRealQuery()

{

  using ( var context=new  AdventureWorksDWEntities())

  {

    var query =   context.Customers.Where(c => c.InternetSales.Any()).Take(100);

    var customers = query.ToList();

    EnumerateCustomers(customers);

  }

}



private static void EnumerateCustomers(List<Customer> customers)

{

  foreach (var c in customers)

  {

    WriteCustomers (c);

    

  }

}



private static void WriteCustomer(Customer c)

{

  Console.WriteLine

 ("CustomerName: {0} First Purchase: {1}  # Orders: {2}",

    c.FirstName.Trim() + "" + c.LastName,  c.DateFirstPurchase, c.InternetSales.Count);

}

Ich beginne mit einer Abfrage, um den Aufwand für das Laden der Entity Data Model (EDM)-Metadaten in den Speicher, die Vorabkompilierung von Ansichten und andere einmalige Prozesse zu erledigen. Mithilfe der DoTheRealQuery-Methode werden ein Teilsatz der Kundenentitäten abgefragt, die Abfrage zu einer Liste ausgeführt und die Ergebnisse enumeriert. Während der Enumeration wird auf die Kundenumsätze zugegriffen. Dadurch wird in diesem Fall ein Lazy Load erzwungen, der auf die Datenbank zugreift, um jeweils die Umsätze für die einzelnen Kunden während der Iteration abzurufen.

Betrachtung der Leistung in einem lokalen Netzwerk

Bei der Ausführung dieses Prozesses auf einer lokal installierten SQL Server-Datenbank im lokalen Netzwerk benötigt der erste Aufruf 233 Millisekunden für die Ausführung. Dies liegt daran, dass nur Kunden abgerufen werden. Wenn der Code die Enumeration ausführt, die den Lazy Load erzwingt, werden 195 Millisekunden benötigt.

Nun ändere ich die Abfrage, sodass sie die Onlineumsätze zusammen mit den Kunden mittels Eager Load abruft:

context.Customers.Include("InternetSales").Where(c => c.InternetSales.Any()).Take(100);

Diese 100 Kunden werden nun zusammen mit den Vertriebsdaten von der Datenbank zurückgegeben. Das sind nun sehr viel mehr Daten.

Der Aufruf query.ToList dauert nun ungefähr 350 Millisekunden und nimmt damit ungefähr 30 Prozent mehr Zeit in Anspruch als die reine Rückgabe der 100 Kunden.

Diese Änderung hat eine weitere Auswirkung: Während der Code die Kunden enumeriert, sind die Vertriebsdaten bereits im Speicher vorhanden. Das bedeutet, dass Entity Framework die Datenbank nicht weitere 100 Mal aufrufen muss. Die Enumeration benötigt einschließlich der Details nur ungefähr 70 Prozent der Zeit, die die Lazy Load-Funktion benötigt hat.

Angesichts der Menge der Daten, der Geschwindigkeit des Computers und des lokalen Netzwerks ist die Lazy Load-Variante in diesem Beispiel etwas schneller, wenn Sie die Daten mittels Eager Load laden. Es ist jedoch immer noch so schnell, dass der Unterschied nicht bemerkt werden kann. Dank Entity Framework werden beide Abfragen rasend schnell ausgeführt.

Abbildung 2 zeigt einen Vergleich zwischen Eager Load und Lazy Load in einem lokalen Netzwerk. Die ToList-Spalte misst die Abfrageausführung. Dies ist folgende Codezeile: var customers = query.ToList();. Die Enumeration misst die EnumerateCustomers-Methode. Und schließlich misst die Query & Enumeration-Spalte die gesamte DoTheRealQuery-Methode, die die Ausführung, die Enumeration, die Instantiierung des Kontextes und die Deklaration der Abfrage selbst verbindet.

image: Comparing Eager Loading to Lazy Loading from a Local Database

Abbildung 2 Vergleich von Eager Load und Lazy Load von einer lokalen Datenbank

Wechsel zur SQL Azure-Datenbank

Ich ändere die Verbindungszeichenfolge nun zur SQL Azure-Datenbank in der Cloud. Es ist ist keine Überraschung, dass es nun Netzwerklatenz zwischen meinem Computer und der Clouddatenbank gibt. Die Abfragen dauern nun im Vergleich zu den Abfragen für die lokale Datenbank länger. Sie können die Latzen in diesem Szenario nicht vermeiden. Die Latenz nimmt jedoch nicht für alle Aufgaben in gleichem Maß zu. Für einige Anfragen ist die Latenz sehr viel höher als für andere. Betrachten Sie Abbildung 3.

image: Comparing Eager Loading to Lazy Loading from SQL Azure

Abbildung 3 Vergleich von Eager Load und Lazy Load von SQL Azure

Das Laden der Graphen mittels Eager Load ist immer noch langsamer, als wenn die Kunden vorab geladen werden. Im Fall der lokalen Datenbank war der Vorgang um 30 Prozent langsamer. Im Fall von SQL Azure dauert er nun jedoch dreimal so lang wie das Laden mittels Lazy Load.

Wie Sie jedoch in Abbildung 3 sehen können, ist der Lazy Load-Prozess am meisten von der Netzwerklatenz betroffen. Nachdem die Onlineumsatzdaten dank Eager Load in den Speicher geladen wurde, wird die Enumeration der Daten genauso schnell wie in der ersten Testreihe durchgeführt. Der Lazy Load-Vorgang führt jedoch zu 100 zusätzlichen Aufrufen der Clouddatenbank. Aufgrund der Latenz benötigen diese Aufrufe nun mehr Zeit, und dieser zeitliche Unterschied ist nun deutlich spürbar. 

Die Enumeration nimmt sehr viel mehr Zeit in Anspruch als die Enumeration aus dem Speicher. Für jeden Aufruf der Datenbank, um die Onlineumsatzdaten eines Kunden aufzurufen, wird deutlich viel Zeit benötigt. Insgesamt, auch wenn es sicherlich sehr viel schneller ist, wenn die Kunden vorab geladen werden, wurde in dieser Umgebung ungefähr sechsmal so viel Zeit für den Abruf sämtlicher Daten mittels Lazy Load benötigt.

Dies soll keine Argumentation gegen SQL Azure sein. SQL Azure hat tatsächlich eine sehr hohe Leistung. Diese Ausführungen sollen Sie darauf aufmerksam machen, dass sich die Wahl des Entity Framework-Abfragemechanismus aufgrund der Netzwerklatenzen negativ auf die Gesamtleistung auswirken kann.

Der besondere Verwendungsfall in dieser Demo ist für eine Anwendung nicht typisch, da Sie in der Regel Daten für große Mengen von Objekten nicht mittels Lazy Load laden. Dies hebt jedoch hervor, dass die Rückgabe einer großen Anzahl von Daten auf einmal (mittels Eager Load) in diesem Fall sehr viel effizienter ist als die Rückgabe der gleichen Anzahl von Daten mittels mehrerer Aufrufe der Datenbank. Wenn Sie eine lokale Datenbank verwenden, ist der Unterschied möglicherweise nicht so deutlich wie bei der Verwendung einer Clouddatenbank. Eine genaue Betrachtung lohnt sich daher, wenn Sie von der lokalen Datenbank zu SQL Azure wechseln.

Abhängig von der Form der Daten, die zurückgegeben werden – vielleicht eine sehr viel komplexere Abfrage mit zahlreichen Includes – kann das Eager Load-Verfahren oder das Lazy Load-Verfahren teurer sein. Es kann in einigen Fällen sogar sinnvoll sein, die Last zu verteilen: Einige Daten werden mittels Eager Load und einige Daten werden mittels Lazy Load geladen, abhängig von den Ergebnissen der Leistungsprofilierung. Und in vielen Fällen wird die ultimative Lösung darin bestehen, die Anwendung ebenfalls in die Cloud zu verschieben. Windows Azure und SQL Azure wurden für die gemeinsame Verwendung entwickelt. Durch die Verschiebung der Anwendung nach Windows Azure und den Abruf der Daten von SQL Azure optimieren Sie die Leistung.

Verwenden von Projizierungen zur Optimierung der Abfrageergebnisse

Bei der Verwendung von lokal ausgeführten Anwendungen können Sie in einigen Fällen die Abfragen überarbeiten, um die Menge der Daten, die von der Datenbank zurückgegeben werden, weiter zu optimieren. Eine Technik besteht in der Verwendung von Projizierungen, die Ihnen mehr Kontrolle über die zurückgegebenen Daten geben. Weder Eager Load noch Lazy Load ermöglichen Ihnen in Entity Framework das Filtern oder Sortieren der zurückgegebenen Daten. Mittels Projizierungen kann dies durchgeführt werden.

Beispielsweise gibt die Abfrage in dieser modifizierten Version der TheRealQuery-Methode nur einen Teilsatz der InternetSales-Entitäten zurück – die Entitäten, deren Wert gleich oder größer 1.000 ist:

private static void TheRealQuery()

    {

      using ( var context=new  AdventureWorksDWEntities())

      {

        Decimal salesMinimum = 1000;

        var query =

            from customer in context.Customers.Where(c => 

            c.InternetSales.Any()).Take(100)

            select new { customer, Sales = customer.InternetSales };

        IEnumerable customers = query.ToList();

        context.ContextOptions.LazyLoadingEnabled = false;

        EnumerateCustomers(customers);

      }

    }

Die Abfrage gibt dieselben 100 Kunden wie die vorherige Abfrage zurück, zusammen mit insgesamt 155 Onlineumsatz-Einträgen. Ohne die Aktivierung des SalesAmount-Filters waren dies 661 Umsatzeinträge.

Beachten Sie diesen wichtigen Hinweis zu Projizierungen und Lazy Load: Bei der Projizierung der Daten erkennt der Kontext die verwandten Daten nicht als geladene Daten. Dies erfolgt in den Fällen, in denen diese mittels der Include-Methode, der Load-Methode oder Lazy Load geladen werden. Daher ist es wichtig, Lazy Load vor der Enumeration zu deaktivieren, wie ich es für die TheRealQuery-Methode getan habe. Andernfalls werden die Onlineumsatz-Daten mittels Lazy Load geladen, auch wenn diese bereits im Speicher vorhanden sind. Dadurch benötigt die Enumeration sehr viel mehr Zeit als notwendig.

Die modifizierte Enumerationsmethode berücksichtigt dies:

private static void EnumerateCustomers(IEnumerable customers)

{

  foreach (var item in customers)

  {

    dynamic dynamicItem=item;

    WriteCustomer((Customer)dynamicItem.customer);

  }

}

Die Methode nutzt auch die Vorteile des dynamic-Typs in C# 4, um späte Bindung durchzuführen.

Abbildung 4 zeigt den deutlichen Leistungsgewinn, der durch die besser optimierte Projizierung erzielt wird.

image: Comparing Eager Loading to a Filtered Projection from SQL Azure

Abbildung 4 Vergleich von Eager Load und einer gefilterten Projizierung aus SQL Azure

Es mag offensichtlich erscheinen, dass die gefilterte Abfrage schneller als die Eager Load-Abfrage ist, die mehr Daten zurückgibt. Der interessantere Vergleich besteht darin, dass die SQL Azure-Datenbank die gefilterte Projizierung ungefähr 70 Prozent schneller durchführt, während die gefilterte Projizierung auf der lokalen Datenbank nur um ungefähr 15 Prozent schneller als die Eager Load-Abfrage ist. Ich vermute, dass die mittels Eager Load geladene InternetSales-Sammlung aufgrund der Art des internen Zugriffs von Entity Network dafür verantwortlich ist, dass die Speicherenumeration im Vergleich zur projizierten Sammlung schneller durchgeführt wird. Da die Enumeration in diesem Fall vollständig im Speicher durchgeführt wird, hat sie keine Auswirkungen auf die Netzwerklatenz. Insgesamt betrachtet überwiegen die Vorteile der Projizierung die Nachteile im Hinblick auf die Enumeration.

Möglicherweise scheint es für Ihr Netzwerk nicht erforderlich zu sein, die Abfrageergebnisse mittels Projizierung zu optimieren. Im Zusammenhang mit SQL Azure kann diese Art der Optimierung jedoch zu wesentlichen Leistungsvorteilen für eine Anwendung führen.

Alles in der Cloud

Die von mir behandelten Szenarien betreffen lokal gehostete Anwendungen oder Dienste, die SQL Azure verwenden. Sie können stattdessen die Anwendung oder den Dienst ebenfalls in der Cloud in Windows Azure hosten. Beispielsweise können Endbenutzer mittels Silverlight auf eine Windows Azure-Webrolle zugreifen, die Windows Communication Foundation ausführt, die wiederum auf Daten in SQL Azure zugreift. In diesem Fall gibt es keine Netzwerklatenzen zwischen dem cloudbasierten Dienst und SQL Azure.

Wie auch immer die Kombination aussieht, der wichtigste Punkt hier ist, dass die Leistung durch Netzwerklatenzen beeinträchtigt werden kann, auch wenn die Anwendung weiterhin wie erwartet funktioniert.  

Julie Lerman ist als Microsoft MVP, .NET-Mentor und Unternehmensberaterin tätig und wohnt in den Bergen von Vermont. Sie hält bei User Groups und Konferenzen in der ganzen Welt Vorträge zum Thema Datenzugriff und anderen Microsoft .NET-Themen. Julie Lerman führt einen Blog unter thedatafarm.com/blog und ist Autorin des hoch gelobten Buchs „Programming Entity Framework“ (Programmieren mit Entity Framework) (O’Reilly Media, 2010). Folgen Sie ihr auf twitter.com: julielerman.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Wayne Berry, Kraig Brockschmidt, Tim Laverty und Steve Yi