Performance

Paradigmenwechsel: High Performance Webanwendungen

Posted on: September 28th, 2011 by crille No Comments

 

Ich programmiere schon seit einiger Zeit Webanwendungen. Selten stieß ich dabei auf irgendwelche Performanzgrenzen. Dynamische Webanwendungen sind vom Prinzip her ziemlich simpel gestrickt:

  1. Die Webanwendung erhält eine Anfrage
  2. Die Anwendung holt sich z.B. Daten aus der Datenbank, um die Anfrage beantworten zu können
  3. Die Webanwendung bereitet die Daten auf und gibt sie als HTML aus, die der Browser hübsch dem Benutzer anzeigt

Das gute, alte EVA-Prinzip: Eingabe, Verarbeitung, Ausgabe

Bei Servern, die nicht sonderlich viele Anfragen erhalten, ist das ein bewertes Prinzip. Ein schon etwas in die Jahre gekommener 1-Kern-Webserver konnte beispielsweise locker 5.000 Besucher am Tag bedienen. Nimmt die Anfragefrequenz einer Webseite jedoch weiter zu, muss man sich etwas überlegen. Eine Möglichkeit ist, sich weitere Server anzumieten, um bestimmte Aufgaben auszugliedern - z.B. ein eigener Datenbankserver.

Man kann auch aufs Cachen setzen: Anstatt bei jeder Anfrage die gleichen Berechnungen durchzuführen und auf die DB zuzugreifen, speichert man das Ergebnis oder die auszugebende Seite im Arbeitsspeicher zwischen, so dass sie von dort ohne teure CPU-Zeit und merklich schneller ausgegeben werden kann.

Und wenn diese Maßnahmen nicht mehr ausreichen? Was dann?

Vor genau diesem Problem stand Steve Huffman bei reddit, die monatlich 270 Millionen Anfragen erhielten. In folgendem Vortrag spricht er über die sieben Lehren aus der Zeit bei reddit (eine Transkription gibt es bei ThinkVitamin):

Lessons Learned while at Reddit from Carsonified on Vimeo.

Neben dem "Open Schema" (oder Entity-Attribute-Value-Model) der Datenbankstruktur finde ich das Zusammenspiel zwischen Anwendung, Cache und Datenbank äußerst interessant. Sämtliche Inhalte von reddit liegen im Cache und sind vorberechnet, sodass die Inhalte ohne Berechnungen angezeigt werden können. Werden Inhalte verändert - etwa Upvote für ein Link, wird der Cache zur Anzeige der höheren Bewertung angelegt, sodass der Benutzer sofort die Rückmeldung bekommt, dass sein Vote erfolgreich war. Des weiteren wird in einer Queue vermerkt, dass der Link einen Vote erhalten hat. Im Hintergrund wird diese Queue abgearbeitet, sodass diese Berechnungen keinen Einfluss auf die Wartezeit des Anwenders haben. Der Vote-Eintrag in der Queue hat z.B. folgende Aktionen zur Folge

  1. Upvote wird in der persistente DB gespeichert, sodass Cache & DB konsistent gehalten werden
  2. Neuberechnung des Caches der betroffenen Übersichten (Listen)
  3. Überarbeitung des Accounts (Cache)
  4. etc.

Fazit

Diese Vorgehensweise der Programmierung einer Webanwendung unterscheidet sich also grundlegend von meiner bisherigen Programmierung: Daten werden redundant gehalten, weil Speicher billig, CPU aber teuer und ein schnell bedienter Kunde (Besucher) wichtig ist. Das Video mit den Einsichten von Steve Huffman in die Abläufe bei reddit stellen für mich einen Paradigmenwechsel dar.

Natürlich: Webanwendungen ohne den Ausblick auf viele Zugriffe, benötigen keine solche Programmierung. Wenn man jedoch auf Skalierbarkeit wert legt, sollte man sich die vorgestellten Lehren besser zu Herzen nehmen...

Ein Lied für unsere Tiere

Posted on: April 1st, 2011 by crille No Comments

 

Was nehmen diese Volksmusiker eigentlich, um auf solche Melodien und vor allem Songtexte zu kommen. Und wie viel Geld und Drogen sind nötig, um eine solche "Performance auf der Bühne" abzuliefern?

[youtube tYgoS4zUHgQ]

Ahhh.. My brain hurts!

MySQL-Import und pdflush

Posted on: Dezember 6th, 2010 by crille No Comments

 

Vermutlich kommt jeder in gewissen Abständen an einen Punkt, an dem man absolutes Neuland betritt, da man mit seinem bisherigen Wissen nicht weiter kommt.

Das Problem

war, dass viele Datensätze (6 Mio) per PreparedStatements in eine Datenbank importiert werden müssen. An sich kann es kein Problem sein, 6 Mio Datensätze zu importieren, aber unser Server machte da nicht mit

  1. war das Wiki kurz nach Beginn des Datenimports nicht mehr erreichbar
  2. einige Zeit später machte der komplette Server die Grätsche

Wir mutmaßten, dass die Wiki-Datenbank auf irgendeine Weise mit dem Importvorgang in einen Konflikt gerät, da diese ja als erstes ausfiel. Dieser Verdacht war falsch, da lediglich die InnoDB der Wiki-Datenbank zum Ausfall führte. Nachdem das Wiki auf MyISAM umgestellt war, funktionierte das Wiki auch während des Imports problemlos.

Das zweite Problem blieb aber bestehen. Nach einigen Versuchen und der Installation von Diagnosetools stand fest, dass die Festplatte durch den pdflush Daemon so stark beansprucht wurde, dass es für andere Prozesse nicht mehr möglich war auf die Festplatte zuzugreifen.

Wir überlegten, die Einstellungen für den pdflush Daemon zu ändern, mir fiel aber vorher auf, dass mir bei der Programmierung des Imports ein Fehler unterlaufen war: Anstatt vor dem Import die Schlüssel zu deaktivieren

ALTER TABLE ... DISABLE KEYS

und nach dem Import die Schlüssel wieder zu aktivieren

ALTER TABLE ... ENABLE KEYS

waren durch einen Zeilenverrutscher die Schlüssel für eine Tabelle schon vor dem Import wieder aktiviert worden und genau das verursachte das Zusammenbrechen des Servers, weil die Indizes während des Imports neu berechnet werden mussten.

 

Wer sich mal ein bißchen näher mit SQL beschäftigt, wird über die Ausdrucksmächtigkeit und Performance erstaunt sein. SQL kann eben doch mehr als simple SELECT-Anweisungen zu verarbeiten.

Mal ein Beispiel, was man alles mit UPDATE-Anweisungen machen kann:

Wir haben folgende Tabellen

1. Team

  • Teamid
  • Teamname
  • AnzahlMitglieder

2. User

  • Userid
  • Username
  • Teamid

Nun wollen wir das noch leere Feld Team.AnzahlMitglieder aus der Tabelle User berechnen. Naiver Ansatz ist wahrscheinlich folgendes UPDATE-Statement, wenn man sich vorher klar macht, dass man Subqueries verwenden kann, um Werte zu setzen:

UPDATE Team 
SET 
	AnzahlMitglieder = 
		(SELECT COUNT(*) 
		FROM User 
		WHERE User.Teamid = Team.Teamid )

Problem an dieser Abfrage ist allerdings, dass für jede Zeile, die aktualisiert werden soll, das Subquery ausgeführt wird. Bei kleineren Datenmengen macht das nichts, wenn es allerdings Millionen an Datensätze sind, braucht das UPDATE ewig.

Wie könnte die Lösung aussehen? Richtig, man führt den Subquery nur 1x für alle Teams aus und selektiert den jeweiligen Eintrag dann für das entsprechende Team:

UPDATE 
    Team,
    (SELECT teamid,
        COUNT(*) as anzahl,
    FROM User) AS usersub
SET 
    Team.AnzahlMitglieder = usersub.anzahl
WHERE
    Team.Teamid = usersub.teamid)

 

Performance: Einzelne SQL-Statements

Was macht das Programm?

  1. Aus einer großen XML-Datei werden per SAXParser Datensätze eingelesen. Insgesamt gibt es 1.109.270 "Datensätze" in der XML-Datei.
  2. Aus jedem Datensatz werden 2 SQL-Queries erzeugt: Ein REPLACE-Statement zum Einfügen/Aktualsieren von Stammdaten und ein INSERT-Statement zum Einfügen von Bewegungsdaten

Es werden also kontinuierlich SQL-Statements abgesetzt.

Laufzeit des Programms

41 Minuten und 36 Sekunden

Performance: Generierung der CSV-Datei & LOAD DATA INFILE

Was macht das Programm?

  1. (identisch mit vorherigem Programm:) Aus einer großen XML-Datei werden per SAXParser Datensätze eingelesen. Insgesamt gibt es 1.109.270 "Datensätze" in der XML-Datei.
  2. Aus jedem Datensatz werden 2 CSV-Dateien um einen Datensatz erweitert. Die erste Datei enthält die Daten für die Stammdaten-Tabelle, die zweite Datei enthält die Bewegungsdaten.
  3. Zwei LOAD DATA INFILE Befehle zum Einlesen der CSV-Dateien.

Das Programm erzeugt also zuerst 2 Dateien und greift erst zum Schluss auf die Datenbank zu.

Laufzeit des Programms

30 Minuten und 9 Sekunden

Performance: Nur LOAD DATA INFILE

Wenn wir annehmen, dass die 2 CSV-Dateien schon vorhanden sind und nur noch die LOAD DATA INFILE-Befehle ausgeführt werden müssen, so sind die immerhin 2.218.540 Datensätze innerhalb von

2 Minuten und 23 Sekunden

importiert.

Performance: Prepared Statements

Prepared Statements sollten nicht nur aus sicherheitsrelevanten Überlegungen eingesetzt werden, sondern bringen - richtig implementiert - auch einen Performance-Vorteil. Dieser Vorteil lässt sich so erklären, dass 1x das SQL-Statement mit "?" als Platzhalter an das DBMS übertragen wird und danach werden für jeden Datensatz nur noch die Parameter übertragen, die in die entsprechenden Platzhalter durch das DBMS eingefügt werden. Es fallen also weniger zu übertragene Daten an, aber vor allem können die Daten so übertragen und verarbeitet werden, dass sie nicht mehr durch das DB-System interpretiert bzw. konvertiert werden müssen. Das Ergebnis kann sich sehen lassen:

Das Programm ist identisch mit dem Programm, welches kontinuierlich, einzelne SQL-Statements absetzt. Allerdings ist es so programmiert, dass es die Performance-Vorteile der Prepared Statements nutzt.

Laufzeit des Programms

28 Minuten und 30 Sekunden

Fazit

Bei so großen Datenmengen, die sequentiell eingelesen werden, kommt es auf die richtige Wahl der Methode an:

Vorteile Prepared Statements:

  • sind einfach zu implementieren
  • sind  im Performance-Vergleich noch ein Stück weit schneller als CSV-Dateien zu erstellen und per LOAD DATA INFILE einzulesen

Nachteile Prepared Statements:

  • die Datenbank wird über die gesamte Zeit stark belastet

Vorteile Generierung von CSV & LOAD DATA INFILE:

  • die Datenbank wird nur für kurze Zeit beansprucht
  • die CSV-Datei kann nach dem Generieren gespeichert bleiben und zu Diagnosezwecke bzw. wiederholten Datenimport verwendet werden

Nachteile Generierung von CSV & LOAD DATA INFILE:

  • kompliziertere Implementierung
  • zusätzlicher Festplattenverbrauch durch CSV-Dateien
  • geringfügig langsamer als Prepared Statements