Éléments de robustesse des sites Web

Parce qu’il est plus facile de prévenir que de guérir, j’aimerais vous faire partager mon expérience et vous apporter quelques réflexes nécessaires à avoir pour réaliser un site Web robuste.

Qu’est ce qu’une application robuste ?

Plusieurs définitions de la robustesse sont possibles. Mais de mon point de vue, il s’agit d’une application conservant un fonctionnement nominal en cas d’indisponibilité de données ou d’un système externe utilisé par l’application. L’idée étant ne pas impacter les fonctionnalités qui ne requièrent pas les données ou les systèmes externes indisponibles.

Le point de fragilité le plus courant que l’on peut observer dans une application Web est le suivant. Lors de l’affichage d’une page, nous effectuons une requête coté serveur pour récupérer une donnée par un appel d’un Web service ou une requête en base de données. Tant que la requête n’a pas retourné de résultat, celle-ci monopolise un thread et une connexion auprès du pool de connexion HTTP du serveur d’application. Pendant ce temps, l’internaute attend que la page s’affiche. Finalement, au bout d’un certain temps, la requête HTTP trop longue à s’exécuter, se termine en timeout et le client reçoit un beau message « Connexion Timeout »

Le risque ici est de dépasser la capacité du pool de threads et/ou de connexions HTTP du serveur d’application. Le serveur ne pouvant plus fournir de connexions, les nouvelles requêtes HTTP qui arrivent sont mises en attente. Coté client, le navigateur est mis systématiquement en attente et le site semble ne plus répondre à aucune page. Ce qui est dommage, c’est que le site aurait peut-être très bien fonctionné sans les données retournées par ce service, à condition que vous l’ayez prévu.

Comment anticiper ce problème ?

On voit ici que la réalisation d’un site Web robuste est très fortement liée aux capacités de votre site à répondre rapidement aux requêtes HTTP et à ne pas monopoliser trop longtemps les threads et les connexions disponibles. Voici quelques pistes et bonnes pratiques pour optimiser ce temps d’attente.

Définition des timeouts

Un temps maximum d’attente (timeout) doit être défini au niveau des connexions vers les systèmes externes. Par défaut en Java, aucun timeout n’est défini sur un socket. Le risque est donc d’attendre indéfiniment.

Le paramétrage doit donc être effectué aussi bien pour les connexions aux bases de données, aux Web services externes ou sur un Active Directory, dès lors que vous avez besoin d’établir une connexion réseau vers l’extérieur.

Petit rappel, pour un socket réseau, il existe 2 types de timeout :

  • Timeout de connexion : temps maximum d’attente d’établissement de la connexion avant abandon et levée d’une exception. Ce type de timeout est nécessaire en cas de filtrage de votre requête au niveau d’un firewall (drop) par exemple. Le risque est d’attendre indéfiniment.

  • Timeout de réception : temps maximum d’attente d’un message en réception avant abandon et levée d’une exception. Ce type de timeout doit être positionné pour ne pas attendre le résultat d’une requête trop longue à s’exécuter.

Appel des Web services en multithread

Lorsque vous avez besoin d’appeler plusieurs Web services pendant le traitement d’une même requête HTTP, il peut s’avérer nécessaire de paralléliser les appels sur plusieurs thread. Le temps d’exécution global reste ainsi borné par le temps d’exécution de l’appel le plus long.

Mise en cache des appels de services

Si vous appelez un service de façon répétée, que celui-ci n’a pas d’état, et donc  que les informations récupérées pour des paramètres données n’évoluent pas (ou peu) dans le temps, la mise en place de mise en cache des résultats est une bonne pratique. D’autant plus si c’est un service peu performant. Elle est bien adaptée pour les services de référentiel, comportant des données peu dynamiques et donc permettant une durée de rétention en cache assez élevée.

Pour des données utilisateurs très limitées (genre nom/prénom/email/…), vous pouvez vous permettre de les mettre en session. Éviter à tout prix de mettre des gros objets en session. C’est une solution de simplicité, mais :

  • la montée en charge de votre serveur serait limitée par sa capacité mémoire. Le risque évident est de la saturer.
  • en cas de mise en place d’un cluster, vous ne pourrez plus activer la réplication de session car celle-ci serait trop coûteuse et ralentirait considérablement votre site. Votre application n’est plus scalable.

Pour des données plus volumineuses, vous pouvez utiliser par exemple :

  • un système de gestion cache, tels que Ehcache, memcached, etc.,
  • une base de données NoSQL (Cassandra, Elasticsearch ou MongoDB).

Gestion des erreurs

C’est sûrement une évidence pour vous, mais il est important de rappeler que vous devez contrôler le retour des services et catcher les exceptions qui peuvent se produire.

En cas d’indisponibilité d’une source de données externe, deux stratégies sont possibles en fonction de la gravité et l’impact sur le fonctionnement de l’application :

  • soit on cache la fonctionnalité inopérante à l’utilisateur,
  • soit on prévoit un message d’erreur adapté pour plus de transparence.

Cette stratégie doit être définie avec la maîtrise d’ouvrage. Les responsables métiers ne vont souvent penser qu’aux cas passants. Il est important de voir avec eux comment ils souhaitent que les cas aux limites soit gérés.

Réalisation de benchs

En fonction de la charge attendue (que vous devez définir impérativement avec le client avant tout développement), la réalisation de benchs significatifs est indispensable.

Les projets de référence pour les benchs sont JMeter (http://jmeter.apache.org/) et Gatling (http://gatling.io). Personnellement, je préconise plutôt l’utilisation de Gatling. Avec JMeter, lorsque le scénario d’exécution devient un peu trop complexe, on arrive très rapidement aux limites. C’est la machine qui lancent JMeter qui se met à ralentir et qui ne produit pas suffisamment de requêtes pour saturer le serveur. D’autre part, les possibilités de scripting de Gatling sont beaucoup plus évoluées, l’enregistrement des scénarios étant effectué en Scala.

Après, vous pouvez également envisager de lancer des benchs en situation dégradée. Pour simuler un service qui rame, vous pouvez ajouter des règles de filtrage à l’aide de la commande iptable pour bloquer les connexions réseaux vers les web service ou vers les bases de données.

iptables -A OUTPUT -j DROP -d

Comment identifier / détecter ce problème ?

Mettre en place un outil de suivi des ressources système

Le premier réflexe lorsqu’un site web rame, c’est de regarder l’utilisation du CPU et que la mémoire n’est pas saturée. C’est un bon début, mais dans le cas où le pool de connexions HTTP de votre serveur d’application est saturé, le CPU et la mémoire des serveurs hébergeant votre site ne seront pas impactés. Par contre, des connexions réseaux en état CLOSE_WAIT apparaissent sur votre serveur (« netstat -a » pour les observer). Celles-ci matérialisent notamment les connexions HTTP qui se sont terminées en timeout coté client.

Afin d’avoir un meilleur suivi, il peut être intéressant d’ajouter une sonde permettant de suivre les connexions réseaux. Je vous conseille l’utilisation de collectd, qui vous permet de mettre en place un certain nombre de métriques sur l’évolution de l’état de vos serveurs avec des beaux graphiques (https://collectd.org/) ; très utile pour investiguer un problème en production.

Pages de diagnostic

Il peut s’avérer très utile de développer au sein de votre site Web, une page de diagnostic (accessible évidemment qu’aux personnes habilités) permettant de tester l’ensemble des Web services et bases de données, en indiquant un taux d’erreur, temps d’accès, messages d’erreur…

Vous me direz, est-ce vraiment le boulot de développeur ? Peut être que non, en effet. C’est à l’exploitant de mettre en place les outils de supervision des systèmes que votre site appelle. Mais dans les faits s’il y a un problème sur votre site, c’est vous qui serez sollicité pour trouver une solution.

De plus, une page de diagnostic est un bon complément car elle permet en un coup d’œil :

  • De tester que votre couche de service parvient bien à communiquer avec les ressources externes (vérification de la compatibilité)
  • De visualiser concrètement, les fonctionnalités inopérantes de votre site et d’en mesurer l’impact immédiatement
  • D’avoir une visualisation instantanée des problèmes d’autorisation réseau (règle de filtrage firewall par exemple)

Pour résumer, c’est un gain de temps pour les mises en production et le support niveau 2 ou 3, que vous devrez réaliser. A moins que vous aimiez perdre du temps à chercher les problèmes dans logs (auquel vous n’avez pas forcement accès…)

Mise en place d’un débrayage des fonctionnalités inopérantes

Lors que vous observez une indisponibilité d’un Web service ou d’une base de données. Il peut paraître idiot de continuer à invoquer le système défaillant. Car malgré les timeouts que vous avez pris le soin de définir, cela peut tout de même ralentir considérablement l’affichage des pages de votre site, voire même de le saturer en cas de forte sollicitation.

Vous avez donc tout intérêt à mettre en place un système de débrayage de la fonctionnalité inopérante. Vous pouvez par exemple, via une page protégée par un accès administrateur, prévoir une interface permettant d’activer ou de désactiver manuellement les fonctionnalités de votre choix.

Puis, avant chaque appel de service, vous vérifiez que la fonctionnalité associée à votre service est bien activée. Le plus commode, me semble-t-il, est de prévoir ce débrayage au niveau de la couche de service.

Après, il est possible également d’aller plus loin et plutôt que de prévoir un débrayage manuel, de prévoir un débrayage automatique en cas d’indisponibilité prolongée d’un service. Vous devez pour cela prévoir pour chaque service que vous voulez débrayer automatiquement les paramétrages suivants :

  • ratio « nombre d’échecs/nombre d’appels » maximum avant débrayage automatique,
  • nombre minimum d’appels avant de tenir compte du ratio,
  • temps d’échantillonnage pour la comptabilisation du nombre d’échecs/nombre d’appels,
  • temps minimum d’attente avant ré-embrayage.

Vous devrez adapter ces paramétrages en fonction de la fréquence à laquelle vous l’appelez.