Title: Apache-Tuning




	Weitere Themen
	
	
		

Apache 2.0 ist ein Allzweck-Webserver, der entworfen wurde, um ein ausgewogenes Maß an Flexibilität, Portierbarkeit und Leistungsfähigkeit zu bieten. Obwohl Apache 2.0 nicht speziell daraufhin ausgelegt wurde, neue Benchmark-Höchstwerte zu erreichen, ist er dennoch in der Lage, in vielen praktischen Einsatz- fällen eine hohe Performance zu liefern.

Im Vergleich mit der Apache-Version 1.3 enthält er viele zusätzliche 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 es andere dem Administrator erlauben, _auf_Kosten_von Funktionalität zusätzliche Geschwindigkeit zu erzielen.
Aspekte der Hardware und des Betriebssystems

Der zentrale Hardware-Aspekt für die Webserver-Leistung ist der Arbeitsspeicher. Ein Webserver sollte niemals dazu gezwungen sein, Hauptspeicherinhalte auf langsamere Speichermedien auslagern zu müssen, weil dies die Wartezeit für jede Anfrage in einen Bereich verschiebt, den der Benutzer nicht mehr als 'hinreichend schnell' empfinden wird. Der Anwender klickt dann auf 'Stop' und (danach auf) 'Neu laden', wodurch er die Serverbelastung noch erhöht. Sie können - und sollten - die Einstellung der Direktive MaxClients so wählen, daß Ihr Server nicht so viele Kindprozesse startet, daß er dadurch beginnt, zu swappen. Das kann auf einfache Weise geschehen: Ermitteln Sie über die Prozessliste oder mit Hilfe eines Programms wie top die Größe des durchschnittlichen Apache-Prozesses und teilen Sie die Größe des verfügbaren RAM durch diesen Wert (wobei Sie etwas Platz für andere Prozesse übrig lassen sollten).

Der Rest ist banal: Sorgen Sie für eine ausreichend schnelle CPU, eine ausreichend schnelle Netzwerkkarte und hinreichend schnelle 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ühe 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. Falls Sie in Ihren Protokolldateien eine Auflösung von IP-Adressen in Hostnamen benötigen, dann verwenden Sie dafür das Programm logresolve der Apache-Distribution, eines der zahlreichen verfügbaren Programme zur Auswertung von Protokolldateien.

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>

Aber abgesehen von dieser Möglichkeit: Falls Sie DNS-Namen nur in einigen wenigen CGI-Anwendungen benötigen, sollten Sie in Erwägung ziehen, den gethostbyname-Aufruf (nur) in denjenigen CGIs durchzuführen, die ihn benötigen.

AllowOverride

Wird im URL-Raum das Überschreiben erlaubt (normalerweise in den .htaccess-Dateien), versucht der Apache für jede Komponente des Dateinamens, eine entsprechende (d. h. in dem Verzeichnis des bis dahin gebildeten Pfadnamens liegende) .htaccess-Datei 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 Lösung des Problems ist ä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 überwiegen in der Praxis die Vorteile der Content Negotiation gegenüber den Leistungseinbußen. Aber es gibt eine Situation, in denen der Server beschleunigt werden kann, ohne dabei auf Funktionalität verzichten zu müssen. Anstatt Wildcards wie

DirectoryIndex index

zu benutzen, sollten Sie eine vollständige Optionsliste verwenden:

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

wobei Sie den häufigsten Fall an erster Stelle angeben sollten.

Beachten Sie auch, daß das explizite Anlegen einer type-map-Datei bessere Leistungen ermöglicht, 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.

Falls Ihre Site (unbedingt) Content Negotiation benötigt, 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 betrachten muss (beispielsweise bei der Server-Side-Include-Verarbeitung) nimmt er normalerweise eine Speicherabbildung des Inhalts der 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 Betriebssystemen ist mmap bei wachsender Anzahl von CPUs nicht so gut skalierbar wie read(2). 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 Ihr Prozeß einen Busfehler erzeugen, wenn er das nächste Mal versucht, 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 der Auslieferung des Inhalts statischer Dateien), 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 wurde, insbesondere, wenn die ausführbaren Programmdateien auf einem anderen Rechner erstellt und auf einen Rechner mit fehlerhafter sendfile-Unterstützung übernommen wurden.

  • Bei eingebundenen NFS-Dateien ist der Kernel 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 Einstellungen der Direktiven MinSpareServers, MaxSpareServers und StartServers drastische Auswirkungen auf die Ergebnisse von Vergleichstests. Insbesondere benötigte der Apache eine "Anfahrzeit", um eine ausreichende Anzahl von Kindprozessen für die Bewätigung der anfallenden Last zu erreichen. Nach der Initialisierung der mit StartServers angegebenen Anzahl von Prozessen, 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 trat der Sekundentakt etwas in den Hintergrund. 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 Antwortverhalten dieses Verfahrens scheint so gut 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 Ausgaben 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 werden Sie ihn möglicherweise deutlich erhöhen wollen. Unter SunOS oder einer alten 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 und 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). Ber der Übersetzung 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 allgemeinen 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 gleichzeitig 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 gleichzeitig 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 schlechter Unterstützung 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 Aspekt für die Leistungsfähigkeit ist, sollten Sie bestrebt sein, aktuell nicht benutzte Module zu eliminieren. Wenn die Module als DSOs eingerichtet wurden, muss lediglich die entsprechende LoadModule-Direktive für dieses Modul auskommentiert werden. Dies erlaubt Ihnen, mit dem Entfernen von Modulen zu experimentieren und zu kontrollieren, ob Ihre Seite ohne diese Module noch funktioniert.

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

In diesem Zusammenhang stellt sich natürlich die Frage, welche Module überhaupt benötigt werden und welche nicht. 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, da eine Website auch ohne Fehlerprotokollierung funktionsfähig ist. Dies wird allerdings nicht empfohlen.

Unteilbare Operationen

Einige Module wie z.B. mod_cache und neuere Entwicklungs-Builds des MPM worker benutzen das unteilbare API der Apache Portable Runtime (APR). Dieses API stellt unteilbare Operationen 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 unteilbaren API, um die Kompatibilität zu älteren CPU-Modellen zu gewährleisten, denen eine solche Anweisungen fehlen. Wird der Apache für eines dieser Betriebssysteme bei gleichzeitigem Einsatz neuerer CPUs eingerichtet, können Sie zur Übersetzungszeit eine schnellere Implementierung für die unteilbaren Operationen auswählen. Dies geschieht beim Kompilieren mit der Option --enable-nonportable-atomics:

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

Die Option --enable-nonportable-atomics ist für folgende Betriebssysteme relevant:

  • Solaris auf SPARC
    Standardmäßig verwendet die APR unter Solaris/SPARC Mutex-basierte unteilbare Operationen. 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 unteilbaren Operationen 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 unteilbare Operationen. 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 unteilbare Operationen, 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 mehrere 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 bezüglich der in Version 2.0 des Apache-HTTP-Servers durchgeführten Änderungen noch nicht vollständig aktualisiert. Ein Teil der (vorliegenden) Information mag weiterhin relevant sein, aber gehen Sie vorsichtig damit um.

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 alle nicht beschäftigten Prozesse gleichzeitig auf neue Verbindungen abfragen. Eine naive Implementierung sieht etwa folgendermaßen aus (diese Beispiele entsprechen nicht dem Code, sie dienen lediglich der Veranschaulichung):

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 besteht die ernsthafte Gefahr, daß sich der Server in nutzlosen Berechnungen erschöpft. Mehrere Kindprozesse führen diese Schleife gleichzeitig aus, so dass auch mehrere Prozesse von select blockiert werden, wenn sie gerade keine Anfrage bearbeiten. 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 dre Zeitplanung ab). Jeder betritt die Schleife und versucht, die Verbindung zu akzeptieren (accept). Aber nur ein Prozess hat Erfolg (vorausgesetzt, nur eine Verbindung ist bereit), der Rest wird von accept blockiert. Dieses blockiert diese Kindprozesse tatsächlich derartig, daß sie nur noch Anforderungen von diesem einen Socket und keinem (der vielen) anderen bedienen können. Sie sitzen dort fest, bis genügend neue Anfragen für dieses Socket erscheinen, um sie alle zu reaktivieren. Dieses 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. Angenommen, Sie haben zehn unbeschäftigte Kindprozessen für select und nur eine eingehende Verbindung. Dann werden neun dieser Prozesse aktiviert und sie versuchen alle die Verbindung zu akzeptieren (accept), schlagen fehl und 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 wechselseitig ausschließende Semaphore. Nur ein Kindprozesse kann jeweils den Mutex zu einem gegebenen Zeitpunkt besitzen. Für die Implementierung der Mutexe stehen mehrere Möglichkeiten zur Verfügung. 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 zurückgesetzt wird (siehe auch ipcs(8)-Man-Page). Zum anderen ermöglicht das Semaphoren-API Denial-of-Service-Attacken durch CGI-Skripte zu, die unter der gleichen User-ID 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 auf 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 Hängen des Webservers führt.

Verwendet ein System eine hier nicht aufgeführte Methode für die Serialisierung, dann kann es sinnvoll sein, die APR um den ent- sprechenden Code zu erweitern.

Eine weitere Lösung, die aber niemals implementiert wurde, ist eine partielle Serialisierung der Schleife, was bedeutet, dass nur eine bestimmte Anzahl von Prozessen hinein gelassen 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 mögliches Feld für zukünftige Bemühungen, wobei die Priorität niedrig ist, da hochgradig parallele Webserver nicht die Norm sind.

Idealerweise sollten Sie auf die Verwendung mehrfacher Listen-Anweisungen verzichten, 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 Kernel nämlich 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 Übrigen im Kernel zirkulieren und sich selbst wieder 'schlafen legen', wenn sie bemerken, daß für sie keine zu bearbeitende Verbindung vorliegt. 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 gutmütiger verhalten, wenn auch im Fall eines einzelnen Socket eine Serialisierung vorgenommen wird. Dies entspricht in der Tat fast in allen Fällen dem Standardverhalten des Apache. 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 von weniger 3% gegenüber der nicht serialisierten Variante geführt haben. Beim nicht serialisierten einzelnen Socket kamen aber pro Anfrage 100 ms Wartezeit hinzu. Bei DFÜ-Verbindungen ist diese Verzögerung höchstwahrscheinlich vernachlässigbar, also lediglich in schnellen LANs tatsächlich ein Problem. Wenn Sie die Serialisierung für den Betrieb mit nur einem Socket außer Kraft setzenwollen, können Sie die symbolische Konstante SINGLE_LISTEN_UNSERIALIZED_ACCEPT definieren; in diesem Fall werden Server mit nur einem Socket nicht serialisieren

Schleichender Verbindungsabbau

Wie in Abschnitt 8 des Beitrags draft-ietf-http-connection-00.txt 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 korrekt implementiert.

Als diese Fähigkeit in Apache eingebaut wurde, sorgte sie aufgrund einer Kurzsichtigkeit (bei der gewählten Implementierung) für eine Reihe von Problemen auf verschiedenen Unix-Versionen. Die TCP-Spezifikation verlangt nicht, daß der Zustand FIN_WAIT_2 nur zeitlich begrenzt existieren darf, sie schließt dies jedoch auch nicht aus. Bei Systemen ohne timeout-Mechanismus 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. Bei Fällen, in denen der Hersteller nie entsprechende Patches freigegeben hat (beispielsweise SunOS4 - wobei Leute mit einer Source-Lizenz die entsprechenden Patches selbst durchführen können) haben wir uns entschieden, dieses Feature zu deaktivieren.

Es gibt zwei Möglichkeiten, dies zu bewirken. Eine davon ist die (Verwendung der) Socket-Option SO_LINGER. Leider wurde sie für die meisten TCP/IP-Stacks niemals korrekt implementiert. Selbst bei Stacks mit einer korrekten Implementierung (z. B. Linux 2.0.31) erweist sich dieses Verfahren als aufwändiger (CPU-Zeit) als die nachfolgend beschriebene Lösung.

Zum größten Teil 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. Mit der zunehmenden Verbreitung von HTTP/1.1 und der (damit verbundenen) Persistenz aller Verbindungen, amortisiert sich dieser Aufwand mit zunehmender Menge von Anfragen. Wenn Sie mit dem Feuer spielen und dieses Feature deaktieren wollen, können Sie die Konstante NO_LINGCLOSE definieren, aber davon wird grundsätzlich abgeraten. Je mehr die HTTP/1.1-Verbindungen zum Standard werden, um so unverzichtbarer wird lingering_close (und Pipeline-Verbindungen sind schneller und sollten unterstützt werden).

Scoreboard-Datei

Die Apache-Prozesse und Kindprozesse kommunizieren über das sogenannte Scoreboard miteinander. Dieses sollte idealerweise im Shared Memory implementiert werden. Für diejenigen Betriebssysteme, zu denen wir selbst entweder Zugang haben oder für die uns eine detaillierte Portierung vorliegt, wird es normalerweise im Shared Memory implementiert. Bei den übrigen wird standardmäßi 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. Die Definition einer dieser beiden Konstanten (sowie des entsprechenden ihrer beiden Gegenstücke HAVE_MMAP bzw. HAVE_SHMGET) bewirkt die Aktivierung des mitgelieferten Shared-Memory-Codes.Besitzt Ihr System einen andern Typ von Shared Memory, dann müssen Sie die Datei http_main.c bearbeiten und die entsprechenden Hooks hinzufügen, um ihn in Apache verwenden zu können.

Historische Anmerkung: Die Portierung von Apache nach Linux verwendete erst seit der Version 1.2 Shared Memory. Diese Nachlässigkeit führte zu einen mangelhaften und unzuverlässigem Verhalten früherer Versionen von Apache für Linux.
DYNAMIC_MODULE_LIMIT

Sollten Sie nicht vorhaben, dynamisch nachladbare Module zu verwenden (was vermutlich der Fall sein wird, wenn Sie diese Zeilen lesen und versuchen, das letzte bißchen Performance aus ihrem Server herauszukitzeln), dann sollten Sie bei der Übersetzung des Servers -DDYNAMIC_MODULE_LIMIT=0 angeben. Dies spart RAM, der sonst nur für die Unterstützung dynamisch geladener Module reserviert werden müßte.

Anhang: Ausführliche Analyse eines Trace

Nachfolgend sehen Sie einen Trace der Systemaufrufe von Apache 2.0.38 unter Verwendung des Worker-MPM unter Solaris 8. Dieser Trace wurde durch das Kommando truss -l -p httpd_child_pid erzeugt.

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 Kernelebene) 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, über welche diese Verbindung akzeptiert wurde. In vielen Situationen kann dieser Aufruf unterbleiben (wenn beispielsweise keine virtuellen Hosts vorhanden sind oder wenn Listen-Direktiven benutzt werden, die keine Adressen mit Wildcards haben). Bisher wurde jedoch kein Versuch unternommen, Optimierungen in dieser Hinsicht durchzuführen.

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

Die brk(2)-Aufrufe allokieren Speicher im Heap. Solche Aufrufe sind in einem Systemaufruf selten zu beobachten, 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 zu erhalten, aus denen es die eigenen Speicherallokatoren erzeugt.

/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 handhabt.

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

Der worker-Thread liest die 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 ist es für jedes Verzeichnis im Pfad zur angeforderten Datei weder erforderlich, einen lstat(2)-Aufruf durchzuführen (um auf symbolische Links zu testen), noch, es nach (= auf die Existenz) einer .htaccess-Datei zu prüfen. 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. Beachten Sie, daß in diesem Trace unter anderem kein time(2)-Aufruf auftaucht. 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 mit lingering_close. .

/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 ihm 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 (aufgrund der Kontrollflußlogik des worker-MPM , der den Listener bremst, wenn die verfügbaren worker-Threads alle beschäftigt sind). Auch wenn das in diesem 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