Title: Apache-Tuning
Weitere Themen

Der Apache 2.0 ist ein universeller Webserver mit einem ausgewogenen Maß an Flexibilität, Portierbarkeit und Leistungsfähigkeit. Zwar wurde er nicht speziell für neue Benchmark-Höchstwerte entwickelt, dennoch erweist er sich im praktischen Einsatz als ein Hochleistungsserver.

Im Vergleich mit der Apache-Version 1.3 enthält er viele neue ergänzende Optimierungen, die den Durchsatz und die Skalierbarkeit erhöhen. Die meisten dieser Verbesserungen sind standardmäßig aktiviert. Für die Laufzeit- und Kompilerkonfiguration gibt es aber Auswahlmöglichkeiten, die sich spürbar auf die Leistungen auswirken. Im folgenden werden die Konfigurationsoptionen beschrieben, mit denen der Administrator eine Feinabstimmung bei der Apache 2.0-Installation vornehmen kann. Einige dieser Konfigurationsoptionen ermöglichen dem httpd-Server eine bessere Ausnutzung der Möglichkeiten der Hardware und des Betriebssystem, während andere die Schaffung eines ausgewogenen Verhältnisses zwischen Funktionalität und Geschwindigkeit erlauben.

Aspekte der Hardware und des Betriebssystems

Der zentrale Hardware-Aspekt für die Webserver-Leistung ist der RAM-Speicher. Ein Webserver sollte niemals Swapping durchführen müssen, weil dies die Wartezeit für jede Anfrage in einen Bereich verschiebt, die den Benutzer ungeduldig werden lässt. Der Benutzer unterbricht dann den Ladevorgang und löst ihn erneut aus, was die Serverbelastung noch erhöht. Sie können und müssen mit der Anweisung MaxClients die maximal zu bedienende Anzahl der Clients kontrollieren, damit der Server nicht zu viele Kindprozesse starten muss, die das Swapping verursachen. Das kann auf einfache Weise geschehen: Ermitteln Sie über die Prozessliste oder ein Programm wie top die Größe des durchschnittlichen Apache-Prozesses und dividieren Sie diesen Wert durch die verfügbare RAM-Größe (berücksichtigen Sie dabei auch andere Prozesse).

Der Rest ist banal: Sorgen Sie für eine ausreichend schnelle CPU, eine ausreichend schnelle Netzwerkkarte und genügend Festplatten. Was "ausreichend schnell" bedeutet, muss in der Praxis ausprobiert werden.

Die Auswahl des Betriebssystems ist meistenteils eine Frage lokaler Gegebenheiten. Einige Richtlinien haben sich aber als grundsätzlich nützlich erweisen:

  • Verwenden Sie die letzte stabile Version und die entsprechenden Patches des Betriebssystems Ihrer Wahl. Bei vielen Betriebssystemen wurden die TCP-Stacks und Thread-Bibliotheken in den letzten Jahren deutlich verbessert.

  • Wenn das Betriebssystem einen sendfile(2)-Systemaufruf unterstützt, dann sorgen Sie dafür, dass die Version und/oder die Patches für dessen Aktivierung installiert sind. (Für Linux bedeutet das beispielsweise die Linux-Version 2.4 oder spätere Versionen. Frühere Solaris 8-Versionen benötigen möglicherweise einen Patch.) Bei Betriebssystemen, wo der Systemaufruf zur Verfügung steht, ermöglicht er dem Apache 2-Server eine schnellere Auslieferung statischer Inhalte bei einer niedrigeren CPU-Belastung.

Aspekte der Laufzeitkonfiguration mod_dir mpm_common mod_status AllowOverride DirectoryIndex HostnameLookups EnableMMAP EnableSendfile KeepAliveTimeout MaxSpareServers MinSpareServers Options StartServers
Suche nach Hostnamen und andere DNS-Aspekte

Vor der Apache-Version 1.3, war die HostnameLookups-Direktive standardmäßig auf On gesetzt. Das führt zu zusätzlicher Wartezeit für jede Anfrage, weil eine DNS-Suche durchgeführt werden muss, bevor die Anfrage abgeschlossen werden kann. Bei der Apache-Version 1.3 wird standardmäßig der Wert Off gesetzt. Müssen in den Protokolldateien Adressen in Hostnamen aufgelöst werden, dann benutzen Sie das Programm logresolve , das Bestandteil der Apache-Distribution und eines der zahlreichen Protokollprogramme ist, die zur Verfügung stehen.

Es ist zu empfehlen, diese Form der Nachbereitung der Protokolldateien auf einem anderen Rechner als dem eigentlichen Webserver durchzuführen, damit sie sich nicht auf die Leistungsfähigkeit des Servers auswirkt.

Wird eine der Anweisungen Allow from domain oder Deny from domain verwendet (Verwendung eines Host- oder Domänennamens anstelle einer IP-Adresse), dann ist eine doppelt reverse DNS-Suche erforderlich (eine reverse gefolgt von einer vorwärts gerichteten Suche, um sicherzustellen, dass die reverse Suche nicht getäuscht wird). Um keine Leistungseinbußen hinnehmen zu müssen, sollten daher bei diesen Direktiven möglichst immer IP-Adressen anstatt Namen verwendet werden.

Die Direktiven können auch auf einen Bereich beschränkt werden, beispielsweise auf einen <Location /server-status>-Abschnitt. In diesem Fall werden die DNS-Suchen nur für Anfragen durchgeführt, für die die Kriterien zutreffen. Im folgenden Beispiel werden die Suchen außer für .html- und .cgi-Dateien deaktiviert:

HostnameLookups off
<Files ~ "\.(html|cgi)$">
HostnameLookups on
</Files>

Werden DNS-Namen nur in einigen CGI-Skripten benutzt, sollte der gethostbyname-Aufruf in dem entsprechenden CGI-Skript benutzt werden.

AllowOverride

Wird im URL-Raum das Überschreiben erlaubt (normalerweise in den .htaccess-Dateien), versucht der Apache, die Datei .htaccess für jede Komponente des Dateinamens zu öffnen. Bei der Anweisung

DocumentRoot /www/htdocs
<Directory />
AllowOverride all
</Directory>

und einer Anfrage nach dem URI /index.html, versucht der Apache die Dateien /.htaccess, /www/.htaccess und /www/htdocs/.htaccess zu öffnen. Die erforderlichen Maßnahmen sind ähnlich wie für Options FollowSymLinks. Für höchste Leistungen sollte überall im Dateisystem AllowOverride None verwendet werden.

Content-Negotiation

Falls möglich, sollte eine Content-Negotiation vermieden werden, wenn alle Möglichkeiten zur Leistungssteigerung ausgeschöpft werden sollen. Allerdings wiegen die Vorteile der Content-Negotiation in der Praxis die Leistungseinbußen auf. In einer Situation kann der Server beschleunigt werden. Anstatt Jokerzeichen wie

DirectoryIndex index

zu benutzen, kann eine vollständige Optionsliste verwendet werden:

DirectoryIndex index.cgi index.pl index.shtml index.html

An erster Stelle in der Liste steht der häufigste Fall.

Auch das explizite Anlegen einer type-map-Datei ermöglicht bessere Leistungen als die Verwendung von MultiViews, weil die erforderlichen Informationen durch Lesen einer einzigen Datei ermittelt werden können, anstatt das Verzeichnis nach Dateien durchsuchen zu müssen.

Ist die Content-Negotiation erforderlich, sollten type-map-Dateien anstelle der Direktive Options MultiViews benutzt werden. In der Beschreibung der Content-Negotiation werden die Methoden des Aushandelns und die Anweisungen zum Erzeugen von type-map-Dateien ausführlich behandelt.

Speicherzuordnungen

In Situationen, in denen der Apache 2.0 auf den Inhalt einer auszuliefernden Datei schauen muss (beispielsweise bei der Server-Side-Include-Verarbeitung) nimmt er normalerweise eine Speicherzuordnung für die Datei vor, wenn das Betriebssystem eine Form der Funktion mmap(2) unterstützt.

Bei einigen Betriebssystemen steigert die Speicherzuordnung die Leistung. Manchmal kann sie aber auch die Leistung mindern oder gar die Stabilität des Servers gefährden:

  • Bei einigen Betriebssystem passt sich mmap nicht so gut wie read(2) an, wenn die Anzahl CPUs zunimmt. Bei Solaris-Servern mit mehreren Prozessoren liefert der Apache 2.0 beispielsweise manchmal vom Server analysierte Dateien schneller aus, wenn mmap deaktiviert ist.

  • Wird eine Speicherzuordnung für eine in einem eingebundenen NFS-Dateisystem befindliche Datei vorgenommen und ein Prozess eines anderer NFS-Client-Rechners löscht oder verkürzt die Datei, kann es zu einem Busfehler kommen, wenn das nächste Mal versucht wird, auf den Inhalt der zugeordneten Datei zuzugreifen.

Bei Installationen, wo eine dieser Möglichkeiten zutrifft, sollte die Speicherzuordnung ausgelieferter Dateien mit EnableMMAP off deaktiviert werden. (Hinweis: Diese Direktive kann auf Verzeichnisebene überschrieben werden.)

Sendfile

In Situationen wo der Apache 2.0 den Inhalt der auszuliefernden Datei ignorieren kann (beispielsweise bei statischem Dateiinhalt), wird normalerweise die sendfile-Unterstützung des Servers genutzt, wenn das Betriebssystem die sendfile(2)-Operation unterstützt.

Bei den meisten Betriebssystem verbessert die Verwendung von sendfile die Leistungen, weil separate Lese- und Sendemechanismen wegfallen. In bestimmten Fällen kann die Verwendung von sendfile jedoch die Stabilität des httpd-Programms gefährden:

  • Bei manchen Betriebssystem ist die sendfile-Unterstützung fehlerhaft, was vom build-System nicht erkannt wird, insbesondere, wenn die Binärdateien auf einem anderen Rechner erstellt und auf einen Rechner mit fehlerhafter sendfile-Unterstützung verschoben wurden.

  • Bei eingebundenen NFS-Dateien ist der Serverkern möglicherweise nicht in der Lage, die Netzwerkdatei über den eigenen Zwischenspeicher zuverlässig zu bedienen.

Trifft einer dieser Umstände zu, sollte sendfile mit der Anweisung EnableSendfile off für die Auslieferung von Dateiinhalten deaktiviert werden. (Hinweis: Diese Direktive kann auf Verzeichnisebene überschrieben werden.)

Prozesserzeugung

Vor der Apache-Version 1.3 hatten die MinSpareServers-, MaxSpareServers- und StartServers-Einstellungen drastische Auswirkungen auf die Ergebnisse von Vergleichstests. Insbesondere benötigte der Apache eine "Anfahrzeit", um eine ausreichende Anzahl von Kindprozessen zu erreichen. Nach der Initialisierung der StartServers-Prozesse, wurde nur ein Kindprozess pro Sekunde erzeugt, um die mit MinSpareServers angegebene Anzahl zu erreichen. Bei der Voreinstellung von 5 für StartServers und bei 100 gleichzeitig zugreifenden Clients dauerte es 95 Sekunden, bis genug Kindprozesse gestartet waren. Das funktioniert in der Praxis, weil im normalen Betrieb nicht häufig Neustarts durchgeführt werden. Bei einem Vergleichstest, der sich nur über 10 Minuten erstreckt, führt das aber zu mageren Ergebnissen.

Mit der Anzahl von einem Kindprozess pro Sekunde sollte vermieden werden, dass der Server in der Startphase mit diesem Vorgang überlastet wird. Wenn der Rechner damit beschäftigt ist, neue Kindprozesse zu starten, kann er andere Anfragen nicht bedienen. Aber die starken Auswirkungen auf die Leistung machte eine Abhilfe erforderlich. Mit der Apache-Version 1.3 wurde der Sekundentakt aufgegeben. Jetzt wird zuerst ein Prozess gestartet, eine Sekunde gewartet und dann zwei Prozesse gestartet. Nach einer weiteren Sekunde werden 4 Prozesse gestartet usw., bis 32 Kindprozesse pro Sekunde gestartet werden. Ist der mit MinSpareServers angegebene Wert erreicht, wird der Vorgang abgebrochen.

Das scheint so durchdacht zu sein, dass es meist unnötig ist, die Einstellungen für MinSpareServers, MaxSpareServers und StartServers zu verändern. Werden mehr als 4 Kindprozesse pro Sekunde gestartet, wird eine Meldung ins Fehlerprotokoll geschrieben. Taucht diese Meldung wiederholt auf, müssen die Einstellungen eventuell geändert werden. Die Ausgabe von mod_status kann hier als Orientierung dienen.

In Verbindung mit der Prozesserzeugung steht die Prozessbeendigung, die durch die MaxRequestsPerChild-Einstellung ausgelöst wird. Die Standardeinstellung liegt bei 0 und bedeutet, dass es keine Obergrenze für die Anzahl der von einem Kindprozess bedienten Anfragen gibt. Liegt der Wert sehr niedrig, beispielsweise bei 30, dann sollte er deutlich erhöht werden. Unter SunOS oder einer älteren Version von Solaris sollte die Grenze bei zirka 10000 liegen, um Speicherlöcher zu vermeiden.

Werden Keep-Alives verwendet, sind die Kindprozesse damit beschäftigt, nichts anderes zu tun, als auf Anfragen über die bereits geöffnete Verbindung zu warten. Die Voreinstellung für KeepAliveTimeout mit 15 Sekunden, versucht diesen Effekt zu mildern. Hier muss zwischen Netzwerkbandbreite Serverressourcen abgewogen werden. Auf keinen Fall sollte der Wert bei über 60 Sekunden liegen, da sonst die meisten Vorteile verloren gehen.

Beim Kompilieren zu berücksichtigende Aspekte
Auswahl eines MPM

Apache 2.x unterstützt die Auswahl mehrerer sogenannter Multi-Processing Module (MPMs). Beim Aufbau des Apache muss ein MPM ausgewählt werden. Für einige Betriebssysteme gibt es spezielle MPMs: beos, mpm_netware, mpmt_os2 und mpm_winnt. Für die gängigen Unix-Systeme gibt es mehrere MPMs, unter denen gewählt werden kann. Die Auswahl des MPM kann sich auf Geschwindigkeit und Anpassungsfähigkeit des Servers auswirken:

  • Das worker-MPM verwendet mehrere Kindprozesse mit jeweils vielen Threads. Jeder Thread bedient eine Verbindung. Das Modul eignet sich gut für Server mit viel Datenverkehr, weil es weniger Arbeitsspeicher beansprucht als prefork.
  • Das prefork-MPM benutzt mehrere Kindprozesse mit jeweils einem Thread. Jeder Prozess bedient eine Verbindung. Bei vielen Systemen ist dieses MPM hinsichtlich der Geschwindigkeit mit dem MPM worker vergleichbar, es beansprucht aber mehr Speicherplatz. Die geringe Thread-Anzahl ist in manchen Situationen vorteilhaft, insbesondere bei nicht Thread-sicheren Modulen anderer Hersteller. Außerdem ist die Fehlersuche bei Betriebssystemen mit wenig Möglichkeiten für die Thread-Fehlersuche einfacher.

Weiter Informationen zu diesen und anderen MPMs finden Sie in der MPM-Dokumentation.

Module

Da die Speicherauslastung ein wichtiger Punkt für die Leistungsfähigkeit ist, sollten eigentlich nicht benötigte Module eliminiert werden. Wenn die Module als DSOs eingerichtet wurden, muss lediglich die entsprechende LoadModule-Direktive für dieses Modul als Kommentar gekennzeichnet werden. Das Entfernen von Modulen kann so experimentell durchgeführt werden, indem überprüft wird, ob die Site anschließend noch funktioniert.

Wurden Module dagegen statisch in den binären Apache-Server eingebunden, muss der Server erneut kompiliert werden, wenn unerwünschte Module entfernt wurden.

In diesem Zusammenhang stellt sich natürlich die Frage, welche Module überhaupt benötigt werden. Die Antwort variiert selbstverständlich von Website zu Website. Eine minimale Modulliste enthält in der Regel die Module mod_mime, mod_dir und mod_log_config, wobei letzteres optional ist, den eine Website ist auch ohne Fehlerprotokollierung funktionsfähig. Allerdings ist dazu nicht zu raten.

Kernoperationen

Einige Module wie z.B. mod_cache und neuere Entwicklungs-Builds des MPM worker benutzen das Kern-API der Apache Portable Runtime (APR). Dieses API stellt Kernoperationen zur Verfügung, mit denen eine einfache Thread-Synchronisation möglich ist.

Standardmäßig implementiert die APR diese Operationen mit dem effizientesten Mechanismus des jeweiligen Betriebssystems. Viele moderne CPUs verfügen beispielsweise in der Hardware über die Kernfunktion Compare-and-Swap (CAS). Bei anderen Betriebssystemen verwendet die APR standardmäßig eine langsamere, Mutex-basierte Implementierung der Kern-API, um die Kompatibilität zu älteren CPU-Modellen zu gewährleisten, denen eine solche Anweisung fehlt. Wird der Apache für eines dieser Betriebssysteme bei gleichzeitigem Einsatz neuerer CPUs eingerichtet, kann eine schnellere Kernimplementierung ausgewählt werden. Dies geschieht beim Kompilieren mit der Option --enable-nonportable-atomics:

./buildconf
./configure --with-mpm=worker --enable-nonportable-atomics=yes

Die Option --enable-nonportable-atomics spielt für folgende Betriebssysteme eine Rolle:

  • Solaris auf SPARC
    Standardmäßig verwendet die APR unter Solaris/SPARC Mutex-basierte Kernoperationen. Bei Angabe der Option --enable-nonportable-atomics erzeugt die APR jedoch Code, der einen SPARC v8plus-Opcode für schnelle Compare-and-Swap-Operationen auf Hardware-Basis verwendet. Wird der Apache mit dieser Option konfiguriert, sind die Kernoperationen effektiver (geringere CPU-Auslastung und mehr gleichzeitige Zugriffe), das ausführbare Programm läuft aber nur mit UltraSPARC-CPUs.
  • Linux auf x86
    Standardmäßig verwendet die APR unter Linux auf x86 Mutex-basierte Kernoperationen. Bei Angabe der Option --enable-nonportable-atomics erzeugt die APR jedoch Code, der einen 486-Opcode für schnelle Compare-and-Swap-Operationen auf Hardware-Basis verwendet. Das Ergebnis sind effizientere Kernoperationen, das ausführbare Programm läuft aber nur mit 486er CPUs oder Folgemodellen (nicht mit 386er CPUs).
mod_status und ExtendedStatus On

Wird beim Kompilieren und Ausführen des Apache-Servers mod_status eingebunden und gleichzeitig ExtendedStatus On gesetzt, dann führt der Apache bei jeder Anfrage zwei Aufrufe von gettimeofday(2) oder times(2) (je nach Betriebssystem) sowie mehrerer zusätzliche Aufrufe von time(2) (vor 1.3) durch. Das geschieht zur Protokollierung von Zeitangaben im Statusreport. Um beste Leistungen zu erzielen, sollte ExtendedStatus auf off gesetzt werden (was der Voreinstellung entspricht).

accept-Serialisierung - Mehrere Sockets Achtung:

Dieser Abschnitt wurde noch nicht bezüglich der Änderungen des Apache HTTP-Servers mit der Version 2.0 aktualisiert. Die Angaben können aber müssen nicht mehr alle gültig sein.

Dieser Abschnitt befasst sich mit einem Mangel des Socket-API von Unix. Überwacht der Webserver mit mehreren Listen-Anweisungen mehrere Ports oder Adressen, dann verwendet der Apache select(2), um jedes Socket auf eine bereite Verbindung hin zu überprüfen. select(2) zeigt an, ob für ein Socket null oder wenigstens eine wartende Verbindung vorliegt. Das Apache-Modell enthält mehrere Kindprozesse, wobei die nicht beschäftigten Prozesse gleichzeitig auf neue Verbindungen abfragen. Eine naive Implementierung kann folgendermaßen ausschauen (diese Beispiele entsprechen nicht dem Code, sie dienen lediglich der Anschauung):

for (;;) {
for (;;) {
fd_set accept_fds;

FD_ZERO (&accept_fds);
for (i = first_socket; i <= last_socket; ++i) {
FD_SET (i, &accept_fds);
}
rc = select (last_socket+1, &accept_fds, NULL, NULL, NULL);
if (rc < 1) continue;
new_connection = -1;
for (i = first_socket; i <= last_socket; ++i) {
if (FD_ISSET (i, &accept_fds)) {
new_connection = accept (i, NULL, NULL);
if (new_connection != -1) break;
}
}
if (new_connection != -1) break;
}
process the new_connection;
}

Bei dieser naiven Implementierung gibt es ein ernstes Problem. Mehrere Kindprozesse führen diese Schleife gleichzeitig aus, so dass auch mehrere Prozesse von select blockiert werden, wenn sie zwischen Anfragen liegen. Alle diese blockierten Kindprozesse werden reaktiviert und kehren von select zurück, wenn eine einzige Anfrage an einem beliebigen Socket erscheint (die Anzahl der Kindprozesse, die reaktiviert werden, hängt vom Betriebssystem und Zeitaspekten ab). Sie betreten alle die Schleife und versuchen die Verbindung zu akzeptieren (accept). Aber nur ein Prozess hat Erfolg (vorausgesetzt, nur eine Verbindung ist bereit), der Rest wird von accept blockiert. Dadurch werden diese Kindprozesse daran gehindert, Anfragen von diesem einen und keinem der anderen Socket zu bedienen. Sie sitzen dort fest, bis genügend neue Anfragen für dieses Socket erscheinen, um sie alle zu reaktivieren. Diese Problem wurde im PR#467 zum erstenmal dokumentiert. Es bieten sich mindestens zwei Lösungen an.

Zum einen kann die Blockade durch die Sockets ausgeschlossen werden. In diesem Fall blockiert accept die Kindprozesse nicht und sie dürfen sofort fortfahren. Dadurch wird aber CPU-Zeit vergeudet. Bei zehn unbeschäftigten Kindprozessen für select und einer eingehenden Verbindung, werden neun Prozesse aktiviert. Sie versuchen die Verbindung zu akzeptieren (accept). Das schlägt fehl und sie stehen wieder bei select, ohne etwas zu verrichten. Zwischenzeitlich hat keiner dieser neun Prozesse Anfragen, die über andere Sockets eingehen, bedient. Insgesamt scheint dies keine glückliche Lösung zu sein, es sei denn, es gibt so viele wartende CPUs (in einem Multiprocessor-Rechner), wie es wartende Kindprozesse gibt, was eine sehr unwahrscheinliche Situation ist.

Bei der zweiten Lösung, die der Apache benutzt, wird der Eintritt in die innere Schleife serialisiert. Die Schleife sieht folgendermaßen aus (die Unterschiede sind hervorgehoben):

for (;;) {
accept_mutex_on ();
for (;;) {
fd_set accept_fds;

FD_ZERO (&accept_fds);
for (i = first_socket; i <= last_socket; ++i) {
FD_SET (i, &accept_fds);
}
rc = select (last_socket+1, &accept_fds, NULL, NULL, NULL);
if (rc < 1) continue;
new_connection = -1;
for (i = first_socket; i <= last_socket; ++i) {
if (FD_ISSET (i, &accept_fds)) {
new_connection = accept (i, NULL, NULL);
if (new_connection != -1) break;
}
}
if (new_connection != -1) break;
}
accept_mutex_off ();
process the new_connection;
}

Die Funktionen accept_mutex_on und accept_mutex_off implementieren eine wechselseitige Ausschlusssemaphore. Nur ein Kindprozesse kann jeweils den Mutex besitzen. Für die Implementierung der Mutexe gibt es mehrere Auswahlmöglichkeiten. Die Auswahl ist in der Datei src/conf.h (bei Version 1.3) oder in der Datei src/include/ap_config.h (ab Version 1.3) definiert. Bei einigen Architekturen wurde keine Auswahl getroffen, so dass bei ihnen die Verwendung mehrerer Listen-Anweisungen nicht sicher ist.

Mit der Direktive AcceptMutex kann die gewählte Mutex-Implementierung während der Laufzeit geändert werden.

AcceptMutex flock

Diese Methode verwendet den flock(2)-Systemaufruf, um eine Sperrdatei zu blockieren (ihr Name wird mit der LockFile-Direktive definiert).

AcceptMutex fcntl

Diese Methode verwendet den fcntl(2)-Systemaufruf, um eine Sperrdatei zu blockieren (ihr Name wird mit der LockFile-Direktive definiert).

AcceptMutex sysvsem

(Ab Version 1.3) Diese Methode verwendet Semaphoren im SysV-Stil, um den Mutex zu implementieren. Semaphoren im SysV-Stil haben leider unangenehme Nebeneffekte. Zum einen ist es möglich, dass der Apache beendet wird, ohne das die Semaphore vorher geleert wird (siehe auch ipcs(8)-Man-Page). Zum anderen lässt das Semaphoren-API Denial-of-Service-Attacken durch CGI-Skripte zu, die unter der gleichen uid wie der Webserver ausgeführt werden (d.h.. alle CGI-Skripte, wenn nicht etwas ähnliches wie suexec oder cgiwrapper benutzt wird). Aus diesen Gründen wird diese Methoden außer von IRIX (wo die beiden zuvor genannten bei den meisten IRIX-Rechnern unverhältnismäßig aufwändig sind) von keiner anderen Architektur benutzt.

AcceptMutex pthread

(Ab Version 1.3) Diese Methode verwendet POSIX-Mutexe und sollte bei jeder Architektur funktionieren, die die vollständige POSIX-Thread-Spezifikation implementiert. Allerdings scheint sie nur unter Solaris (ab Version 2.5) zu funktionieren und auch dort nur bei bestimmten Konfigurationen. Wenn mit dieser Methode experimentiert wird, sollte darauf geachtet werden, ob der Server hängt und nicht mehr reagiert. Bei Servern mit ausschließlich statischen Inhalten kann sie gut funktionieren..

AcceptMutex posixsem

(Ab Version 2.0) Diese Methode verwendet POSIX-Semaphoren. Die Semaphoreneigentümerschaft wird nicht wiederhergestellt, wenn ein Thread, der während des Prozesses den Mutex besitzt, einen Segmentfehler verursacht, was zum Absturz des Webservers führt.

Verwendet ein System eine hier nicht aufgeführte Methode für die Serialisierung, dann kann es sinnvoll sein, die APR entsprechend zu ergänzen.

Eine weitere Lösung, die aber niemals implementiert wurde, ist eine partielle Serialisierung der Schleife, was bedeutet, dass nur eine bestimmte Anzahl von Prozessen zugelassen wird. Das ist nur für Multiprozessor-Rechner interessant, wo mehrere Kindprozesse gleichzeitig ausgeführt werden können und die Serialisierung die volle Bandbreite eigentlich nicht ausnutzt. Dies ist ein Feld für zukünftige Bemühungen, wobei die Priorität niedrig ist, da parallele Webserver nicht die Norm sind.

Im Idealfall sollten die Server ohne mehrfache Listen-Anweisungen auskommen, wenn gute Leistungen im Vordergrund stehen. Damit ist das Thema aber noch nicht abgeschlossen.

accept-Serialisierung - Einzelnes Socket

Die oben beschriebenen Verfahren eignen sich hervorragend für Server mit mehreren Sockets, aber wie sieht es bei Servern mit einem einzigen Socket aus? Rein theoretisch sollte keines der genannten Probleme auftreten, da alle Kindprozesse nur bis zum Ankommen der nächsten Verbindung in accept(2) blockiert werden können. In der Praxis verbirgt sich aber dahinter fast das gleiche Verhalten, wie es für die nicht blockierende Lösung beschrieben wurde. Bei der Art und Weise, wie die meisten TCP-Stacks implementiert sind, aktiviert der Serverkern aber alle in accept blockierten Prozesse, wenn eine einzige Verbindung ankommt. Einer dieser Prozesse übernimmt die Verbindung und kehrt in den Benutzerbereich zurück, während der Rest im Serverkern zirkuliert und deaktiviert wird, wenn keine Verbindung anliegt. Dieses Zirkulieren ist im Anwendercode nicht zu erkennen, findet aber trotzdem statt und kann zum gleichen leistungsmindernden Verhalten führen, wie die nicht blockierende Lösung bei mehreren Sockets.

Es hat sich herausgestellt, dass viele Systeme sich besser verhalten, wenn auch im Fall eines einzelnen Socket eine Serialisierung vorgenommen wird. Dies entspricht in der Tat fast in allen Fällen dem Standard. Versuche unter Linux (Version 2.0.30 auf einem Rechner mit Dual Pentium pro 166 128Mb RAM) haben gezeigt, dass die Serialisierung im Fall des einzelnen Sockets zu einer Abnahme der Anfragen pro Sekunde 3% gegenüber der nicht serialisierten Variante geführt haben. Beim nicht serialisierten einzelnen Socket kamen aber pro Anfrage 100 ms Wartezeit hinzu. Möglicherweise entstehen diese Wartezeiten durch lange Transportwege und sind nur in LANs ein Problem. Die Serialisierung für einzelne Sockets kann mit der Definition SINGLE_LISTEN_UNSERIALIZED_ACCEPT überschrieben werden.

Schleichender Verbindungsabbau

Wie in dem Beitrag draft-ietf-http-connection-00.txt Abschnitt 8 erörtert wird, muss ein HTTP-Server für eine zuverlässige Implementierung des Protokolls jede Richtung der Kommunikation unabhängig voneinander beenden (denn eine TCP-Verbindung ist bidirektional, wobei die beiden Teile voneinander unabhängig sind). Diese Tatsache wird von anderen Servern häufig übersehen, vom Apache wird sie aber seit der Version 1.2 berücksichtigt.

Als diese Eigenschaft dem Apache hinzugefügt wurde, kam es infolge einer gewissen Kurzsichtigkeit bei verschiedenen Unix-Varianten zu einer Reihe von Problemen. Die TCP-Spezifikation legt nicht fest, dass der Status FIN_WAIT_2 ein Zeitlimit hat, sie verbietet es aber auch nicht. Bei Systemen ohne Zeitlimit verursachte der Apache 1.2 ein ewiges Hängen vieler Sockets im FIN_WAIT_2-Status. In vielen Fällen kann das durch ein Upgrade mit den aktuellen TCP/IP-Patches des Herstellers behoben werden. Für Fälle, in denen der Hersteller keine Patches entwickelt hat (wie für SunOS4, auch wenn Eigentümer einer Source-Lizenz sich selbst helfen können) soll diese Eigenschaft deaktiviert werden.

Dies kann zum einen mit der Option SO_LINGER geschehen. Leider wurde sie für die meisten TCP/IP-Stacks niemals korrekt implementiert. Aber selbst bei Stacks mit einer korrekten Implementierung (z. B. Linux 2.0.31) erweist sich dieses Verfahren als aufwändiger (CPU-Zeit) als die folgende Lösung.

In den meisten Fällen findet sich diese Apache-Implementierung in der Funktion lingering_close (in der Datei http_main.c). Sie sieht ungefähr so aus:

void lingering_close (int s)
{
char junk_buffer[2048];

/* shutdown the sending side */
shutdown (s, 1);

signal (SIGALRM, lingering_death);
alarm (30);

for (;;) {
select (s for reading, 2 second timeout);
if (error) break;
if (s is ready for reading) {
if (read (s, junk_buffer, sizeof (junk_buffer)) <= 0) {
break;
}
/* just toss away whatever is here */
}
}

close (s);
}

Dadurch entsteht natürlicherweise ein gewisser Aufwand beim Beenden einer Verbindung, der jedoch für eine zuverlässige Implementierung unverzichtbar ist. Da HTTP/1.1 immer mehr vorherrscht und alle Pipeline-Verbindungen dauerhaft sind, amortisiert sich dieser Aufwand nach mehreren Anfragen. Wer das Risiko liebt, kann NO_LINGCLOSE definieren, um diese Eigenschaft zu deaktivieren, davon ist aber grundsätzlich abzuraten. Je mehr die HTTP/1.1-Verbindungen zum Standard werden, um so unverzichtbarer wird lingering_close (und Pipeline-Verbindungen sind schneller uns sollten unterstützt werden).

Scoreboard-Datei

Die Apache-Prozesse und Kindprozesse kommunizieren über das sogenannte Scoreboard miteinander. Es sollte idealerweise im gemeinsam genutzten Speicher implementiert werden. Bei den Betriebssystemen, zu denen Zugang besteht, oder die zugewiesene Ports haben, wird es normalerweise im gemeinsam genutzten Speicher implementiert. Bei den übrigen wird eine Datei auf der Festplatte verwendet. Eine solche Datei ist nicht nur langsam, sie ist auch unzuverlässiger. Überprüfen Sie die Datei src/main/conf.h ihres Systems und suchen Sie nach den Definitionen USE_MMAP_SCOREBOARD oder USE_SHMGET_SCOREBOARD. Mit ihnen (und den Definitionen HAVE_MMAP oder HAVE_SHMGET) wird der vorhandene Code für den gemeinsam genutzten Speicher aktiviert. Besitzt das System einen anderen Typ gemeinsam genutzten Speichers, muss die Datei src/main/http_main.c geöffnet und die erforderlichen Hooks für Apache hinzugefügt werden.

Zur Entwicklung: Der Linux-Port des Apache verwendet erst seit der Version 1.2 den gemeinsam genutzten Speicher. Diese Nachlässigkeit führt zu einen mangelhaften und unzuverlässigem Verhalten früherer Versionen von Apache für Linux.
DYNAMIC_MODULE_LIMIT

Sollen keine dynamisch geladenen Module verwendet werden (was der Fall sein kann, wenn entsprechend dieser Ausführungen das Optimum an Leistung erreicht werden soll), dann sollte beim Aufbau des Servers die Option -DDYNAMIC_MODULE_LIMIT=0 angegeben werden. Das spart RAM-Speicher, der sonst für dynamisch geladene Module benötigt wird.

Anhang: Ausführliche Analyse eines Trace

Der hier behandelte Trace der Systemaufrufe unter Apache 2.0.38 mit dem worker-MPM unter Solaris 8 wurde wie folgt erstellt:

truss -l -p httpd_child_pid.

Die Option -l weist das Programm truss an, die ID des LWP (Lightweight Process - die Solaris-Variante eines Thread auf Serverkernebene) zu protokollieren, der den Systemaufruf durchführt.

Für andere Betriebssysteme gibt es entsprechende Programme wie zum Beispiel strace, ktrace oder par. Sie produzieren alle ähnliche Ausgaben.

Bei diesem Trace hat ein Client eine 10 kByte große statische Datei vom Hauptprogramm httpd angefordert. Traces nicht statischer Anfragen oder Anfragen mit Content-Negotiation sehen ganz anders (und manchmal auch sehr unschön) aus.

/67:    accept(3, 0x00200BEC, 0x00200C0C, 1) (sleeping...)
/67:    accept(3, 0x00200BEC, 0x00200C0C, 1)            = 9

In diesem Trace wird der Listener-Thread in LWP #67 ausgeführt.

Beachten Sie die fehlende accept(2)-Serialisierung. Bei diesem speziellen Betriebssystem verwendet der worker-MPM standardmäßig die nicht serialisierte Variante, wenn nicht mehrere Ports überwacht werden.
/65:    lwp_park(0x00000000, 0)                         = 0
/67:    lwp_unpark(65, 1)                               = 0

Beim Akzeptieren einer Verbindung, aktiviert der listener-Thread einen worker-Thread für die Verarbeitung der Anfrage. In diesem Trace ist der worker-Thread für die Anfrage LWP #65 zugeordnet.

/65:    getsockname(9, 0x00200BA4, 0x00200BC4, 1)       = 0

Um die virtuellen Hosts implementieren zu können, muss der Apache die lokale Socket-Adresse für das Akzeptieren der Verbindung kennen. In vielen Situationen kann dieser Aufruf unterbleiben (wenn beispielsweise keine virtuellen Hosts vorhanden sind oder wenn Listen-Direktiven benutzt werden, die keine Adressen mit Jokerzeichen haben). Bisher wurden jedoch keine Anstrengungen für diese Optimierungen unternommen.

/65:    brk(0x002170E8)                                 = 0
/65:    brk(0x002190E8)                                 = 0

Die brk(2)-Aufrufe allokieren Speicher im Heap. Das ist in einem Systemaufruf-Trace selten zu erkennen, weil das httpd-Programm für die Verarbeitung der meisten Anfragen eigene Speicherallokatoren verwendet (apr_pool und apr_bucket_alloc). In diesem Trace wurde das Programm httpd gerade gestartet, deshalb muss es malloc(3) aufrufen, um die Speicherblöcke für die eigenen Speicherallokatoren zu erhalten.

/65:    fcntl(9, F_GETFL, 0x00000000)                   = 2
/65:    fstat64(9, 0xFAF7B818)                          = 0
/65:    getsockopt(9, 65535, 8192, 0xFAF7B918, 0xFAF7B910, 2190656) = 0
/65:    fstat64(9, 0xFAF7B818)                          = 0
/65:    getsockopt(9, 65535, 8192, 0xFAF7B918, 0xFAF7B914, 2190656) = 0
/65:    setsockopt(9, 65535, 8192, 0xFAF7B918, 4, 2190656) = 0
/65:    fcntl(9, F_SETFL, 0x00000082)                   = 0

Als nächstes setzt der worker-Thread die Verbindung zum Client (Dateideskriptor 9) in den nicht blockierenden Modus. Die setsockopt(2)- und getsockopt(2)-Aufrufe sind ein Nebeneffekt davon, wie libc von Solaris fcntl(2) für Sockets behandelt.

/65:    read(9, " G E T   / 1 0 k . h t m".., 8000)     = 97

Der worker-Thread liest Anfrage des Clients.

/65:    stat("/var/httpd/apache/httpd-8999/htdocs/10k.html", 0xFAF7B978) = 0
/65:    open("/var/httpd/apache/httpd-8999/htdocs/10k.html", O_RDONLY) = 10

Dieses httpd-Programm wurde mit Options FollowSymLinks und AllowOverride None konfiguriert. Daher wird lstat(2) nicht für jedes Verzeichnis im Pfad zur angeforderten Datei benötigt. Auch die .htaccess-Dateien müssen nicht überprüft werden. Es wird lediglich stat(2) aufgerufen, um zu überprüfen, dass: 1. die Datei existiert und 2. eine reguläre Datei und kein Verzeichnis ist.

/65:    sendfilev(0, 9, 0x00200F90, 2, 0xFAF7B53C)      = 10269

In diesem Beispiel ist das httpd-Programm in der Lage, den HTTP-Antwort-Header und die angeforderte Datei mit einem einzigen sendfilev(2)-Systemaufruf zu versenden. die Sendfile-Semantik variiert bei den unterschiedlichen Betriebssystemen. Bei einigen muss vor dem Aufruf von sendfile(2) ein write(2)- oder ein writev(2)-Aufruf erfolgen, um die Header zu senden.

/65:    write(4, " 1 2 7 . 0 . 0 . 1   -  ".., 78)      = 78

Diesen write(2)-Aufruf zeichnet die Anfrage im Zugriffsprotokoll auf. Auffällig ist, dass in diesem Trace ein time(2)-Aufruf fehlt. Anders als Apache 1.3 verwendet Apache 2.0 gettimeofday(3), um die Zeit zu ermitteln. Einige Betriebssystem wie Linux oder Solaris besitzen eine optimierte Implementierung von gettimeofday, die nicht soviel zusätzlichen Aufwand wie ein normaler Systemaufruf erfordert.

/65:    shutdown(9, 1, 1)                               = 0
/65:    poll(0xFAF7B980, 1, 2000)                       = 1
/65:    read(9, 0xFAF7BC20, 512)                        = 0
/65:    close(9)                                        = 0

Der worker-Thread schließt die Verbindung.

/65:    close(10)                                       = 0
/65:    lwp_park(0x00000000, 0)         (sleeping...)

Am Ende schließt der worker-Thread die Datei, die er gerade ausgeliefert hat und blockiert, bis der Listener eine andere Verbindung zuweist.

/67:    accept(3, 0x001FEB74, 0x001FEB94, 1) (sleeping...)

In der Zwischenzeit ist der Listener-Thread in der Lage, eine andere Verbindung anzunehmen, wenn er diese Verbindung einem worker-Thread zugeteilt hat (was Gegenstand der Flusskontrolllogik im worker-MPM ist, der den Listener bremst, wenn die verfügbaren worker-Threads beschäftigt sind). Auch wenn das im Trace nicht ersichtlich ist, kann der nächste accept(2)-Aufruf parallel mit der Behandlung der gerade vom worker-Thread akzeptierten Verbindung durchgeführt werden (was normalerweise bei starker Belastung auch geschieht).



---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to