event Une variante du MPM worker conçue pour ne mobiliser des threads que pour les connexions en cours de traitement MPM event.c mpm_event_module

Le module multi-processus (MPM) event est, comme son nom l'indique, une implémentation asynchrone basée sur les évènements et conçu pour permettre le traitement d'un nombre accru de requêtes simultanées en déléguant certaines tâches aux threads d'écoute, libérant par là-même les threads de travail et leur permettant de traiter les nouvelles requêtes.

Pour utiliser le MPM event, ajoutez --with-mpm=event aux arguments du script configure lorsque vous compilez le programme httpd.

Le MPM worker
Relations avec le MPM Worker

Le MPM event s'inspire du MPM worker qui implémente un serveur hybride multi-processus et multi-threads. Un processus de contrôle unique (le parent) est chargé de lancer des processus enfants. Chaque processus enfant crée un nombre de threads serveurs défini via la directive ThreadsPerChild, ainsi qu'un thread d'écoute qui surveille les requêtes entrantes et les distribue aux threads de travail pour traitement au fur et à mesure de leur arrivée.

Les directives de configuration à l'exécution sont identiques à celles que propose le MPM worker, avec l'unique addition de la directive AsyncRequestWorkerFactor.

Comment tout cela fonctionne

Ce module MPM a été conçu à l'origine pour résoudre le "problème keep alive" de HTTP. Lorsqu'un client a effectué une première requête, il peut garder la connexion ouverte et envoyer les requêtes suivante en utilisant le même socket, ce qui diminue considérablement la charge qui aurait été induite par la création de nouvelles connexions TCP. Cependant, le fonctionnement du serveur HTTP Apache impose de réserver un couple processus enfant/thread pour attendre les données en provenance du client, ce qui présente certains inconvénients. Pour résoudre ce problème, le MPM Event utilise un thread d'écoute dédié pour chaque processus associé à un jeu de threads de travail, partageant les files d'attentes spécifiques aux requêtes en mode keep-alive (ou plus simplement en mode "lisible"), à celles en mode écriture des résultats, et à celles en court de fermeture ("closing"). Une boucle d'attente d'évènements déclenchée en fonction du statut de la disponibilité du socket ajuste ces files d'attente et distribue le travail au jeu de threads de travail.

Cette nouvelle architecture, en exploitant les sockets non blocants et les fonctionnalités des noyaux modernes mis en valeur par APR (comme epoll de Linux), n'a plus besoin du Mutex mpm-accept pour éviter le problème de "thundering herd".

La directive AsyncRequestWorkerFactor permet de définir le nombre total de connexions qu'un bloc processus/thread peut gérer.

Connexions asynchrones

Avec les MPM précédents, les connexions asynchrones nécessitaient un thread de travail dédié, mais ce n'est plus le cas avec le MPM Event. La page d'état de mod_status montre de nouvelles colonnes dans la section "Async connections" :

Writing
Lors de l'envoi de la réponse au client, il peut arriver que le tampon d'écriture TCP soit plein si la connexion est trop lente. Si cela se produit, une instruction write() vers le socket renvoie en général EWOULDBLOCK ou EAGAIN pour que l'on puisse y écrire à nouveau après un certain temps d'inactivité. Le thread de travail qui utilise le socket doit alors être en mesure de récupérer la tâche en attente et la restituer au thread d'écoute qui, à son tour, la réattribuera au premier thread de travail disponible, lorsqu'un évènement sera généré pour le socket (par exemple, "il est maintenant possible d'écrire dans le socket"). Veuillez vous reporter à la section à propos des limitations pour plus de détails.
Keep-alive
La gestion des connexions persistantes constitue la principale amélioration par rapport au MPM Worker. Lorsqu'un thread de travail a terminé l'envoi d'une réponse à un client, il peut restituer la gestion du socket au thread d'écoute, qui à son tour va attendre un évènement en provenance du système d'exploitation comme "le socket est lisible". Si une nouvelle requête arrive en provenance du client, le thread d'écoute l'attribuera au premier thread de travail disponible. Inversement, si le délai KeepAliveTimeout est atteint, le socket sera fermé par le thread d'écoute. Les threads de travail n'ont donc plus à s'occuper des sockets inactifs et ils peuvent être réutilisés pour traiter d'autres requêtes.
Closing
Parfois, le MPM doit effectuer une fermeture progressive, c'est à dire envoyer au client une erreur survenue précédemment alors que ce dernier est en train de transmettre des données à httpd. Envoyer la réponse et fermer immédiatement la connexion n'est pas une bonne solution car le client (qui est encore en train d'envoyer le reste de la requête) verrait sa connexion réinitialisée et ne pourrait pas lire la réponse de httpd. La fermeture progressive est limitée dans le temps, mais elle peut tout de même être assez longue, si bien qu'elle est confiée à un thread de travail (y compris les procédures d'arrêt et la fermeture effective du socket). A partir de la version 2.4.28, c'est aussi le cas lorsque des connexions finissent par dépasser leur délai d'attente (le thread d'écoute ne gère jamais les connexions, si ce n'est attendre et dispatcher les évènements qu'elles génèrent).

Ces améliorations sont disponible pour les connexions HTTP ou HTTPS.

Les états de connexions ci-dessus sont gérés par le thread d'écoute via des files d'attente dédiées qui, jusqu'à la version 2.4.27, étaient lues toutes les 100ms pour déterminer quelles connexions avaient atteint des limites de durées définies comme Timeout et KeepAliveTimeout. C'était une solution simple et efficace mais qui présentait un inconvénient : ces lectures régulières forçaient le thread d'écoute à se réveiller, souvent sans nécessité (alors qu'il était totalement inactif), ce qui consommait des ressources pour rien. A partir de la version 2.4.28, ces files d'attente sont entièrement gérées selon une logique basées sur les évènements, et ne font donc plus l'objet d'une lecture systématique. Les environnements aux ressources limitées, comme les serveurs embarqués, seront les plus grands bénéficiaires de cette amélioration.

Arrêt de processus en douceur et utilisation du scoreboard

Ce MPM présentait dans le passé des limitations de montée en puissance qui provoquaient l'erreur suivante : "scoreboard is full, not at MaxRequestWorkers". La directive MaxRequestWorkers permet de limiter le nombre de requêtes pouvant être servies simultanément à un moment donné ainsi que le nombre de processus autorisés (MaxRequestWorkers / ThreadsPerChild), alors que le scoreboard représente l'ensemble des processus en cours d'exécution et l'état de leurs threads de travail. Si le scoreboard est plein (autrement dit si aucun des threads n'est dans un état inactif) et si le nombre de requêtes actives servies est inférieur à MaxRequestWorkers, cela signifie que certains d'entre eux bloquent les nouvelles requêtes qui pourraient être servies et sont en l'occurrence mises en attente (dans la limite de la valeur imposée par la directive ListenBacklog). La plupart du temps, ces threads sont bloqués dans un état d'arrêt en douceur car ils attendent de terminer leur travail sur une connexion TCP pour s'arrêter et ainsi libérer une entrée dans le scoreboard (par exemple dans le cas du traitement des requêtes de longue durée, des clients lents ou des connexions en keep-alive). Voici deux scénarios courants :

  • Pendant un graceful restart, le processus parent demande à tous ses processus enfants de terminer leur travail et de s'arrêter pendant qu'il recharge la configuration et lance de nouveaux processus. Si les processus existants continuent de s'exécuter pendant un certain temps avant de s'arrêter, le scoreboard sera partiellement occupé jusqu'à ce que les entrées correspondantes soient libérées.
  • La charge du serveur diminue suffisamment pour que httpd commence à stopper certains processus (par exemple pour respecter la valeur de la directive MaxSpareThreads). Cette situation est problèmatique car lorsque la charge augmente à nouveau, httpd va essayer de lancer de nouveaux processus. Si cette situation se répète, le nombre de processus peut augmenter sensiblement, aboutissant à un mélange d'anciens processus tentant de s'arrêter et de nouveaux processus tentant d'effectuer un travail quelconque.

A partir de la version 2.4.24, mpm-event est plus intelligent et peut traiter les arrêts graceful de manière plus efficace. Voici certaines de ces améliorations :

  • Utilisation de toutes les entrées du scoreboard dans la limite de la valeur définie par ServerLimit. Les directives MaxRequestWorkers et ThreadsPerChild permettent de limiter le nombre de processus actifs, alors que la directive ServerLimit prend aussi en compte les proccessus en arrêt graceful pour permettre l'utilisation d'entrées supplémentaires du scoreboard en cas de besoin. L'idée consiste à utiliser ServerLimit pour indiquer à httpd conbien de processus supplémentaires seront tolérés avant d'atteindre les limites imposées par les ressources du système.
  • Les processus en arrêt graceful doivent fermer leurs connexions en keep-alive.
  • Lors d'un arrêt graceful, s'il y a plus de threads de travail en cours d'exécution que de connexions ouvertes pour un processus donné, ces threads sont arrêtés afin de libérer les ressources plus vite (ce qui peut s'avérer nécessaire pour lancer de nouveaux processus).
  • Si le scoreboard est plein, empêche d'arrêter d'autres processus en mode graceful afin de réduire la charge jusqu'à ce que tous les anciens processus soient arrêtés (sinon la situation empirerait lors d'une remontée en charge).

Le comportement décrit dans le dernier point est bien visible via mod_status dans la table des connexions avec les deux nouvelles colonnes "Slot" et "Stopping". La première indique le PID et la seconde si le processus est en cours d'arrêt ou non ; l'état supplémentaire "Yes (old gen)" indique un processus encore en exécution après un redémarrage graceful.

Limitations

La gestion améliorée des connexions peut ne pas fonctionner pour certains filtres de connexion qui se sont déclarés eux-mêmes incompatibles avec le MPM Event. Dans ce cas, le MPM Event réadoptera le comportement du MPM worker et réservera un thread de travail par connexion. Notez que tous les modules inclus dans la distribution du serveur httpd sont compatibles avec le MPM Event.

Une restriction similaire apparaît lorsqu'une requête utilise un filtre en sortie qui doit pouvoir lire et/ou modifier la totalité du corps de la réponse. Si la connexion avec le client se bloque pendant que le filtre traite les données, et si la quantité de données produites par le filtre est trop importante pour être stockée en mémoire, le thread utilisé pour la requête n'est pas libéré pendant que httpd attend que les données soient transmises au client.
Pour illustrer ce cas de figure, nous pouvons envisager les deux situations suivantes : servir une ressource statique (comme un fichier CSS) ou servir un contenu issu d'un programme FCGI/CGI ou d'un serveur mandaté. La première situation est prévisible ; en effet, le MPM Event a une parfaite visibilité sur la fin du contenu, et il peut utiliser les évènements : le thread de travail qui sert la réponse peut envoyer les premiers octets jusqu'à ce que EWOULDBLOCK ou EAGAIN soit renvoyé, et déléguer le reste de la réponse au thread d'écoute. Ce dernier en retour attend un évènement sur le socket, et délègue le reste de la réponse au premier thread de travail disponible. Dans la deuxième situation par contre (FCGI/CGI/contenu mandaté), le MPM n'a pas de visibilité sur la fin de la réponse, et le thread de travail doit terminer sa tâche avant de rendre le contrôle au thread d'écoute. La seule solution consisterait alors à stocker la réponse en mémoire, mais ce ne serait pas l'option la plus sure en matière de stabilité du serveur et d'empreinte mémoire.

Matériel d'arrière-plan

Le modèle event a été rendu possible par l'introduction de nouvelles APIs dans les systèmes d'exploitation supportés :

  • epoll (Linux)
  • kqueue (BSD)
  • event ports (Solaris)

Avant que ces APIs soient mises à disposition, les APIs traditionnelles select et poll devaient être utilisées. Ces APIs deviennent lentes si on les utilise pour gérer de nombreuses connexions ou si le jeu de connexions possède un taux de renouvellement élevé. Les nouvelles APIs permettent de gérer beaucoup plus de connexions et leur performances sont meilleures lorsque le jeu de connexions à gérer change fréquemment. Ces APIs ont donc rendu possible l'écriture le MPM Event qui est mieux adapté à la situation HTTP typique où de nombreuses connexions sont inactives.

Le MPM Event suppose que l'implémentation de apr_pollset sous-jacente est raisonnablement sure avec l'utilisation des threads (threadsafe). Ceci évite au MPM de devoir effectuer trop verrouillages de haut niveau, ou d'avoir à réveiller le thread d'écoute pour lui envoyer un socket keep-alive. Ceci n'est possible qu'avec KQueue et EPoll.

Prérequis

Ce MPM dépend des opérations atomiques compare-and-swap d'APR pour la synchronisation des threads. Si vous compilez pour une plate-forme x86 et n'avez pas besoin du support 386, ou si vous compilez pour une plate-forme SPARC et n'avez pas besoin du support pre-UltraSPARC, ajoutez --enable-nonportable-atomics=yes aux arguments du script configure. Ceci permettra à APR d'implémenter les opérations atomiques en utilisant des instructions performantes indisponibles avec les processeurs plus anciens.

Ce MPM ne fonctionne pas de manière optimale sur les plates-formes plus anciennes qui ne gèrent pas correctement les threads, mais ce problème est sans objet du fait du prérequis concernant EPoll ou KQueue.

CoreDumpDirectory EnableExceptionHook Group Listen ListenBacklog SendBufferSize MaxRequestWorkers MaxMemFree MaxConnectionsPerChild MaxSpareThreads MinSpareThreads PidFile ScoreBoardFile ServerLimit StartServers ThreadLimit ThreadsPerChild ThreadStackSize User AsyncRequestWorkerFactor Limite le nombre de connexions simultanées par thread AsyncRequestWorkerFactor facteur 2 server config Disponible depuis la version 2.3.13

Le MPM event gère certaines connexions de manière asynchrone ; dans ce cas, les threads traitant la requête sont alloués selon les besoins et pour de courtes périodes. Dans les autres cas, un thread est réservé par connexion. Ceci peut conduire à des situations où tous les threads sont saturés et où aucun thread n'est capable d'effectuer de nouvelles tâches pour les connexions asynchrones établies.

Pour minimiser les effets de ce problème, le MPM event utilise deux méthodes :

  • il limite le nombre de connexions simultanées par thread en fonction du nombre de processus inactifs;
  • si tous les processus sont occupés, il ferme des connexions permanentes, même si la limite de durée de la connexion n'a pas été atteinte. Ceci autorise les clients concernés à se reconnecter à un autre processus possèdant encore des threads disponibles.

Cette directive permet de personnaliser finement la limite du nombre de connexions par thread. Un processus n'acceptera de nouvelles connexions que si le nombre actuel de connexions (sans compter les connexions à l'état "closing") est inférieur à :

ThreadsPerChild + (AsyncRequestWorkerFactor * nombre de threads inactifs)

Il est possible d'effectuer une estimation du nombre maximum de connexions simultanées pour tous les processus et pour un nombre donné moyen de threads de travail inactifs comme suit :

(ThreadsPerChild + (AsyncRequestWorkerFactor * number of idle workers)) * ServerLimit

Exemple ThreadsPerChild = 10 ServerLimit = 4 AsyncRequestWorkerFactor = 2 MaxRequestWorkers = 40 idle_workers = 4 (moyenne pour tous les processus pour faire simple) max_connections = (ThreadsPerChild + (AsyncRequestWorkerFactor * idle_workers)) * ServerLimit = (10 + (2 * 4)) * 4 = 72

Lorsque tous les threads de travail sont inactifs, le nombre maximum absolu de connexions simultanées peut être calculé de manière plus simple :

(AsyncRequestWorkerFactor + 1) * MaxRequestWorkers

Exemple ThreadsPerChild = 10 ServerLimit = 4 MaxRequestWorkers = 40 AsyncRequestWorkerFactor = 2

Si tous les threads de tous les processus sont inactifs, alors :

idle_workers = 10

Nous pouvons calculer le nombre maximum absolu de connexions simultanées de deux manières :

max_connections = (ThreadsPerChild + (AsyncRequestWorkerFactor * idle_workers)) * ServerLimit = (10 + (2 * 10)) * 4 = 120 max_connections = (AsyncRequestWorkerFactor + 1) * MaxRequestWorkers = (2 + 1) * 40 = 120

Le réglage de la directive AsyncRequestWorkerFactor nécessite de connaître le trafic géré par httpd pour chaque style d'utilisation spécifique ; si vous modifiez la valeur par défaut, vous devrez par conséquent effectuer des tests approfondis en vous appuyant étroitement sur les données fournies par mod_status.

La directive MaxRequestWorkers se nommait MaxClients avant la version 2.3.13. La valeur ci-dessus montre que cet ancien nom ne correspondait pas à sa signification exacte pour le MPM event.

La directive AsyncRequestWorkerFactor accepte des valeurs d'argument de type non entier, comme "1.5".