Die Strukturverbesserer aus München

High Performance Performance Optimierung für Drupal

Performance matters

In diesem Artikel zeige ich auf, welche technischen Möglichkeiten man einsetzen kann, um ein Drupal Projekt auf Performance zu optimieren. Bevor man sich Gedanken über die Details macht, sollte man die Struktur für die Infrastruktur festlegen. Im Artikel High Performance Drupal Infrastruktur werden dazu unterschiedliche Szenarien vorgestellt.

1. Ziele festlegen

Bevor man mit der Optimierung beginnt, legt man die Ziele fest, welche durch die Optimierung erreicht werden sollen. Nur mit festgelegten Zielen ist eine Erfolgskontrolle möglich.

Hier einige Parameter, die als Grundlage verwendet werden können:

  • Ausfallsicherheit: wie hoch muss die Verfügbarkeit des Gesamtsystems und der einzelnen Komponenten sein
  • Reaktionszeit bei n nicht-angemeldeten Benutzern
  • Reaktionszeit bei n angemeldeten Benutzern
  • Reaktionszeit bei einem Peak n nicht-angemeldete Benutzer
  • Reaktionszeit bei einem Peak n angemeldete Benutzer

2. Monitoring und Analyse

Bei der Erfolgskontrolle werden folgende Fragen beantwortet:

  • Hat sich die Performance wirklich gesteigert?
  • Um wie viel Prozent hat sich die Performance verbessert?
  • Ist die Performance-Verbesserung nachweislich auf die Optimierung zurück zu führen?
  • Gibt es Nebeneffekte durch die Optimierung?

Um diese Fragen beantworten zu können benötigt man ein Monitoring Tool, welches den historischen Verlauf der Messungen aufzeigt. Opensource Beispiele hierfür sind:

  • Cacti (in PHP geschrieben). Cacti ist eine Benutzeroberfläche für RRDtool, welches Daten über SNMP, SSH und andere Quellen einlesen kann.
  • Munin (in Python geschrieben)
  • Zappix (in PHP geschrieben)
  • Nagios (in C/PHP geschrieben)

Das Monitoring Tool sollte sich nicht auf dem selben Server befinden, welcher überwacht wird (ich will das nur erwähnt haben, um sicher zu stellen, das niemand auf andere Ideen kommt ;))

3. Hosting

Wie für jedes andere Projekt wird auch für einen Drupal Projekt ein Hosting Plan erstellt, welcher aus den Kennzahlen des Projektes entwickelt wird. Einige Hosting Szenarien werden im Artikel High Performance Drupal Infrastruktur aufgezeigt.

4. Performance Optimierung Drupal Einstellungen

4.1 Leistung

  • Caching-Modus: External
  • Seitenkompression: Aktiviert
  • CSS-Dateien optimieren: Aktiviert
  • JavaScript-Dateien optimieren: Aktiviert
  • Page compression: Disable - Die Seitenkomprimierung sollte über Apache erfolgen (siehe weiter unten)

4.2 Bildoptimierung

Um das Ladeverhalten zu verkürzen sollte die Dateigröße der Bilder möglichst klein gehalten werden. Entsprechend sind die Einstellungen der Imagefields, ImageCache und der ImageAPI zu treffen. Um die Dateigröße der Bilder automatisch so klein wie möglich zu erzeugen, kann das Drupal Modul ImageAPI Optimize eingesetzt werden.

4.3 Logging

Um Drupal Meldungen zu loggen sollte Syslog (diese Funktionalität wird über ein Drupal Core Modul bereit gestellt) und nicht Database logging verwendet werden. Dadurch werden Schreibzugriffe auf die Datenbank eingeschränkt. Weiterhin sollte auf den Einsatz des Statistics Modul verzichtet werden, um zusätzliche Schreibzugriffe auf die Datenbank zu verhindern. Um Auswertungen über das Benutzerverhaltung und den Seitentraffic zur erhalten, kann anstelle dessen Piwik oder Google Analytics eingesetzt werden.

4.4 Fehlerseiten

Es wird sich nicht vermeiden lassen, dass im Laufe der Zeit 403 oder 404 Fehler erzeugt werden (schon alleine die vielen Bots werden dafür sorgen). Diese Seiten sollten so wenig wie möglich Last erzeugen, um den Server nicht unnötig damit zu belasten. Hier bietet es sich an sehr leichtgewichtige Seiten aufzubauen. Um den Bootstrap von Drupal zu minimieren kann das Drupal Modul Fast 404 verwendet werden.

5. Performance Optimierung bei der Entwicklung

Da Drupal auch nur mit PHP gestrickt wird, gelten auch hier die Punkte, die für alle anderen PHP Skripte/Anwendungen gültig sind, wenn es um die Entwicklung geht. Die Liste dieser Punkte findet man im Artikel Performance Optimierung bei der PHP Entwicklung.

5.1 Umgang mit Drupal Modulen

Die Anzahl der Drupal Module ist minimal zu halten. Jedes zusätzlich aktivierte Drupal Modul wird die Performance des Projektes verschlechtern, da in allen Modulen überprüft wird, ob ein Hook für eine "hookable"-Funktion vorhanden ist. (Beispiel: werden in eine Seite 10 Funktion verwendet, die über Hooks überschrieben werden könne und 50 Module sind vorhanden, finden 500 Überprüfungen statt - falls ein Hook gefunden wird, wird dieser ausgeführt).
Kann ein Modul die Funktionalität erfüllen, welche über mehrere einzelne "Spezial"-Module bereit gestellt wird, sollte man sich für das eine generellalistische Modul entscheiden.

Viele Module unterscheiden zwischen Backend und Frontend (UI) indem sie für diese Bereiche jeweils ein eigenes Modul anbieten. Auf einem Produktivsystem sollten die Frontend Module deaktiviert werden. Beispiele hierfür sind Views mit Views UI, Context mit UI oder Organic Groups mit Organic Groups UI.
Hat man eine Entwicklungsumgebung und ein Produktivsystem, können die unterschiedlich benötigten Module über verschiedene Installationsprofile oder über die Umsetzung spezieller Features gelöst werden.

5.2 Umgang mit CSS Dateien

Werden eigene CSS Dateien verwendet, werden diese mit drupal_add_css hinzugefügt. Dadurch werden die CSS Dateien automatisch aggregiert (sofern diese Option aktiviert ist).

Beispiel:

drupal_add_css(drupal_get_path('module', 'mein_modul') .'/css/meine_datei.css');

Wann immer möglich sollte auf inline CSS Code verzichtet werden, da dieser nicht gecached werden kann.

5.3 Umgang mit Javascript Dateien

Werden eigene Javascript Dateien verwendet, werden diese mit der Funktion drupal_add_js hinzugefügt. Die Javaskript Dateien werden automatisch aggregiert, sofern diese Option aktiviert ist.

drupal_add_js(drupal_get_path('module', 'mein_modul') . '/meine_datei.js');

Als dritter Parameter kann der Scope angegeben werden, in dem die Datei ausgegeben werden soll (Header oder Footer). Um das Ladeverhalten zu verbessern sollte hier soweit es geht immer 'footer' verwendet werden.

Javascript sollte soweit möglich in externe .js Dateien ausgelagert werden, da diese Dateien cachbar sind und auch als statischer Inhalt über den Reverse Proxy oder einen CDN ausgeben werden können.

5.4 Umgang mit statischen Inhalten (Bilder, Videos, Musikdateien)

Sofern ein CDN (siehe weiter unten) zum Einsatz kommt, ist es häufig der Fall, dass man mit der Integration des statischen Inhaltes entsprechend umgehen muss. Je nachdem für welche CDN Lösung man sich entschieden hat, kann es z.B. erforderlich sein, dass man vor den URLs zu den Mediadateien ein Präfix setzen muss.

5.5 Asynchrone Abarbeitung von Jobs

Wenn immer möglich, sollten alle Jobs die nicht sofort abgearbeitet werden müssen, über einen asynchronen Prozess umgesetzt werden. Mögliche Umsetzungen:

  • über einen Drupal Cronjob
  • über einen eigenen Drush Befehl. Dies ist einem Cronjob vorzuziehen, falls die Häufigkeit der Aufrufe von der des Cronjobs abweicht.

Beide Vorgehensweisen können über Drush ausgelöst werden, was bedeutet, dass bei besonders rechenintensiven Aufgaben sogar ein eigener Server für die Ausführung dieser asynchronen Jobs eingesetzt werden kann. Falls hierfür ein Server nicht mehr ausreicht, können Drush Befehle mit Hilfe von Gearman einem ganzen Cluster von Servern für die Abarbeitung übergeben werden.

Unsere Empfehlung

Image of Pro Drupal 7 Development (Expert's Voice in Open Source)
Autor: Todd Tomlinson
Hersteller.: Apress

6. Performance Backend Optimierung

6.1 Load Balancing

Kommen mehrere Webserver zum Einsatz (oder mehrere Reverse Proxy Server) ist Load Balancing notwendig.

6.1.1 DNS Load Balancing

Vorteile:

  • Billig, da keine zusätzliche Hardware erforderlich ist

Nachteile:

  • bei Ausfall werden für die festgelegte TTL noch Anfragen an den nicht mehr erreichbaren Node gesendet

6.1.2 Einsatz einer Load Balancer Hardware

Vorteile:

  • schnelle Reaktion auf Ausfall durch aktive oder passive Verfügbarkeitsüberprüfungen
  • SSL Terminator (muss von Hardware Load Balancer unterstützt werden)

Nachteile:

  • Zusätzliche Hardware erforderlich (Load Balancer Hardware oder Linux Server als Load Balancer)
  • muss redundant aufgebaut werden, wenn kein SPOF entstehen soll.

Um die Verfügbarkeit der Nodes zu überprüfen, empfiehlt es sich keinen Request auf die Drupal Webseite abzufeuern, da dadurch unnötig Last erzeugt wird. Besser ist die Einrichtung eines zusätzlichen Port auf jeden Host, in dem eine schlanke index.html abgelegt wird.

6.2 Datenbank

6.2.1 MySQL

Drupal verwendet ab Version 7 INNODB als Storage Engine. Falls eine ältere Version von Drupal zum Einsatz kommt, sollte man auch INNODB einsetzen. Dafür sind keine weiteren Anpassungen nötigt.
Damit alle nachträglichen Tabellen auch standardmässig mit INNODB angelegt werden, empfiehlt es sich INNODB als Standard Engine auf dem Server festzulegen:

(alle Einstellungen für die Mysql Server Optimierung werden in der Datei /etc/mysql/my.conf im Abschnitt [mysqld] gemacht)

default-storage-engine = InnoDB

Eine Entscheidung sollte nun getroffen werden: werden ausschließlich INNODB Tabellen zum Einsatz kommen oder werden auch MyISAM Tabellen verwendet. Verzichtet man komplett auf den Einsatz von MyISAM Tablellen, hat das folgende Vorteile:

  • Optimierung kann voll auf INNODB durchgeführt, also auch der gesamte Speicher zur Optimierung für INNODB eingesetzt werden
  • die Konfiguration/Optimierung wird deutlich übersichtlicher und einfacher, da man nur noch eine Storage Engine optimieren muss

Hier einige Einstellungen, die sich auf die INNODB Optimierung beziehen:

# void double buffering and reduce swap pressure, in most cases this setting improves performance. Though be careful if you do not have battery backed up RAID cache as when write IO may suffer.
innodb_flush_method=O_DIRECT

# If you do not have too many tables use this option, so you will not have uncontrolled innodb main tablespace growth which you can’t reclaim. This option was added in MySQL 4.1 and now stable enough to use.
innodb_file_per_table = 1

# 4M is good for most cases unless you’re piping large blobs to Innodb in this case increase it a bit.
innodb_log_buffer_size = 4M

# If you’re not concern about ACID and can loose transactions for last second or two in case of full OS crash than set this value. It can dramatic effect especially on a lot of short write transactions.
innodb_flush_log_at_trx_commit = 2

# 70-80% of memory is a safe bet. I set it to 12G on 16GB box
innodb_buffer_pool_size = 1024M

All diese Einstellung können mit der internen Storage Engine von MySQL durchgeführt werden. Ab Version 5.1 unterstützt MySQL Storage Engines über Plugins hinzuzuladen oder auszutauschen.
Verwendet man das InnoDB Plugin, welches die interne InnoDB Storage Engine ersetzt, wird man einen Geschwindigkeit-Boost feststellen können.

Um die interne Storage Engine für InnoDB zu deaktivieren und das Plugin zu aktivieren, setzt man folgende Einstellung:

ignore-builtin-innodb
plugin-load=innodb=ha_innodb_plugin.so;innodb_trx=ha_innodb_plugin.so;innodb_locks=ha_innodb_plugin.so;innodb_lock_waits=ha_innodb_plugin.so;innodb_cmp=ha_innodb_plugin.so;innodb_cmp_reset=ha_innodb_plugin.so;innodb_cmpmem=ha_innodb_plugin.so;innodb_cmpmem_reset=ha_innodb_plugin.so
plugin_dir=/usr/lib/mysql/plugin

Sofern AppArmor auf dem Server aktiv ist (wie das beispielsweise bei Ubuntu 10.04 der Fall ist), muss in der Datei /etc/apparmor.d/usr.sbin.mysqld folgender Eintrag aufgenommen werden:

/usr/lib/mysql/plugin/* m,

In den unendlichen Weiten der MySQL Optimierung verliert man sich gerne. Es gibt einige Skripte, die Vorschläge zur Optimierung machen und einem dadurch viel Zeit abgenommen wird:

6.2.2 MySQL Replikation

Drupal 6 ist nicht für den Einsatz von Datenbank Replikation ausgelegt. Wenn Datenbank Replikation eingesetzt werden soll, muss Pressflow oder Drupal 7 verwendet werden.

Die Konfiguration auf Drupal Seite ist sehr einfach. Kommt Pressflow zum Einsatz, wird folgender Abschnitt in die sites/default/settings.php eingefügt, über den festgelegt wird, welcher Server als Master und welche als Slave verwendet wird.

$db_url = array();
$db_url['default'] = 'mysqli://user:password@master-host/database';

$db_slave_url = array();
$db_slave_url['default'] = array();
$db_slave_url['default'][] 'mysqli://user:password@slave-host-1/database';
$db_slave_url['default'][] 'mysqli://user:password@slave-host-2/database';

6.2.3 MySQL Cluster

MySQL Cluster wird von Drupal nicht unterstützt. Für den Einsatz eines MySQL Clusters muss als Storage Engine NDBCLUSTER für die Datenbank Tabellen verwendet werden.
Wer den steinigen Weg dennoch wagen will, findet im Artikel (Englisch) Running Drupal in a MySQL Cluster eine Anleitung.

6.2.4 MongoDB

MongoDB kann MySQL oder sqlite nicht 100% für Drupal ersetzen. MongoDB wird zusätzlich für bestimmte Anwendungsfälle über das Drupal Modul MongoDB eingesetzt:

  • als Caching Backend (nur Drupal 7)
  • als Field Storage (nur Drupal 7)
  • als Backend für die Benutzer-Sessions (nur Drupal 7)
  • als Backend für Watchdog
  • als Backend für andere Drupal Module

6.3 Data Storage

6.3.1 Hardware

Um Datensicherheit und hohe Performance zu erhalten, empfiehlt sich ein RAID 10 System. Wird Datenbank Replikation mit mehreren Slaves verwendet, kann für die Slaves auch RAID 0 zum Einsatz kommen.

6.3.2 .htaccess deaktivieren

Um Zugriffe auf das Dateisystem einzuschränken, wird die .htaccess Datei von Drupal bzw. Pressflow in die vhost Konfigurationsdatei von Apache verschoben. Damit verhindert man, dass Apache alle Verzeichnisse nach .htaccess Datei absucht. Folgende Direktive muss in der vhost Konfigurationsdatei aufgenommen werden:

<Directory /path/to/DocumentRoot/>
  AllowOverride None
</Directory>

Da durch diese Umstellung keine .htaccess Datei mehr zum Einsatz kommt, wird der Inhalt der .htaccess Datei in die vhost Konfigurationsdatei konvertiert.

6.3.3. noatime zur Dateisystemeinbindung

Die Datensystem werden mit der Option 'noatime' eingebunden, sofern das Dateisystem diese Option vorsieht. Dadurch verhindert man, dass bei jedem Zugriff auf eine Datei ein Schreibzugriff erfolgt. Besonders wichtig ist dies auf Partitionen, auf denen die Datenbank Daten abgelegt werden.

6.4 Webserver

Hier wird der Einsatz von Apache prefork beschrieben. Um keine unnötige Verzögerungen zu erzeugen, will man mit dem Start des Webservers möglichst viele Apache Prozesse auf Abruf schon vorladen. Die Anzahl dieser Prozesse richtet sich nach dem verfügbaren Hauptspeicher.

StartServers 40
Mit dem Start des Webservers werden 40 Prozesse gestartet.

MinSpareServers 40
40 Prozesse werden immer beibehalten, auch wenn der Traffic niedrig ist.

MaxSpareServers 40
Steigt die Anzahl der Prozesse über diesen Wert, werden solange Prozess beendet, bis sie wieder unterhalb diesen Wert liegen.

MaxClients 80
Wenn der Trafic es erfordert, werden die Prozesse auf maximal 80 erhöht.

Metrik zur Berechnung: verfügbares RAM / RES-WERT - 10% für andere Prozesse und Filecache

MaxRequestsPerChild 10000
Dieser Wert legt fest wieviele Requests ein Kind-Prozess bearbeiten darf, bis er beendet wird. Hindergrund dafür ist, dass manche Prozesse sehr verschwenderisch mit Speicher umgehen. Damit keine Probleme durch Speicherlecks auftreten, ist dieser Wert ungleich 0 zu setzen.

KeepAlive On

Alle Prozesse werden über eine bestehende Verbindung abgearbeitet - es wird dafür nur ein Webserverprozess verwendet.
Ein Abschalten von KeepAlive macht dann Sinn, wenn man genau weiß, dass in den ausgelieferten Seiten kein weiterer Request durch den Browser gestellt wird. Dies ist der Fall, wenn wirklich alle statischen Inhalte über ein CDN ausgeliefert werden (Vorsicht ist hier mit Seiten geboten, die AJAX Aufrufe beinhalten).

MaxKeepAliveRequests 100
Die maximale Anzahl der KeepAlive Requests.

KeepAliveTimeout 5

Die Zeit in Sekunden, in der das KeepAlive aufrecht erhalten bleiben soll. Je höher dieser Wert ist, desto mehr Prozesse werden parallel existieren. Daher ist das Ziel diesen Wert möglichst gering zu halten.

Beispiel: Es wird die Drupal Startseite als ein Request aufgerufen. Innerhalb dieser Seite werden CSS Dateien, Javascript Dateien und Bilder vom Webserver angefordert - alle Dateien stellen einen eigenen Requests dar. Das KeepAliveTimeout sollte also so hoch gesetzt werden, dass all die zusätzlichen Requests abgearbeitet werden kann, die von einem Seitenaufruf einhergehen. Reicht die Zeit nicht aus, wird ein neuer Prozess gestartet - und das kosten Zeit und Last.

6.5 PHP

Das Hauptziel der PHP Optimierung liegt darin, dass ein PHP Prozess möglich wenig Speicher benötigt. Da jeder HTTP Request an den Webserver auch einen PHP Prozess startet, ist die Größe eines Apache Prozesses abhängig vom Speicherbedarf des mod_php Modules.

max_execution_time = 30
Die maximale Länge, die für die Ausführung eines Skriptes erlaubt ist.

max_input_time = 60
Die maximale Länge, die für das entgegennehmen von Daten für ein Skript erlaubt ist.

memory_limit = 128M
Mit dieser Konfigurationsvariable legt man fest, wieviel Speicher für einen PHP Prozess maximal verwendet werden darf. Diese Konfigurationsvariable dient nicht wirklich der Performance-Optimierung. Sie ist dennoch sehr hilfreich um Speicherleaks in PHP Skripten ausfindig zu machen. Sofern eine Drupal Seite mehr als 128MB benötigt, sollte man den Profiler anschalten und das Projekt nach möglichem Fehlverhalten untersuchen. Eine schlechte Lösung ist einfach das Speicherlimit zu erhöhen. Hierdurch geht wertvoller Hauptspeicher verloren, der an anderer Stelle verwendet werden kann (und dadurch wird natürlich auch die mögliche Anzahl der gleichzeitigen Prozesse gesenkt).
Allerdings ist hier zu beachten, dass dieser Wert über den Wert post_max_size und upload_max_filesize liegen muss.

register_argc_argv = Off

Hiermit wird die Vorbelegung der Variablen $argc und $argv abgeschaltet, die von Drupal nicht benutzt werden.

always_populate_raw_post_data = Off

Drupal verwendet die Variable HTTP_RAW_POST_DATA nicht. Deshalb kann diese Option auf Off gesetzt und dadurch Speicher und CPU Leistung gespart werden.

session.auto_start = Off
Eine Session sollte nicht automatisch gestartet werden, da nicht alle Requests eine Session benötigen.

session.gc_divisor steht standardmässig auf 100 (abhängig von session.gc_probability=1). Das bedeutet, dass bei jeden 100sten Request der Session Garbage Collektor aktiviert wird. Bei einer Seite mit viel Traffic ist dies viel zu häufig und erzeugt viel zu viel Last. Der Wert wird je nach Traffic auf 10.000 oder höher gestellt.

6.6 Reverse Proxy

Vorteile:

  • Webserver wird entlastet, da die cachebaren Seiten überhaupt nicht am Webserver ankommen und daher auch kein PHP Code oder eine Datenbankverbindung verwendet wird

Nachteile:

  • Logfile Auswertungen werden schwieriger. Verwendet man ein Analyse-Tool wie awstats, wird ohne spezielle Anpassungen nur noch der Traffic berücksichtigt, der auch beim Webserver ankommt. Der Traffic, den der Reverse Proxy abfängt wird nicht aufgezeigt.
  • kann nicht mit SSL verwendet werden

<img src="http://alphanodes.de/sites/default/files/images/performance_reverse_proxy.png" width="492" height="219" alt="Statistikausgabe nach Umstellung auf Reverse Proxy" title="Statistikausgabe nach Hier sieht man durch die Seitenaufrufe von Montag auf Dienstag die Umstellung stattgefunden hat. Die Seitenaufrufe gehen von 5505 auf 274 zurück. Daraus kann man entnehmen, dass am Webserver 274 Anfragen angekommen sind und die restlichen Anfragen vom Reverse Proxy abgefangen wurden.

Hinweis: Falls auf HTTP Header basierend Redirects durchgeführt werden sollen (wie z.B. für mobile Endgeräte), sollte dies über den Reverse Proxy umgesetzt werden und nicht über den Webserver. Auch die Last für den Redirect kann dadurch dem Webserver abgenommen werden.

Umgang mit Logfile Auswertungen

  • arpa Modul
  • Zusammenführen von Logfiles
  • Einsatz über Frontend Messungen (Piwik, Googl Analytics, u.s.w.)

6.6.1 Varnish Konfiguration

Es gibt zwei Möglichkeiten den Reverse Proxy vor den Webserver zu schalten:

1) Der Reverse Proxy wird auf Port 80 eingerichtet und der Webserver auf einen anderen Port. Alle externen Anfragen kommen am Port 80 an und werden vom Reverse Proxy zum Webserver weitergereicht, sofern die Anfrage nicht aus dem Cache beantwortet werden kann.

2) Der Webserver lauscht an Port 80 und der Reverse Proxy verwendet einen anderen Port (z.B. 8081). Um den Traffic nun aber doch durch den Reverse Proxy zu leiten, wird NAT verwendet. Diese Methode hat den Vorteil, dass ein deaktivieren des Reverse Proxies durch eine Änderung der NAT Regel möglich ist, ohne den Webserver neu starten zu müssen. Da der Einsatz eines Reverse Proxy eine zusätzliche Komponente im System ist die ausfallen kann, hat man dadurch eine elegante Lösung geschaffen, falls Probleme oder Konfigurationsänderungen am Reverse Proxy vorliegen - ohne eine Ausfallzeit hinnehmen zu müssen. Durch die NAT Umleitung entstehen minimale Performance einbußen, die aber durch die Vorteile übertroffen werden.

Die NAT Umleitung wird mit iptables wie folgt umgesetzt:

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8081

Nun ändern wir den Namen der Varnish Instanz auf "drupal", indem wir in der Datei /etc/default/varnish

INSTANCE=drupal

setzen. In der gleichen Datei werden dann auch die Parameter für die Varnish Daemon festgelegt:

DAEMON_OPTS="-a :8081 \
             -T localhost:6082 \
             -f /etc/varnish/default.vcl \
             -s file,/var/lib/varnish/$INSTANCE/varnish_storage.bin,1G"

Mit dieser Einstellung wird die Cache-Größe auf 1 GB begrenzt.

Hier ein Beispiel der Varnish Konfigurationsdatei für Drupal: /etc/varnish/drupal.vcl

backend default {
.host = "127.0.0.1";
.port = "80";
.connect_timeout = 600s;
.first_byte_timeout = 600s;
.between_bytes_timeout = 600s;
}

sub vcl_recv {
  // Remove has_js and Google Analytics cookies.
  set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(__[a-z]+|has_js)=[^;]*","");

  // The  is an analytics or other Javascript add-on.
  // You should do this here, before the other cookie stuff, or by adding
  // to the regular-expression above.

  // Remove a ";" prefix, if present.
  set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");
  // Remove empty cookies.
  if (req.http.Cookie ~ "^\s*$") {
    unset req.http.Cookie;
  }

  // No varnish for install or update.php
  if (req.url ~ "install.php|update.php") {
    return (pass);
  }

  // Normalize the Accept-Encoding header
  // as per: http://varnish-cache.org/wiki/FAQ/Compression
  if (req.http.Accept-Encoding) {
    if (req.url ~ ".(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") {
      # No point in compressing these
      remove req.http.Accept-Encoding;
    } elsif (req.http.Accept-Encoding ~ "gzip") {
      set req.http.Accept-Encoding = "gzip";
   } elsif (req.http.Accept-Encoding ~ "deflate") {
      set req.http.Accept-Encoding = "deflate";
    } else {
      # unkown algorithm
      remove req.http.Accept-Encoding;
    }
  }

  // Let's have a little grace
  set req.grace = 30s;

}

// Strip any cookies before an image/js/css is inserted into cache.
// Also: future-support for ESI.
// Disabled pending error resolution
// sub vcl_fetch {
//  if (req.url ~ ".(png|gif|jpg|swf|css|js)$") {
//    unset obj.http.set-cookie;
//  }
//  esi;
// }

sub vcl_hash {
  if (req.http.Cookie) {
    set req.hash += req.http.Cookie;
  }
}

sub vcl_error {
  // Let's deliver a slightly more friedly error page.
  // You can customize this as you wish.
  set obj.http.Content-Type = "text/html; charset=utf-8";
  synthetic {"
  <?xml version="
1.0" encoding="utf-8"?>
  <!DOCTYPE html PUBLIC "
-//W3C//DTD XHTML 1.0 Strict//EN"
   "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  <html>
    <head>
      <title>"
} obj.status " " obj.response {"</title>
      <style type="
text/css">
      #page {width: 400px; padding: 10px; margin: 20px auto; border: 1px solid black;
background-color: #FFF;}
      p {margin-left:20px;}
      body {background-color: #DDD; margin: auto;}
      </style>
    </head>
    <body>
    <div id="
page">
    <h1>Page Could Not Be Loaded</h1>
    <p>We're very sorry, but the page could not be loaded properly. This should be
fixed very soon, and we apologize for any inconvenience.</p>
    <hr />
    <h4>Debug Info:</h4>
    <pre>
Status: "
} obj.status {"
Response: "
} obj.response {"
XID: "
} req.xid {"
</pre>
      </div>
    </body>
   </html>
   "
};
   return(deliver);
}

Die Debug Informationen sollten auf einem Produktivserver entfernt werden.

Nun wird noch das Verzeichnis erstellst, in dem Varnish die gecachten Daten ablegen wird (der Name des Verzeichnisses muss mit dem Dateiname der vlc Datei übereinstimmen - ohne Endung):

mkdir -p /var/lib/varnish/drupal/

Nun muss noch die Drupal settings.php angepasst werden indem folgender Abschnitt darin aufgenommen wird:

# Varnish reverse proxy on localhost
$conf['reverse_proxy'] = TRUE;
$conf['reverse_proxy_addresses'] = array('127.0.0.1');

Sofern Varnish auf dem selben Server wie der Webserver läuft, ist diese Einstellung richtig. Wenn ein anderer Server eingesetzt wird, muss hier die IP Adresse des Servers verwendet werden, an dem Varnish zu finden ist.
Kommen mehr als ein Reverse Proxy zum Einsatz muss für Drupal6 Pressflow dieser Patch eingespielt werden, damit alle IP Adressen berücksichtigt werden (Drupal 7 kann ohne Patch schon damit umgehen).

6.6.2 Cache invalidieren

Die Problematik mit dem Einsatz eines Reverse Proxies ist, dass der Proxy davon nichts mit bekomt, wenn sich Inhalte ändern (da die Lebensdauer der gecachten Seiten meist weit in die Zukunft gesetzt ist). Abhilfe schafft das Drupal Modul cache_actions in Verbindung mit dem varnish Drupal Module. Damit kann man über einer rules Action bestimmte URLs inkl. regulären Ausdrücken invalidieren.
Damit das cache_actions Modul richtig funktioniert, muss in der Varnish Modul Konfiguration die Einstellung "Varnish cache clearing" auf "none" gesetzt werden.

drush vset --yes varnish-cache-clear 0;

Das Bild zeigt eine Action, um beim löschen eines Nodes die dazugehörige URL aus dem Varnish Cache zu löschen:

Rules Action, die gechachte URL mit Varnish invalidiert

6.7 Solr

Anstelle der Standardsuche von Drupal sollte auf Solr umgestellt werden. Durch das Modul Apache Solr wird eine gelungene Integration angeboten. Wie man Solr einrichtet ist im Artikel Apache Solr mit Debian Lenny einrichten nachzulesen.

6.8 Caching

Caching wird in mehreren Ebenen durchgeführt:

  1. statischer Inhalt und auch die Ausgabeseite für Gäste werden durch den Reverse Proxy gecached. Dadurch wird die komplette Last vom Webserver ferngehalten - es wird kein PHP geparst und auch keine Datenbankverbindung ist notwendig.
  2. Es wird ein Opcode Cache der PHP Skripte erzeugt. Man kann sich das so vorstellen wie das Kompilieren eines Skriptes zur Laufzeit. Das bedeutet, dass das PHP Skript nicht bei jedem Aufruf neu geparst werden muss, sondern stattdessen auf den Opcode zurückgegriffen wird.
  3. Die nächste Ebene stellt der Memcache dar. Darüber werden die Caching Tabellen und die Benutzer Sessions im Hauptspeicher abgelegt - und nicht wie standardmässig von Drupal in der Datenbank)
  4. Der letzte Caching Ebene ist der Query Cache der MySQL Datenbank. Falls keine Caching Ebene vorher greift, werden dadurch sich wiederholende Datenbankanfragen gecached. Als Strategie sollte der Query Cache nicht in einem Konzept auftauchen, sondern er sollte er als Fallback gesehen werden.

6.8.1 Reverse Proxy

Ein Reverse Proxy wird dafür eingesetzt, um die Anfragen vor dem Apache abzufangen. Da ein Prozess bzw. Thread über einen Reverse Proxy wesentlich schlanker ist, im Vergleich zu einem Apache Prozess, wird dadurch die Anzahl der möglichen Requests gesteigert. Als Reverse Proxy kann Squid, Varnish oder auch Nginx eingesetzt werden. Siehe für weitere Informationen für den Einsatz von Varnish Kapitel 6.6.

6.8.2 Opcode Caching

Als Opcode Cache wird APC empfohlen. Die Standardeinstellungen sind von APC zu verwenden, bis auf folgende Ausnahmen:

; Speicher, der APC zur Verfügung gestellt wird
apc.shm_size=72M
; Optimierung der include_once und require_once Anweisungen, nur für Drupal 6 (funktioniert nicht mit Drupal 7)
apc.include_once_override = 1

Die Einstellung apc.include_once_override = 1 muss getestet werden, damit dadurch keine Fehler erzeugt werden (mit Drupal 7 habe ich es noch nicht geschafft diese Option zu verwenden). Der APC Speicher muss über ein Monitoring Tool unter Last beobachtet werden. 72MB ist ein guter Startwert, allerdings können Anpassungen nach oben bis 128MB je nach Projektkomplexität mit Drupal notwendig sein.

Weitere Informationen zur Installation von APC und Konfiguration kann im Artikel APC installieren nachgelesen werden.

6.8.3 Memcache

Mit dem Drupal Memcache Modul werden alle Caching Tabellen in den Memcache bzw. Hauptspeicher verlagert. Der Hauptspeicher ist wesentlich schneller als die Datenbank, und dank der Memcache Technik auch über das Netzwerk nutzbar.
Ein weiteres Feature des Drupal memcache Moduls ist, dass auch die Benutzer Sessions über den Memcache abgebildet werden können. Allerdings ist hierbei zu beachten, dass memcache kein permanenter Speicher ist - wird der memcached Dienst neu gestartet, sind alle Sessions verloren und alle Benutzer müssen sich neu im Projekt einloggen.

Voraussetzung für den Einsatz:

Durch folgenden Codeabschnitt aktiviert man die Memcache Nutzung (es ist keine Aktivierung des Modules über den Adminbereich erforderlich):

$conf['memcache_key_prefix'] = 'drupal';
$conf['cache_inc'] = './sites/all/modules/memcache/memcache.inc';
$conf['session_inc'] = './sites/all/modules/memcache/memcache-session.inc'; # session support
$conf['memcache_servers'] = array('localhost:11211' => 'default');

Im Beispiel wird erwartet, dass das memcache Modul im Verzeichnis sites/all/modules zu finden ist. Der memcached Server ist auf dem gleichen Server installiert, auf dem auch der Webserver läuft und verwendet den Port 11211.

6.8.4 Datenbank/MySQL

Folgenden Abschnitt fügt man in die Datei /etc/mysql/my.cnf ein im Abschnitt [mysqld], um den Query Cache zu aktivieren:

query_cache_limit = 2M
query_cache_size = 48M

Mit query_cache_limit wird festgelegt, dass die maximale Größe einer Query 2MB betragen darf, um gecached zu werden. 48MB ist die Größe des Speichers, der insgesamt für den Query Cache zur Verfügung steht.

Diese Werte sind Richtwerte und müssen entsprechend der Performance Analyse angepasst werden.

7. Performance Frontend Optimierung

7.1 Weniger HTTP Requests

  • Einsatz von CSS Sprites. Sofern mehrere Bilder über CSS eingebunden sind, können diese über CSS Sprites
    in ein Bild zusammengefasst werden. Dadurch werden weniger HTTP Requests an den Server gestellt. Die Folge ist geringere Serverlast und schnellerer Seitenaufbau im Browser, meist auch Reduzierung des Traffics.
  • Einsatz von Imagemags, um HTTP Requests einzusparen.

7.2 Einsatz eines Content Delivery Network (CDN)

Über ein Content Delivery Network werden statische Inhalte ausgeliefert, welches die Javascript und CSS Dateien bei Drupal sein können. Dazu zählen aber auch Bilder, Videos, Musikdateien, u.s.w.
Der Einsatz eines CDN hat die Vorteile, dass der Webserver weniger Requests abbekommt und daher auch weniger Last zur Auslieferung einer Seite benötigt.
Der CDN Provider hat in der Regel viele Server weltweit verfügbar. Ruft ein Benutzer die Webseite auf, wird der statische Inhalt von ihm an kürzesten entfernten Server ausgeliefert - und hat daher auch eine eine schnellere Antwortzeit. Besonders bei internationalen Projekten ist der Einsatz eines CDN sehr empfohlen.

Da die Nutzung über einen CDN Provider für kleinere Projekt nicht profitabel ist, kann man den statischen Inhalt auch über einen speziell dafür ausgerichteten Server in der eigenen Infrastruktur einsetzen (der Vorteil der Benutzernähe ist dadurch allerdings nicht gegeben).
Für die Umsetzung dieses "lokalen" CDN Dienstes eignet sich nginx oder lighttpd. Drupal 7 und auch Pressflow ist nur über Zusatzmodule oder einen Patch CDN tauglich.

Vorteile eines CDN Servers (also ohne Provider):

  • statischer Inhalt kann auf anderen Host ausgegeben werden, um die 2 Request Problematik zu umgehen
  • durch den Einsatz eines anderen Hostnamen werden auch keine Cookies zu diesen Requests mitgesendet (zusätzlicher Traffic)
  • einfache Konfiguration (im Vergleich Varnish um diese Eigenschaften zu ergänzen)
  • einfache Skalierbarkeit durch Hinzuschalten weiterer Nodes
  • kann durch externen CDN Provider erstetzt werden, ohne am Projekt Änderungen durchführen zu müssen

Nachteile (ohne Provider):

  • ein weiterer Dienst, der ausfallen kann ohne Redundanz
  • zusätzlicher Speicherbedarf (gering)

Noch ein Hinweis zur Nutzung: für die Auslieferung der Inhalte sollte nicht die Domain des Projektes verwendet werden. Setzt man dafür eine andere Domain ein, wir kein Cookie mit jedem Request mitgesendet (welches durch die Anwendung gesetzt wird) - und dadurch werden weniger Daten übertragen.

Ein Problem mit dem Einsatz ist der Umgang mit Imagecache Bildern. Die Bilder werden über Imagecache erzeugt, benötigen also PHP und müssen somit über den Apache ausgeliefert werden. Mit folgender Anweisung löst man das Problem beim Einsatz von nginx:

Für Drupal 6/Pressflow:

# imagecache fix for pressflow
location ~ ^/sites/default/files/imagecache/ {
  root   /www/htdocs;
  try_files $uri @rewrite;
}
location @rewrite {
  access_log off;
  proxy_pass http://www.meinapacheserver.com;
}

Für Drupal 7:

# imagecache fix for drupal 7
location ~ ^/sites/default/files/styles/ {
  root   /www/htdocs;
  try_files $uri @rewrite;
}
location @rewrite {
  access_log off;
  proxy_pass http://www.meinapacheserver.com;
}

Folgendes wird dadurch erreicht:
Fall 1 (das Bild wurde noch nicht über Imagecache erstellt und liegt nicht im Dateisystem):
In diesem Fall wird die Anfrage an dem Apache durchgereicht. Imagecache erstellt dadurch das Bild.

Fall 2 (das Bild wurde bereits erstellt und liegt schon im Dateisystem):
Nginx liefet das Bild aus. Apache wird hier nicht benutzt.

7.3 Expire Headers einsetzen

Drupal setzt mit der mitgelieferten .htaccess Datei standardmässig Expire auf 2 Wochen:

<IfModule mod_expires.c>
  # Enable expirations.
  ExpiresActive On

  # Cache all files for 2 weeks after access (A).
  ExpiresDefault A1209600

  <FilesMatch .php$>
    ExpiresActive Off
  </FilesMatch>
</IfModule>

Die Verfallszeit wird im folgenden Abschnitt auf 10 Jahre gesetzt:

<IfModule mod_expires.c>
  # Enable expirations.
  ExpiresActive On

  # Cache all files for 10 years after access (A).
  ExpiresDefault "access plus 10 years"

  <FilesMatch .php$>
    ExpiresActive Off
  </FilesMatch>
</IfModule>

Damit der Expire Header gesetzt wird, ist es erforderlich, das mod_expire Modul von Apache zu aktivieren.

7.4 Komprimierung von CSS und Javascript (Gzip)

Komprimierung aller CSS und Javascript Dateien wird über das Apache Deflate Modul sicher gestellt. Durch die Komprimierung verringert sich einerseits der Traffic und andererseits verkürzt sich das Ladeverhalten des Clients, da weniger Daten übertragen werden müssen.

Durch folgende Ergänzung in der vhost Konfiguration wird dies erreicht:

<FilesMatch ".(js|css|html|)$">
  SetOutputFilter DEFLATE
</FilesMatch>

7.5 CSS Ausdrücke vermeiden

Über die expression-Methode kann mit CSS eine Javascript-Ausdruck ausgeführt werden. CSS Ausdrücke wirken sich hauptsächlich auf die Performance der Seite aus, nachdem sie geladen sind. Die starke Browser Abhängigkeiten (bzgl. CSS) ist ein weitere Punkt, über den sehr leicht ein Fehlverhalten der Webseite und dadurch meist auch Performance-Probleme einhergehen. Auf CSS Ausdrücke ist generell zu verzichten.

7.6 DNS Lookups reduzieren

Die Anzahl der unterschiedlichen Hostnamen bei der Umsetzung der Ausgabeseiten sollte minimal gehalten werden. Wieso werden überhaupt unterschiedliche Hostnamen verwendet? Hier einige Gründe:

  • Einbindung von Werbung (meist das schlimmste Übel)
  • Einsatz eines CDN
  • Einbinden externe Mediadateien, z.B. von Flickr, Youtube, u.s.w.
  • Analyse Tools, wie Google Analytics, Piwik, u.s.w.

Da die DNS Anfragen sehr zeitintensiv sind cachen die Webbrowser diese Anfragen auch für einen bestimmten Zeitraum. Eine Anfrage um die IP Adresse für einen Hostnamen nachzuschlagen dauert ohne Caching zwischen 20 und 120 Millisekunden. Werden viele Hostnamen verwendet, summiert sich das schnell zu einer oder mehr Sekunden. Der Internet Explorer cached die Anfragen standardmässig 30 Minuten, Firefox 1 Minute (oder man schließt den Browser, womit der Cache invalidiert wird). Für den Firefox gibt es ein Addon namens Fasterfox, mit dem man die Lebenszeit des Caches nach oben setzen kann, allerdings darf man bei der Optimierung des Projektes nicht davon ausgehen, dass der normale Besucher dieses Addon im Einsatz hat.

7.7 Minify Javascript

Minify bedeutet, dass aus dem Javascript Quelltext alle unnötigen Zeichen entfernen werden, ohne die Funktionalität zu verändern. Dadurch kann die Dateigröße deutlich verringert werden. Da zum Browser weniger Daten übertragen werden, wird die Seite schneller laden und gleichzeitig wird der Traffic des Servers reduziert. Das Drupal Modul Javascript Aggregator bietet diese Funktionalität über JSMin an.

7.8 ETags

Entweder richtig konfigurieren oder komplett darauf verzichten. Werden ETags falsch eingesetzt, verschlechtert sich die Performance der Webseite.

7.9 Ajax Anfragen cachen

Um Ajax Anfragen zu cachen, kann das Drupal Modul AJAX Cache eingesetzt werden (funktioniert auch für angemeldete Benutzer).

Ressourcen

Kategorie: 
RSS Feed