[APACHE DOCUMENTATION]

Apache HTTP Server Version 1.3

Discussion "en profondeur" sur l'adressage d'hôtes virtuels

Le code des hôtes virtuels a été complètement réécrit dans la version 1.3 d'Apache. Ce document tente d'expliquer exactement ce qu'Apache fait lorsque il décide de servir une requête de tel ou tel hôte virtuel qu'il héberge. Grâce à la nouvelle directive NameVirtualHost, la configuration des hôtes virtuels devrait être beaucoup plus facile et plus sûre que les versions antérieures à la version 1.3 d'Apache.

Si vous désirez simplement exploiter cette fonctionnalité sans entrer dans le détail de son fonctionnement, voici quelques exemples qui vous seront utiles.

Analyse du fichier de configuration

Un serveur principal est défini par toutes les définitions apparaissant en dehors des sections <VirtualHost>. Les hôtes virtuels, lorsqu'ils existent, sont définis par les sections <VirtualHost>

Les directives Port, ServerName, ServerPath, et ServerAlias peuvent apparaître à n'importe quel endroit à l'intérieur de la définition d'un serveur virtuel. Quoiqu'il en soit, chaque apparition de ces directives surcharge les apparitions précédentes de ces mêmes directives (à l'intérieur d'une même définition de serveur).

La valeur par défaut du champ Port est 80 pour le serveur principal. En revanche, les ServerPath, ou ServerAlias n'ont pas de valeurs par défaut pour le serveur principal. Le nom du serveur par défaut ( ServerName ) est déduit de l'adresse de IP des serveurs.

La directive Port du serveur principal a deux fonctions, découlant de la compatibilité avec les fichiers de configuration NCSA. La première fonction consiste en la détermination du port réseau qu'Apache reconnaît par défaut. Cette valeur par défaut est surchargée par toute apparition d'une directive Listen. Le second usage est la spécification du numéro du port utilisé, pour la redirection d'URI absolues.

A la différence du serveur principal, les ports d'un hôte virtuel n'affectent pas les ports qu'écoute Apache.

Chaque adresse apparaissant dans la directive Virtual Host peut optionnellement spécifier un port. Si le port n'est pas explicitement spécifié, la valeur par défaut choisie est la valeur la plus récente de la directive Port du serveur principal. Le port spécial noté * indique par l'usage de ce metacaractère une écou1.3 d'Apache, la directive HostnameLookups avait la valeur On par défaut. Ce réglage ajoutait un temps de traitement à chaque requête, car une résolution DNS devait être effectuée avant de terminer le traitement de la requête. Dans la version 1.3 d'Apache, la nouvelle valeur par défaut est Off. Cependant, (1.3 ou postérieure), si vous utilisez une quelconque directive allow from domain ou deny from domain, alors vous le "paierez" d'une double résolution DNS inverse (une inverse, suivie d'une résolution normale pour être sûr que la résolution inverse n'a pas été piratée). De ce fait, pour obtenir les meilleures performances, vous éviterez d'utiliser ces directives (il est beaucou plus efficace d'utiliser directement les adresses IP plutôt que des noms de domaines).

Notez qu'il est possible de limiter l'application de ces directives à un contexte précis, comme dans une clause <Location /server-status>. Dans ce cas, la double résolution DNS inverse ne seront effectuées que si la requête entre dans le champ d'action de cette section. Voici un exemple qui désactive la résolution excepté pour les fichiers d'extension .html et .cgi :

HostnameLookups off
<Files ~ "\.(html|cgi)$>
    HostnameLookups on
</Files>
Mais même dans ce cas, si vous ne nécessitez des noms DNS que dans certains CGI bien identifiés, vous pouvez envisager d'utiliser l'appel gethostbyname pour ces programmes.

FollowSymLinks et SymLinksIfOwnerMatch

Dans n'importe quel sous-espace de votre espace d'URL qui ne comporte pas d'Options FollowSymLinks, ou pour lequel est inscrit Options SymLinksIfOwnerMatch, Apache devra exécuter des appels système supplémentaires pour vérifier les liens symboliques. Le nombre de ces appels est de l'ordre d'un appel système par composante du nom de fichier. Par exemple, si vous aviez :

DocumentRoot /www/htdocs
<Directory />
    Options SymLinksIfOwnerMatch
</Directory>
et qu'une requête était faite sur l'URI /index.html. Alors Apache devra exécuter une commande lstat(2) pour /www, une pour /www/htdocs, et une pour /www/htdocs/index.html. Les résultats de ces lstats ne sont jamais enregistrées en cache, et ces commandes devront donc être répétées à chaque nouvelle requête. Si vous souhaîtez vraiement exploiter une sécurité sur les liens symboliques, vous pouvez utiliser une formulation comme suit :
DocumentRoot /www/htdocs
<Directory />
    Options FollowSymLinks
</Directory>
<Directory /www/htdocs>
    Options -FollowSymLinks +SymLinksIfOwnerMatch
</Directory>
Ceci élimine au moins les vérifications inutiles pour le chemiun DocumentRoot. Notez que vous devrez ajouter des sections similaires si vous avez spécifié une directive Alias ou écrit une règle RewriteRule qui pointe hors de l'espace délimité par DocumentRoot. Pour de meilleures performances, et aucune protection quant aux liens symboliques, définissez FollowSymLinks partout, et n'inscrivez aucune directive SymLinksIfOwnerMatch.

AllowOverride

Dès que vous autorisez, dans votre espace des URLs, la surcharge de droits (par des directives de niveau .htaccess), Apache essayera d'ouvrir les fichiers .htaccess pour chacune des composantes du nom de fichier. Par exemple, si est inscrit :

DocumentRoot /www/htdocs
<Directory />
    AllowOverride all
</Directory>
et qu'une requête est faite sur l'URI /index.html, alors Apache essayera d'ouvrir les fichiers /.htaccess, /www/.htaccess, et /www/htdocs/.htaccess. Les solutions sont similaires à celles du cas Options FollowSymLinks. Pour de meilleures performances, utilisez AllowOverride None à tous les niveaux de votre système de fichiers.

Négociation

Pour autant que possible, évitez de recourir à la négociation de contenu si vous souhaîtez vraiement travailler dans des performances optimales. En pratique, les avantages de la négociation de contenu sont acquis au prix de restrictions certaines des performance. Il reste un cas cependant dans lequel vous pouvez accélérer le serveur. Au lieu d'employer un métacaraactère comme dans l'écriture :

DirectoryIndex index
Utilisez une liste exhaustivée des options :
DirectoryIndex index.cgi index.pl index.shtml index.html
dans laquelle vous mentionnerez les choix les plus communs en tête.

Création des processus

Avant la version 1.3 d'Apache, les paramètres MinSpareServers, MaxSpareServers, et StartServers avaient une influence énorme sur les résultats d'essais. En particulier, Apache affichait une montée en charge en "rampe ascendante", période lui étant nécessaire pour créer le nombre de processus suffisants à supporter la charge requise. Après la période initiale de dédoublement créant les StartServers processus fils, un fils sera créé par seconde jusqu'à satisfaire la contrainte MinSpareServers. Ainsi, un serveur accédé par 100 clients simultanés, et prenant par défaut une valeur StartServers de 5 mettrait environ 95 secondes pour créer les processus nécessaires pour supporter les 100 utilisateurs. Ceci fonctionne correctement en pratique sur des serveurs réels, du fait que ceux-ci ne sont pas amenés à rédémarrer très fréquemment. Ceci aurait par contre des conséquences assez facheuses pour des serveurs qui seraient par exemple relancés toutes les dix minutes.

Cette règle de "un par seconde" a été implémentée dans le but d'éviter que la machine ne soit complètement "essoufflée" par la création de tous les processus fils au démarrage. Si la machine prend trop de temps à générer les processus, elle ne peut répondre aux requêtes. Mais cette technique à de telles conséquences visibles sur pendant la phase de démarrage d'Apache que nous avons dû là remplacer. Depuis la version 1.3 d'Apache, le code s'est éloigné de cette règle du "un par seconde". Apache créera un fils, puis attendra une seconde, puis lancera deux fils, puis réattendra une seconde, puis en relancera quatre, et ainsi de suite exponentiellement jusqu'à ce qu'à chaque tour 32 fils soient créés. Il s'arrêtera de générer des processus dès que la condition MinSpareServers est remplie.

Cette méthode apparaît être suffisament réactive et il n'est guère plus besoin d'ajuster les réglages MinSpareServers, MaxSpareServers et StartServers. Lorsque plus de 4 fils sont générés par seconde, un message sera émis via ErrorLog. Si vous voyez apparaître un grand nombre de ces erreurs, alors seulement sera t-il peut-être nécessaire de retoucher les valeurs de ces paramètres. Utilisez la sortie mod_status pour vous guider.

Directement en rapport avec la création des processus, on devra aussi s'intéresser à la mort de ces processus induite par le réglage MaxRequestsPerChild. Par défaut, la valeur de ce dernier est de 30, ce qui est certainement beaucoup trop bas sauf si votre serveur exploite des modules tels que mod_perl qui augmentent considérablement la taille de leur image mémoire. Si votre serveur ne sert principalement que des pages statiques, alors vous pouvez considérer d'augmenter cette valeur aux alentours de 10000. Le code est suffisamment robuste pour que cela ne puisse poser un problème.

Lorsque le support "keep-alive" est avtivé, les processus fils pourront rester occupés "à rien faire" en attendant que des requêtes complémentaires n'arrivent sur la connexion déjà établie. La valeur par défaut du paramètre KeepAliveTimeout, fixée à 15 secondes, tente de minimiser cet effet. La concession, ici, se fait entre la bande passante du réseau, et les ressources du serveur. Il est tout à fait déraisonnable d'augmenter cette valeur au delà de 60 secondes, car tout l'intérêt risque d'être perdu.

Influence de la configuration de la compilation

mod_status and Rule STATUS=yes

Si vous incluez mod_status et de plus inscrivez Rule STATUS=yes lorsque vous compilez Apache, alors pour chaque requête, Apache appellera deux fois la fonction gettimeofday(2) (ou times(2) suivant celle qui est disponible sur votre système d'exploitation), plus (avant 1.3) quelques fois supplémentaires la fonction time(2). Tout ceci pour que le rapport d'état puisse afficher des informations temporelles. Pour améliorer les performances, laissez Rule STATUS=no.

Accepter la sérialisation - sockets multiples

Cette discussion présuppose une légère intrusion dans l'API des sockets d'Unix. Supposons que votre serveur Web utilise de multiples directives Listen pour "écouter" plusieurs ports ou adresses simultanément. Pour tester, sur chacun des sockets, si une connexion est active, Apache utilise l'appel à select(2). select(2) indique que pour un socket donné, aucune ou au moins une connexion est en attente. Le modèle d'Apache prévoit de multiples processus fils en réserve, chacun de ces processus de réserve testant les nouvelles connexions simultanément. Une implémentation naive de ceci ressemblerait à ce qui suit (ces exemples ne correspondent pas au code réel, mais ne sont cités ici qu'à titre pédagogique) :

    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;
    }

Mais cette implémentation simpliste avait un sérieux problème. Souvenez-vous que tous les processus fils en attente exécutent cette boucle au même moment, et de ce fait, de nombreux processus vont se bloquer sur l'instruction select entre deux réception de requêtes. Tous ces fils bloqués se réveilleront par retour de la fonction select dès qu'une seule requête apparaît sur un quelconque socket (le nombre des fils qui se sont réveillés varie suivant le système d'exploitation et les contraintes temporelles locales). Ils repartent alors dans la boucle et essaient d'accepter la connexion. Seulement un pourra le faire (en supposant qu'il y ait toujours une connexion en attente), tous les autres se retrouvant bloqués dans l'appel accept. Ceci vérouille malheureusement tous ces fils sur l'attente hypothétique d'une requête sur ce socket exclusivement, et resteront bloqués tant que suffisamment de nouvelles requêtes n'ont pas été reçues sur ce socket précis. Ce problème a été exposé pour la première fois dans le document PR#467. On en connaît au moins deux solutions.

Une des solutions est de rendre les sockets non blocants. Dans ce cas, l'appel accept ne vérouillera pas les fils, lesquels pourront continuer à exécuter la boucle d'attente. Mais ceci consomme du temps CPU. Supposons que vous ayez dix fils en attente sur appel à select, et qu'une connexion arrive. Alors neuf d'entre eux se réveilleront, essaieront d' accepter la connexion, échoueront, et se bloqueront à nouveau dans un appel à select par effet de la boucle, en ayant remué beaucoup d'octets pour pas grand chose. Pendant ce temps, aucun de ces processus n'aura pu servir une requête arrivant sur un autre socket, du moins tant que l'appel à select n'a pas été relancé. Cette solujtion ne semble pas globalement optimale, à moins que vous n'ayez autant de CPU en attente (dans un environnement multiprocesseur) que de processus fils en veille, ce qui sera loin d'être probable.

Une autre solution, celle exploitée par Apache, est de sérialiser les entrées dans la boucle la plus intérieure. La boucle ressemble un peu à ceci (nous soulignons les différences majeures):

    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;
    }
Les fonctions accept_mutex_on et accept_mutex_off implémentent un sémaphore d'exclusion mutuelle. Un fils et un seul peut disposer du sémaphore à un moment donné. Nous avons plusieurs méthodes pour implémenter ces sémaphores d'exclusion mutuelle. Les choix sont décrits dans src/conf.h (avant 1.3) ou src/main/conf.h (1.3 ou postérieur). Certaines architectures ne permettent aucune implémentation de tels interblocages ; sur celles-ci, l'utilisation de multiples directives Listen peut se révéler hasardeuse.
USE_FLOCK_SERIALIZED_ACCEPT
Cette méthode utilise un appel système flock(2) pour verrouiller un fichier verrou (dont la position est donnée par la directive LockFile).
USE_FCNTL_SERIALIZED_ACCEPT
Cette méthode utilise un appel système fcntl(2) pour verrouiller un fichier verrou (dont la position est fixée par la directive LockFile).
USE_SYSVSEM_SERIALIZED_ACCEPT
(1.3 ou postérieure) Cette méthode exploite des sémaphores de type Système V pour réaliser la primitive d'exclusion mutuelle. Malheureusement, les sémaphores de type Système V produisent certains effets de bord assez facheux. L'un d'entre eux fait qu'il reste possible qu'Apache termine sans détruire le sémaphore (voir pour cela la page ipcs(8) du man UNIX). Un autre est que l'API du sémaphore permet d'ourdir une attaque par n'importe quel CGI qui s'exécuterait sous le même utilisateur que le serveur, produisant un refus de service (en somme, tous les CGI sauf si vous utilisez le support suEXEC ou un autre "lieur"). Pour les raisons qui précèdent, cette méthode ne sera pas utilisée excepté sur des architectures IRIX (sur lequel les deux méthodes précédentes ont un coût en ressource prohibitif sur la plupart des containers IRIX).
USE_USLOCK_SERIALIZED_ACCEPT
(1.3 ou postérieure) Cette méthode n'est disponible que sur système IRIX, et utilise un appel usconfig(2) pour créer une exclusion mutuelle. Bien que cette méthode permette d'éviter la lourdeur d'implémentation des sémaphores de style Système V, elle n'est pas la méthode par défaut for IRIX, pour la simple raison que sur une implémentation de container IRIX monoprocesseur (5.3 ou 6.2) le code de la primitive uslock() et plus lent que celui des sémaphore type Système 5 d'environ deux ordres de grandeur. Pour des containers multi-processeurs, le code de la primitive uslock() est plus rapide que celui des sémaphores d'environ un ordre de grandeur. Le genre de situation qui ne facilite pas les choix. De ce fait, si vous utilisez IRIX en version multiprocesseur, alors il sera profitable de recompiler votre serveur en ajoutant -DUSE_USLOCK_SERIALIZED_ACCEPT à la liste d'EXTRA_CFLAGS.
USE_PTHREAD_SERIALIZED_ACCEPT
(1.3 ou postérieure) Cette méthode exploite les exclusions mutuelles telles que définies par POSIX et devrait fonctionner sur tout système qui implémente la spécification complète des processus POSIX, bien qu'il semble que cela ne foncionne tout à fait correctement que sur Solaris (2.5 ou postérieure). Ce mode est le mode par défaut sur les systèmes Solaris à partir de la version 2.5.

Si votre système utilise une autre méthode de sérialisation qui n'est pas mentionnée dans la liste ce-avant, il faudra sans doute ajouter du code personnalisé pour la prendre en compte (et nous vous serons reconnaissant de nous renvoyer le patch).

Une autre solution, imaginée mais jamais implémentée, serait de ne sérialisez que partiellement la boucle -- c'est-à-dire, laisser actifs un certain nombre de processus. Ceci n'aurait qu'un intérêt sur des architectures multiprocessus sur lesquelles il est possible de faire exécuter plusieurs fils simultanément, et pour lesquels la sérialisation peut être effectivement considérée come une limitation de la bande passante potentielle. Ce thème ouvre un champ de recherche nouveau, mais qui reste d'une priorité secondaire, du fait que des serveurs Web construits sur des principes à fort parallélisme sont actuellement hors normes.

Normalement, vous exécuterez le serveur en évitant le recours à des directives Listen multiples, si toutefois vous souhaitez en obtenir les meilleures performances.

Accepter la sérialisation - socket unique

Tout ce que nous venons d'exposer est bien beau pour des serveurs à multiples sockets, mais que dire d'un serveur tournant sur socket unique ? En théorie, ils ne devraient pas tomber sur ce cas de figure du fait qu'il est alors admissible que tous les fils se bloquent sur un accept(2) en attendant qu'une connexion arrive. Tous les fils bloqués seront tôt ou tard libérés par les nouvelles requêtes arrivant sur le seul socket possible. En pratique, un comportement similaire à celui décrit ci-avant existe pourtant bel et bien. Comme sont construites aujourd'hui la majorité des piles TCP, le kernel réveille effectivement tous les processus bloqués dans un accept lorsqu'une connexion arrive. L'un de ces processus récupère la connexion et retourne dans le groupe des processus actifs (éligibles), les autres sont repris par le kernel et se rendorment lorsque ce dernier s'apperçoit qu'il n'y a plus de connexion à servir. Cette rotation des processus est invisible pour l'utilisateur, mais n'en est pas moins marginale. Il peut enrésulter des crêtes de charge inexplicables, comme elles peuvent apparaître sur une version non-bloquante évoquée précédemment.

Pour cette raison, nous avons conclu que la majorité des architectures se comportait fort bien si nous sérialisions tous les cas y compris celui des sockets uniques. c'est pour cela que nous avons choisi ce fonctionnement comme défaut. Des expériences serrées sur Linux (2.0.30 sur double Pentium pro 166 / 128Mo RAM) ont montré que la sérialisation dans le cas d'un socket unique ne réduisait pas plus de 3% le taux de requêtes par seconde par rapport à une implémentation non sérialisée. Par contre, une implémentation à simple socket non sérialisé a fait apparaître un temps d'attente supplémentaire de 100 ms pour chaque requête. Cette attente n'est largement pas significative pour des connexions via un réseau commuté ou lignes louées, mais devient significatif sur des LAN. Si vous souhaitez outrepasser la sérialisation des sockets uniques, ajoutez le flag SAFE_UNSERIALIZED_ACCEPT et aucune sérialisation ne sera effectuée.

Clôture retardée

Comme il est discuté dans la section 8 du document draft-ietf-http-connection-00.txt, l'implémentation fiable du protocole HTTP sur un serveur demande de clôturer chacune des direction de communication indépendemment. Rappelez-vous qu'une connexion TCP est bi-directionnelle par construction, chaque demi-connexion étant indépendante de l'autre. Cet état de fait est souvent négligé par d'autres serveurs, mais est parfaitement géré par les versions 1.2 et supérieure d'Apache.

Lorsque cette prise en compte a été implantée à Apache, cela a causé une multitude de problèmes, et ce sur plusieurs versions d'Unix, à cause d'un petit malentendu. La spécification TCP ne précise jamais si l'état FIN_WAIT_2 doit être soumis à une temporisation, tout du moins ne l'interdit-elle pas non plus. Sur des systèmes où cette temporisation est absente, le fonctionnement de la version 1.2 d'Apache fait que de nombreux sockets restent "coincés" indéfiniment par un état FIN_WAIT_2 de la liaison TCP. Dans de nombreux cas ceci peut être corrigé en mettant à jour la pile TCP/IP à l'aide des dernières remises à jour pour votre système. Dans le cas où votre fournisseur ne fournit aucun patch (par exemple, pour SunOS4 -- bien que des possesseurs de licences "source" puissent effectuer la modification eux-mêmes), nous avons décidé de désactiver cette fonctionnalité.

Il y a deux méthodes pour ce faire. L'une réside dans l'option système SO_LINGER pour les sockets. Mais comme si le sort s'y acharnait dessus, cette solution n'a jamais été correctement implémentée dans la plupart des piles TCP/IP. Et même sur les piles correctement programmées (par exemple, Linux 2.0.31) cette méthode se révèle être bien plus coûteuse (en temps machine) que la seconde solution.

Dans la plupart des situations, Apache implémente cette solution dans une fonction appelée lingering_close (écrite dans le fichier source http_main.c). Cette fonction ressemble en gros à ceci :

    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) {
		read (s, junk_buffer, sizeof (junk_buffer));
		/* just toss away whatever is here */
	    }
	}

	close (s);
    }
Ceci ralentit effecivement la terminaison d'une connexion, mais est nécessaire si l'on désire une implémentation robuste. Au fur et à mesure que HTTP/1.1 devient prédominant, et que les connexion se font de plsu en plus surle mode rémanent, o, peut considérer que ce surcoùut puisse être "amorti" sur plusieurs requêtes. Si vous souhaitez jouer avec le feu, et désactiver cette fonction, vous pouvez définir la constante NO_LINGCLOSE, mais nous vous le déconseillons vivement. en particulier, lorsque les connexions HTTP/1.1 se font en chaîne sur le mode persistant alors l'usage de lingering_close est d'une nécessité absolue (et ces connexionss'effectuent plus rapidement).

Fichier "ScoreBoard"

Les processus père et enants d'Apache communiquent entre-eux au moyen d'un objet système appelé "scoreboard". Cet objet devrait être idéalement implanté dans une zone de mémoire partagée. Pour les systèmes auxquels nous pouvons avoir accès, ou qui savent réserver des ports spéciaux à cet usage, ce "scoreboard" sera effectivement implémenté en mémoire partagée. Pour les autres, il est convenable d'utiliser un fichier partagé sur disque dur. Ce ficheir physique a le défaut d'être lent, mais n'est pas non plus très fiable (et dispose de moins de possiblités). Consultez le fichier src/main/conf.h pour votre architecture, et vérifiez l'existance d'une définition de HAVE_MMAP ou de HAVE_SHMGET. La définition d'une de ces deux constantes active l'une ou l'autre méthode de gestion de mémoire partagée. Si votre système utilise un système de partage de mémoire d'un autre type, éditez le fichier src/main/http_main.c et ajoutez les dérivations nécessaires pour qu'Apache puisse les utiliser. (Envoyez-nous si possible un patch).

Note Historique : Le port Linux pour Apache ne permet pas de mettre en place la mémoire partagée avant la version 1.2. Cette conrainte rendait les premières versions Linux d'Apache peu fiables et relativement pauvres en fonctionnalités.

DYNAMIC_MODULE_LIMIT

Si vous n'avez pas l'intention d'utiliser des modules chargés dynamiquement (ce que vous ne souhaiterez pas après avoir lu ceci et paramétré votre serveur pour obtenir le meilleur de ses performances), alors il vous faudra ajouter la ligne : -DDYNAMIC_MODULE_LIMIT=0 avant de reconstruire le serveur. Ceci permet d'économiser la RAM destinée à permettre le chargement et l'édition de liens dynamique.

Appendice : Analyse détaillée d'une trace d'exécution

Nous vous présentons ici une trace des appels système call obtenue sur une version 1.3 d'Apache sous Linux. Le fichier de configuration d'exécution est le fichier par défaut, plus les modifications suivantes :

<Directory />
    AllowOverride none
    Options FollowSymLinks
</Directory>

Le fichier objet de la requête est un fichier statique de 6 Ko de contenu quelconque. La trace de requêtes non statiques ou requêtes avec négociation de contenu sont très différentes (et parfois assez indigestes dans certains cas). Nous donnons ici d'abord la trace entière, puis l'examinerons plus en détails. (Ce qui suit a été généré par le programme strace, mais aurait pu l'être à partir de programmes similaires tels que truss, ktrace, ou par.)

accept(15, {sin_family=AF_INET, sin_port=htons(22283), sin_addr=inet_addr("127.0.0.1")}, [16]) = 3
flock(18, LOCK_UN)                      = 0
sigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0
getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
setsockopt(3, IPPROTO_TCP1, [1], 4)     = 0
read(3, "GET /6k HTTP/1.0\r\nUser-Agent: "..., 4096) = 60
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
time(NULL)                              = 873959960
gettimeofday({873959960, 404935}, NULL) = 0
stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
open("/home/dgaudet/ap/apachen/htdocs/6k", O_RDONLY) = 4
mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000
writev(3, [{"HTTP/1.1 200 OK\r\nDate: Thu, 11"..., 245}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 6144}], 2) = 6389
close(4)                                = 0
time(NULL)                              = 873959960
write(17, "127.0.0.1 - - [10/Sep/1997:23:39"..., 71) = 71
gettimeofday({873959960, 417742}, NULL) = 0
times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747
shutdown(3, 1 /* send */)               = 0
oldselect(4, [3], NULL, [3], {2, 0})    = 1 (in [3], left {2, 0})
read(3, "", 2048)                       = 0
close(3)                                = 0
sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0
munmap(0x400ee000, 6144)                = 0
flock(18, LOCK_EX)                      = 0

Notez l'accept de la sérialisation :

flock(18, LOCK_UN)                      = 0
...
flock(18, LOCK_EX)                      = 0
Ces deux appels peuvent être supprimés en définissant SAFE_UNSERIALIZED_ACCEPT comme il est indiqué ci-avant.

Notez la manipulation SIGUSR1 :

sigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0
...
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
...
sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0

Elle est une conséquence de l'implémentation du redémarrage "en douceur". Lorsque le processus père reçoit un signal SIGUSR1, il envoie un signal SIGUSR1 à chacun de ses fils (et incrémente en mêm temps un "compteur de génération" en mémoire partagée). Tous les fils en attente (entre deux connexions) vont se terminer immédiatement sur réception de ce signal. Tous les fils traitant des connexions KeepAlive, mais en attente entre deux requêtes, meurent également immédiatement. Par contre, les fils en attente de la première requête sur une connexion nouvellement établie, restent en service.

Pour comprendre pourquoi cela est nécessaire, considérez la façon dont un navigateur réagit à une fermeture de laison. Si la connexion était établie en mode KeepAlive et la dernière requête émise n'était pas la première de la série, alors le navigateur serait en mesure de réitérer sa demande de façon transparente sur une nouvelle connexion. Il est nécessaire qu'un navigateur prévoit ce fonctionnement dans la mesure où le serveur reste toujours libre de rompre unilatéralement une connexion KeepAlive, même si la transaction n'est pas terminée (par exemple, dû à une temporisation ou une limite sur le maximum de requêtes servies sur une connexion). Cependant, si la connexion est refermée avant que la PREMIERE réponse ait été reçue, un eimplémentation typique de navigateur afficherait le message "document contains no data" (ou une icône d'image cassée). Cette réaction repose sur la supposition légitime que le serveur peut être tombé d'une façon ou d'une autre (ou est peut-être trop surchargé pour répondre). Apache essaye d'éviter à tout prix (pour autant que possible) de refermer une connexion avant d'avoir donné un premier élément de réponse. Les manipulations de SIGUSR1 en sont la conséquence.

Notez qu'il est en théorie possible d'éliminer ces trois appels. Mais nous avons démontré par des tests sommaires, que le gain devait être négligeable.

Pour implémenter ses hôtes virtuels, Apache doit connaître l'adresse du socket local sur lequel les connexions sont acceptées :

getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
Il est possible de se passer de cet appel dans de nombreux cas (notemment lorsqu'aucun hôte virtuel n'est utilisé, ou lorsque sont écrites des directives Listen sans métaadresses). Nous ne nous sommes pas encore penché sur les optimisations possibles dans ce cas.

Apache désactive l'algorithme de Nagle :

setsockopt(3, IPPROTO_TCP1, [1], 4)     = 0
dû à un problème identifié et décrit dans une note de John Heidemann.

Notez les deux appels à la fonction time :

time(NULL)                              = 873959960
...
time(NULL)                              = 873959960
L'un d'entre eux intervient au début de la requête, et le deuxième au moment de l'écriture dans le fichier de trace. L'un au moins de ces appels est nécessaires pour une implémentation conforme de HTTP 1.1. Le deuxième n'est effectué que parce que le Common Log Format spécifie que lm'enregistrement de journalisation doit posséder un étiquettage temporel daté de la fin du traitemetn de la requête. L'utilisation d'un module de trace personnalisée peut rendre inutile ce deuxième appel.

Comme décrit plus tôt, Rule STATUS=yes provoque deux appels de la fonction gettimeofday et un appel à times:

gettimeofday({873959960, 404935}, NULL) = 0
...
gettimeofday({873959960, 417742}, NULL) = 0
times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747
Ces appels peuvent être sautés si l'on élimine le module mod_status ou en spécifiant Rule STATUS=no.

L'appel à stat peut paraître étrange :

stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
Il est généré par un algorithme destiné à calculer la variable PATH_INFO pour l'utilisation de CGI. En fait, si la requête était dirigée vers l'URI /cgi-bin/printenv/foobar, alors l'on noterait deux appels à stat. Le premier pour atteindre /home/dgaudet/ap/apachen/cgi-bin/printenv/foobar, lequel n'existe pas, et le deuxième vers /home/dgaudet/ap/apachen/cgi-bin/printenv, qui existe quant à lui. Dans tous les cas, au moins un appel à stat est nécessaire pour des requêtes visant des pages statiques, ne serait-ce que pour récupérer la taille du fichier et sa date de dernière modification pour la génération des en-têtes HTTP (telles que Content-Length:, Last-Modified:) et l'implémentation de fonctionnalités autour du protocole (telles que If-Modified-Since). Un serveur quelque peu plus malin pourrait éviter l'appel système à stat pour des fichiers dynamiques. Apache ne peut le faire sans remettre en cause son concept modulaire.

Tous les fichiers statiques sont servis via un appel à mmap:

mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000
...
munmap(0x400ee000, 6144)                = 0
Sur certaines architectures, la lecture de petits fichiers via mmap peut être plus lent que l'utilisation de l'accès classique read. La définition MMAP_THRESHOLD permet de régler le seuil minimum de taille du fichier pour pouvoir passer par mmap. Par défaut , elle est fixée à 0 (sauf sur SunOS4 pour lequel une expérimentation réelle a prouvé qu'une valeur de 8192 donnait de meilleurs résultats). A l'aide d'un outil tel que lmbench vous pourrez déterminer le meilleur réglage pour votre environnement.

Vous souhaiterez peut-être aussi effectuer quelques expériences de redéfinition de la définition MMAP_SEGMENT_SIZE (32768 par défaut) qui détermine le nombre maximal d'octets qui peut être inscrit en une fois par une fonction mmap(). Apache ne fait que réactiver la temporisation cliente entre deux écritures. Augmenter cette valeur peut bloquer des clients à faible débit à moins que vous n'augmentiez aussi la valeur de temporisation.

Il peut aussi arriver que votre architecture ne fournisse pas la primitive mmap. Si c'est le cas, la définition de USE_MMAP_FILES pourrait fonctionner (si cela marche, faites-nous le savoir).

Apache essaye pour autant que possible de ne pas multiplier des copies de données en mémoire. La première écriture pour toute requête est convertie en un enregistrement writev qui combine l'en-tête et le premier segment de données :

writev(3, [{"HTTP/1.1 200 OK\r\nDate: Thu, 11"..., 245}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 6144}], 2) = 6389

Lors de l'encodage des segments HTTP/1.1, Apache générera jusqu'à quatre writev. Le principe est de repousser la copie des octets au niveau du noyau Unix, car elle devra se produire de toutes façons (afin d'assembler les paquets réseau). Lors des tests, de nombreuses implémentations d'Unix (BSDI 2.x, Solaris 2.5, Linux 2.0.31+) on correctement assemblé les éléments writev en paquets réseau. Les versions Linux antérieures à 2.0.31 n'effectuent pas l'assemblage, et créent un paquet pour chacun des éléments, et il sera judicieux dans ce cas d'effectuer la remise à jour. La définition de NO_WRITEV désactive cet assemblage, mais en réduisant les performances nominales de l'encodage.

Suit l'écriture dans la trace :

write(17, "127.0.0.1 - - [10/Sep/1997:23:39"..., 71) = 71
laquelle peut être différée en définissant la constante BUFFERED_LOGS. Dans ce cas, jusqu'à PIPE_BUF octets (une constante POSIX) de trace pourront être tamponnées avant écriture. Le dernier enregistrement du tampon ne devra jamais dépasser de cette limite de PIPE_BUF octets car son écriture en trace deviendrait non atomique. (c'est-à-dire que les entrées provenant de différents fils pourraient se retrouver mélangées). Le code d'Apache fait tout son possible pour nettoyer correctement ce tampon lorsqu'un fils meurt.

La gestion de la fermeture de liaison différée génère quatre appels :

shutdown(3, 1 /* send */)               = 0
oldselect(4, [3], NULL, [3], {2, 0})    = 1 (in [3], left {2, 0})
read(3, "", 2048)                       = 0
close(3)                                = 0
lesquels ont été décrits plus haut.

Appliquons certaines de ces optimisations : -DSAFE_UNSERIALIZED_ACCEPT -DBUFFERED_LOGS et Rule STATUS=no. Voici la trame résultante obtenue :

accept(15, {sin_family=AF_INET, sin_port=htons(22286), sin_addr=inet_addr("127.0.0.1")}, [16]) = 3
sigaction(SIGUSR1, {SIG_IGN}, {0x8058c98, [], SA_INTERRUPT}) = 0
getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
setsockopt(3, IPPROTO_TCP1, [1], 4)     = 0
read(3, "GET /6k HTTP/1.0\r\nUser-Agent: "..., 4096) = 60
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
time(NULL)                              = 873961916
stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
open("/home/dgaudet/ap/apachen/htdocs/6k", O_RDONLY) = 4
mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400e3000
writev(3, [{"HTTP/1.1 200 OK\r\nDate: Thu, 11"..., 245}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 6144}], 2) = 6389
close(4)                                = 0
time(NULL)                              = 873961916
shutdown(3, 1 /* send */)               = 0
oldselect(4, [3], NULL, [3], {2, 0})    = 1 (in [3], left {2, 0})
read(3, "", 2048)                       = 0
close(3)                                = 0
sigaction(SIGUSR1, {0x8058c98, [], SA_INTERRUPT}, {SIG_IGN}) = 0
munmap(0x400e3000, 6144)                = 0
Soient en tout 19 appels système, dont 4 peuvent être assez facilement évités, bienqu'il semble que cela n'en vaille pas la peine.

Appendice : Le modèle Pre-Forking d'Apache

Apache (sous Unix) est un serveur basé sur un modèle appelé pre-forking. Le processus père n'est responsable que du dédoublement en processus fils, il ne sert aucune requête et ne traite aucun paquet réseau. Ce sont les processus fils qui traitent les connexions, servent des connexions multiples (une à la fois) avant de mourir. Le père génère de nouveaux fils, ou tue des anciens fils inactifs pour répondre à la cariation de charge du serveur (il effectue cela par l'intermédiaire d'un fichier de marquage (scoreboard) que les fils renseignent également).

NdT : Ce modèle s'oppose en ce sens à un modèle post-forking qui consiste à dédoubler le processus une fois la connexion reçue.

Ce modèle offre à ce serveur une robustesse qui fait défaut aux autres serveurs. En particulier, le code du père est très simple, et l'on peut, avec un degré de certitude extrêmement haut, penser qu'il effectuera toujours la tâche à laquelle il est assigné même en cas d'erreurs graves de traitement des requêtes. Le code des fils est complexe, et, lorsque vous ajoutez des modules extérieurs, le risque de fautes de segmentation ou d'autres types d'erreur est important. Cependant, même si cela se produit, la faute n'affecte en fait qu'une seule connexion, les autres pouvant continuer à être servies correctement si le cas de faute n'estpas reproduit. Le pèreremplacera de plus rapidement le fils corrompu par un nouveau.

Le modèle Pre-forking assure en outre une très bonne portabilité entre les différents dialectes Unix. Historiquement, cela a toujours été un souci constant d'Apache, et le reste encore actuellement.

Le modèle Pre-forking admet certaines critiques, notemment en termes de performances. En particulier, on pourra citer les pertes dues au simple fait de "forker", puis à la commutation des environnements entre les différents processus, et puis la perte de ressources mémoire due à la multiplication de processus indépendants. De plus, il offre très peu de possibilités de "cacher" des données entre deux requêtes (en gérant par exemple un ensemble de fichiers mmappés). Il existe de nombreux autres modèles, à propos desquels une analyse extensive peut être consultée dans les annales du projet JAWS. Enparatique, ces autres modèles ont des coûts d'implémentation très importants suivant les plates-formes.

Le code du noyau Apache est déjà prête pour le multitâche, et la version 1.3 d'Apache pour Windows NT l'est effectivement. Deux autres expérimentations multitâches d'Apache ont été tentées (l'une exploitant le code de la version 1.3 sur DCE, l'autre conservant le code de la version 1.0 sur un gestionnaire multitâche artisanal développé pour la circonstance. Aucune de ces deux versions n'est publiée). Une partie de nos modifications prévues pour la version 2.0 d'Apache inclueront une abstraction du modèle de serveur, de façon à nous permettre de continuer à utiliser le modèle Pre-forking actuel, tout en supportant plusieurs autres modèles multitâches.