Leistung
Skalierungsstrategien für ASP.NET-Anwendungen
Richard Campbell and Kent Alstad
Themen in diesem Artikel:
- Skalieren von ASP.NET-Anwendungen und -Datenbanken
- Optimieren von Code
- Effizientes Zwischenspeichern
- Affinität und Lastenausgleich
|
In diesem Artikel werden folgende Technologien verwendet:
ASP.NET
|

Inhalt
Als ASP.NET-Leistungsberater werden wir in der Regel bei einem Projekt hinzugezogen, wenn bereits Schwierigkeiten aufgetreten sind. In vielen Fällen werden wir erst angesprochen, wenn die Anwendung bereits in Produktion ist. Was während der Entwicklung hervorragend funktioniert hat, funktioniert für den Benutzer leider nicht. Das Problem: die Website ist zu langsam. Die Geschäftsleitung möchte wissen, warum dies nicht beim Testen festgestellt wurde. Die Entwicklungsabteilung kann das Problem nicht reproduzieren. Mindestens ein Argument wird vorgebracht, dass ASP.NET nicht vernünftig skalieren kann. Kommt Ihnen das bekannt vor?
Einige der am stärksten ausgelasteten Websites der Welt werden auf ASP.NET ausgeführt. MySpace ist ein hervorragendes Beispiel. Die Website wurde nach ASP.NET migriert, nachdem sie auf einer Reihe verschiedener Plattformen ausgeführt wurde. Tatsache ist, dass sich beim Skalieren Leistungsprobleme in Anwendungen einschleichen können, und wenn dies der Fall ist, müssen Sie feststellen, worin das eigentliche Problem besteht, und die besten Strategien zur Problembehandlung finden. Die größte Herausforderung, der Sie gegenüberstehen werden, ist das Erstellen eines Satzes von Messungen, mit denen die Leistung Ihrer Anwendung durchgängig abgedeckt wird. Wenn Sie sich nicht mit dem gesamten Problem auseinandersetzen, erkennen Sie nicht, worauf Sie sich konzentrieren müssen.
Die Leistungsgleichung
Im September 2006 veröffentlichten Peter Sevcik und Rebecca Wetzel von NetForecast einen Artikel mit dem Titel „Field Guide to Application Delivery Systems“ (Praktische Anleitung für Anwendungsbereitstellungssysteme). Der Artikel konzentrierte sich auf das Verbessern der WAN-Anwendungsleistung und enthielt die in Abbildung 1 dargestellte Gleichung. Bei der Gleichung geht es zwar um die WAN-Leistung, aber mit einigen kleinen Änderungen kann sie zum Messen der Webanwendungsleistung verwendet werden. Die geänderte Gleichung ist in Abbildung 2 aufgeführt, und die einzelnen Elemente werden in Abbildung 3 erläutert.

Figure 3 Elemente der Leistungsgleichung
| Variable |
Definition |
| A |
Antwortzeit. Die Gesamtzeit von dem Zeitpunkt an, zu dem der Benutzer eine Seite (durch Klicken auf einen Link und so weiter) anfordert, bis zum Rendern der gesamten Seite auf dem Computer des Benutzers. Wird in der Regel in Sekunden gemessen. |
| Nutzlast |
Gesamtzahl der an den Browser gesendeten Bytes, einschließlich Markup und aller Ressourcen (z. B. CSS-, JS- und Bilddateien). |
| Bandbreite |
Übertragungsrate zum und vom Browser. Diese kann asymmetrisch sein und mehrere Geschwindigkeiten aufweisen, wenn eine bestimmte Seite aus mehreren Quellen generiert wird. Meistens wird daraus eine einzelne Bandbreite als Durchschnitt erstellt, die in Bytes pro Sekunde ausgedrückt wird. |
| AnwRunden |
Die Anzahl von Ressourcendateien, die für eine bestimmte Seite erforderlich sind. Diese Ressourcendateien umfassen CSS-, JS-, Bild- und andere Dateien, die vom Browser während des Renderingprozesses der Seite abgerufen werden. In der Gleichung wird die HTML-Seite separat ausgewiesen, indem die Roundtrip-Zeit (RTT) vor dem AnwRunden-Ausdruck dazugezählt wird. |
| RTT |
Die für den Roundtrip erforderliche Zeit, unabhängig von den übertragenen Bytes. Jede Anforderung dauert mindestens eine RTT für die Seite selbst. Wird in der Regel in Millisekunden gemessen. |
| Gleichzeitige Anforderungen |
Anzahl von gleichzeitigen Anforderungen, die ein Browser für Ressourcendateien durchführt. Standardmäßig führt Internet Explorer zwei gleichzeitige Anforderungen durch. Diese Einstellung kann angepasst werden, was jedoch nur selten geschieht. |
| Rs |
Rechenzeit auf dem Server. Dies ist die Zeit, die zum Ausführen von Code, Abrufen von Daten aus der Datenbank und Verfassen der Antwort erforderlich ist, die an den Browser gesendet wird. Wird in Millisekunden gemessen. |
| Rc |
Rechenzeit auf dem Client. Dies ist die Zeit, die der Browser benötigt, um die HTML-Datei auf dem Bildschirm zu rendern, JavaScript auszuführen, CSS-Regeln zu implementieren und so weiter. |
Abbildung 1 Die ursprüngliche Leistungsgleichung (Klicken Sie zum Vergrößern auf das Bild)
Abbildung 2 Die Webversion der Leistungsgleichung (Klicken Sie zum Vergrößern auf das Bild)
Die Formel liegt nun vor – jetzt besteht die Herausforderung im Messen der einzelnen Elemente. Der Endwert und die Antwortzeit lassen sich relativ einfach messen. Es gibt eine Reihe von Tools, die genau messen, wie lange der gesamte Prozess dauert.
Die Nutzlast kann mithilfe verschiedener Tools gemessen werden (
websiteoptimization.com/services/analyze ist eine gute Option). Dies gilt auch für die Bandbreite (siehe speedtest.net) und die Roundtrip-Zeit (mithilfe von Ping). Tools wie
websiteoptimization.com/services/analyze melden dazu die Größe einer Website in Bezug auf HTML, CSS, JavaScript, Bilder und so weiter. Gleichzeitige Anforderungen sind im Grunde eine Konstante (die Standardeinstellung von Internet Explorer
® ist 2).
Somit verbleiben Rs und Rc, für die zusätzlicher Entwicklungsaufwand erforderlich ist. Es ist relativ einfach, Code auf einer ASP.NET-Seite zu schreiben, der genau aufzeichnet, wann die zweite Ausführung der Seite beginnt, und diese Zeit nach Abschluss der Ausführung von der aktuellen Zeit subtrahiert. Dasselbe gilt für die Clientseite. Etwas JavaScript kann ganz oben auf der HTML-Seite ausgeführt werden, um die Zeit aufzuzeichnen und diese Zeit dann zu dem Zeitpunkt zu subtrahieren, wenn das OnLoad-Ereignis bei Vollständigkeit der Seite ausgelöst wird.
Tatsächlich können alle diese Elemente codiert werden, wenn Sie einen Debugmodus in Ihre Website integrieren wollen, der die Leistungsgleichung verwendet. Dafür gibt es einen guten Grund: Wenn die Leistungsgleichungselemente im Browser regelmäßig gerendert werden, kann problemlos festgestellt werden, wo Leistungsprobleme liegen.
Angenommen es liegt Ihnen eine ASP.NET-Anwendung vor, deren Benutzer sich auf einem anderen Kontinent befinden und nur über niedrige Bandbreite verfügen. Bei hohen Ping-Zeiten (> 200 ms) und niedriger Bandbreite (< 500 KBit/s) würden die Benutzer die gesamte Nutzlast und die Anzahl von Roundtrips in der Anwendung stark wahrnehmen. Die Betrachtung der Anwendung im Kontext dieser Benutzer ist wichtig, da sich ihre Erfahrung stark von Ihrer eigenen unterscheiden wird.
Skalierungsprobleme
Als Berater wissen wir, dass es sich wahrscheinlich um ein Skalierungsproblem handelt, wenn die Anwendung in der Testumgebung gut funktioniert, aber schlecht in der Praxis ausgeführt wird. Meisten besteht der einzige Unterschied zwischen beiden Fällen in der Anzahl gleichzeitiger Benutzer. Wenn die Anwendung die ganze Zeit über schlecht ausgeführt würde, läge ein Leistungsproblem anstelle eines Skalierungsproblems vor.
Ihnen stehen drei Strategien für eine Verbesserung der Skalierung zur Verfügung: Spezialisierung, Optimierung und Verteilung. Sie werden unterschiedlich angewendet, aber die eigentlichen Strategien sind unkompliziert und einheitlich.
Ziel der Spezialisierung ist das Aufteilen Ihrer Anwendung in kleinere Teile, um das Problem zu isolieren. Sie könnten beispielsweise das Verschieben statischer Ressourcendateien wie Bild-, CSS- und JS-Dateien von den ASP.NET-Servern in Betracht ziehen. Ein gut auf ASP.NET abgestimmter Server ist nicht besonders gut zum Bereistellen dieser Art von Dateien geeignet. Aus diesem Grund kann eine separate Gruppe von IIS-Servern, die auf das Bereitstellen von Ressourcendateien abgestimmt ist, einen wesentlichen Unterschied bei der Skalierbarkeit der von Ihnen ausgeführten Anwendung bewirken.
Wenn Sie viel Komprimierung oder Verschlüsselung (für SSL) durchführen, kann das Einrichten von speziellen SSL-Servern helfen. Es gibt sogar spezielle Hardwaregeräte für die Komprimierung und SSL-Beendigung.
Obwohl Sie aufgrund traditionellerer Strategien für das Zerlegen von Serverstufen separate Server für Datenzugriff, komplexe Berechnungen und so weiter in Betracht ziehen könnten, die unabhängig von der eigentlichen Generierung der Webseiten sind, würde ich anstelle von drei Webservern und zwei Geschäftsobjektservern fünf Webserver bevorzugen, die alle Funktionen ausführen. Die prozessexternen Aufrufe zwischen den Webservern und den Geschäftsobjektservern führen nämlich zu hohem Aufwand.
Eine Spezialisierung sollte nur für einen bekannten und erwarteten Nutzen durchgeführt werden. Außerdem ist die schnellste Lösung nicht immer die beste. Ziel der Skalierbarkeit ist beständige Leistung. Sie wollen bei zunehmender Last den Leistungsbereich verkleinern. Egal, ob es sich um einen oder tausend Benutzer handelt, soll eine bestimmte Seite für alle Benutzer innerhalb derselben Zeit gerendert werden.
Schließlich werden Sie für eine wirksamere Skalierung Ihren Servercode optimieren müssen. Praktisch jeder Aspekt der Leistungsgleichung mit Ausnahme der Rechenzeit auf dem Server skaliert linear. Sie können mehr Bandbreite hinzufügen (und es ist ziemlich einfach herauszufinden, wann dies der Fall sein sollte), und die Rechenzeit auf dem Client ändert sich mit zunehmender Anzahl von Clients nicht. Die anderen Elemente der Leistungsgleichung bleiben beim Skalieren ebenfalls konstant. Doch die Rechenzeit auf dem Server muss abgestimmt werden, wenn die Anzahl der Benutzer zunimmt.
Optimieren des Codes
Der Trick beim Optimieren von Servercode besteht im Durchführen von Tests, mit denen eine Verbesserung nachgewiesen wird. Sie sollten Profilierungstools verwenden, um die Anwendung zu analysieren und herauszufinden, für welche Vorgänge die meiste Zeit aufgewendet wird. Der gesamte Prozess sollte empirisch sein: Verwenden Sie Tools, um den zu verbessernden Code zu finden, verbessern Sie den Code, führen Sie Tests durch, um festzustellen, ob die Leistung tatsächlich verbessert wurde, und wiederholen Sie das Ganze immer wieder. Bei wirklich großen Websites wird die Leistungsabstimmung oft mit der Aufgabe verglichen, die Golden Gate-Brücke anzustreichen: Wenn alles gestrichen wurde, ist es an der Zeit, wieder ganz von vorne anzufangen.
Ich bin immer wieder überrascht, wie viele Entwickler der Überzeugung sind, dass der Ausgangspunkt des Skalierens die Verteilung ist. Die betreffenden Entwickler sind überzeugt, dass mehr Hardware erforderlich ist. Zweifellos kann das Hinzufügen von Hardware hilfreich sein. Doch ohne Spezialisierung und Optimierung ist der Nutzen möglicherweise relativ gering.
Mithilfe der Spezialisierung können Sie kleinere Teile der Anwendung bei Bedarf verteilen. Wenn Sie beispielsweise Bilder auf separaten Servern untergebracht haben, ist es einfach, die Bilddienste unabhängig von der übrigen Anwendung zu skalieren.
Die Optimierung bietet ebenfalls Vorteile bei der Verteilung, indem der Arbeitsaufwand für einen bestimmten Vorgang verringert wird. Dies führt direkt dazu, dass weniger Server zum Skalieren derselben Anzahl von Benutzern erforderlich sind.
Lastenausgleich
Zum Implementieren der Verteilung müssen Sie Server hinzufügen, die Anwendung auf ihnen duplizieren und einen Lastenausgleich implementieren. Für den Lastenausgleich können Sie Netzwerklastenausgleich (Network Load Balancing, NLB) verwenden, einen Dienst, der Teil aller Editionen von Windows Server® 2003 ist. Mit NLB ist jeder Server ein gleichwertiger Partner in der Lastenausgleichsbeziehung. Alle Server verwenden denselben Algorithmus zum Lastenausgleich, und alle hören auf einer freigegebenen virtuellen IP-Adresse den gesamten Datenverkehr ab. Basierend auf dem Lastenausgleichsalgorithmus wissen alle Server, welcher Server eine bestimmte Anforderung bearbeiten sollte. Jeder Server im Cluster sendet ein Signal, um die anderen Server wissen zu lassen, dass er aktiv ist. Wenn ein Server fehlschlägt, wird das Signal für diesen Server unterbrochen, und die anderen Server kompensieren dies automatisch.
NLB funktioniert gut, wenn eine große Anzahl von Benutzern mit recht ähnlichen Anforderungen vorliegt. Der Kompensierungsmechanismus funktioniert jedoch nicht so gut, wenn einige Anforderungen eine viel größere Last als andere verursachen. Glücklicherweise gibt es für solche Situationen Hardwarelösungen für den Lastenausgleich.
Affinität
Im Endeffekt liegt die Herausforderung bei einer wirksamen Verteilung im Beseitigen der Affinität. Wenn beispielsweise nur ein Webserver zur Verfügung steht, ist das Speichern von Sitzungsdaten auf diesem Server durchaus sinnvoll. Doch wo speichern Sie Sitzungsinformationen, wenn mehr als ein Webserver vorhanden ist?
Ein Ansatz besteht darin, diese Informationen auf dem Webserver zu speichern und die Affinität zu verwenden. Im Grunde bedeutet dies, dass bei der ersten Anforderung eines bestimmten Benutzers ein Lastenausgleich durchgeführt wird. Anschließend werden alle folgenden Anforderungen dieses Benutzers/dieser Sitzung wie die erste Anforderung an denselben Server gesendet. Dies ist ein einfacher Ansatz, der von jeder Lastenausgleichslösung unterstützt wird, und in einigen Fällen ist er sogar sinnvoll.
Auf lange Sicht führt Affinität jedoch zu Problemen. Sitzungsdaten vorgangsintern zu halten, mag schnell sein, doch wenn der ASP.NET-Arbeitsprozess neu gestartet wird, sind alle diese Sitzungen inaktiv. Arbeitsprozesse werden aus den unterschiedlichsten Gründen neu gestartet. Bei hoher Last könnte IIS den Arbeitsprozess von ASP.NET neu starten, weil der Eindruck besteht, dass der Prozess stecken geblieben ist. Tatsächlich wird in IIS 6.0 ein Arbeitsprozess standardmäßig alle 23 Stunden neu gestartet. Sie können dies anpassen, aber dennoch laufen die Benutzer Gefahr, ihre Sitzungsdaten während des Neustarts zu verlieren. Bei einer kleinen Website ist dies nicht weiter schlimm, doch wenn die Site nach und nach größer wird und immer stärker ausgelastet ist, kann dies ziemlich problematisch werden. Das ist aber noch nicht alles.
Beim Durchführen des Lastenausgleichs nach IP-Adresse wird ein Server möglicherweise von einem Megaproxy (wie AOL) angesprochen und kann die gesamte Last nicht allein bereitstellen. Zudem wird das Aktualisieren der Server mit einer neuen Version Ihrer Anwendung schwieriger. Sie müssen entweder stundenlang warten, bis alle Benutzer den Besuch Ihrer Website abgeschlossen haben, oder Sie beenden die Benutzersitzungen und lösen Verärgerung aus. Ihre Zuverlässigkeit wird damit ebenfalls zum Problem: Bei Verlust eines Servers verlieren Sie viele Sitzungen.
Das Beseitigen der Affinität ist ein Hauptziel der Verteilung. Dazu müssen Sitzungszustandsdaten prozessextern verschoben werden, wobei eine Leistungsabnahme akzeptiert werden muss, um höhere Skalierbarkeit zu bieten. Wenn Sie Sitzungen prozessextern verschieben, werden Sitzungsdaten an einem Speicherort aufgezeichnet, auf den alle Webserver zugreifen können, entweder auf dem SQL Server® oder auf dem ASP.NET-Zustandsserver. Dies wird in web.config konfiguriert.
Zum Unterstützen von prozessexternen Sitzungen ist dazu ein gewisser Codierungsaufwand erforderlich. Alle im Session-Objekt gespeicherten Klassen müssen mit dem Serializable-Attribut gekennzeichnet werden. Alle Daten in der Klasse müssen also entweder serialisierbar oder als NonSerialized gekennzeichnet sein, damit sie ignoriert werden. Wenn die Serialisierung zum prozessexternen Speichern der Sitzungsdaten ausgeführt wird und Sie die Klassen nicht kennzeichnen, erhalten Sie Fehler.
Schließlich ist das prozessexterne Verschieben von Sitzungen eine hervorragende Möglichkeit festzustellen, ob zu viele Daten im Sitzungsobjekt vorliegen, denn jetzt zahlen Sie bei jeder Seitenanforderung einen Preis für das zweimalige Verschicken dieser großen Datenmengen über das Netzwerk (einmal zum Abrufen am Anfang der Seite und einmal beim Zurückgeben am Ende der Seite).
Wenn Sie das Problem mit dem Session-Objekt gelöst haben, lösen Sie andere Affinitätsprobleme wie Mitgliedschafts- und Rollenverwaltung. Für jedes Problem stellen sich eigene Herausforderungen beim Beseitigen der Affinität. Doch damit Ihre ASP.NET-Anwendung wirklich hoch skalierbar ist, müssen Sie alle Formen von Affinität suchen und beseitigen.
Die bisher erörterten Strategien gelten für praktisch alle Webanwendungen, die skaliert werden müssen. Tatsächlich treffen diese Strategien auf das Skalieren praktisch aller Anwendungen mithilfe beliebiger Technologien zu. Im Folgenden werden ASP.NET-spezifische Verfahren näher beleuchtet.
Minimieren der Nutzlast
Beim Betrachten der Leistungsgleichung sehen Sie, dass die Nutzlast eine bedeutende Rolle spielt, speziell bei begrenzter Bandbreite. Ein Verkleinern der Nutzlast verbessert die Antwortzeit. Sie erhalten dazu gewisse Skalierungsvorteile, da weniger Bytes verschoben werden, und Sie könnten sogar Geld bei den Bandbreitenkosten sparen.
Eine der einfachsten Maßnahmen zum Verkleinern der Nutzlast besteht im Einschalten der Komprimierung. In IIS 6.0 können Sie angeben, ob statische Dateien, dynamisch generierte Antworten (beispielsweise ASP.NET-Seiten) oder beides komprimiert werden sollen (siehe Abbildung 4).
Abbildung 4 Serverweites Konfigurieren der Komprimierung in IIS 6.0 (Klicken Sie zum Vergrößern auf das Bild)
IIS 6.0 komprimiert statische Dateien bei Bedarf und speichert sie in einem von Ihnen angegebenen Zwischenspeicher für komprimierte Dateien. Für dynamisch generierte Antworten wird keine Kopie gespeichert, da sie jedes Mal komprimiert werden. IIS 7.0 ist beim Komprimieren intelligenter, denn es werden nur häufig verwendete Dateien komprimiert.
Die Komprimierung kostet Prozessorzyklen, aber in der Regel ist auf einem dedizierten Webserver viel zusätzliche Prozessorkapazität vorhanden. IIS 7.0 ist jedoch noch weiter optimiert: Wenn der Prozessor wirklich stark ausgelastet ist, wird die Komprimierung angehalten. Es gibt auch spezielle Geräte, die die Komprimierung unabhängig vom Webserver durchführen.
Ein weiterer Bereich, der erheblich zu Nutzlastverringerungen beiträgt, ist ViewState. Während der Entwicklung kann die ViewState-Verwendung leicht außer Kontrolle geraten. Die meisten Websteuerelemente verwenden ViewState, und auf steuerungsintensiven Seiten kann ViewState zu Tausenden von Bytes anwachsen. Um die Verwendung von ViewState zu verringern, schalten Sie die Funktion bei Steuerelementen aus, für die sie nicht erforderlich ist. In einigen Fällen entfernen Entwickler sogar Steuerelemente, um ViewState zu verringern. Doch das ist nicht immer notwendig. Die meisten modernen Websteuerelemente sind gegenüber dem Problem übermäßiger ViewState-Funktionen sensibel und bieten daher eine fein abgestufte Größensteuerung. Es gibt auch Hardwaregeräte, mit denen sich ViewState entfernen und ersetzen lässt, ohne dass der Code oder die Anwendungsausführung geändert werden.
Eine der wirksamsten Technologien zum Verringern der Nutzlastgröße ist AJAX. Doch AJAX verringert die Nutzlastgröße nicht wirklich. Es wird einfach nur die wahrgenommene Größe der Nutzlast verringert, während sich die Gesamtzahl der an den Browser gesendeten Bytes erhöht. Beim Verwenden von AJAX ist die übergeordnete Seite kleiner, sodass die anfänglichen Renderingzeiten kürzer sind. Einzelne Elemente auf dieser Seite senden dann eigene Anforderungen zur Datenauffüllung an den Server.
AJAX verteilt die Nutzlast praktisch über einen Zeitraum, sodass dem Benutzer etwas angezeigt wird, während andere Elemente geladen werden. AJAX verbessert daher die Benutzerfunktionalität insgesamt, doch Sie sollten wieder die Leistungsgleichung verwenden, um die tatsächlichen Kosten zu messen. In der Regel erhöht AJAX die Rechenzeit bisweilen dramatisch, sodass die Leistung dadurch möglicherweise unannehmbar wird.
Wenn die AJAX-Roundtrips zum Server zum Auffüllen einzelner Elemente ganze Seitenanforderungen ersetzen, ergibt sich eine Nettoabnahme an Roundtrips. Doch in vielen Fällen werden Sie feststellen, dass sich die Gesamtanzahl der Roundtrips für einen bestimmten Benutzer erhöht. Sie müssen einfach nur sorgfältig testen, um festzustellen, ob sich die Leistung durch AJAX verbessert oder verringert hat.
Zwischenspeichern
Fachleute im Skalieren von ASP.NET-Anwendungen führen eine eingehende Diskussion zum Thema Zwischenspeichern. Grundsätzlich geht es beim Zwischenspeichern darum, die Daten näher zum Benutzer zu verschieben. Bevor überhaupt bedeutsame Optimierungsarbeiten durchgeführt wurden, befinden sich in einer typischen ASP.NET-Anwendung praktisch alle Daten, die der Benutzer braucht, in der Datenbank. Diese werden bei jeder Anforderung von dort abgerufen. Das Zwischenspeichern ändert dieses Verhalten. ASP.NET unterstützt drei Formen der Zwischenspeicherung: das Zwischenspeichern von Seiten (auch als Ausgabezwischenspeicherung bezeichnet), das Zwischenspeichern von Teilseiten und das programmgesteuerte Zwischenspeichern (oder Zwischenspeichern von Daten).
Das Zwischenspeichern von Seiten ist bei weitem die einfachste Form der Zwischenspeicherung. Dazu fügen Sie Ihrer ASP.NET-Seite eine @OutputCache-Direktive hinzu und integrieren eine Regel, wann die Direktive ablaufen soll. Sie könnten beispielsweise angeben, dass die Seite 60 Sekunden lang zwischengespeichert werden soll. Mithilfe dieser Direktive wird die erste Anforderung dieser Seite normal verarbeitet, wobei auf die Datenbank und andere Ressourcen zugegriffen wird, die zum Generieren der Seite erforderlich sind. Anschließend verbleibt die Seite 60 Sekunden lang im Speicher des Webservers, und alle Anforderungen werden während dieser Zeit direkt aus dem Speicher bereitgestellt.
Obwohl dieses Beispiel einfach ist, wird dabei leider eine grundlegende Realität beim Zwischenspeichern von Seiten außer Acht gelassen: praktisch keine ASP.NET-Seite ist so statisch, dass Sie die ganze Seite für längere Zeit zwischenspeichern können. Hier ist das Zwischenspeichern von Teilseiten nützlich. Dabei können Sie Teile einer ASP.NET-Seite entsprechend kennzeichnen, sodass nur die Teile der Seite, die sich regelmäßig ändern, berechnet werden. Dies ist komplizierter, aber wirksam.
Die leistungsfähigste (und komplexeste) Form des Zwischenspeicherns dürfte das programmgesteuerte Zwischenspeichern sein, das sich auf die von der Seite verwendeten Objekte konzentriert. Am häufigsten wird das programmgesteuerte Zwischenspeichern zum Speichern von Daten verwendet, die aus der Datenbank abgerufen werden.
Das offensichtlichste Problem beim Zwischenspeichern von Daten besteht darin, dass sich die zugrunde liegenden Daten seit dem Zwischenspeichern geändert haben könnten. Die Ablaufzeit beim Zwischenspeichern ist die größte Herausforderung, die Sie beim Implementieren jeder Form von Zwischenspeicherung bewältigen müssen. Sie sollten aber auch an den Speicherplatz denken.
Bei einem ausgelasteten ASP.NET-Server gestaltet sich der Speicherplatz aus verschiedenen Gründen äußerst problematisch. Beim Berechnen einer ASP.NET-Seite wird jedes Mal Speicherplatz verwendet. Microsoft® .NET Framework ist so eingerichtet, dass Speicher sehr schnell zugeordnet, aber relativ langsam durch die Garbage Collection freigegeben wird. Für die Erörterung der Garbage Collection und der .NET-Speicherzuordnung ist ein eigener Artikel erforderlich. Zu diesem Thema gibt es bereits mehrere Veröffentlichungen. Hier soll nur darauf hingewiesen werden, dass der 2-GB-Speicher, der auf einem ausgelasteten Webserver für eine ASP.NET-Anwendung zur Verfügung steht, sehr gefragt ist. Im Idealfall ist der größte Teil dieser Speicherauslastung vorübergehend, da der Speicher durch Variablen und Strukturen zum Berechnen der Website belegt wird.
Doch was permanente Speicherobjekte wie beispielsweise prozessinterne Sitzungs- und Cacheobjekte betrifft, wird die Speicherauslastung erheblich problematischer. Natürlich werden diese Probleme erst ersichtlich, wenn die Anwendung wirklich ausgelastet ist.
Ziehen Sie dieses Szenario in Betracht: Auf Ihrer Website ist aufgrund einer neuen Marketingaktion viel los, Tausende von Benutzern rufen die Website auf, und Sie verdienen viel Geld. Um gute Antwortzeiten aufrechtzuerhalten, verwenden Sie für Teile von Seiten und Gruppen von Datenobjekten möglichst eine Zwischenspeicherung. Jede Seitenanforderung durch einen Benutzer verbraucht etwas Speicherplatz, sodass die genutzte Speichermenge ständig zunimmt. Je mehr Benutzer vorhanden sind, desto schneller nimmt diese Speichermenge zu. Es gibt auch große Zunahmen durch die Zwischenspeicherung und Sitzungsobjekte.
Wenn die gesamte genutzte Speichermenge auf 90 Prozent der standardmäßigen ASP.NET-Zwischenspeicherbeschränkung zugeht, wird ein Garbage Collection-Ereignis aufgerufen. Der Garbage Collector durchläuft den Speicher, verschiebt gespeicherte Speicherobjekte (beispielsweise Cacheobjekte und Sitzungsobjekte) und gibt nicht mehr verwendeten Speicher frei (Speicherplatz, der zum Berechnen der Webseiten verwendet wurde). Das Freigeben von nicht genutztem Speicher erfolgt schnell, doch das Verschieben von gespeicherten Objekten geht langsam vor sich. Die Aufgabe des Garbage Collector wird also umso schwieriger, je mehr gespeicherte Objekte vorhanden sind. Dieses Problem kann in perform.exe durch eine hohe Anzahl von gen-2-Sammlungen identifiziert werden.
Denken Sie auch daran, dass während der Freispeichersammlung von diesem ASP.NET-Server keine Seiten bereitgestellt werden können. Alles befindet sich in einer Warteschlange und wartet auf den Abschluss des Garbage Collection-Prozesses. Das Ganze wird auch von IIS beobachtet. Wenn der Prozess zu viel Zeit in Anspruch zu nehmen scheint und abstürzen könnte, wird der Arbeitsthread neu gestartet. Obwohl hierdurch schnell eine große Speichermenge freigesetzt wird, da alle gespeicherten Speicherobjekte verworfen werden, dürften Ihre Kunden verärgert sein.
Es gibt jetzt einen Patch für ASP.NET, der automatisch Objekte aus dem programmgesteuerten Cache entfernt, wenn nur noch wenig Speicher vorhanden ist, was oberflächlich betrachtet eine gute Idee zu sein scheint. Es ist besser als ein Absturz. Doch bedenken Sie, dass Ihr Code jedes Mal, wenn Sie etwas aus dem Cache entfernen, das Entfernte wieder einfügen wird.
In dem Augenblick, in dem Daten zwischengespeichert werden, besteht das Risiko, dass sie bereits falsch sind. Ein Beispiel wäre eine Produktdatenbank und die entsprechende Bestellseite. Bei der anfänglichen Variation der Produktseite kommt es bei jedem Rendering dieser Seite zu einer Anforderung der Anzahl von Produkten, die sich noch im Bestand befinden, durch die Datenbank. Wenn Sie diese Anforderungen analysieren, werden Sie feststellen, dass zu 99 Prozent immer wieder dieselbe Zahl abgerufen wird. Warum also die Zahl nicht einfach zwischenspeichern?
Eine einfache Möglichkeit bestände darin, die Zwischenspeicherung nach Zeit durchzuführen. Sie speichern den Bestand an Produkten also für eine Stunde zwischen. Der Nachteil dieses Verfahrens besteht darin, dass jemand ein Produkt kauft, anschließend zu dieser Seite zurückkehrt und sieht, dass der Bestand immer noch derselbe ist. Dies führt zu Beschwerden von Seiten der Benutzer. Doch noch schwieriger wird es, wenn jemand Ihr Produkt erwirbt und sieht, dass es im Bestand vorhanden ist, obwohl es tatsächlich ausverkauft ist. Sie könnten ein Lieferrückstandssystem erstellen, aber dennoch wird Ihr Kunde enttäuscht sein.
Vielleicht wird das Problem durch das Ablaufschema verursacht: Die Zeit allein reicht nicht aus. Sie könnten die Bestandszählung zwischenspeichern, bis jemand ein Produkt erwirbt, und dann das Cacheobjekt ablaufen lassen. Das ist logischer, doch was geschieht, wenn mehr als ein ASP.NET-Server vorhanden ist? Abhängig vom jeweiligen Server erhalten Sie unterschiedliche Bestandszählungen für das Produkt. Bedenken Sie, dass der Eingang von neuem Bestand (wodurch sich die Zahl erhöht) nicht über die Webanwendung abläuft, sodass wieder alles völlig falsch sein kann.
Die Synchronisierung von Abläufen auf mehreren ASP.NET-Servern ist möglich, aber mit Vorsicht anzugehen. Der zwischen Webservern generierte Datenaustausch steigt geometrisch an, wenn die Anzahl von Cacheobjekten und Webservern zunimmt.
Auch die Auswirkungen des Cacheablaufs auf die Leistung muss sorgfältig untersucht werden. Bei hoher Last kann das Ablaufen eines Cacheobjekts viele Schwierigkeiten verursachen. Nehmen Sie beispielsweise an, es liegt eine aufwändige Abfrage vor, die 30 Sekunden braucht, um von der Datenbank zurückgegeben zu werden. Sie haben diese Abfrage zwischengespeichert, um Aufwand einzusparen, denn bei hoher Belastung wird diese Seite einmal pro Sekunde angefordert.
Der Code zum Behandeln von Cacheobjekten ist ziemlich einfach. Statt die Daten bei Bedarf aus der Datenbank abzurufen, prüft die Anwendung, ob das Cacheobjekt aufgefüllt ist. In diesem Fall werden die Daten aus dem Cacheobjekt verwendet. Wenn dies nicht der Fall ist, wird der Code ausgeführt, um die Daten aus der Datenbank abzurufen und das Cacheobjekt dann mit diesen Daten aufzufüllen. Der Code wird anschließend normal ausgeführt.
Das Problem besteht darin: Wenn eine Abfrage vorliegt, die 30 Sekunden in Anspruch nimmt, und die Seite einmal pro Sekunde ausgeführt wird, gehen in der Zeit, die zum Ausfüllen des Cacheelements erforderlich ist, 29 andere Anforderungen ein, die alle versuchen, das Cacheelement mit ihren eigenen Abfragen an die Datenbank aufzufüllen. Um dieses Problem zu lösen, können Sie eine Threadsperre hinzufügen, um die anderen Seitenausführungen daran zu hindern, die Daten von der Datenbank anzufordern.
Doch führen Sie sich das Szenario erneut vor Augen: Die erste Anforderung geht ein, stellt fest, dass das Cacheelement nicht aufgefüllt ist, wendet eine Sperre auf den Code an und führt die Abfrage zum Auffüllen des Cacheobjekts aus. Die zweite Anforderung geht eine Sekunde später ein, während die erste noch ausgeführt wird, stellt fest, dass das Cacheobjekt nicht aufgefüllt, aber die Sperre vorhanden ist, und wird daher gesperrt. Dasselbe geschieht mit den nächsten 28 Anforderungen. Dann wird die Verarbeitung der ersten Anforderung beendet, die Sperre wird entfernt, und die Verarbeitung wird fortgesetzt. Was geschieht mit den anderen 29 Anforderungen? Sie sind nicht mehr gesperrt, sodass sie ebenfalls weiter ausgeführt werden. Doch sie haben bereits überprüft, ob das Cacheobjekt aufgefüllt ist (was zu jenem Zeitpunkt nicht der Fall war). Die Anforderungen versuchen daher, eine Sperre zu erhalten. Einer Anforderung gelingt dies, und sie führt die Abfrage erneut aus.
Sehen Sie, worin das Problem besteht? Weitere Anforderungen, die eingehen, nachdem die erste Anforderung das Auffüllen des Cacheobjekts abgeschlossen hat, werden normal ausgeführt, doch die Anforderungen, die eingehen, während die Abfrage ausgeführt wird, haben es schwer. Sie müssen Code schreiben, um dieses Problem zu behandeln. Wenn eine Anforderung auf eine Sperre stößt, sollte sie nach Aufhebung der Sperre erneut prüfen, ob das Cacheobjekt aufgefüllt ist, wie in Abbildung 5 dargestellt. Es ist wahrscheinlich, dass das Cacheobjekt jetzt aufgefüllt wird, denn das ist der Grund, warum die Sperre ursprünglich gesetzt wurde. Es ist jedoch möglich, dass dies nicht der Fall ist, denn in der Zwischenzeit hat ein anderer Codeabschnitt das Cacheobjekt wieder ablaufen lassen.

Figure 5 Überprüfen, Sperren und erneutes Prüfen eines Cacheobjekts
// check for cached results
object cachedResults = ctx.Cache["PersonList"];
ArrayList results = new ArrayList();
if (cachedResults == null)
{
// lock this section of the code
// while we populate the list
lock(lockObject)
{
// only populate if list was not populated by
// another thread while this thread was waiting
if (cachedResults == null)
{
...
}
}
}
Das Schreiben von Code für die Zwischenspeicherung ist harte Arbeit, die jedoch sehr lohnend sein kann. Die Zwischenspeicherung macht Anwendungen jedoch komplexer, sodass sie mit Bedacht verwendet werden sollte. Stellen Sie sicher, dass die Komplexität wirklich Vorteile mit sich bringt. Testen Sie den Code für die Zwischenspeicherung immer im Hinblick auf diese komplexen Szenarios. Was geschieht bei mehreren gleichzeitigen Anforderungen? Was geschieht, wenn kurze Ablaufzeiten vorliegen? Sie müssen die Antworten auf diese Fragen kennen. Schließlich soll der Code für die Zwischenspeicherung nicht zu größeren Skalierungsproblemen führen.
Skalieren von Datenbanken
Der normale Ansatz für das Skalieren von Websites besteht im Ausskalieren, nicht im Aufwärtsskalieren. Dies ist größtenteils auf die Thread- und Speicherbeschränkungen von ASP.NET in Kombination mit der Kurzlebigkeit von Webanforderungen zurückzuführen.
Beim Skalieren von Datenbanken besteht die normale Praxis jedoch im Aufwärtsskalieren – ein riesiger Bereich oder vielleicht zwei in einer Clusterkonfiguration (obwohl tatsächlich jeweils nur ein Bereich die Datenbank ausführt). Doch schließlich kann eine einzelne Datenbank in einer umfangreichen Webanwendung die Last nicht verkraften. Sie müssen ausskalieren. Dies ist möglich. Sie müssen einfach nur dieselben Strategien anwenden, die auf die Webanwendung selbst angewendet werden. Der erste Schritt besteht immer in der Spezialisierung, also im Unterteilen der Datenbank in logische Partitionen. Diese Partitionen könnten datenzentrisch und beispielsweise nach Region unterteilt sein. Es könnten also mehrere Datenbanken vorliegen, die jeweils einen Teil der ganzen Datenbank enthalten. Ein Server enthält beispielsweise die Ostküstendaten, während der andere die Westküstendaten enthält.
Bei wirklich umfangreichen Webanwendungen erfolgt das Partitionieren der Datenbanken in Lesedatenbanken und Schreibdatenbanken (siehe Abbildung 6). Die Lesedatenbanken sind schreibgeschützt. Sie erhalten ihre Daten von den Schreibdatenbanken über Replikation. Alle Datenabfragen werden an die Lesedatenbanken gerichtet, die für das möglichst schnelle Lesen von Daten optimiert sind. Lesedatenbanken sind von Natur aus äußerst verteilbar.
Abbildung 6 Verteilte Datenbank
Architektur (Klicken Sie zum Vergrößern auf das Bild)
Alle Datenschreibanforderungen werden an die Schreibdatenbanken gesendet, die partitioniert und für ein effizientes Schreiben abgestimmt sind. Die Replikation verschiebt die neuen Daten von den Schreib- zu den Lesedatenbanken.
Das Erstellen solch spezialisierter Datenbanken führt zu Wartezeiten: Es dauert nun einige Zeit, bis ein Schreibvorgang an die Lesedatenbanken verteilt wird. Doch wenn Sie die Wartezeit behandeln können, ist das Skalierungspotenzial enorm hoch.
Endloser Skalierungsaufwand
Solange Ihre Anwendung weiter wächst, wird Ihr Skalierungsaufwand ebenfalls zunehmen. Die ASP.NET-Verfahren, die effektiv für 10.000 gleichzeitige Benutzer arbeiten, sind bei 100.000 Benutzern nicht so wirksam, und die Regeln ändern sich bei einer Million Benutzern erneut. Natürlich hängt die Leistung möglicherweise vollständig von Ihrer Anwendung ab. Es gibt Anwendungen, die bei weniger als tausend Benutzern Skalierungsprobleme aufweisen.
Der Schlüssel zum wirksamen Skalieren besteht in Messungen, bevor Einschnitte durchgeführt werden: Führen Sie Tests durch, um sicherzugehen, dass Sie sich auf die richtigen Bereiche konzentrieren. Testen Sie Ihre Arbeit, um zu gewährleisten, dass tatsächlich eine Verbesserung und nicht nur eine Änderung erzielt wurde. Selbst ganz am Ende eines Entwicklungszyklus zum Optimieren der Skalierbarkeit sollten Sie wissen, wo die Anwendung am langsamsten ist. Es bleibt jedoch zu hoffen, dass Ihre Anwendung für heutige Benutzer schnell genug ist, damit Sie bereits an den zukünftigen Anforderungen Ihrer Benutzer arbeiten können.
Richard Campbell ist Microsoft Regional Director, MVP für ASP.NET und Co-Moderator von
.NET Rocks, der Internet-Audio-Talkshow für .NET-Entwickler (
dotnetrocks.com). Über mehrere Jahre hinweg war er als Unternehmensberater in den Bereichen Leistung und Skalierung von ASP.NET tätig. Zudem ist er einer der Mitbegründer von Strangeloop Networks.
Kent Alstad ist CTO von Strangeloop Networks (
strangeloopnetworks.com) und Inhaber oder mitwirkender Autor aller angemeldeter Strangeloop-Patente. Vor der Gründung von Strangeloop beschäftigte er sich mit der Erstellung äußerst skalierbarer Hochleistungs-ASP.NET-Anwendungen und fungierte als Berater in diesem Bereich.