Drupal 7 Performance Optimierung

Performance Optimierung ist ein wichtiges Thema bei der Arbeit mit Drupal 7. Denn Drupal ist ein System, dass zum Installationszeitpunkt schnell ist, wie der Wind, aber sich mit wachsender Seitenkonfiguration nach und nach verlangsamt. Das hat zum einen mit der Tatsache zu tun, dass Drupal eine starke Bindung an seine Datenbank hat und die Abfragen bei jedem Page-Request bei unvorsichtiger Konfiguration schnell mal in den dreistelligen Bereich steigen können. Zum anderen liegt das auch an der modularen Architektur, denn bei jedem Bootstrap werden alle aktivierten Module inkl. ihrer Hooks und Konfigurationseinstellungen abgearbeitet und zusammengestellt. Der Segen des Einen wird zum Fluch des Anderen, denn verzichtet man auf Module, schränkt man genau die Vorteile von Drupal ein, die das CMS möglicherweise überhaupt erst zum System der Wahl gemacht haben. Aber natürlich gibt es Abhilfe und eine High Performance Seite auf Basis von Drupal ist mit ein paar Grundregeln und einigen Anpassungen möglich.

Webserver

Wo sollte die Optimierung sonst losgehen, wenn nicht beim Fundament: dem Webserver. 

Architektur

Gehen wir mal davon aus, dass unsere Drupal Installation in einer typischen LAMP-Umgebung läuft: Linux, Apache, MySQL, PHP. Idealerweise betreibt man Datenbank und Webserver auf derselben Maschine oder - wenn das aus Architektur-Gründen nicht möglich ist - auf Maschinen die möglichst nah beieinander stehen. Z.B. bei virtuellen Maschinen könnte es eine VM auf demselben physikalischen Server sein. Bei manchen Anbietern (z.B. 1&1) werden die Datenbank-Server aus Kostengründen gerne mal in andere Umgebungen verlagert. Das hat Vorteile für den Kunden (v.a. niedrigere Rechnungen) und Vorteile für den Anbieter (bessere Verteilung der Ressourcen, mehr Flexibilität bei der Verwaltung günstiger kleiner Webhosting-Pakete). Aber als Kunde können wir nicht mehr sehen, wie weit der Weg vom Webserver zum Datenbank-Server ist und wie viele Router dazwischen stehen, die eine Anfrage verlangsamen können. Wege durch Netzwerke sind grundsätzlich langsamer als eine kurze schnelle Anfrage an den eigenen localhost.

Konfiguration

Die Konfiguration des Servers spielt eine weitere entscheidende Rolle. Zum Beispel sollten Daten komprimiert übertragen werden, um die Datenmenge bei Page-Requests gering zu halten. Das geht mit mod_deflate. Dazu unter Ubuntu einfach

sudo a2enmod deflate

und anschließend in /etc/apache2/mods-available/deflate.conf die Werte in Zeile 3 ergänzen um die Dateitypen, die komprimiert werden sollen.

Neben diesen üblichen Apache-Server-Einstellungen wie mod_deflate oder Keep-Alives etc. ist es hilfreich Caching-Systeme zu installieren. Zwei sehr gut Mechanismen sind APC und Memcached. APC cached den kompilierten PHP-Quelltext, so dass das ressourcenaufwendige Kompilieren der Skripte stark reduziert werde kann. Memcached ist ein Caching-Server, der Daten im Arbeitsspeicher der Maschine auf der er läuft, zwischenspeichert. Das ermöglicht rasend schnelle Zugriffszeiten für eben solche Daten und teure Datenbankabfragen sowie Festplattenzugriffe können verringert werden.

Will man diese Caching Mechanismen konfigurieren, ist die Hauptarbeit auf dem Server zu erledigen. In Drupal folgt dann (nur bei Memcached) nur noch eine kleine Ergänzung der settings-Datei. Es gibt zwar Module für Drupal, aber die sind nur für das Monitoring hilfreich und sollten im Sinne einer Performance-Optimierung auch nicht unnötig im System herumgeistern. Hier eine kurze Anleitung wie man APC und Memcached unter Ubuntu einrichtet:

APC

In der Kommandozeile folgenden Befehl eingeben:

sudo pecl install apc

Anschließend die php.ini in /etc/php5/apache2/php.ini ergänzen um

extension=apc.so
apc.enabled = 1
apc.shm_size = 512

Danach wieder die Kommandozeile mit diesem Befehl befeuern:

sudo aptitude install php-apc

Danach den Apache neustarten mit

sudo service apache2 restart

Wenn man eine Übersicht möchte, kann man noch apc.php.gz von /usr/share/doc/php-apc herunterladen und apc.php entpacken und bearbeiten. Dazu den Benutzername auf admin setzen und ein Passwort festlegen. Danach die apc.php im Webspace Root /var/www platzieren. Jetzt kann über diese Datei ein Monitoring des Caches erfolgen. In Drupal ist nichts weiter zu tun.

Memcached

sudo apt-get install php5-memcache
sudo apt-get install memcached
sudo pecl install memcache

Anschließend die php.ini in /etc/php5/apache2/php.ini ergänzen um

extension=memcache.so

In /etc/memcached.conf den Wert bei -m ändern um mehr Arbeitsspeicher zur Verfügung zu stellen. Zum Beispiel so:

-m 1028

Danach sowohl den Memcache Daemon als auch Apache neustarten:

sudo service memcached restart
sudo service apache2 restart

Jetzt nur noch in der settings.php in Drupal folgende kleine Ergänzung vornehmen:

$conf['cache_backends'][] = 'sites/all/modules/memcache/memcache.inc';
$conf['cache_default_class'] = 'MemCacheDrupal';
$conf['memcache_key_prefix'] = 'EindeutigerSchluessel';
$conf['cache_class_cache_form'] = 'DrupalDatabaseCache';

Varnish

Auch Varnish ist ein potentes Caching System, das sich laut Berichten gut für Drupal nutzen lässt. Da mir hier aber die Erfahrung fehlt, weise ich nur darauf hin.

Drupal intern

Ist der Webserver erstmal mit APC und Memcached konfiguriert und hochgefahren, müsste bereits ein dramatischer Geschwindigkeitsunterschied spür- und/oder messbar sein. NIchtsdestotrotz gibt es noch viel, das man tun kann.

Leistung (admin/config/development/performance)

Drupal bietet von Haus aus einige Funktionen um die Leistung der Seite zu optimieren. Unter admin/config/development/performance finden sich eben diese Einstellungen.

  1. Seiten für anonyme Benutzer cachen: Erklärt sich fast von selbst. HTML-Seiten werden für anonyme Benutzer zwischengespeichert. Macht hochgradig Sinn für Webpräsenzen, auf denen die textlichen Inhalte weitestgehend gleich bleiben (Magazine, Blogs, Newspages, etc.)
  2. Blöcke cachen: Auch das treibt die Geschwindigkeit deutlich in die Höhe, aber hier gilt es mit Vorsicht zu walten. Denn Blöcke werden auch gerne mal genutzt um dynamischen Inhalt zu zeigen. Zum Beispiel in einem Shop-System ist ein typischer Block die Warenkorb-Übersicht. Der muss sich natürlich verändern, wenn ein Kunde ein weiteres Produkt in den Warenkorb legt. Ist dieser Cache aktiviert, wird da nichts passieren. Das heisst natürlich nicht, dass man bei dynamischen Seiten drauf verzichten muss. Aber die eigenen dynamischen Blöcke sollten zumindest mit einen DRUPAL_NO_CACHE versehen werden. Eine Übersicht über die Cache-Controls für Blöcke gibt es hier: https://api.drupal.org/api/drupal/includes!common.inc/group/block_caching/7
  3. CSS-Dateien aggregieren und komprimieren: Wer viele Module nutzt, oder aufgrund einer sauberen Struktur seinen CSS Code in viele kleine Dateien aufteilt, kann hiervon profitieren. Statt viele kleinen Dateien auszuliefern (die alle beim Abruf eine eigene Verbindung benötigen), stellt Drupal aus allen CSS Dateien eine einzelne zusammen und komprimiert diese vor der Übertragung.
  4. JavaScript-Dateien aggregieren: Das gleiche Vorgehen ist auch für Javascript Dateien verfügbar. Hier habe ich allerdings schon ab und an Fehlermeldungen bekommen, vor allem in Verbindung mit dem jQuery Update Modul oder selbst eingebrachten Javascript Bibliotheken.

Keine Base-Themes verwenden

Eine gängige Praxis in Drupal ist die Verwendung von Base-Themes zur einfacheren Erstellung von Layouts. Was dabei aus Komfortgründen oft unberücksichtigt bleibt ist die Tatsache, dass Base-Themes auch einiges an Code-Overhead erzeugen und die Seite potentiell verlangsamen können. Große, funktionsreiche Base-Themes wie z.B. Omega können da durchaus ein Problem sein und sollten vermieden werden. Bei einem Test mit einer gigantischen Modul- und Block-Basis (über 300 aktivierte Module und 15 dynamische Blöcke auf der Startseite) habe ich mit dem inkludierten Bartik Theme eine Waiting Time von 900ms gemessen und mit dem Omega-Theme ganze 2500ms.

Wenig und die richtigen Module installieren

Die Vielzahl an extrem guten Modulen ist das, was Drupal für mich persönlich zum besten verfügbaren CMS auf dem gesamten Markt macht. Egal, was man umsetzen will, es gibt bereits ein Modul dafür. Mein beliebtester Google-Suchbegriff bei der Arbeit mit Drupal ist "Drupal [Kurzbeschreibung meiner Funktion]". Treffer 1 ist immer das passende Drupal-Modul. Das verleitet aber schnell dazu, jede gewünschte Funktion einfach in wenigen Klicks mit einem Community Modul bereitzustellen. Das muss aber eigentlich gar nicht sein. Denn viele Module stellen viel mehr Funktionalität bereit, als man wirklich braucht und machen die Seite damit schnell sehr langsam. Anfangen kann man schon mit einigen Core Module. Zum Beispiel der Update Manager oder Statistics sind nicht wirklich nötig für den live Betrieb einer Seite.

Braucht man eine E-Mail-Benachrichtigung wenn sich ein Benutzer registriert? Kein Grund direkt "Rules" zu installieren. Mit zwei kleinen Hooks in einem eh schon existierenden eigenen Modul, lässt sich eine solche E-Mail-Nachricht schnell selbst programmieren (wobei Rules schon fast zur Standardausrüstung jeder Drupal-Webseite gehört, aber es geht ums Prinzip). Oft lohnt sich auch ein Blick ins Modul, um zu lernen wie der Modul-Entwickler die entsprechende Teil-Funktionalität, die man benötigt, realisiert hat. Hat man das verstanden, kann man den Code einfach adaptieren und somit schlank halten. Auch macht es Sinn, in eigenen Modulen die Admin-Funktionalitäten in eigene Dateien auszulagern, um keinen unnötigen Code bei Page-Requests von regulären Besuchern laden zu müssen.

Natürlich gibt es auch Module, die hochgradig sinnvoll sind (wie z.B. Image Lazyloader um nur Bilder zu laden, die auch wirklich sichtbar sind) oder Module, die man im Zuge der Performance Optimierung einmalig einsetzt und dann wieder entfernen kann (z.B. missing module, um herauszufinden ob in der DB irgendwelche Module aktiviert sind, die im Dateisystem bereits gelöscht wurden --> extremer Performance Killer).

drupal_static(__FUNCTION__)

Diese Funktion kann man in eigenen Methoden nutzen, die während eines Page-Requests mehrfach ausgeführt werden. Hat man z.B. eine Funktion geschrieben, die den Registrierungs-Status für einen bestimmten Nutzer zurückgeben soll, könnte die z.B. so aussehen:

function MYMODULE_return_user_status($uid) {
  //Hier wird Code ausgeführt, der am Ende eine Variable namens $status produziert
  return $status;
}

Je nach Seite kann es sein, dass der Code mehrfach pro Seiten-Aufruf ausgeführt wird. Zum Beispiel könnten wir die Funktion nutzen um den Status in einem Block darzustellen. Aber wir könnten Sie auch nutzen, um bestimmte Berechtigungen oder Freigaben zu prüfen. Ausgeführt wird immer dieselbe Funktion. Ist die Ausführung dieser Funktion teuer (z.B. weil viele Datenbankabfragen damit zusammenzuhängen), macht es Sinn diese nur einmal auszuführen und die darauffolgenden Male über drupal_static abzufangen.

Das funktioniert so:

function MYMODULE_return_user_status($uid) {
  $status = &drupal_static(__FUNCTION__);
  if(!isset($status[$uid])) {
    //Hier wird Code ausgeführt, der am Ende eine Variable namens $status_tmp produziert
    $status[$uid] = $status_tmp;
  }
  return $status[$uid];
}

cache_get()

Der Cache von Drupal kann natürlich auch in eigenen Modulen genutzt werden. Eine typische Cache-Schleife sieht so aus:

if($cached = cache_get('monthlyusers')) {
  $monthlyusers = $cached->data;
}
else {
  //Erzeuge $monthlyusers
  cache_set('monthlyusers', $monthlyusers);
}

Zuerst wird geprüft ob im Cache etwas vorliegt. Wenn ja, wird dies verwendet. Wenn nicht, werden die Daten erzeugt und in den Cache geschrieben. Standard-Einstellung ist CACHE_PERMANENT, was bewirkt, dass nur mit dem Aufruf von cache_clear_all() der Cache wieder gelöscht wird. Alternativen sind CACHE_TEMPORARY (Cache wird beim nächsten generellen Cache-Wipe entfernt) oder ein Unix-Timestamp, der die Laufzeit des Caches beschreibt. Nach Ablauf dieser Laufzeit verhält der Cache sich dann wie CACHE_TEMPORARY. Ein weiterer Weg, den ich oft wähle ist, dass eine Information eine exakte Zeit lang (z.B. 1 Stunde) aus dem Cache geholt werden und für den ersten Call nach diesem Ablauf aktualisiert werden soll. Das ist z.B. nützlich für Statistiken, die in der Regel nur sehr teuer zu generieren sind, aber dennoch von Zeit zu Zeit aktualisiert werden müssen. Hier ein Beispiel-Code, der den Cache für eine Statistik namens $monthlyusers genau 24h vorhält und beim ersten Aufruf nach Ablauf von 24h löscht und neu generiert:

$time = time();
$cacheexpire = $time - 86400; //Der Wert von 86400 entspricht 24h in Sekunden
$cached = cache_get('monthlyusers');
if($cached && $cached->created > $cacheexpire) {
  $monthlyusers = $cached->data;
}
else {
  cache_clear_all('monthlyusers', 'cache', TRUE);
  //Erzeuge $monthlyusers
  cache_set('monthlyusers', $monthlyusers);
}


Weitere gute Hinweise

http://colans.net/blog/drupal-7-performance-optimization-options-and-checklist