La performance des gestionnaires de contenus Web (notion prise au sens large, depuis le petit site web sous Joomla! jusqu’au système tentaculaire Facebook) devient un enjeu crucial dès que la volumétrie et le nombre d’utilisateurs deviennent importants.
En mode « non connecté » c’est à dire avec des utilisateurs anonymes visualisant tous le même contenu des travaux très nombreux existent et des améliorations très importantes ont été apportées à tous les CMS (Drupal, WordPress, Typo3, Spip, Joomla!, Liferay…).
Si autant d’efforts ont été apportés à ce cas d’usage c’est que l’essentiel du web fonctionne de cette façon.
Les seules exceptions notables sont les systèmes de forums (PHPBB, VBulletin, IPB, Discourse…) et les blogs à haute fréquentation (WordPress, Dotclear…).
Le mode connecté est plus courant dans les entreprises associées au besoin de contrôler la diffusion en fonction des rôles dans l’entreprise. Les utilisateurs s’authentifient dès leur arrivée sur la page principale (ou tout de suite après) et ne peuvent accéder à leurs informations qu’après authentification.
On utilise à ce propos la notion d’intranet en faisant l’amalgame entre réseau interne à l’entreprise (intra-net) et site web interne accessible via ce réseau. Jusqu’à récemment ces intranets, bien que connectés, ne présentaient pas de difficultés majeures de performances : souvent développés sur mesure ils ne servaient qu’à diffuser un peu d’informations, gérer ses demandes de congés, accéder aux notes de service, parfois partager un peu de documentation. L’essentiel des échanges « lourds » passant par des applications tierces (GED, Mail, ERP, « Systèmes métiers »…).
Pour ces modes d’usages les CMS « prévus » pour l’internet pouvaient être encore utilisés quasiment « tels quels » avec quelques aménagements fonctionnels (interfaçage avec un système d’agendas externe, newsletters, contrôles d’accès sur certaines parties des sites…). Au pire le gonflement des infrastructures et la mise en place de systèmes de caches « basiques » permettaient de faire face à la charge induite et de ternir des performances acceptables.
Avec l’avènement des « RSE » (sites web intranets essayant de reproduire à l’échelle de l’entreprise les interactions que l’on trouve sur les « réseaux sociaux » type Facebook, LinkedIn, Viadeo, Pinterest, Instagram, Twitter, Diaspora…) des fonctionnalités initialement relevant des GED ou des Forums ont été recherchées. Et les utilisateurs supposés fortement s’impliquer dans l’utilisation de cet outil (Dans certaines grandes entreprises, Atos étant la première à en faire une « révolution interne » on a même défini un objectif symbolique de fonctionnement « zéro mail »). L’utilisateur devient en quelque sorte contributeur du RSE.
Dans ce cas d’usage les pages affichés sont potentiellement impossibles à mettre en cache en utilisant les mécanismes de cache standard. En effet le principe des caches « anonymes » est de stocker les pages générées « à la fin » pour pouvoir les resservir telles quelles à la demande suivante.
Quand une page donnée affiche tout le temps la même chose quel que soit l’utilisateur ça fonctionne très bien. Mais quand une page de portail doit changer les contenus qui remontent dans ses « blocs » en fonction de l’utilisateur connecté l’utilisation d’un tel cache entraînerait des anomalies fonctionnelles passagères gênantes : Si le premier utilisateur connecté affiche bien sa page les suivants affichent la page de ce premier utilisateur pendant toute la durée de vie du cache avec pour conséquence :
- Le manque d’informations pour des utilisateurs ayant accès à d’autres périmètres.
- L’accès anormal à des informations de la part d’utilisateurs n’ayant pas accès aux mêmes périmètres.
Pour ces raisons de sécurité les CMS utilisent beaucoup moins les mécanismes de caches lorsque les utilisateurs sont connectés.
Au résultat : En mode connecté les pages utilisent beaucoup moins les mécanismes de cache et génèrent donc plus de charge :
- Au niveau des requêtes en bases de données
- Au niveau de la charge de calcul des serveurs web
Les travaux effectués pour répondre à cette problématique par les grands fournisseurs de services (Facebook, Twitter, Instagram…) tournent autour de grands principes :
- Accélérer l’accès aux données :
- La puissance des serveurs de bases de donnes peut être augmentée (scaling vertical) et/ou on peut passer en mode distribué avec des clusters de bases de données (scaling horizontal).
- Des mécanismes de cache pour l’accès aux données sont mis en place au niveau du système de bases de données. Il s’agit de « cache de requêtes ».
- Cacher les données brutes dans un système dédié :
- Il s’agit de ne pas re-poser la « question » à une base de données lorsqu’on vient de le faire. Cette approche est par exemple beaucoup utilisée chez Facebook (de plus en mode distribué) mais elle nécessite une gestion très fine (et active) de l’invalidation de caches.
- Cacher des blocs constitutifs de pages :
- Il s’agit de rendre l’approche des caches plus « fine » en termes de granularité. En mode connecté il faut également rajouter une notion de « version » pour qu’il existe en cache plusieurs versions d’un même bloc caché (avec un contenu différent : par utilisateur ou par groupe d’utilisateurs partageant les mêmes attributs)
Toutes ces approches sont plus ou moins immédiates à utiliser en fonction de ce que permettent les applications nativement et de ce que permettent les briques de l’architecture.
Par exemple PostgreSQL ne gère pas de cache de requêtes contrairement à MariaDB (ex. MySQL) Drupal déployé avec PostgreSQL dans une configuration par défaut non optimisé est moins performant qu’avec MySQL.
Améliorer les performances de la base de donnée avec PostgreSQL
PostgreSQL repose, pour ce qui est du cache, essentiellement sur le cache du système d’exploitation. Ce qui revient à utiliser pour cacher les données un cache tout à fait optimisé pour ça mais dans le même temps à refaire les requêtes (avec les opérations de correspondance et de filtrage liées aux jointures).
Ce parti pris des concepteurs de PostgreSQL pose au niveau du développement applicatif une exigence de qualité dans la structuration de la base de données, la mise en place d’index voire la mise en place d’un mécanisme de persistance des données (Comme ce qu’offrent Hibernate dans le monde Java™ ou Doctrine dans le monde PHP).
Dans ce cas la mise en cache des requêtes n’est pas possible du tout. Cette piste d’optimisation sera donc caduque lorsqu’on utilisera PostgreSQL avec Drupal.
Lorsque les applications deviennent très complexes et n’intègrent pas de persistance de données « native » il faut donc essayer d’optimiser :
- En simulant ou interposant des mécanismes de persistance ou approchant la notion de persistance.
- En augmentant les performances brutes au niveau de la base de données
- En mettant en place des stratégies de cache partiel
Un axe d’amélioration reposera donc sur la mise en place de disques très rapides (SSD) au niveau de la base de données PostgreSQLPooling de connexions
Une des caractéristiques de PostgreSQL en configuration brute est qu’il « forke », à chaque connexion d’un client, un processus dédié qui va traiter les requêtes.
Cette initiation de processus est une opération lourde et surtout lente qui impacte les performances globales de la chaîne requêtes HTTP→ prise en charge par une process PHP → initiation de connexion Pg→ fork de processus Pg → traitement des requêtes…
Une des réponses possibles sur ce plan est la mise en place de PgBouncer qui crée une forme de persistance au niveau des connexions PostgreSQL.
PgBouncer répond à la notion de persistance des connexions en offrant un « pool » de connexions « vivantes » à la base de données qui va permettre d’économiser tous les temps de « fork »
PgBouncer est un projet opensource de « pooler » de connexions poids plume pour PostgreSQL :
- Son wiki : https://wiki.postgresql.org/wiki/PgBouncer
- Sa forge : https://pgbouncer.github.io
C’est un projet dont la dernière version stable est la 1.7 au moment de la rédaction de ce billet.
Il s’interpose entre l’application cliente et la base PostgreSQL et communique avec l’un et l’autre en utilisant le protocole natif de PostgreSQL : ça ne nécessite donc pas de changement applicatif.
Le principe est de le faire tourner au choix sur les machines clientes ou sur le serveur PostgreSQL. Dans tous les cas il sera vu comme un serveur PostgreSQL par l’application cliente.
La mise en place ne nécessite, si on le fait tourner sur le serveur PostgreSQL, que de changer les ports :
- Faire écouter PostgreSQL sur un nouveau port
- Interposer PGBouncer à l’écoute sur l’ancien port et connecté à PostgreSQL sur le nouveau port
La configuration de PGBouncer repose sur un fichier INI. La mise en œuvre de PGBouncer peut nécessiter des ajustements avec en particulier une attention sur les paramètres qui impactent les performances. :
- default_pool_size : Nombre de connexions au serveur à autoriser par couple utilisateur/base pair. Ce paramétre est configuré par défaut à 20.
- reserve_pool_size : Nombre de connexions additionnnelles à autoriser pour un pool. Ce paramétre est desactivé par défaut (0);
- max_db_connections : Nombre de connexions maximum par base (Indépendamment de l’utilisateur). Note : une fois cette limite atteinte la fermeture d’une connexion par un client ne rend pas possible sa réutilisation immédiate par un autre utilisateur vu qu’elle reste allouée. Une fois que le serveur ferme la connexion non utilisée (idle_timeout) une nouvelle connexion peut être utilisée par un pool en demande. Ce paramétre est configuré par défaut pour ne pas limiter le nombre de connexion.
L’emploi de PGBouncer peut notablement fluidifier l’établissement de connexions entre l’application et le serveur PostgreSQL.
En conclusion, pour combler l’absence de cache de requêtes dans PostgreSQL, deux axes d’amélioration sont possibles :
- Le renforcement hardware avec en particulier l’utilisation de disques SSD.
- La mise en place d’un pooler de connexion spécifique Pgbouncer pour réduire les temps de latence.
Plusieurs études et les avis des utilisateurs montrent des avis partagés. Les performances sont variables entre PostgreSQL et MySQL en fonction des cas d’usage sans que l’un ou l’autre ressort du lot. Il convient cependant de noter que l’optimisation via Pgbouncer pour PostgreSQL est une condition indispensable pour avoir une performance comparable à MySQL.
NFS : Tuning & gestion des locks
Le fonctionnement de la répartition de charges peut impliquer l’usage d’un partage de répertoires.
Il s’agit parfois de la totalité du webroot drupal. Et parfois seulement des éléments « modifiables » (files = éléments uploadés mais aussi éléments générés comme les caches sur fichiers).
Lorsqu’ils sont partagés par NFS cela peut entraîner des lenteurs essentiellement dues aux écritures et aux prises de verrous assez mal implémentées dans NFS.
Si la partie gestion des verrous peut faire l’objet de mesures palliatives (en remplaçant les locks par une convention reposant sur le décompte de hardlinks posés sur un fichier). Le tuning fin de NFS peut donner d’excellents résultats sur les temps d’écriture et de récupérations de fichiers partagés par NFS.
En particulier pour optimiser les transferts via NFS il convient de s’intéresser :
- Au protocole de soutien (UDP ou TCP)
- Aux tailles de buffers de lecture et d’écriture (rsize et wsize)
- Aux timeouts et aux contrôles des retransmissions
- Nombre d’instances de daemon NFSD
- Aux limites de mémoire sur les queues rmem. Par exemple on peut les augmenter fortement pour permettre des écritures et lectures beaucoup plus rapides. A noter : ce paramétrage peut être affiné et testé pour s’assurer qu’il ne crée pas d’instabilité système.
- Désactiver l’auto-négociation des débits et du duplex sur les équipements réseau et les cartes des serveurs.
Comportement synchrone ou asynchrone pour NFS V2 & NFS V3
Le comportement par défaut de NFS pour les versions V2 et V3 du protocole sous Linux (c’est exploité par exportfs dans les versions de nfs-utils antérieures à la V1.0.1) est asynchrone.
Cette approche permet au serveur de répondre aux demandes des clients dès que les requêtes ont été traitées et transmises au filesystem sous-jacent, et ce sans attendre que les données soient réellement écrites sur les disques.
On repère cette modalité de fonctionnement avec le flag « async » indiqué dans la liste des exports du serveur. Cela offre de meilleures performances au prix d’une augmentation du risque de corruption des données si le serveur redémarre avant que les données ou métadonnées en caches d’écriture soient écrites pour de bon sur les disques.
Cette corruption potentielle des données est impossible à repérer quand elle se produit puisque async demande au serveur de « mentir » à ses clients sur la bonne réalisation des opérations d’écriture.
Comportement synchrone ou asynchrone pour NFS V4
Dans les versions plus récentes de NFS sous Linux l’option par défaut est « sync » (ce qui est plus proche des modes de fonctionnement d’autres systèmes propriétaires supportant NFS – Solaris, HP-UX, RS/6000… –)
Dans ce cas l’option sync est implicite est masquée dans la liste des exports du serveur.
Cette option peut entraîner quelques complications dans le cas d’une infrastructure web partageant des fichiers sur plusieurs nœuds avec répartition de charge : en cas de montée de charge les écritures prenant plus de temps des phénomènes de congestion pénalisants peuvent apparaître ralentissant très fortement l’ensemble de l’infrastructure.
Autres considérations sur NFS et la synchronicité des écritures.
En complément le client peut explicitement demander un comportement complètement asynchrone indépendamment du protocole via l’emploi du flag O_SYNC
Dans ce cas le serveur attend la fin des écritures disque pour envoyer son code de retour. Les performances deviennent alors strictement liées aux performances techniques (hardware, réseau) et identiques quelle que soit la version du protocole.
Cependant si le flag ASYNC est utilisé il est prioritaire et rend O_SYNC sans effet quelle que soit la version du protocole.
Nota : l’utilisation sur un partage NFS de fsync() essayera de forcer le serveur à écrire tout ce qui est en cache d’écriture sur disque avant de rendre la main. Durant l’opération le serveur ne rend pas la main au client ce qui entraîne un gel temporaire du partage NFS.
Avec le flag ASYNC cependant, comme le serveur ment au client, l’opération est seulement simulée côté serveur ce qui retire de fait le risque de gel.
Configuration des verrous
Lorsque des accès concurrents sont envisagés sur des partages NFS il faut que rpc.lockd et rpc.statd puissent utiliser leurs ports respectifs entre le client et le serveur (en plus de portmap).
A noter : portmap apportant son lot de problématiques sécurité il est fréquent de rajouter sur les points de montage partagés les flags nosuid et root_squash qui permettent de prévenir les escalades de privilèges sur les fichiers côté serveur. Les aspects sécurité spécifiques à portmapper ne faisant pas l’objet de cette veille ce sujet ne sera pas creusé plus avant.
Selon les versions du kernel utilisé flock() ou fcntl() sont à utiliser pour obtenir un verrou sur un fichier. Le verrou permet de réserver un fichier et de s’assurer que des éditions simultanées ne se produisent pas. L’objectif étant d’éviter la corruption mutuelle d’un fichier par des processus concurrents :
Dans les deux cas les implémentations du processus de verrouillage sont connues comme un sujet délicat à travers NFS. La pose de verrous prenant un temps considérable et amenant souvent une saturation due à l’empilement de processus attendant le relâchement du verrou.
Des contournements sont fréquemment mis en place pour pallier cette difficulté : soit utiliser un mécanisme très classique reposant sur la pose et le décompte de « hardlinks » sur les fichiers, ou l’usage de fichiers verrous, ou en se reposant sur la mise en place d’un registre indépendant via un « serveur de locks » ou via un enregistrement spécifique en base de données.
Ces approches alternatives peuvent donner de bons résultats mais nécessitent de mettre en place un maquettage pour valider le principe et le tester sous charge avant mise en production.
Nota : Idéalement l’usage de NFS peut être avantageusement remplacé par un montage « local » via i-SCSI de disques distants à condition que le filesystem supporte les montages multiples (ce qui exclut les formats ext3 et ext4). Par exemple :
- GFS : access.redhat.com
- ou Lustre : lustre.org
En conclusion, lorsque on utilise le « scaling horizontal » les divers nœuds se partagent des fichiers (files) :
- S’ils sont partagés par NFS sur un NAS central il convient de tuner NFS avec attention en particulier si des éléments de cache fréquemment accédés y sont déposés.
- D’une façon générale il est préférable d’utiliser d’autres méthodes de partage reposant sur des filesystems type GFS ou Lustre.