Title: Apache-Tuning
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.
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 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.
Vor der Apache-Version 1.3, war die
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
oder 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:
<Files ~ "\.(html|cgi)$">
Werden DNS-Namen nur in einigen CGI-Skripten benutzt,
sollte der gethostbyname-Aufruf in dem entsprechenden
CGI-Skript benutzt werden.
Ist irgendwo im URL-Raum die Anweisung Options
FollowSymLinks oder Options SymLinksIfOwnerMatch
vorhanden, muss der Apache zusätzliche Systemaufrufe durchführen, um die
symbolischen Links zu überprüfen ( jeweils einen Aufruf für jede Komponente
des Dateinamens). Ein Beispiel:
<Directory />
Erfolgt eine Anfrage nach dem URI /index.html, ruft
der Apache lstat(2) für /www,
/www/htdocs und /www/htdocs/index.html
auf. Die Ergebnisse dieser Aufrufe werden nicht zwischengespeichert und
werden daher für jede Anfrage durchgeführt. Soll wirklich eine Überprüfung
der symbolischen Links stattfinden, kann dies auch wie folgt geschehen:
<Directory />
<Directory /www/htdocs>
Das verhindert zumindest zusätzliche Überprüfungen für den Pfad
DocumentRoot, müssen die entsprechenden
Abschnitte hinzugefügt werden. Für höchste Leistungen ohne Überprüfung
der symbolischen Links sollte immer FollowSymLinks
und niemals SymLinksIfOwnerMatch gesetzt werden.
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
<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.
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
zu benutzen, kann eine vollständige Optionsliste verwendet werden:
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.
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
mmapnicht so gut wieread(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, wennmmapdeaktiviert 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.)
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 fehlerhaftersendfile-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.)
Vor der Apache-Version 1.3 hatten die
5 für
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
Das scheint so durchdacht zu sein, dass es meist unnötig ist,
die Einstellungen für
In Verbindung mit der Prozesserzeugung steht die Prozessbeendigung,
die durch die
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
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.
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:
- 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 alsprefork . - 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 MPMworker 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.
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
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
Einige Module wie z.B. 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:
./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-atomicserzeugt 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-atomicserzeugt 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).
Wird beim Kompilieren und Ausführen des Apache-Servers
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).
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
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):
FD_ZERO (&accept_fds);
for (i = first_socket; i <= last_socket; ++i) {
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 (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 (;;) {
FD_ZERO (&accept_fds);
for (i = first_socket; i <= last_socket; ++i) {
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 (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
Mit der Direktive
-
AcceptMutex flock -
Diese Methode verwendet den
flock(2)-Systemaufruf, um eine Sperrdatei zu blockieren (ihr Name wird mit derLockFile -Direktive definiert). -
AcceptMutex fcntl -
Diese Methode verwendet den
fcntl(2)-Systemaufruf, um eine Sperrdatei zu blockieren (ihr Name wird mit derLockFile -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 gleichenuidwie der Webserver ausgeführt werden (d.h.. alle CGI-Skripte, wenn nicht etwas ähnliches wiesuexecodercgiwrapperbenutzt 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
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.
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:
{
/* shutdown the sending side */
shutdown (s, 1);
signal (SIGALRM, lingering_death);
alarm (30);
for (;;) {
if (error) break;
if (s is ready for reading) {
/* 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).
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.
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.
Der hier behandelte Trace der Systemaufrufe unter Apache 2.0.38 mit dem
worker-MPM unter Solaris 8 wurde wie folgt erstellt:
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.
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
/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]
