The Log Book of Manuel Kiessling / Thu, 02 Sep 2010 12:29:30 +0000 http://wordpress.org/?v=2.9.2 de hourly 1 Testgetriebene Administration – test driven administration /2010/09/01/testgetriebene-administration-test-driven-administration/ /2010/09/01/testgetriebene-administration-test-driven-administration/#comments Wed, 01 Sep 2010 22:57:25 +0000 Manuel Kiessling /?p=188 Ich hatte tatsächlich einmal eine ganz eigene Idee. Und sie war gut, auch nachdem ich sie mehrmals durchgekaut und von allen Seiten beleuchtet hatte.

Wieso eigentlich sollte man die Prinzipien und Methodiken von testgetriebener Softwareentwicklung nicht auch auf den Bereich der IT-Systemadministration übertragen? Also in aller Kürze: Ich definiere Tests, die das vom noch zu implementierenden System erwartete Verhalten prüfen, sehe zu wie diese Tests fehlschlagen, und erfülle dann schrittweise diese Tests, indem ich das System aufbaue. Test driven administration – TDA.

Da war ich ganz alleine drauf gekommen, und ich war sehr stolz.

Dann habe ich gegoogelt. Die Idee existiert seit mindestens 2006.

Aber hey, gut ist die Idee trotzdem, also beschreibe ich sie hier.

Warum möchte man testgetrieben administrieren? Die Gründe sind dieselben wie bei testgetriebener Entwicklung: Habe ich Tests, bin ich gegen Regression geschützt, d.h. ändert ein Stück Code / ein System sein Verhalten aufgrund von Änderungen, weisen mich die Tests darauf hin.

Gehe ich testgetrieben vor, sind die Tests nicht irgendwas, das ich ganz unbedingt machen sollte, das aber doch am Ende runterfällt, sondern sie sind garantiert vorhanden. Mit den bekannten angenehmen Begleiterscheinungen, dass die Tests einen zwingen, sich Gedanken darüber zu machen, wie das Ziel eigentlich beschaffen sein soll, und automatisch dazu führen, die Lösung schlank und elegant umzusetzen.

Code und IT-Systeme sind aber nicht dasselbe, wie würde man also in der Praxis konkret vorgehen? Hier mein Vorschlag.

Zuerst benötigt man ein Testwerkzeug. Um in der Softwareentwicklung Unittests zu bauen, benutzt man Tools aus der xUnit Familie wie JUnit oder phpUnit. Das Äquivalent zu diesen Tools in der Systemadministration sind Monitoringsysteme wie Nagios oder Zabbix.

In der Softwareentwicklung formuliert man Unittest so, dass man eine kleine Einheit des Gesamtsystems, also in der Regel die einzelnen Methoden einer Klasse, mit einer gewissen Erwartungshaltung (“wenn ich diese Parameter reingebe, erwarte ich jenen Rückgabewert”) aufruft, und dann die erwartete Rückgabe mit der tatsächlichen vergleicht.

Was wäre dementsprechend “erwartetes Verhalten” bei einem IT-System? Nehmen wir an, die Anforderungen lauten wie folgt:

Benötigt wird ein Linux-System, welches unter der IP 123.456.789.000 einen Webserver bereitstellt, und die Festplattengröße des Systems soll 100 GB betragen.

In der Realität wären die Anforderungen natürlich umfangreicher, aber ich halte das Beispiel einfach.

Aus den Anforderungen lässt sich das gewünschte Verhalten ableiten:

  • Bei einem Ping auf 123.456.789.000 muss eine Antwort erfolgen
  • Die Abfrage des Betriebssystems unter dieser IP muss “Linux” ergeben
  • Ein HTTP Request gegen diese IP unter Port 80 muss eine HTTP Antwort zur Folge haben
  • Bei der Abfrage der Festplattengröße muss ein Wert von 100 GB zurückgeliefert werden

Daraus wiederum kann man im Monitoringsystem Tests formulieren. Diese lässt man einmalig laufen, um zu verifizieren, dass sie tatsächlich fehlschlagen. Und dann beginnt man damit, ein System aufzusetzen, das die Testbedingungen erfüllt, bis schliesslich alle Tests “grün” sind.

Das ist der Kern der Idee. Im weiteren Verlauf überwacht man die Tests regelmäßig (was man mit einem Monitoringsystem ja eh tut), und hat damit das Thema Continuous Integration gleich mit erschlagen. Ansonsten geht man genauso wie auch beim TDD vor: Möchte man Änderungen an einem System vornehmen, passt man zuerst die Tests an, verifiziert dass sie fehlschlagen, und ändert dann das System, um die Tests wieder zu erfüllen.

]]>
/2010/09/01/testgetriebene-administration-test-driven-administration/feed/ 0
<angular/> – ein radikal neuer Weg, Ajax Applikationen zu schreiben /2010/08/25/angular-ein-radikal-neuer-weg-ajax-applikationen-zu-schreiben/ /2010/08/25/angular-ein-radikal-neuer-weg-ajax-applikationen-zu-schreiben/#comments Wed, 25 Aug 2010 09:21:13 +0000 Manuel Kiessling /?p=158 <angular/> bringt JavaScript-Logik und das dazugehörige HTML Dokument deutlich näher zueinander als bestehende Frameworks wie beispielsweise jQuery. Es entfernt gleich mehrere Ebenen an Abstraktion, die ein Stück JavaScript-Code und das DOM-Element, auf welchem der Code operieren möchte, voneinander trennen.]]> JavaScript, Ajax und DHTML sind nicht wirklich meine Welt. Zum einen, weil ich einfach grundsätzlich eher mit dem Backend einer Software als mit dem Frontend zu tun habe, zum anderen, weil ich immer schon das ungute Gefühl hatte, in diesem Bereich muss man einfach deutlich zu viel Code produzieren um damit dann gefühlt deutlich zu wenig zu erreichen.

Umso mehr hat <angular/> mein Interesse geweckt. Die Autoren versprechen:

Write less code. A lot less. Forget about writing all that extra JavaScript to handle event listeners, DOM updates, formatters, and input validators. comes with autobinding and built-in validators and formatters which take care of these. And you can extend or replace these services at will. With these and other services, you’ll write about 10x less code than writing your app without .

In einem Video versucht Miško Hevery zu erklären, was <angular/> eigentlich ist, und stellt fest dass diese Erklärung schwierig ist:

Mein Verständnis ist in erster Linie: <angular/> bringt JavaScript-Logik und das dazugehörige HTML Dokument deutlich näher zueinander als bestehende Frameworks wie beispielsweise jQuery. Es entfernt gleich mehrere Ebenen an Abstraktion, die ein Stück JavaScript-Code und das DOM-Element, auf welchem der Code operieren möchte, voneinander trennen.

Während man bei traditioneller JavaScript-Programmierung stets gezwungen ist, explizit das HTML Dokument mit JavaScript-Code zu manipulieren, macht <angular/> die Verbindung zwischen Logik und HTML-Repräsentation implizit – etwas, das mich übrigens stark an die Mechanik erinnert, die Max Winde für siqqel einsetzt.

Spielen wir ein einfaches Beispiel durch, welches ich dank der rein clientseitigen Arbeitsweise von <angular/> problemlos direkt hier im Post zum laufen bringen kann.

Zuerst binde ich die <angular/> JavaScript Bibliothek ein:

<script type="text/javascript" src="http://angularjs.org/ng/js/angular-debug.js" ng:autobind>
</script>

Nun definiere ich ein Input Feld sowie einen <angular/>-Platzhalter, welche beide in einer Beziehung zueinander stehen:

Dein Name: <input type="text" name="deinname" value="Manuel"/>
<br />
Hallo {{deinname}}!

Wodurch entsteht diese Beziehung? Sie ist dank Autobinding implizit, und mappt alleine aufgrund des Formularfeldnamens und des Platzhalternamens beide zusammen.

Das Ergebnis sieht man hier – einfach den Inhalt des Textfeldes ändern:

Eingabe:
– Hallo {{yourname}}!

Dieses Beispiel geht natürlich maximal als Spielerei durch. Es zeigt aber schon, wieviel weniger Code nötig ist, als dies mit einem klassischen Framework der Fall wäre.

Ein etwas praxisnäheres Beispiel findet man unter http://angularjs.org/Cookbook:BasicForm. In diesem Beispiel geht es um den klassischen Fall, Eingaben in ein Textfeld per JavaScript clientseitig zu validieren – dies ist einfach möglich durch folgende schlanke und ausdrucksstarke Syntax:

<input type="text" name="user.address.state" size="2" ng:required ng:validate="regexp:/^\w\w$/"/>

Für weitere Informationen verweise ich auf http://angularjs.org/Overview.

]]>
/2010/08/25/angular-ein-radikal-neuer-weg-ajax-applikationen-zu-schreiben/feed/ 0
Tutorial: Testgetriebene Entwicklung mit PHP /2010/08/23/tutorial-testgetriebene-entwicklung-mit-php/ /2010/08/23/tutorial-testgetriebene-entwicklung-mit-php/#comments Mon, 23 Aug 2010 16:24:50 +0000 Manuel Kiessling /?p=107 Einleitung

Testgetriebene Entwicklung (test driven development) ist eine Arbeitsmethodik, die Softwareentwickler dabei unterstützt, wichtige Qualitätsprinzipien bei der Erstellung von Code zu befolgen:

  • Lose Kopplung (loose couping) – weil man beim Schreiben von Unittests, dem zentralen Werkzeug der Methodik, ganz automatisch dazu verführt wird, innerhalb der Tests von Codeunits (Klassen, Methoden usw.) auszugehen, die möglichst wenige Abhängigkeiten zu anderen Modulen haben – einfach deshalb, weil das Schreiben der Tests dann zu nervig wird.
  • Saubere Trennung von Verantwortlichkeiten (separation of concerns) – aus ganz ähnlichen Gründen wie der erste Punkt: Jeder Test testet genau ein gewünschtes Verhalten, und dies führt ganz automatisch dazu, dass man später den Code, der die Tests erfüllen muss, in sauber voneinander getrennte und logisch strukturierte Einheiten teilt.
  • Schlanke Lösungen – testgetrieben bedeutet eben auch, dass man von den Tests getrieben ist, im besten Sinne: Man tut alles, um einen noch fehlschlagenden Test zu erfüllen; aber eben auch nur genau das und nicht mehr. Salopp gesagt: Man programmiert nicht mehr “einfach rum”, sondern arbeitet äußerst zielgerichtet und erzeugt Code, der nur genau das tut was er tun muss, was ganz automatisch zu einer schlanken und damit eleganten Lösung führt, in der sich zum Beispiel Bugs sehr viel schlechter verstecken können.

Darüber hinaus hat der testgetriebene Ansatz weitere nützliche Nebeneffekte:

  • Die im Laufe der Zeit aufgebaute Sammlung von Unittests kann man benutzen, um die mit Tests versehenen Units automatisiert immer wieder testen zu können, zum Beispiel um beim Mergen eines Entwicklungszweigs mit einem anderen Zweig (oder auch nach jedem einzelnen Commit in ein Versionskontrollsystem) sicherzustellen, dass sich alle Units auch nach der Zusammenführung zweier Entwicklungslinien noch so verhalten wie erwartet. Das Stichwort für weiterführende Lektüre ist hier die Kontinuierliche Integration (continuous integration).
  • Ein Unittest ist in der Praxis nicht nur ein Stück Code, sondern immer auch Dokumentation des erwarteten Verhaltens eines Systems – zumindest in einer für Programmierer lesbaren Form. Um als Unbeteiligter ein Stück Code oder ganze Teile eines Systems kennen zu lernen, ist es häufig effizienter, die dazugehörigen Tests zu lesen, als den Code selbst.
  • Hat man erst einmal die Tests komplett geschrieben, welche die noch zu erzeugenden Units testen sollen, ist es sehr einfach, die Arbeit am eigentlichen Code einfach mittendrin auch für längere Zeit zu unterbrechen – die Tests geben einem sofort einen Anhaltspunkt, wo man “weiterprogrammieren” muss, selbst wenn man gedanklich längst aus dem Thema war.
  • Testgetrieben zu entwickeln, erzeugt ein gutes Gefühl. Das mag banal klingen, aber es ist ein realer und wichtiger Faktor. Irgendwo habe ich mal eine sehr gute Definition des Begriffs “legacy code” gelesen: “legacy code” ist Code, vor dem man sich fürchtet – weil man nicht genau weiss was er tut, und deshalb Angst hat, ihn zu verändern. Testgetriebene Entwicklung ist die beste Vorsorge gegen legacy code – man weiss, es gibt eine Instanz die überwacht und aussagt, was der Code tun soll. Es wächst das Vertrauen in den eigenen Code und damit auch in die eigenen Fähigkeiten.

Die Unterteilung in zentrale Effekte und Nebeneffekte ist subjektiv. Ich habe die Erhöhung der Codequalität an sich für mich als wichtiger erlebt als zum Beispiel die Tatsache, dank der sich entwickelnden Testsammlung Regressionstests durchführen zu können. Geschadet hat mir jedenfalls noch kein einziger durch testgetriebene Entwicklung entstandener Effekt.

Voraussetzungen

Was benötigt man nun, um in PHP testgetrieben zu entwickeln? Im Wesentlichen vier Dinge:

  • Eine Arbeitsmethodik, um effizient zu testgetrieben entwickeltem Code zu kommen
  • Ein Organisationsprinzip, um Tests und zu testenden Code sinnvoll strukturieren zu können
  • Ein PHP Framework, um Testfälle schreiben zu können
  • Ein Tool, um Testfälle ausführen und auswerten zu können

Beginnen wir mit den letzten beiden Punkten, denn dank der Maßstäbe setzenden Arbeit von Sebastian Bergmann (http://sebastian-bergmann.de/) existiert ein Softwareprojekt, welches beide Anforderungen hervorragend erfüllt und längst der de-facto Standard für Unittesting unter PHP ist: PHPUnit.

Unter http://www.phpunit.de/manual/current/en/installation.html befindet sich eine ausführliche Anleitung für die in der Regel sehr einfache Installation.

PHPUnit ist sowohl ein Framework aus PHP Klassen, die es erlauben, Unittests für den eigenen PHP Code zu schreiben, als auch Kommandzeilen-Werkzeug, um die eigenen Tests auszuführen und in verschiedenen Formaten die Testergebnisse darzustellen.

Im weiteren Verlauf des Tutorials gehe ich davon aus, dass PHPUnit installiert und funktionsfähig ist.

Im Mittelpunkt von testgetriebener Entwicklung stehen aber nicht die Werkzeuge, sondern der Arbeitsprozess. Dieser folgt stets diesem Muster:

  • Schreiben des Tests für eine neu zu implementierende Funktionalität
  • Erfüllen des Tests mit so wenig Aufwand wie möglich, so dass dieser fehlerfrei durchläuft
  • Überarbeiten des Codes, der den Test erfüllt, so dass dieser keine Duplizierungen enthält, sauber abstrahiert ist, und dem eigenen Code-Style entspricht – und dabei immer noch den Test erfüllt

Diese Schritte werden immer wieder wiederholt, bis man keine neuen sinnvollen Tests mehr findet für die neue Funktionalität.

Möchte man bereits vorhandene Funktionalität ändern, die bereits mit Tests versehen ist, bedeutet testgetriebene Entwicklung, dass man zuerst die Tests ändert, um das neue erwartete Verhalten widerzuspiegeln, sicherstellt, dass die veränderten Tests fehlschlagen, und dann erst den Code anpasst, um die veränderten Tests wieder zu erfüllen.

Wäre noch die Frage der Testorganisation zu klären – einfacher ausgedrückt: Wohin mit den Tests? Meiner Meinung nach ist der einzig wirklich sinnvolle Ansatz, Code und Tests identisch zu strukturieren. Das bedeutet, der Test für die Klasse DefaultUser in

lib/core/user/default_user.php

sollte in der Datei

tests/core/user/default_user_test.php

in der Testklasse DefaultUserTest liegen.

Aber solange wir noch kein Beispiel für einen Unittest durchgespielt haben, bleibt vieles sehr abstrakt, also beginnen wir den praktischen Teil des Tutorials.

Ein erstes Beispiel

Angenommen, wir möchten mithilfe von PHP ein Forum programmieren. Auf die ein oder andere Art und Weise wird diese Software eine Unit enthalten müssen, die eine E-Mail Adresse auf Gültigkeit prüft. Wir haben also eine Erwartungshaltung, was der Code später einmal tun soll. Der Einfachheit halber definieren wir diese Erwartungshaltung in diesem Beispiel so:

Wenn eine E-Mail Adresse ohne @-Zeichen übergeben wird, dann liefere mir FALSE zurück, sonst TRUE

Diese Erwartungshaltung gießen wir nun in Form von PHP Code in einen Unittest. Da wir testgetrieben arbeiten, existiert noch keinerlei Code der diese Erwartungen erfüllen könnte.

Wir geben der Unit, die später einmal unsere formulierte Erwartung erfüllen soll, den Namen Verify. Daraus leitet sich als Klassenname für den Unittest die Bezeichnung VerifyTest ab.

Wir erzeugen daher folgende Datei:

tests/verify_test.php

Und füllen sie mit folgendem Grundgerüst:

<?php
require_once('/usr/lib/php/PHPUnit/Framework.php');

class VerifyTest extends PHPUnit_Framework_TestCase {}

Dieser Code repräsentiert einen Testcase, der noch keine Tests enthält. Wir inkludieren das PHP-Klassen Framework von PHPUnit, da wir unsere Testcase-Klassen von einer Klasse dieses Frameworks ableiten müssen. Je nach Plattform liegt die zu inkludierende Framework.php auch schon mal unter /usr/share/php/PHPUnit/Framework.php.

Den Testcase selbst formulieren wir, indem wir eine Klasse definieren, deren Name auf Test endet, und die von PHPUnit_Framework_TestCase erbt.

Dieser Testcase kann nun mithilfe des PHPUnit Kommandozeilentools ausgeführt werden. Dazu starten wir folgenden Befehl an der Kommandozeile:

phpunit tests/verify_test.php

Dadurch erhalten wir die folgende Ausgabe:

PHPUnit 3.4.13 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 7.25Mb

There was 1 failure:

1) Warning
No tests found in class "VerifyTest".

PHPUnit wertet den Testlauf als nicht erfolgreich (“Failure”), da keinerlei Tests innerhalb des Testcases gefunden wurden. Als nächstes fügen wir daher einen Test hinzu:

<?php

require_once('/usr/lib/php/PHPUnit/Framework.php');

class VerifyTest extends PHPUnit_Framework_TestCase {

public function test_falseIfNoAtSign() {
$actual = Verify::checkEmail('manuel.kiessling.net');
$this->assertFalse($actual);
}

}

Einen Test innerhalb eines Testcase formuliert man, indem man der Testcase-Klasse eine Methode hinzufügt, deren Name mit test beginnt.

Innerhalb der Methode schreibt man nun den Code, der notwendig ist, um den oder die Werte von der zu testenden Unit zu bekommen, mithilfe derer man das erwartete Verhalten verifizieren kann.

Die von der Unit erhaltenen Werte testet man nun gegen eine Behauptung, einen assert: Wir drücken hier also aus, dass der Test erwartet, dass der zu testende Wert FALSE ist.

Letztendlich muss man sich aber immer bewusst machen: Man möchte Verhalten testen, nicht Daten. Daten drücken nur das Ergebnis eines Verhaltens aus. Entsprechen die tatsächlichen (actual) Daten den erwarteten (expected) Daten, dann entspricht das tatsächliche Verhalten dem im Test erwarteten.

Nun lassen wir den neu formulierten Testcase erneut durchlaufen, mit folgendem Ergebnis:

bash$ phpunit tests/verify_test.php
PHPUnit 3.4.13 by Sebastian Bergmann.

PHP Fatal error: Class 'Verify' not found in tests/verify_test.php on line 8

Wenig überraschend beschwert sich PHP (nicht PHPUnit!), dass wir eine Klasse verwenden, die nirgends definiert wurde. Tun wir dies also, indem wir eine Datei lib/verify.php erzeugen und mit folgendem Inhalt füllen:

< ?php

class Verify {}

Dann muss im Testcase noch sichergestellt werden, dass die Datei mit dieser Klasse auch inkludiert wird:

<?php

require_once('/usr/lib/php/PHPUnit/Framework.php');
require_once('lib/verify.php');

class VerifyTest extends PHPUnit_Framework_TestCase {

public function test_falseIfNoAtSign() {
$actual = Verify::checkEmail('manuel.kiessling.net');
$this->assertFalse($actual);
}

}

Lassen wir den Testcase nun laufen, ändert sich das Bild:

bash$ phpunit tests/verify_test.php
PHPUnit 3.4.13 by Sebastian Bergmann.

PHP Fatal error: Call to undefined method Verify::checkEmail() in tests/verify_test.php on line 9

Wir rufen eine Methode auf, die noch nicht existiert, also muss diese implementiert werden:

<?php

class Verify {

public static function checkEmail($email) {}

}

Nun steht zumindest die Codestruktur komplett, so dass PHPUnit ohne Fatals durchlaufen kann:

bash$ phpunit tests/verify_test.php
PHPUnit 3.4.13 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 7.00Mb

There was 1 failure:

1) VerifyTest::test_falseIfNoAtSign
Failed asserting that is false.

tests/verify_test.php:10

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Eine Zwischenbemerkung: Das Vorgehen ist hier natürlich sehr kleinschrittig – ob man die offensichtlichen Dinge wie das Anlegen der benötigten Klassen und Methoden nicht gleich in einem Rutsch macht, bleibt Geschmackssache. Ich persönlich habe Gefallen gefunden an dem Vorgehen, meine ganze Energie in die Tests zu stecken, und dann in einen anderen Modus zu schalten und ganz stupide Schritt für Schritt immer wieder die Implementierung anzupassen und den Testlauf neu zu starten, bis keinerlei Fehler mehr auftreten.

Wie auch immer, PHPUnit läuft nun wieder ohne PHP Fehler durch, bestätigt aber wenig überraschend, dass die nunmehr vorhandene Code-Unit nicht das Verhalten zeigt, welches wir laut Test von ihr erwarten. Wechseln wir nun also auf die inhaltliche Ebene der Implementierung und sorgen dafür, dass unser Code sich wie gewünscht verhält:

<?php

class Verify {

public static function checkEmail($email) {
if (!strstr($email, '@')) return FALSE;
}

}

Nun besteht unser Testcase alle Tests:

bash$ phpunit tests/verify_test.php
PHPUnit 3.4.13 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 7.00Mb

OK (1 test, 1 assertion)

Damit wäre der erste Testzyklus komplett. Stellt sich die Frage, ob uns noch weitere Verhaltensweisen für unsere Unit einfallen, die wir von ihr erwarten. Es liegt auf der Hand, dass wir den Positivfall ebenfalls testen wollen, nämlich dass eine E-Mail Adresse mit @-Zeichen als valide erkannt wird. Natürlich würde man in der Realität noch viel mehr Ansprüche an die Validierung einer E-Mail Adresse stellen, aber in diesem Beispiel bleibe ich der Einfachheit halber unrealistisch.

Eine Faustregel der testgetriebenen Entwicklung lautet, immer nur ein Verhalten pro Test zu überprüfen, anders ausgedrückt “ein assert pro Test”. Dies hilft, die einzelnen Tests übersichtlich und nachvollziehbar zu halten, und hat auch ganz praktischen Nutzen, da PHPUnit bei der Ausgabe eines Failures innerhalb eines Tests nicht darauf hinweist, welcher assert genau nicht erfüllt wurde, sondern immer den gesamten Test als fehlgeschlagen zu melden – hat man einen Test mit 20 asserts geschrieben, wird die Fehlersuche aufwendig.

Formulieren wir also einen weiteren Test:

<?php

require_once('/usr/lib/php/PHPUnit/Framework.php');
require_once('lib/verify.php');

class VerifyTest extends PHPUnit_Framework_TestCase {

public function test_falseIfNoAtSign() {
$actual = Verify::checkEmail('manuel.kiessling.net');
$this->assertFalse($actual);
}

public function test_trueIfAtSign() {
$actual = Verify::checkEmail('manuel@kiessling.net');
$this->assertTrue($actual);
}

}

Danach sollte man allerdings, obwohl kleinschrittig, auf jeden Fall den Testcase einmal durchlaufen lassen und ihm beim Fehlschlagen zusehen: Auch beim Schreiben von Tests können Fehler passieren, und es kommt vor, dass man einen neuen Test formuliert, der wegen eines Fehlers in der Implementation oder im Test sofort erfüllt wird – geht man nach dem Schreiben des Tests sofort an die Implementation, ohne zuvor den Test einmal fehlschlagen gesehen zu haben, übersieht man möglicherweise einen Bug in der Implementation oder im Test, wenn man erst dann den Test laufen lässt und dieser dann ohne Fehler durchläuft.

Dann sorgt gar nicht die eigene Änderung an der Implementation für das funktionieren des Tests, sondern ein Bug, den man aber eben nicht bemerkt.

Also stellen wir sicher, dass unser neuer Test fehlschlägt:

bash$ phpunit tests/verify_test.php
PHPUnit 3.4.13 by Sebastian Bergmann.

.F

Time: 0 seconds, Memory: 7.00Mb

There was 1 failure:

1) VerifyTest::test_trueIfAtSign
Failed asserting that is true.

tests/verify_test.php:15

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

Und nun ändern wir die Implementation, um ihn zu erfüllen:

<?php

class Verify {

public static function checkEmail($email) {
if (!strstr($email, '@')) return FALSE;
return TRUE;
}

}

Nun laufen beide Tests im Testcase erfolgreich durch:

bash$ phpunit tests/verify_test.php
PHPUnit 3.4.13 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 7.00Mb

OK (2 tests, 2 assertions)

Das hier beschriebene Beispiel ist natürlich banal, aber im Grunde ist alles Wichtige zur Methodik der testgetriebenen Entwicklung gesagt.

Aber auch in der eigenen Praxis, auch bei spannenden Projekten, wird man aber immer wieder dem Gefühl begegnen, dass der einzelne Test im Grunde trivial ist. Aber das ist auch völlig in Ordnung: Selbst komplexeste Softwareprojekte sind letztendlich die Verknüpfung kleiner und für sich betrachtet trivialer Funktionsinheiten – aber aus dem Zusammenspiel dieser vielen einfachen Module ergibt sich die Lösung komplexer Probleme für den Anwender.

]]>
/2010/08/23/tutorial-testgetriebene-entwicklung-mit-php/feed/ 0
Empfehlung: Barbecue Sauce “Bone Suckin’ regular thicker style” von Ford’s Food /2010/07/13/empfehlung-barbeque-soss-bone-suckin-sauce-regular/ /2010/07/13/empfehlung-barbeque-soss-bone-suckin-sauce-regular/#comments Tue, 13 Jul 2010 11:17:46 +0000 Manuel Kiessling /?p=98 Klar, über Geschmack lässt sich immer streiten, aber über den Geschmack von Barbecue Saucen wahrscheinlich am meisten.

Die beste Sauce, die zu probieren ich bisher das Vergnügen hatte, ist jedenfalls völlig zweifelsfrei die “Bone Suckin’ Sauce regular” von Ford’s Food.

Phil Ford war Immobilienmakler und hat aus einem Hobby heraus angefangen, diese Sauce zu entwickeln. Sie hat nur wenig Raucharoma und ist nicht scharf, im Vordergrund steht vor allem ein fantastisches Tomatenaroma und eine gewisse Süße, die es aber auch nicht übertreibt. Man muss sich sehr beherrschen, sie nicht direkt auszulöffeln.

Die offizielle Homepage zur Sauce ist www.bonesuckin.com, zu beziehen ist sie in Deutschland zum Beispiel über BOS FOOD in Meerbusch:

Bone Suckin´ Sauce regular, Barbecue-Sauce (dickflüssig), Ford´s Food, 454 g bei bosfood.de.

]]>
/2010/07/13/empfehlung-barbeque-soss-bone-suckin-sauce-regular/feed/ 0
Empfehlung: “Der Kuchenladen” in Berlin /2010/07/13/empfehlung-der-kuchenladen-in-berlin/ /2010/07/13/empfehlung-der-kuchenladen-in-berlin/#comments Tue, 13 Jul 2010 11:08:46 +0000 Manuel Kiessling /?p=89 Die Konditorei “Der Kuchenladen” ist sogar die nervige Parkplatzsuche auf der Kantstraße wert. Handgemachte Torten, Kuchen und Tarts, die klasse aussehen und einfach gut schmecken.

Das Wichtigste: Auf übertriebene effekthascherische Zuckerguß-Ungetüme wird verzichtet – die Torten zeichnet neben der spürbaren handwerklichen Qualität vor allem aus, dass sie nicht zu süß sind.

Als Schwiegersohn einer Konditoreimeisterin bin ich sehr verwöhnt, aber der Kuchenladen hat mich noch nie enttäuscht.

Der Kuchenladen

Kantstraße 138

10623 Berlin

Telefon: 030 / 310 184 24

E-Mail: uwe_gundelach@yahoo.de

http://der-kuchenladen.com/

]]>
/2010/07/13/empfehlung-der-kuchenladen-in-berlin/feed/ 0
Alte Homepage wieder verfügbar /2010/04/30/alte-homepage-wieder-verfugbar/ /2010/04/30/alte-homepage-wieder-verfugbar/#comments Fri, 30 Apr 2010 12:36:52 +0000 Manuel Kiessling /?p=26 http://old.manuel.kiessling.net/ erreichbar.]]> Meine alte Homepage (2000-2005) ist wiederauferstanden und unter http://old.manuel.kiessling.net/ erreichbar.

]]>
/2010/04/30/alte-homepage-wieder-verfugbar/feed/ 0
siqqel: SQL-Abfragen direkt aus HTML heraus ausführen und darstellen /2010/04/08/siqqel-ein-sehr-nutzliches-tool-fur-entwickler-business-analysten-produktmanager-und-qaler/ /2010/04/08/siqqel-ein-sehr-nutzliches-tool-fur-entwickler-business-analysten-produktmanager-und-qaler/#comments Wed, 07 Apr 2010 23:15:47 +0000 Manuel Kiessling http://localhost/wordpress/?p=6 Ein Kollege von mir, Max Winde, hat in den vergangenen Wochen ein Tool geschrieben welches sich innerhalb kürzester Zeit zu einem Renner in den verschiedensten Abteilungen entwickelt hat, und schon jetzt aus dem Arbeitsalltag kaum noch wegzudenken ist: siqqel.

Welchen Zweck erfüllt siqqel?

Die verschiedensten Leute in einem Unternehmen müssen aus den verschiedensten Gründen auf relationale Datenbanken zugreifen. Klassischerweise gibt es zwei Szenarien:

  • Ich brauche eine einfache und kurze Information
  • Beispiel: “Wie war noch gleich der ‘name’ des ‘product’ mit der Id 12345?” oder “Wieviele Einträge waren doch gleich in der ‘city’ Tabelle?”

Üblicherweise schmeisst man dafür direkt die SQL Kommandozeile an, oder man benutzt ein Tool wie Toad, phpMyAdmin, oder irgend einen anderen Query-Browser.

  • Ich benötige eine komplexe Auswertung wichtiger Kennzahlen, inklusive historischer Betrachtung und Querverweisen, und diese brauche ich langfristig und regelmäßig
  • Beispiel: “Wir müssen die Conversions unserer User auswerten” oder “Ich brauche eine täglich aktualisierte Auswertung unserer Produktverkäufe”

Üblicherweise werden hierfür komplexe und spezialisierte Enterprise-Tools wie Data Warehouses benutzt und manchmal auch selbst implementiert.

Das ist auch alles fein, und die Tools für beide Szenarien sind vielfältig und ausgereift. In der Praxis gibt es aber ein weiteres Szenario, welches sozusagen “dazwischen” liegt: Hier ein paar Beispiele:

  • Die QA Abteilung soll einem Bug nachspüren und muss dafür über einen Zeitraum von einigen Tagen einige mittelkomplexe Datenanalysen fahren und diese regelmäßig aktualisieren (“zu welcher Tageszeit kommt es vor dass User aus Gruppe X auf Seite Y Aktion Z durchführen, und dann die Kombination der Daten aus Tabelle A, B, und C gleich D ergibt?”)
  • Ein Produktmanager soll ein neues Feature konzeptionieren und benötigt dafür über einen sehr begrenzten Zeitraum eine Auswertung über verschiedene Business-Kennzahlen. Da die Analyse auf ganz neuen Annahmen beruht, helfen die im Data Warehouse vorhandenen Reports nicht weiter.
  • Ein Softwareentwickler arbeitet an der Anbindung eines externen Webservice, und möchte während der Implementations- und Testphase alle Tabellen und die zusammengehörenden Daten, die aus Webservice-Calls resultieren, im Blick haben, ohne jedes Mal 30 einzelne Queries abfeuern und miteinander in Verbindung bringen zu müssen.
  • Ein Business Analyst soll einen größeren Report vorbereiten, möchte aber erst mal ein Gefühl dafür bekommen welche Daten er benötigt und wie er diese sinnvoll miteinander verknüpfen kann.

Alle diese Beispiele haben eines gemeinsam: Die “kleine” Lösung, direkt einzelne Queries nacheinander an die DB zu schicken und die Ergebnisse dann händisch zusammenzutragen und miteinander in Verbindung zu bringen, ist zu klein, damit zu anstrengend und ineffektiv. Man kennt das, man fängt dann an sich die Queries in irgendein Textfile zu pasten damit sie nicht verloren gehen, oder man hat in Tools wie phpmyadmin plötzlich 15 Browsertabs auf und wird langsam wahnsinnig.

Die “große” Lösung ist aber wiederum zu groß:  Es lohnt in der Regel nicht, einen Business-Analysten mehrere Stunden oder Wochen mit dem Bau eines Reports aus dem Data Warehouse zu beauftragen, nur weil man wenige Tage lang etwas beobachten oder nur vorübergehend Daten debuggen muss.

Der Kompromiss sieht dann häufig so aus, dass man anfängt eine Zwischen-Notlösung auf irgend einer Insel zu bauen: Man fängt an, mit irgendwelchen ODBC Kontrukten und Excel. Schick mir so eine Excel Datei, und ich sehe nichts, denn ich habe ODBC gerade nicht richtig eingerichtet. Oder der Produktmanager, der seine temporäre, aber komplexe Auswertung braucht, bekommt eine virtuelle Maschine mit einer Basisinstallation von PHP, ein Developer gibt ihm einen Crash-Kurs in PHP-Entwicklung, und los geht das Gefrickel. Irgendwo fliegen dann diese Skripte rum, nach ein paar Monaten, wo sie vielleicht für eine neue, ähnliche Analyse noch mal nützlich gewesen wären, findet sie dann auch keiner mehr. Der PM schlägt sich mit Programmierung rum, Sysops meckert zu Recht, dass sie jetzt auch noch diese Spielkiste managen müssen, alle sind unglücklich, und irgendwo in der Ferne fängt ein kleines Kind an zu weinen.

Das muss nicht sein!

Denn genau diese Nische zwischen “einfach mal ein Query” und “das große böse komplette Data Warehouse” besetzt siqqel exzellent, ohne die Probleme der frickeligen Insellösungen einzuführen.

Wie funktioniert siqqel?

Die Mächtigkeit von siqqel liegt darin, dass es den Applikationsstack, der benötigt wird, um Anfragen an die Datenbank zu übermitteln, die Antwort entgegenzunehmen und die empfangenen Daten darzustellen, auf etwas recht bekanntes und verbreitetes beschränkt: den Browser.

SQL Queries werden direkt in einer statischen HTML Datei notiert. Per Ajax werden diese an ein zentral abgelegtes Backend-Skript übermittelt. Das Result Set wird an den Browser zurückgeliefert und direkt dort per DHTML dargestellt. Mit (D)HTML Bordmitteln, JavaScript und CSS kann man direkt innerhalb des HTML Dokuments dann beliebig flexibel mit den Result Sets arbeiten.

Richtig, ganz ohne PHP geht es nicht. Es braucht einen Punkt im Backend, welcher den SQL Query vom Browser entgegennimmt, an die DB übermittelt, und das Result Set als JSON an den Browser zurückliefert. Aber man beachte die Vorteile zur vorhin beschriebenen Insellösung:

  • Das Skript wird einmalig an zentraler Stelle in der Serverlandschaft hinterlegt – zum Beispiel an dieselbe Location, an der bereits der phpMyAdmin läuft; dann hat man vielleicht sogar gleich die Frage der Zugriffsrechte erschlagen, denn (üblicherweise) haben nur die richtigen Personen im Unternehmen Zugriff auf diese Ressource, und Sicherheitsmechanismen, die für die Zugriffsicherung des phpMyAdmin bereits implementiert wurden (wie .htaccess, SSL Public Keys etc.), greifen ohne zusätzlich notwendige Handgriffe auch für das PHP Backend von siqqel.
  • Nun kann jeder sofort anfangen, Reports auf Basis von siqqel zu bauen – alles was er braucht: Zugriffsrecht auf die HTTP Location des PHP Backend Skripts – und einen Browser!

Wie funktioniert das nun im Detail?

Angenommen, es gibt im Intranet eine MySQL Datenbank mit einer Tabelle, in der stehen alle Produkte des Unternehmens. Nennen wir sie ‘product’, und nehmen an sie befindet sich im Schema ‘data’. Nehmen wir weiterhin an, es gibt einen Server, auf dem wurde phpmyadmin installiert, damit man über diese Datenbank browsen kann. Dieses ist erreichbar unter unter http://intranet/secure/phpmyadmin. /secure ist der mit Zugriffsrechten versehene Teil des Servers.

Nun muss ein Systemadministrator die PHP Backendskripte unter http://intranet/secure/siqqel/ hinterlegen, und die Konfiguration anpassen um dem siqqel PHP Code Zugriff auf die genannte Datenbank zu ermöglichen.

Ein siqqel User muss dann nur eine HTML Datei erzeugen (auf seinem Desktop oder wo auch immer, ein LAMP Kontext wird ja nicht benötigt), die folgendes enthält:

<!DOCTYPE html>
<html>
<head>

<script type="text/javascript" src="http://intranet/secure/siqqel/siqqel.js.php"> </script>

</head>
<body>

<table sql="SELECT * FROM data.product"></table>

</body>

Öffnet er diese Datei lokal in seinem Browser, wird das SQL Statement im Attribut der Table an das Backend Skript übermittelt, das Result Set als JSON zurückgegeben, und der Inhalt der Datenbanktabelle automatisch in das table Element gerendert.

Von hier aus hat man alle Möglichkeiten: Man möchte mehrere Tabellen auf einmal anzeigen? Man erzeugt einfach mehrere table Elemente mit den entsprechenden Queries. Man möchte alle Zeilen im Result Set, bei denen die Spalte name mit “a” beginnt in der HTML Tabelle hervorheben? Kein Problem, jede Tabelle, Zeile und Spalte liefert ein “loaded” Event, also hat man mit einem JavaScript-Konstrukt wie


$('td.name').live('loaded', function(name) {
// do something useful.
});

alle Möglichkeiten. Der Client Teil von siqqel basiert auf jQuery, also kann man schnell und einfach Reports bauen mit allen sinnvollen und sinnlosen Möglichkeiten, die jQuery bietet.

Was sind die weiteren Vorteile? Nun, die HTML Datei ist nicht nur der View des Reports, die HTML Datei IST der Report. Man kann ihn in die vielleicht vorhandenen Coderepositories im Unternehmen packen, man kann ihn per Mail verschicken, man, wenn die Wikisoftware es zulässt, seine Reports sogar direkt nativ in eine Wikiseite packen und so besonders effizient mit den Kollegen im Unternehmen teilen.

Die Projektseite von siqqel ist http://github.com/MyHammerOpenSource/siqqel. Nicht wundern, bis vor kurzem hieß das Projekt noch “sqlHammer”, der Begriff mag noch an verschiedenen Stellen auftauchen.

Bei Fragen zu siqqel empfehle ich, ein Issue Ticket bei github zu öffnen, oder wendet euch an opensource@myhammer.com.

]]>
/2010/04/08/siqqel-ein-sehr-nutzliches-tool-fur-entwickler-business-analysten-produktmanager-und-qaler/feed/ 0
Database Change Management mithilfe von VCS: Teil 1 /2010/02/26/database-change-management-mithilfe-von-vcs-teil-1/ /2010/02/26/database-change-management-mithilfe-von-vcs-teil-1/#comments Fri, 26 Feb 2010 12:39:01 +0000 Manuel Kiessling /?p=28 Dieser Artikel ist Work in Progress!

Vorüberlegungen

Dieses Dokument beschreibt Werkzeuge und Prozesse, um Datenbankänderungen innerhalb von großen Softwareprojekten einfach, fehlerfrei und nachvollziehbar durchzuführen und zu managen.

Zentraler Ansatz dieser Lösung ist: Datenbankänderungen und Codeänderungen sind prinzipiell genau dasselbe. Denn Datenbankänderungen haben genau wie Codeänderung die folgenden Eigenschaften:

  • Sie ändern das Verhalten des Softwaresystems
  • Sie entwickeln sich verteilt in verschiedenen Projekten bzw. Branches, und müssen für Abnahme und Rollout/Release zusammengeführt werden
  • Beim Zusammenführen kann es Überschneidungen und Konflikte geben, die man mitbekommen und lösen können möchte
  • Man möchte sie auch später noch nachvollziehen können, also sehen wer wann was gemacht hat
  • Man möchte diese Änderungen ggf. einem Reviewprozess unterziehen

Wenn wir Datenbankänderungen in diesem Sinne genau wie Codeänderungen verstehen, macht es auch Sinn, Datenbankänderungen genau wie Codeänderungen zu behandeln. Und das bedeutet, diese innerhalb des bereits vorhandenen Entwicklungsprozesses zu managen und im selben VCS Repository zu verwalten.

Abbildung der Datenbankänderungen im VCS

Unter Datenbankänderungen müssen wir verstehen: Alle SQL Statements, welche die Strukturen oder Inhalte einer Datenbank verändern.

Eine Datenbankänderung im Zuge eines Projekts, Bugfixes oder sonstigen Tickets ist daher folgerichtig eine Sammlung von SQL Statements, welche zusammen mit den Codeänderungen des zugehörigen Tickets im selben Branch vom Entwickler hinterlegt wird. Hinzu kommt, dass es eine klar definierte Lokalität für diese Änderung geben muss, damit ein Raum geschaffen ist, in dem Konflikte entstehen (und gelöst werden) können. So wie die gleichzeitige Änderung an der Datei myFile.txt in zwei verschiedenen, zu mergenden Branches zu einem Konflikt führt – da in beiden Branches die Datei den selben Speicherort, also dieselbe Lokalität besitzt – müssen auch Änderungen an derselben Tabelle in zwei Branches innerhalb derselben Lokalität des jeweiligen Branches stattfinden. Der vorgeschlagene Ansatz ist daher, die Struktur der Datenbank, also die Databases mit den darunterliegenden Tables, in einer analog aufgebauten Ordner-Datei-Struktur abzubilden.

Die Lokalität für die Tabelle users.hobbies wäre beispielsweise die Datei /dbchanges/users/hobbies.sql innerhalb des VCS. Abgebildet wird die gesamte DB Struktur, also alle Databases mit allen ihren Tables:


/dbchanges/users/hobbies.sql
/dbchanges/users/contact.sql
...
/dbchanges/products/colors.sql
/dbchanges/products/forms.sql
...

und so weiter. Gerade bei komplexen Datenbanken macht es natürlich Sinn, diese Struktur mit einem Skript zu erzeugen, für MySQL kann man dazu in einem beliebigen Verzeichnis auf dem DB Server folgenden Code ausführen (geht davon aus, dass die MySQL Daten unterhalb /var/lib/mysql liegen):


find /var/lib/mysql -type f -name *.frm -exec dirname {} \;| cut -d "/" -f 5| xargs mkdir -pfind /var/lib/mysql -type f -name *.frm | cut -d "/" -f 5,6 | sed "s/.frm/.sql/g" | xargs touch

Diese Dateien nenne ich im folgenden DB Change Container.

Prozessbeschreibung

Während der Produktion eines neuen Release

Wichtig ist, dass sämtliche DB Change Container nach einem Release, nachdem diese Änderungen also auf dem Produktivsystem angewendet wurden, wieder leer sind – denn zum Start der Produktion eines neuen Releases liegen noch keine neuen Änderungen für die DB vor.

Nun beginnen die Entwickler, Tickets (Feature Requests, Bugs etc.) umzusetzen, einige gemeinsam in einem Branch, einige in eigenen Branches. Sind im Zuge einer Implementation Datenbankänderungen notwendig, hinterlegt der Entwickler innerhalb des zugehörigen Branches diese Änderungen nach folgendem Muster:

  • Case 1: Die Tabelle user.hobbies soll verändert werden (neues Feld, Feld löschen, Index anlegen oder löschen, einfügen, löschen oder ändern von Einträgen etc.)

    Der Entwickler legt alle benötigten Statements in der Datei /dbchanges/users/hobbies.sql ab:


    USE users;
    ALTER TABLE hobbies ADD newfield1 INT NOT NULL AFTER userId;
    ALTER TABLE hobbies DROP oldfield;
    ALTER TABLE hobbies ADD newfield2 TINYINT NOT NULL;
    ALTER TABLE hobbies ADD INDEX (newfield2);
    INSERT INTO hobbies ( id, name, value ) VALUES (1234, 'hobbyname', 'hobbyvalue');

  • Case 2: Der Entwickler legt eine komplett neue Tabelle pets im vorhandenen Schema users an

    Er erzeugt dazu eine neue Datei /dbchanges/users/pets.sql und füllt sie mit dem CREATE Statement (sowie ggf. INSERT Statements):


    USE users;
    CREATE TABLE pets(
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    petname VARCHAR( 64 ) NOT NULL,
    FULLTEXT ( petname )
    );

  • Case 3: Der Entwickler legt eine neue Database products und darin eine neue Tabelle colors an

    Er erzeugt einen neuen Ordner /dbchanges/products und darin eine Datei colors.sql mit folgendem Inhalt:

    CREATE DATABASE products;
    USE products;
    CREATE TABLE colors (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    colorname VARCHAR( 24 ) NOT NULL);

  • Case 4: Der Entwickler löscht die Tabelle colors in der Database products

    Er füllt die Datei /dbchanges/products/colors.sql mit folgendem Inhalt:


    USE products;
    DROP TABLE colors;

Ansonsten läuft der Entwicklungsprozess wie gewohnt.

Merge aller Tickets für den Release

Werden nun verschiedene Tickets für den Release gebündelt, werden die einzelnen Branches wie gehabt gemerged. In Hinblick auf die DB Änderungen passiert nun folgendes:

Sämtliche Änderungen in den einzelnen Branches unterhalb von /dbchanges werden naturgemäß unterhalb /dbchanges im Merge zusammengeführt. Hierbei greifen die bekannten VCS Mechanismen: Wurden Änderungen in einer Datei nur in einem einzigen Branch oder Commit vorgenommen, werden diese Änderungen einfach angewendet. Wurden Änderungen an einer Datei (also innerhalb derselben Lokalität) in mehreren Branches vorgenommen, kommt es zu einem Konflikt.

Dies ist der erste wichtige Mechanismus der hilft, die drei Anforderungen – einfach, fehlerfrei und nachvollziehbar – zu gewährleisten: Da der Konflikt garantiert eintritt, ist auch garantiert, dass der Vorgang völlig automatisch die notwendige Aufmerksamkeit erzeugt und nicht übersehen werden kann.

Nun muss, wie auch bei Codekonflikten, gelöst werden: Machen beide Änderungen Sinn, oder widersprechen sie sich? Wie genau kann man sie am sinnvollsten zusammenführen? Relevant ist hier nur, dass am Ende ein Set an Änderungsanweisungen in den Approval committet wird, welches in sich rund ist. Falls es eine eigene Test oder QA Datenbank gibt auf die diese Änderungen angewendet werden müssen, wird dies gemacht nachdem alle Tickets fertig gemerged wurden.

Durchführung des Release

Wurde im Vorfeld alles richtig gemacht, muss im Zuge des Rollout oder Release nur noch das zusammengefasste Set an Änderungen ermittelt werden, und diese müssen dann, entsprechend ihrer jeweiligen Eigenschaft, ausgeführt werden. Die Summe der Änderungen ergibt sich aus der Summe aller Anweisungen in den DB Change Containern unterhalb /dbchanges – hier macht es natürlich Sinn, dass man diese mithilfe eines Skripts “zusammensammelt”, aber ich gehe hier nicht näher darauf ein.

Nach dem Rollout/Release, und vor dem Erzeugen neuer Branches, müssen dann im Trunk sämtliche Datenbank-Änderungsanweisungen aus den DB Change Containern entfernt werden (auch hier macht ein Skript wie z.B.


for f in `find . -type f -name *.sql`; do echo -n "" > $f; done

Sinn, um diesen Schritt zu vereinfachen), und dies muss in den Trunk (oder von wo aus auch immer neue Branches gebildet werden) committet werden – denn sonst würden dieselben Änderungen beim nächsten Rollout erneut angewendet werden.

]]>
/2010/02/26/database-change-management-mithilfe-von-vcs-teil-1/feed/ 0
Wie man Replikationsunterbrechung durch Deadlocks bei INSERT INTO … SELECT verhindert /2007/08/07/wie-man-replikationsunterbrechung-durch-deadlocks-bei-insert-into-select-verhindert/ /2007/08/07/wie-man-replikationsunterbrechung-durch-deadlocks-bei-insert-into-select-verhindert/#comments Tue, 07 Aug 2007 13:04:17 +0000 Manuel Kiessling /?p=36 Der My-Hammer Auftragsradar, der unsere Auftragnehmer auf Wunsch regelmässig per E-Mail über neu eingestellte Auktionen anhand einstellbarer Filterkriterien informiert, baut bei jedem Durchlauf eine eigene Suchtabelle auf. Diese wird gefüllt mit einer Untermenge der Daten unserer Haupt-Auktionstabelle, nämlich nur den derzeit laufenden Auktionen.

Die Verwendung von INSERT INTO … SELECT ist hier naheliegend, zum Beispiel so:

INSERT INTO Suchtabelle
 SELECT a, b, c FROM Auktionstabelle WHERE x = y

Es ergab sich folgendes Problem: Der Query wird wie jeder andere auch auf die Datenbankslaves repliziert. Dort wurde er auch korrekt ausgeführt. Jedoch nicht immer auf dem Master: hier kam es regelmäßig zu Deadlocks auf der Auktionstabelle, da dies eine InnoDB Tabelle ist (bei MyISAM Tabellen können Deadlocks nicht auftreten).

Wenn ein MySQL Slave jedoch feststellt, dass beim gleichen Query auf dem Master und auf dem Slave unterschiedliche Fehler auftreten (Slave: no error; Master: deadlock), unterbricht dieser die Replikation. Es hilft dann nur ein SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1; START SLAVE;.

Ich habe mich daraufhin nach Lösungen umgeschaut. Erste Anlaufstelle ist das Kapitel Vom Umgang mit Deadlocks im MySQL Handbuch.

Mein erster Versuch war, den 4. Tipp dieses Kapitels zu befolgen: Das Einstellen eines niedrigeren Isolationslevels. Da perfekte Datenkonsistenz für die benötigte Suchtabelle nicht nötig ist (dirty reads also akzeptabel sind), verwendete ich gleich den niedrigsten Level READ UNCOMMITED. Das Ergebnis war gelinde gesagt verheerend, es traten noch mehr Deadlocks auf als zuvor.

Deshalb bin ich dazu übergegangen, die beteiligten Tabellen explizit mit einem READ LOCK zu sperren – viele Artikel zu diesem Thema haken diese Vorgehensweise sofort als nicht gangbar ab, da die Performance darunter leide. Da es sich beim Auftragsradar jedoch um einen Cronjob handelt, der nur alle paar Minuten einmal läuft, und der INSERT INTO … SELECT Query sehr schnell durchläuft, erschien mir das Risiko, eine unserer wichtigsten Tabellen für diesen Query zu sperren, als gering.

Wie sich zeigte, brachte dies den gewünschten Erfolg: Seitdem sind an dieser Stelle keinerlei Deadlocks mehr aufgetreten, und der Rest der Plattform zeigt sich von den seltenen und kurzen Locks völlig unbeeindruckt.

]]>
/2007/08/07/wie-man-replikationsunterbrechung-durch-deadlocks-bei-insert-into-select-verhindert/feed/ 0
Recycelter Artikel: “My-Hammer, das Fernsehen und die Serverlast” /2007/07/17/recycelter-artikel-my-hammer-das-fernsehen-und-die-serverlast/ /2007/07/17/recycelter-artikel-my-hammer-das-fernsehen-und-die-serverlast/#comments Mon, 16 Jul 2007 23:22:28 +0000 Manuel Kiessling http://localhost/wordpress/?p=13 Vor mittlerweile auch schon wieder einer halben Ewigkeit hatte ich mal eine kurze Artikelserie zum Thema Serverlast-Problemlösungen bei MyHammer online, die ich nun wieder ausgegraben habe. Vieles entspricht gar nicht mehr den aktuell bei MyHammer eingesetzten Lösungen, aber verwahrenswert finde ich den Schrieb allemal. Leider fehlen die Grafiken, vielleicht finde ich die noch mal irgendwo.

Hier der Artikel:

Vergangenen Donnerstag zeigte das ProSieben Magazin Galileo einen ca. 10-minütigen Beitrag über My-Hammer (kurze Infos zur Sendung hier). Vom Ansatz her ging es um “Branchenbuch vs. My-Hammer”, aber für die Betrachtung hier ist das gar nicht so sehr von Interesse – es ist noch nichtmal von Interesse, ob so ein Beitrag positiv oder negativ für uns ist (in dem Fall war’s wie fast immer positiv) – sobald das Magazin, in dem über uns berichtet wird, genug Reichweite hat, schießen die Zugriffe in die Höhe. Die wichtigste Erkenntnis, die wir immer wieder machen: zumindest bei den Privaten scheinen die Zuschauer sprichwörtlich mit dem Laptop auf den Knien vorm Fernseher zu sitzen. Die Zugriffe kommen extrem schnell und gebündelt (beim Galileo-Beitrag war aber interessant, dass die Zugriffe wieder auf einen Schlag kamen, aber erst in dem Moment, in dem der Beitrag vorbei war).

Genau dieses plötzliche Auftreten so vieler Zugriffe ist natürlich die Herausforderung – dieselbe Anzahl User auf nur 15 Minuten verteilt wären kein Problem, aber TV sorgt dafür, dass das meiste innerhalb der ersten 5 Minuten passiert. Und es ist wirtschaftlich natürlich ziemlich unvernünftig, die für diese 5 Minuten benötigte Rechenpower anzuschaffen, nur damit sie die anderen 525.595 Minuten im Jahr vor sich hindümpelt.

Trotzdem kann man eine Webseite auch auf solche Extremsituationen vorbereiten – My-Hammer hat am Donnerstag perfekt standgehalten, lediglich eine leichte Verzögerung in den Ladezeiten war während der kritischen Phase spürbar.

Um kurz die Dimensionen klarzumachen, erstmal eine Grafik, welche den ein- und ausgehenden IP Traffic für unser Netzwerk anzeigt. Man sieht sehr deutlich den Sprung auf das gut 2,5-fache des normalen Werts. Der Faktor selbst klingt vielleicht erstmal nicht so dramatisch, aber wie erwähnt geht es nicht um die Masse an sich, sondern das extrem gebündelte Auftreten dieser Masse an Zugriffen:

(Die Grafik ist leider nicht mehr auffindbar)

Ich behaupte mal, man erkennt recht gut, wann die Sendung lief…

Also, wie kann man die Serversysteme auf so etwas vorbereiten? Klar: mehr Server kaufen. Das ist durchaus ein Aspekt, aber nicht das Allheilmittel. Vor allen Dingen kann das sehr ineffektiv und unwirtschaftlich sein. Angenommen, man hat Server A mit einer gewissen Leistungsfähigkeit. Nun kann man sich Server B mit doppelt so schnellem Prozessor, doppeltem Arbeitsspeicher und doppelt so schnellen Festplatten kaufen. Dann hat man schon Unmengen von Geld ausgegeben, und hat gerade mal eine Steigerung der Leistungsfähigkeit von 100% (mal davon abgesehen, dass die Rechnung “doppelt so schnelle Hardware, doppelt so viel Leistung” in der Praxis auch nicht wirklich hinhaut). Dagegen kann ein einziger geschickt gesetzter Index in der Datenbank manchmal 1000% bessere Performance bringen, ohne dass man etwas an der Hardware tut.

Wenn man den Anschaffungspreis neuer Hardware mal auf den Stundenlohn eines Entwicklers umrechnet, wird man schnell zu dem Schluss kommen, dass es sich auch finanziell durchaus rechnen kann, diesen einige Tage lang auf die Datenbank anzusetzen um zu schauen, ob nicht doch irgendwo ein wichtiger Index vergessen wurde oder einige Tabellenstrukturen besser ganz anders aufgebaut sein sollten.

Das sind nur ein paar grundsätzliche Überlegungen. Spürbaren Erfolg wird man nur haben, wenn man ein ganzes Bündel an Massnahmen ergreift und vor allem immer das Gesamtsystem vom Code über die Datenbank bis hin zu den Servern und dem Netzwerk im Überblick hat. Die vielleicht wichtigste Faustregel, wenn man über Performanceoptimierung von Webseiten spricht, scheint mir daher zu sein: Coder und Admins an einen Tisch! Es hilft nichts, wenn die Entwickler meinen, die Geschwindigkeit des Systems sei doch schliesslich Sache des Admins. Umgekehrt ist es extrem hilfreich, wenn die Programmierer auch einen gewissen Sysadmin-Background haben, und die Admins umgekehrt auch Programmiererfahrung haben; was bei uns glücklicherweise sogar sehr ausgeprägt der Fall ist.

Die weiteren Teile befassen sich mit den konkreten Massnahmen, die man ergreifen kann um sich auf einen TV Beitrag vorzubereiten.
Hinweis: Thematisch durchaus verwandt berichtet Tom Bachem über die Systemarchitektur von sevenload.

Welche Massnahmen kann man nun konkret ergreifen, um sich auf einen TV Beitrag über die eigene Webseite vorzubereiten? Ich versuche so allgemein wie möglich zu bleiben, aber da es um konkrete Ratschläge gehen soll und ich holprige Umschreibungen vermeiden möchte, wird das Vokabular ab jetzt etwas LAMP-lastig; bitte entsprechend auf die eigene Technik ummünzen.

Massnahme 1: Datenbankoptimierung

Wurde ja schon erwähnt: die Indizes. Ich verrate wahrscheinlich nicht einmal DB Anfängern etwas neues, wenn ich betone, dass dies essentiell ist. Wenn man die Indizes nicht im Griff hat, braucht man sich die anderen Punkte noch gar nicht anschauen. Deshalb: Ins Slow-Log gucken. Vor allem: Immer wieder. Einen Status Quo gibt es nicht! Immer wieder EXPLAIN bemühen, vom stumpf auf die Strukturen in phpMyAdmin gucken findet man die Performancefresser nicht.

Es gibt diese missverständliche Formel “Braucht man Geschwindigkeit, nimmt man MyISAM, braucht man Sicherheit, InnoDB”. InnoDB ist nicht nur einen Blick wert, wenn man Transaktionssicherheit braucht. Im Gegensatz zu MyISAM lockt InnoDB bei schreibenden Queries immer nur die betreffenden Zeilen, MyISAM dagegen grundsätzlich die gesamte Tabelle. InnoDB hat zwar aufgrund der größeren Komplexität etwas mehr “Grundoverhead”, aber das intelligentere Locking kann immens wertvoll sein in bestimmten Szenarien und das mehr als wettmachen. Wenn man eine Tabelle hat die man hinsichtlich Struktur und Indices schon perfekt durchoptimiert hat (genau das aber wiederum erstmal sicherstellen!), und trotzdem tauchen Queries auf diese Tabelle immer noch im Slow Log auf, dann sollte man prüfen, ob diese Queries vielleicht immer auf einen Lock warten. In diesem Fall InnoDB auf jeden Fall eine Chance geben. Das hat bei uns konkret bei den Session und Cachetabellen (dazu später mehr) enorm viel gebracht, weil dort die Lese- und Schreibzugriffe ein ausgewogenes Verhältnis haben.

Ein Aspekt, der wenig berücksichtigt wird, ist die Größe der Felder, auf die man Indices setzt. Es kann sich lohnen, hier sparsam zu sein, denn ein kleinerer Spaltentyp bedeutet auch weniger Speicherplatzverbrauch für den Index auf diese Spalte, und das kann im Zweifel nur gut (= schneller) sein. Man ist halt geneigt, seine Primary IDs immer als INT anzulegen. Aber nehmen wir mal den Klassiker Benutzertabelle: Wird man wirklich in nächster Zeit 4 Milliarden User haben? Das dürfte selbst bei eBay noch ein bisschen dauern. Erstmal tut es also auch ein MEDIUMINT, setzt man diesen UNSIGNED, ist das Limit bei 16 Millionen. Hat man soviele User, bewegt man sich wohl eh in völlig anderen Dimensionen.

Zumal das Umwandeln einer Spalte in einen Typ mit größerem Wertbereich (also z.B. von MEDIUMINT nach INT) unproblematisch ist. Wichtig ist allerdings auch, dass man sämtliche Felder, die einen Fremdschlüssel auf ein MEDIUMINT Feld darstellen, ebenfalls als MEDIUMINT anlegt, sonst hat man bei Joins nichts gewonnen.

Was bei der Skalierung von MySQL immer enorm hilft ist Replikation. Dazu wurde schon so viel geschrieben, dass ich mir die Wiederholung spare, nur dies: Wir fahren bisher sehr gut damit, das Balancing der Nur-Lese Zugriffe direkt in unserer Applikation zu regeln, und nicht über einen eigenen Software- oder Hardware-Loadbalancer. Da bei fast jedem Seitenaufruf der Master sowieso früher oder später konnektiert werden muss, kann man diese Verbindung auch nutzen, um MASTER STATUS und SLAVE STATUS zu vergleichen, um so ein Fallback auf den Master zu realisieren, falls alle Slaves einmal mehr als 0 Sekunden hinter dem Master zurückhängen. Was sich übrigens ziemlich gut vermeiden lässt, wenn man Master und Slaves per Gigabit statt Fast Ethernet anbindet.

Ein oft nicht wahrgenommener Vorteil von Replikation: Man kann einen Slave für’s Backup bereitstellen, auf dem man die Datenbank stoppen und auf Dateisystemebene wegkopieren kann (oder man hält nur den Slave Thread an und macht einen Dump), so dass man einen sauberen Snapshot der Datenbank hat, ohne das Gesamtsystem anhalten zu müssen.

Ein weiterer wichtiger Hebel für die Skalierung ist es, für spezielle Aufgaben jeweils eigene DB Server bereitzustellen, z.B. ein oder mehrere Maschinen nur für die Sessiontabellen, nur für Tabellen mit Cache-Inhalten, nur für Logtabellen; prinzipiell kann jede Tabelle, die nicht in Form von Joins oder Subselects zusammen mit anderen Tabellen gleichzeitig abgefragt werden muss, auch getrennt von den anderen Tabellen auf einem eigenen Server liegen. Darüber hinaus macht die Trennung von sehr verschiedenen Tabellen wie Session- und Logtabellen alleine deshalb schon Sinn, weil man dann die Datenbanksoftware für diese speziellen Aufgaben optimieren kann.

Eine praxisnahe Zusammenstellung der Massnahmen, die sich bei My-Hammer.de bewährt haben:

InnoDB vs MyISAM

Ich schrieb bereits, dass man InnoDB nicht nur dann in Erwägung ziehen sollte, wenn man Transaktionssicherheit benötigt. Eine Tabelle von MyISAM auf InnoDB umzustellen kann unter Umständen Geschwindigkeitsvorteile bringen, nämlich dann, wenn das zweite wichtige Feature von InnoDB neben der Transaktionssicherheit, das Row Level Locking, effektiv zum Zug kommen kann. Um herauszufinden, ob dies der Fall ist, kann man wie folgt vorgehen:

Mitloggen aller Queries

Wenn man für einen bestimmten Zeitraum (bei einer gut besuchten Seite reichen wenige Minuten) einmal alle Abfragen, die an die Datenbank gestellt werden, mitschreibt, kann man aus diesem Log eine Menge interessanter Informationen ziehen. Um festzustellen, ob eine Tabelle vom Row Level Locking profitieren könnte, muss man die lesenden (SELECT) und schreibenden (INSERT, UPDATE, DELETE etc.) Abfragen gegenüberstellen.

Wird aus einer Tabelle sehr häufig gelesen, die Daten in der Tabelle aber nur sehr selten verändert, dann macht das Table Level Locking von MyISAM in der Regel keine Probleme: Zwar wird bei einem UPDATE, INSERT oder DELETE die gesamte Tabelle für nachfolgende Lesezugriffe gesperrt (d.h. diese müssen warten), bis der Schreibprozess abgeschlossen ist. Aber da dies nur selten geschieht, kommt es auch selten vor, dass ein Leseprozess warten muss, so dass daraus keine spürbare Verzögerung im Gesamtsystem resultiert.

Gleiches gilt im umgekehrten Fall: Wird in eine Tabelle praktisch nur geschrieben, aber selten daraus gelesen (wie es z.B. bei Logtabellen häufig der Fall ist), dann kollidieren auch hier die “Interessen” nur so selten, dass nicht mit Performanceeinbußen zu rechnen ist.

Slow Log

Interessant sind also jene Tabellen, bei denen Schreib- und Lesezugriffe in einem ausgeglicheneren Verhältnis stehen. In welcher Relation die beiden Zugriffsarten dabei mindestens stehen müssen, damit es sich “lohnt” InnoDB einzusetzen, ist schwer zu sagen. Ein Blick ins Slow-Log von MySQL hilft hier weiter: Wenn man immer wieder bei denselben Tabellen auf langsame Queries stösst, die nicht wegen des Queries selbst langsam waren, sondern weil sie auf ein Lock warten mussten, hat man auf jeden Fall aussichtsreiche Kandidaten.

SHOW PROCESSLIST

Eine weitere Methode ist, sich einmal für einige Minuten immer wieder die Liste der laufenden Prozesse in MySQL auflisten zu lassen (SHOW PROCESSLIST). Wenn man dort immer wieder dieselben Queries sieht, deren Status Locked ist, dann weiss man wo das Problem liegt. Diese Methode mag zwar auf den ersten Blick wie ein Glücksspiel wirken, aber gerade weil man immer nur die Prozesse sieht, die zufällig gerade laufen wenn man den Befehl absetzt, fallen die problematischen Prozesse erst recht auf, die immer wiederkehren und oft vielleicht sogar während zwei oder mehr SHOW Aufrufen immer noch laufen. Meiner Meinung nach die schnellste Methode, Flaschenhälse zu finden.

Mehr zum Thema Locking gibt es im Kapitel ‘Internal Locking Methods’ des MySQL Handbuchs.

Nehmen wir also an, man hat einige Tabellen identifiziert, bei denen Queries öfter als gesund ist auf einen Lock warten müssen. Dies könnte beispielsweise eine Sessiontabelle sein (falls man z.B. PHP nutzt und die Sessionfunktionen so angepasst hat, dass diese eine MySQL Datenbank als Storage nutzen, ein ziemlich klassisches Szenario). Diese Tabelle wird bei jedem Seitenaufruf zu Beginn einmal gelesen, um die Session des aufrufenden Benutzers zu laden, und am Ende des Skripts wird der Sessioninhalt dieses Benutzers wieder geschrieben. Also ein sehr ausgewogenes Verhältnis zwischen lesenden und schreibenden Zugriffen – jeder Seitenaufruf, der gerade an dem Punkt angelangt ist, an dem die Session geschrieben wird, würde also die Tabelle sperren für sämtliche anderen Seitenaufrufe, die in diesem Moment aus der Sessiontabelle lesen möchten – das Performanceproblem ist ab einer bestimmten Anzahl von gleichzeitigen Benutzern vorprogrammiert.

Klassischerweise geht man nun so vor, dass man die Tabelle in InnoDB umwandelt und wieder einige Zeit das Slow Log oder die Prozessliste beobachtet – sinkt die Lock_Time der Abfragen deutlich, hat man einen Flaschenhals erfolgreich eliminiert.

Nun, es wäre freilich zu schön, wenn es nicht doch den ein oder anderen Haken bei der Sache gibt; zum Glück lassen sich die meisten aber zumindest einigermassen elegant umschiffen.

Eine Einschränkung von InnoDB ist beispielsweise, dass der FULLTEXT Index nicht unterstützt wird. Dies war bei My-Hammer ein Problem, weil wir eine Tabelle, die ziemlich eindeutiger Kandidat für eine Umstellung von MyISAM auf InnoDB war, in einem Teil unserer Applikation auch durchsuchen mussten, und zwar eben gerade einige TEXT-Felder, was ohne FULLTEXT Index nicht wirklich Spass macht.

Die Lösung war, die Tabelle umzuwandeln und damit in der Tabelle selbst auf die FULLTEXT Indizes zu verzichten, per cronjob aber eine weitere Tabelle regelmässig mit den Daten der Ursprungstabelle zu füllen. Geschrieben wurde in diese Tabelle nur durch besagten Crobjobs, ansonsten fanden ausschliesslich Lesezugriffe statt, womit MyISAM wieder die perfekte Wahl war – und wir hatten unsere FULLTEXTs wieder. Schöner Nebeneffekt: durchsucht werden müssen eh nur eine Untermenge aller Zeilen der Ursprungstabelle, und es müssen auch nicht alle der (recht zahlreichen) Spalten in die Suchtabelle übertragen werden.

Dadurch konnten wir nicht nur das Lockingproblem der ursprünglichen Tabelle lösen, sondern aufgrund der schlankeren Datenbasis in der Suchtabelle die Suche deutlich beschleunigen.

Wichtig ist jedoch: diese Lösung ist nur möglich, weil wir in diesem Fall darauf verzichten können, auf absoluten Livedaten zu suchen.

Meiner Erfahrung nach kann man zusammenfassend sagen: Es gibt nur eine einzige Massnahme, die mehr Performance bringt als Caching, und das ist noch mehr Caching. Das gilt, um mal zum Haupthema zurückzukehren, vor allem in Bezug auf Performance bei plötzlichen Besucheranstürmen.

Statische Inhalte

Wenn man einen TV Beitrag über die eigene Plattform überleben will, dann gibt es nichts, aber auch wirklich gar nichts Wichtigeres als dies hier: Die Startseite der Plattform ist eine statische HTML Seite. Und zwar in aller Konsequenz, was heissen soll, dass der Aufruf der Seite nicht nur keine Datenbankverbindung zur Folge hat, sondern dass noch nicht einmal der PHP Interpreter auch nur gestartet wird. Die Startseite von My-Hammer ist eine .html Seite, die im Gegensatz zu den .php Seiten per Apache-Konfiguration mod_php noch nicht mal von Weitem zu sehen bekommt. Selbiges sollte konsequenterweise auch für alle JavaScript und CSS Dateien, die von der Startseite eingebunden werden, gelten. Ob man hierfür nun mit Proxylösungen arbeitet oder Seiten regelmäßig vorgeneriert, ist Geschmackssache.

Man darf nie den Performancevorteil reinen HTMLs unterschätzen – selbst wenn sich die Datenbanken schon alle verabschiedet haben und die Webserver bereits richtig unter Dampf sind: Eine HTML Seite auszuliefern schafft sogar ein Webserver, der schon ziemlich am Ende ist. Und man wahrt vor allem noch am ehesten sein Gesicht, wenn die ganzen neuen Benutzer, die aufgrund des TV Beitrages neugierig geworden sind, zumindest die Startseite zu sehen bekommen. Was immer man neben der Startseite noch an Seiten statisch vorgenerieren kann, ohne dass der angebotene Dienst selber “statisch” wird, sollte man natürlich machen (denn wie oft ändern sich schon Seiten wie Über uns?). Hierbei macht es Sinn, sich mithilfe der Zugriffsstatistiken einmal anzuschauen, welchen Weg neue Benutzer in der Regel auf der Plattform nehmen, um so auch wirklich jene Seiten zu cachen, die bei einem Ansturm am ehesten angesurft werden.

Eine dynamische Seite als statische Seite vorzugenerieren ist dabei natürlich die konsequenteste Version von Caching, aber nicht immer praktikabel. My-Hammer nutzt eine zweite Stufe des Cachings, bei dem zwar weiterhin dynamische Seiten ausgeliefert werden, diese aber ganz oder teilweise in dedizierten Cache-Backends (wir nutzen dazu memcached) abgelegt sind, um Ergebnisse teurer Datenbankabfragen, die nicht immer absolut live zur Verfügung stehen müssen, zwischenzuspeichern. Diese zwischengespeicherten Einträge können zum einen nach einer gewissen Zeit ablaufen und werden dann neu aus der Datenbank erzeugt, oder können gezielt als “dirty” markiert werden, wenn die Datenbestände die sie widerspiegeln sich ändern.

Wie oben erwähnt kann es sich außerordentlich lohnen, diese Cacheinhalte auf dedizierten Maschinen bereitzustellen – was wiederum deutlich zeigt, dass manche Massnahmen zur Performancesteigerung bestenfalls halbgar sind, wenn Admins und Programmierer nicht zusammenarbeiten.

Everybody needs a 304

(oder: Wie ich dem Browser des Users helfe, optimal zu cachen)

Bisher bin ich lediglich auf ein Ziel von Performanceoptimierung eingegangen – zu verhindern, dass die eigenen Server zusammenbrechen, wenn’s mal brenzlig wird. Man muss sich aber unbedingt bewusst machen, dass Optimierungen auf dem Server erstmal keinen Wert an sich darstellen, sondern nur dem eigentlichen Ziel dienen: dem User die Benutzung der eigenen Seite so schnell und angenehm wie möglich zu machen – indem die Seite grundsätzlich erreichbar bleibt, und indem die Seite sich so schnell wie möglich aufbaut.

Wenn man sich das erstmal bewusst gemacht hat, ist auch klar dass es sich sogar lohnen kann, etwas Rechenzeit auf dem Server zu investieren, um sie dem Client (also Browser) abzunehmen.

Aber der Reihe nach. Es gibt ein wichtiges Hilfsmittel, um den Aufbau einer Webseite im Browser deutlich zu beschleunigen (abgesehen von den üblichen Massnahmen wie geringer Dateigröße, möglichst wenig eingebetteten Objekten etc.), und das ist die Verwendung des HTTP Status 304 Not Modified. Diesen kann der Server senden, wenn er anhand der Anfrage des Clients erkennt, dass exakt der Inhalt, den der Browser bereits in seinem Cache hat, nochmal über die Leitung wandern würde – in diesem Fall sendet der Server diesen Inhalt dann eben nicht nochmal, sondern teil dem Browser nur mit, er möge auf den Inhalt seines Caches zurückgreifen.

Dies kann zu erheblichen Performancesteigerungen auf Seiten des Clients führen, denn die Zeit die zum Download des Inhalts einer Seite benötigt wird, entfällt.

Es gibt nun zwei Faktoren, die das Status 304 Handling beeinflussen und spezielle Anpassungen erfordern, um optimales Clientcaching zu ermöglichen: Die Auslieferung von Seiten über PHP Skripte (gilt prinzipiell auch für andere Skriptsprachen) und der Betrieb einer Plattform in einem Webserver-Cluster.

Zuerst zu letzterem: Um in der gegenseitigen Kommunikation festzustellen, ob ein Inhalt vom Server neu ausgeliefert werden muss oder der Browser den Inhalt aus dem eigenen Cache lädt, gibt es den sogenannten Etag. Ein ganz kurzer Abriss, wie die Verwendung abläuft. Der Client fragt eine Ressource beim Server an. Es ist der erste Zugriff innerhalb dieser Browsersitzung, deshalb schickt der Client kein Etag mit. Der Server sendet daraufhin die Inhalte aus, und schickt in den Headern den Etag des aktuellen Inhalts dieser Ressource mit, sagen wir “12345″ (der Server schickt dazu den Header Etag: “12345″).

Fragt der Client nun erneut dieselbe Ressource beim Server an, schickt er in seinen Headern wiederum die Information mit, dass er in seinem Cache bereits die Inhalte mit dem ETag “12345″ gespeichert hat, und der Server ihn informieren möge falls sich die Inhalte nicht geändert haben (der Client schickt dazu den Header If-None-Match: “12345″). Der Server kann dann schauen, ob die Inhalte die er ausliefern würde immer noch das ETag “12345″ haben, und in diesem Fall den erwähnten HTTP Status 304 senden, oder, falls Inhalt und ETag nicht mehr zueinander passen, den neuen Inhalt schicken.

Die Frage ist nun: Wie genau ist denn definiert, was im Etag steht? Nun, im Prinzip gar nicht. Es gibt kein vorgeschriebenes Format, wichtig ist nur die Definition des Etag an sich: dass nämlich ein eindeutiges Etag zu einem eindeutigen Inhalt einer bestimmten Ressource gehört, und deshalb festgestellt werden kann ob sich der Inhalt einer Ressource zwischen zwei Requests geändert hat oder nicht. Man kann sich den Etag deshalb der Einfachheit halber als Checksumme des Inhalts vorstellen (und in der Tat besteht eine Möglichkeit den Etag zu generieren darin, z.B. die MD5 Summe des Inhalts zu berechnen).

Woher kommt der Etag? Beim Apache ist es Teil der Kernfunktionalität, für eine angeforderte Ressource den Etag zu berechnen und mitzusenden, sowie entsprechend zu reagieren wenn ein Client den If-None-Match Header sendet. Alles out-of-the-box also, aber genau hier liegen für uns die Probleme:

Problem 1: Defaultmässig berechnet Apache den Etag für eine Ressource, indem eine Art Quersumme aus diesen Informationen generiert wird: I-Node-Nummer der angefragten Datei, letzter Änderungszeitpunkt (mtime) der angefragten Datei, und Größe der angefragten Datei. Betreibt man eine Webseite auf nur einem Server, hat man kein Problem, denn wenn z.B. die Datei /index.html zwischen zwei Aufrufen nicht verändert wird, hat sie bei beiden Zugriffen denselben Etag, da keiner der drei Faktoren inode, mtime, size zwischenzeitlich verändert wurde.

Betreibt man aber einen Cluster aus mehreren Webservern, und besteht die Möglichkeit, dass ein Client bei zwei aufeinanderfolgenden Aufrufen derselben Ressource zuerst auf einem Webserver, beim zweiten Aufruf aber auf einem anderen landet, dann ist, auch wenn auf beiden Servern die exakt gleiche Datei liegt, der Etag beide Male ein anderer, denn selbst wenn letzter Änderungszeitpunkt und Größe der Datei auf beiden Servern identisch sind: dass die I-Node-Nummer die gleiche ist, ist praktisch ausgeschlossen. Der Server wird also keine 304 Status senden, obwohl er es könnte.

Abhilfe ist zum Glück sehr einfach möglich, und lohnt sich schon beim Wechseln von einem auf zwei Server: Man muss dem Apache mitteilen, dass er die I-Node-Nummer nicht mehr zur Berechnung des Etag heranziehen soll. Dies erledigt an zentraler Stelle die Anweisung FileETag MTime Size. Mehr dazu im Apache Handbuch.

Problem 2: mod_php hebelt die Verwendung von Etag für PHP Skripte aus. Das macht ja prinzipiell auch Sinn: selbst wenn die Skriptdatei /index.php sich zwischen zwei Aufrufen inhaltlich überhaupt nicht geändert hat, kann sie dennoch völlig unterschiedliche Inhalte an den Client ausliefern – genau das ist ja Sinn und Zweck des Einsatzes von dynamischen Seiten.

Trotzdem kann es Sinn machen, dass der Server den Status 304 an einen Client sendet, wenn dieser dieselbe Ressource erneut anfragt. Zum Beispiel bei CSS Skripten, die von jeder Seite der Plattform eingebunden werden, und aus programmiertechnischen Erwägungen als PHP Skripte realisiert sind, aber deren Inhalt sich trotzdem sehr selten ändert. Jeder Seitenaufruf würde den Browser veranlassen, dies referenzierte CSS Datei anzufragen, und der Server würde jedes Mal den Inhalt senden, obwohl sich dieser seit dem letzten Aufruf nicht geändert hat. Das macht den Seitenaufbau im Client langsam, und ist zudem eine Ressourcenverschwendung.

Wie kann man nun sicherstellen, dass ein Client den 304 Status auch beim Abruf von PHP Skripten erhält, falls sich der Inhalt nicht verändert hat, aber auch auf keinen Fall einen 304 Status bekommt, falls der Inhalt sich geändert hat? Die Lösung ist leider nicht ganz so trivial wie beim ersten Problem, aber doch vergleichsweise einfach zu realisieren.

Da wie erwähnt der Zustand der Skriptdatei selbst praktisch keine Rolle spielt, darf man nur mit dem von diesem Skript auszuliefernden Inhalt arbeiten. Eine Lösung wäre, bei den Skripten, für die man den Etag Mechanismus einsetzen möchte, folgenden Code ans Ende anzuhängen (lässt sich natürlich einfach in eine zentrale Funktion kapseln):

<?php
$output = ob_get_clean(); // Gesamte Ausgabe, die an den Client gesendet werden soll, abfangen und zwischenspeichern

$etag = ‘”‘.sha1($output).’”‘; // Prüfsumme der Ausgabe berechnen// Ist der Inhalt identisch mit dem, den der Client gecached hat?

if ($_SERVER['HTTP_IF_NONE_MATCH'] == $etag) // Wenn ja, dann sende nur den Status 304
{
header(‘HTTP/1.x 304 Not Modified’);
header(‘Etag: ‘.$etag);
die();
}
else // Wenn nicht, dann sende den Inhalt inkl. des neuen Etags
{
header(‘Etag: ‘.$etag);
echo $output;
die();
}
?>

Voraussetzung ist dafür die Verwendung von output buffering.

Eines muss man ganz klar festhalten – für den Server fällt exakt dieselbe Arbeit an, egal ob der User den Inhalt schlussendlich zugesendet bekommt oder nur die lapidare Meldung, er möge doch auf seinen Cache zurückgreifen. Es mag nach deutlich zuviel Overhead aussehen, PHP soviel Arbeit erledigen zu lassen, nur um das Ergebnis dieser Arbeit dann wegzuschmeissen; aber der Effekt auf die Lade- und damit Seitenaufbauzeiten beim Client ist wirklich beeindruckend, wenn man diesen Mechanismus geschickt einsetzt.

]]>
/2007/07/17/recycelter-artikel-my-hammer-das-fernsehen-und-die-serverlast/feed/ 0