Hosting einer Hoch Performanten Homepage in der Cloud

Keine Kommentare

Als wir vor 3 Monaten über den Relaunch unserer Homepage nachdachten, kannten wir schon einige, für uns wichtige, Eckdaten:

  • Benutzung von Cloud Infrastruktur
  • Schnelle Antwortzeiten
  • flexible Standardsoftware
  • Benutzerfreundliches Redaktionssystem

Zwar hatten wir in der Vergangenheit OpenCMS für die Homepage benutzt, jedoch entschieden wir uns für den Relaunch für WordPress. WordPress war bei uns schon seit Jahren als Blogsoftware im Einsatz und überzeugte vor allem durch seine Benutzerfreundlichkeit und Funktionsumfang. Es ist ein einfaches, aber doch sehr mächtiges System, welches bereits in der Standardkonfiguration ausreichend Funktionalität liefert. Dazu ist es sehr gut mit einfachen eigenen oder schon fertigen Plugins zu erweitern.

Aber in diesem Beitrag soll es nicht um die Entscheidungsfindung zu WordPress gehen, sondern darum wie wir eine durchschnittlich gut besuchte Homepage und Blog gratis betreiben, ohne dafür Geschwindigkeit und Antwortzeiten zu opfern.

Gratis? – Ja!

Amazon bietet Neukunden eine kostenlose „micro tier“ für ein Jahr: aws.amazon.com/free. Neukunden sind dabei neu erstellte Accounts. Ok, zwar gratis, aber ziemlich begrenzt: Eine virtuelle CPU, 613 MB Ram und man muss Amazon Linux verwenden.

Nach Start dieser micro instanz wird automatisch ein 8G EBS eingebunden. Dorthin installierte ich dann auch alle notwendigen Dienste für WordPress:

sudo yum install httpd mysql mysqld php php-mysql

Zwar sah alles nach der Einrichtung von WordPress ganz nett aus, doch die Performance war relativ schnell am Boden, sobald mehrere Benutzer die Seite besuchten.
Die erste Aktion war es APC zu installieren. APC ist ein PHP Bytecodecache und –optimizer und sollte immer bei PHP Seiten eingesetzt werden:

sudo yum install php-pecl-apc

Die Situation besserte sich schon etwas, jedoch benötigten wir mehr um die Seite stabil zu betreiben.

Bei der Arbeit mit den gratis micro Instanzen lernte ich zwei Dinge:

  • „Steal Time“! nie zuvor sah ich diesen Wert so extrem hoch:
    Cpu(s): 2.0%us, 0.3%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 97.7%st

    Dies bedeutet, dass das System die CPU nutzen möchte, diese aber nicht vom Hypervisor zur Verfügung gestellt bekommt.

  • EBS ist manchmal echt langsam!

Der Prozess der unter diesen Umständen am meisten zu leiden hatte war der mysqld, also kümmerte ich mich zuerst um ihn.

Ich wendete die WordPress mysql tuning Empfehlungen von Matt Mullenweg an, jedoch zeigten sie keinen merklichen Erfolg.
Es war einfach nicht möglich wegen der CPU und IO Begrenzungen aus Mysql gute Performance herauszubekommen. Und in MySQL kann man auch nicht viel cachen, da kaum Speicher zur Verfügung steht.

Also musste ich den Zugriff auf MySQL reduzieren und suchte nach einem guten WordPress Caching Plugin. Und in der Tat es gibt sogar relativ viele:

Ich wählte das Quick Cache Plugin (mittlerweile gibt es das nicht mehr, eine Alternative wäre Total Cache) und wurde mit einer drastischen Verbesserung belohnt. Zusätzliche Besucher verursachten kaum mehr Last, da diese alle gecachte Versionen sahen.

Leider funktionierte dies nicht für eine größere Anzahl an gleichzeitig arbeitenden Redakteuren, da Caching bei Redakteuren ausgeschaltet ist. Und da Amazon die volle CPU Nutzung auf kurze „Bursts“ limitiert, mussten Redakteure manchmal für 30 Sekunden warten. Um dies zu verbessern, limitierte ich die CPU Nutzung durch den Mysqld Prozess:

wget http://centos.alt.ru/repository/centos/5/i386/cpulimit-1.2-1.el5.i386.rpm
rpm -i cpulimit-1.2-1.el5.i386.rpm
nohup sudo cpulimit -v --exe mysqld --limit 50 &

Nun da CPU und IO Nutzung weitgehend durch cleveres Caching optimiert waren, konnte ich mich an die Feinjustierung der httpd.conf machen um sicherzustellen, dass der Server die richtige Anzahl von gleichzeitigen Requests verarbeitet. Da wir mod_prefork einsetzen schaute ich nach der durchschnittlichen Speicherbenutzung der Apache Workerprozesse. Um diese zu optimieren entfernte ich noch einige unnötige Module und gelangte so auf etwa 25 bis 35MB Speichernutzung pro Prozess. Mysql nutzte rund 80MB, also wählte ich 15 Worker Prozesse, welche dann zusammen fast den gesamten Speicher nutzten.

StartServers 15
MinSpareServers 15
MaxSpareServers 15
ServerLimit 15
MaxClients 15
MaxRequestsPerChild 500

MaxServersPerChild ist dabei wichtig, damit die Prozesse in regelmäßigen Abständen neu erstellt werden. Gerade bei so knapp bemessenem Speicher könnte ein leakender Prozess sich negativ auswirken.

15 gleichzeitige Verbindungen klingt nach einem sehr geringem Limit, also musste ich sicherstellen, dass die Serverkommunikation auf ein Minimum reduziert wird.

Web Performance Optimization

Da der Server nun solide aufgestellt war, fanden sich die einfach zu erreichenden Optimierungen auf der Browserseite. Der Standardoutput eines WordPress Themes mit einigen Plugins ist relativ umfangreich und unübersichtlich. Da ich das Theme nicht großartig anpassen wollte, und ich auch den Code der Plugins nicht modifizieren konnte ohne ihn bei Upgrades wieder zu brechen, benötigte ich eine bessere Alternative.

Und diese Alternative ist das fantastische mod_pagespeed Modul für den httpd von Google.

sudo rpm -i mod-pagespeed-*.rpm
sudo service httpd restart

Der Effekt war signifikant. Die durch mod_pagespeed gesetzten aggressiven Cachingeinstellungen verhinderten fast alle Serveranfragen, wodurch die wenigen Worker Prozeses entlastet wurden. Auch die automatisch reduzierten Bildgrößen waren sehr bemerkbar. Besonders in einem Content Management Systemen, wo man relativ wenig Einfluss auf die von Autoren hochgeladenen Bilder hat, ist die automatische Rekomprimierung sehr effektiv.
Jedoch muss mod_pagespeed dafür selbst die Bilder Anfragen, sie werden dann weiterhin bei jedem Seitenaufruf lokal geladen um den Cache auf Gültigkeit zu prüfen:

46.137.99.225 - - [07/Aug/2011:03:54:37 +0200] "GET /wp-content/uploads/2011/07/wanted-memory.png HTTP/1.1" 200 116616 "-" "Serf/0.7.2 mod_pagespeed/0.9.18.6-886"

Hier kommt mod_expires ins Spiel:

ExpiresActive On
ExpiresDefault "access plus 10 days"

Die sinnvolle Gültigkeitszeit für ein Bild hängt dabei natürlich von der Art der Bilder ab. Niemals sich ändernde Bilder können Jahre gecached werden, andere vielleicht nur Minuten. Als Startpunkt ist eine Woche sicherlich garnicht schlecht. Da es hierbei auch nur im den lokalen mod_pagespeed Cache handelt kann man diesen zur Not auch löschen.

Um das Theme dann doch noch etwas zu entrümpeln entfernte ich viele Einstellungen über die functions.php:

remove_action('wp_head', 'rsd_link');
remove_action('wp_head', 'wlwmanifest_link');
remove_action('wp_head', 'feed_links_extra', 3);
remove_action('wp_head', 'feed_links', 2);
remove_action('wp_head', 'remote_login_js_loader');
function remove_generator() { return ''; }
add_filter('the_generator', 'remove_generator');

WordPress liefert standardmäßig eine eigene jQuery Javascript Bibliothek aus, aber um noch mehr von Caching zu profitieren und weitere Requests zu sparen nutzen wir einfach die jQuery Bibliothek aus dem Content Delivery Network von Google.
Ausserdem laden wir nicht mehr die l10n.js, da wir sie nicht benötigen:

function init_scripts() {
wp_deregister_script( 'jquery' );
wp_register_script( 'jquery', 'http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js');
wp_deregister_script('l10n');
}
 
if (!is_admin()) {
add_action('init', 'init_scripts');
}

Die meisten dieser Optimierungen werden jedoch nicht für die Administrationsseiten durchgeführt. Die Adminseiten sind so komplex, dass Seiteneffekte schwer zu überschauen sind.

Das Resultat meiner Bemühungen ist eine pagespeed Bewertung von www.codecentric.de von 95/100.

Für einen einzelnen Benutzer sollte die Seite in weniger als einer Sekunde komplett geladen sein. Für viele Benutzer wahrscheinlich sogar in unter 300ms.

So, wie skaliert das Ganze nun? Um das herauszufinden führte ich ein Benchmark gegen unsere alte dedizierte Maschine und die neue EC2 Micro Instanz durch. 4 echte CPUs gegen eine virtuelle. 2GB Ram gegen 613 MB. Faierweise muss ich sagen, dass ich nie die alte Homepage derartig optimiert habe.
Für das Benchmark verwende ich apachebench, zuerst mit 5 gleichzeitigen Benutzern und 50 Requests für die Homepage.

Alt:

Requests per second:    3.07 [#/sec] (mean)
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        9   71 424.8      9    3014
Processing:   989 1513 461.3   1352    3420
Waiting:      597  865 344.2    768    2861
Total:       1004 1584 603.1   1373    4287

Neu:

Requests per second:    18.04 [#/sec] (mean)
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       38   44   4.4     44      52
Processing:   183  224  47.3    220     493
Waiting:       58   89  41.4     80     335
Total:        222  268  49.5    265     544

Trotz der längeren Connect Zeit in das Amazon Datacenter in Dublin, sind die Ergebnisse beeindruckend.

Da ich bereits wusste, dass der Server nur 15 parallele Requests verarbeiten kann, versuchte ich 45 Requests mit jeweils 20 Anfragen.

Requests per second:    50.00 [#/sec] (mean)
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       39   42   4.8     43      78
Processing:   300  839 116.7    869    1181
Waiting:      179  711 115.2    742    1049
Total:        340  881 116.8    912    1225

Auch mit so hoher Last sind die Antwortzeiten relativ stabil unter einer Sekunde. Cool!

Ist „micro“ genug?

Ich bin mir relativ sicher, dass uns eine Micro Instanz lange ausreichen wird. Aber es gab doch ein Problem, welches ich zum Anlass für folgenden Tweet nahm:

Als wir das DNS Update veranlassten passierte etwas unheimliches. Die Seite wurde echt langsam! Ich hatte keine Ahnung wieso, hatte ich doch alles geprüft.
Aber unwissentlich hatten wir einen distributed denial of service (DDoS) Angriff ausgelöst. Als ich in das access_log sah, fand ich heraus wer der Schuldige war.
Sämtliche mir bekannte Suchmaschinen indexierten unsere Seite und Blog. Hinzu kamen noch jede Menge mir bis dahin unbekannter Suchmaschinen und Crawler. Um dem Ganzen die Krone aufzusetzen hatten wir im internen Chat und via e-Mail den Relaunch bekanntgegeben. Also luden zusätzlich noch über hundert codecentrics die Seite.
Und alle Caches waren noch kalt 🙁

Rettung durch die Cloud!

Da es sich um Amazon Server handelt, konnte ich diese Situation ganz einfach lösen. Ich stoppte die Instanz, stellte sie von t1.micro auf m1.small und startete sie wieder.
Die Maschine kam schnell wieder hoch und wies sich durch ein kleines Script automatisch die virtuelle IP wieder zu. Ich drehte noch schnell die Apache Worker Prozesse auf 50 hoch und schon lief die Seite akzeptabel.
Dieser gesamte „Upscaling“ Prozess dauerte keine 30 Sekunden, und war kaum bemerkbar für die Benutzer, welche sowieso schon etwas unter der Performance litten.
Der Server war zwar immer noch stark belastet, aber die Antwortzeiten waren auf stabilen 1-2 Sekunden.

Nach vier Stunden war der Spuk vorbei. Die Suchmaschinen waren davongezogen und die codecentrics im Wochenende.
30 Sekunden später war der Server wieder eine t1.micro, und gratis. Kosten für diese Peak Load: 16 cents.

Ich bin mir sicher, dass wir eine derartig hohe Last lange Zeit nicht mehr haben werden. Und falls doch? Dann können wir in weniger als einer Minute hochskalieren.
Beispielsweise an Freitagen, wo viele Mitarbeiter Blogbeiträge schreiben, oder Inhalt für die Homepage einstellen.

ich hoffe, Ihnen hat mein Erfahrungsbericht nützliche Informationen geliefert. Falls Sie in die Cloud gehen wollen, oder sie Performance im Browser oder auf dem Server optimieren wollen, kontaktieren Sie uns doch einfach.

Fabian Lange ist Lead Agent Engineer bei Instana und bei der codecentric als Performance Geek bekannt. Er baut leidenschaftlich gerne schnelle Software und hilft anderen dabei, das Gleiche zu tun.
Er kennt die Java Virtual Machine bis in die letzte Ecke und beherrscht verschiedenste Tools, die JVM, den JIT oder den GC zu verstehen.
Er ist beliebter Vortragender auf zahlreichen Konferenzen und wurde unter anderem mit dem JavaOne Rockstar Award ausgezeichnet.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.