lundi 26 novembre 2012

Chrome ne réspecte plus les Keep-alive timeout

Il y a quelques jours j'ai eu l'occasion de faire un peu de debug réseau, à chaque fois je suis surpris par la créativité des éditeurs de nos navigateurs préférés. Chaque nouvelle version apporte de nouvelles choses au niveau de la gestion des connections afin d'améliorer encore la rapidité coté navigateur mais qu'il faut ensuite gérer coté serveur.

La dernière innovation de Chrome est le choix de garder des connexions ouvertes vers le serveur pendant très longtemps (plusieurs dizaines de minutes) au mépris de la durée de timeout spécifiée par le serveur.

Tout d'abord un petit rappel :

Que disent les spécifications ?

Les navigateurs communiquent avec les serveurs via des connexions TCP.
Encore aujourd'hui, deux versions de la spécifications cohabitent HTTP/1.0 et HTTP/1.1 bien que le premier soit des moins en moins utilisé.

En HTTP/1.0 tout était simple : quand l'utilisateur chargeait une page, le navigateur ouvrait une nouvelle connexion vers le serveur, envoyait la requête, le serveur envoyait la réponse et fermait la connexion.
Problème : l'ouverture d'une nouvelle connexion pour chaque fichier téléchargé prend du temps car ouvrir la connexion nécessite l'échange de 3 paquets réseau (en https c'est même beaucoup plus). Le chargement d'une page complète qui contient beaucoup d'éléments (images, css...) est lent.

En HTTP/1.1 a été inventé le Keep-alive : le navigateur peut demander au serveur via un en-tête HTTP  à garder la connexion ouverte pour la réutiliser pour plusieurs requêtes successives.

Connection: Keep-Alive

Le serveur indique alors par un autre en-tête s'il accepte et si oui sous quelles conditions (durée maximale d'inactivité de la connexion et nombre maximal de requêtes à envoyer par la même connexion).

Keep-Alive: timeout=10, max=5

La spécification indique aussi le nombre maximum de connexions qu'un navigateur à le droit d'ouvrir vers un serveur : 2

Par exemple pour télécharger une page qui contient 20 ressources (images, css, scripts) :

  • un navigateur HTTP/1.0 va ouvrir 20 connections successives qui vont être refermées immédiatement
  • un navigateur HTTP/1.1 va ouvrir 2 connections qu'il va garder ouvertes jusqu'à avoir chargé tous les fichiers et même 10 s de plus (timeout) au cas ou la page charge encore des fichier ou si l'utilisateur navigue sur une autre page.
Le HTTP/1.1 est donc plus rapide pour l'utilisateur. La seule contrainte est la présence de connexion avec une durée de vie plus longue qu'il faut gérer coté client et coté serveur.

En pratique que font les navigateurs

Au départ les éditeurs des navigateurs ont joué le jeu et respecté les spécification HTTP/1.1 à la lettre pour profiter des améliorations de performances mais assez rapidement ils ont voulu aller plus loin en ouvrant plus de connexions en parallèle, d'abord 4 puis parfois 12 ou plus dans les dernières versions !

Google Chrome vient d'aller encore un cran plus loin en ne respectant plus le timeout spécifié par le serveur.
En pratique Chrome va ouvrir plusieurs connexions (a priori jusqu'à 6) mais au lieu de les refermer après le timeout, il va les conserver tant que l'utilisateur garde une fenêtre ouverte sur le site. Selon le système d'exploitation, il va même tenter des les maintenir en vie en envoyant des paquets vides à intervalle régulier.

Ces nouveaux comportements sont à prendre en considération lorsqu'on configure une serveur ou qu'on suit ses performances puisque désormais on va avoir des connexions avec une durée de vie très longue et ce même si les clients n'envoient aucune requête au serveur.



vendredi 15 juin 2012

D'ou vient ce ";jsessionid=..." dans l'URL de la page

Dans de nombreux sites web développés en Java, on voit apparaître parfois un peu n'importe ou un paramètre "jsessionid". Ex :
http://www.legifrance.gouv.fr/rechSarde.do;jsessionid=5D1D8F2EA5F4E3B6B7058B65817EE539.tpdjo09v_2?reprise=true&page=1&lettre=null

Parfois ils apparaissent dans une URL provenant d'une redirection (en général quand on arrive sur le site) mais on les trouve aussi dans les URL des liens qui se trouvent dans les pages. Souvent ces derniers disparaissent quand on rafraîchit la page.

Beaucoup de développeurs ne savent pas vraiment pourquoi ils sont la, quel est leur impact et ne s'en soucient guère. Et pourtant il faut savoir que cette fonctionnalité typique des serveurs d'applications java jadis très utile est aujourd'hui une belle source de problèmes.

Comment ça marche ?

La spécification servlet prévoit que pour pouvoir maintenir coté serveur et état lié à l'utilisateur (la session) le serveur doit implémenter au minimum deux techniques :
  • un cookie de session dont le nom est "JSESSIONID"
  • la réécriture des URL avec l'ajout d'un paramètre "jsessionid" ajouté en principe à la fin de l'URL sous la forme ";jsessionid=".
Mais dans quel cas le serveur va t'il utiliser l'une ou l'autre des techniques ?

Tout d'abord la création de la session :
Tout le monde sait qu'elle a lieu quand dans le code on appelle la méthode request.getSession(). Ce que certains ne savent pas c'est qu'elle a aussi lieu quand on passe dans une jsp dans laquelle on a pas spécifié dans la directive page qu'il n'y avait pas besoin de session. Ex : <%@page session="false" %>
Si on n'y fait pas attention, il y a donc souvent des sessions créées inutilement.

Une fois la session créée, le serveur va faire tout son possible pour maintenir cette session il va donc :
  1. envoyer le cookie JSESSIONID
  2. ajouter le paramètre jsessionid dans les URL à condition que celles-ci passent par la méthode response.encodeURL(...) ou response.sendRedirect(...)
La réécriture d'URL est prévue à l'origine pour les navigateurs qui n'acceptent pas les cookies. Initialement à la création de la session, le serveur ne sait pas si le navigateur acceptera son cookie, il va donc bien activer les deux mécanismes simultanément pour être certain de ne pas perdre la session (en théorie).

Ensuite dans les requêtes suivantes, le serveur s'il reçoit le cookie saura qu'il n'est plus nécessaire de réécrire les URL. Le fonctionnement est donc simple coté serveur :
  • s'il y a un cookie de session dans la requête et le paramètre dans l'URL, on peut désactiver la réécriture
  • s'il y a un cookie de session mais par le paramètre dans l'URL, même chose
  • s'il y a uniquement le paramètre dans l'URL mais pas le cookie, il faudra continuer à réécrire toutes les URL tout au long de la navigation pour cet utilisateur !

Quel impact ?

Le mécanisme de réécriture d'URL va donc s'activer à l'arrivée d'un utilisateur sur le site mais aussi si par malheur un utilisateur refuse les cookies... ce qui est le cas pour les crawlers des moteurs de recherche comme Google, résultat : de nombreuses pages sont indexées par les moteurs de recherche avec un jsessionid. Elles peuvent aussi être mises en cache par des proxy.
On peut facilement vérifier ceci en saisissant des Google "inurl:;jsessionid". On obtient 216 000 000 de résultats !

Mais que fait le serveur quand un utilisateur arrive avec l'une de ces URL par accident (à cause d'un cache ou d'un moteur de recherche) ?
Si la session correspondante a expiré, une nouvelle session va être créée avec un nouvel identifiant. Mais si par contre la session est encore valide, on peut se retrouver avec plusieurs internautes qui vont partager la même session.

Autre cas de figure : dans le cas ou l'application est déployée sur une cluster, généralement avec affinité de session, le cookie comporte généralement deux partie dont la première est l'identifiant de session suivi par l'identifiant du noeud cible. Ex : ;jsessionid=5D1D8F2EA5F4E3B6B7058B65817EE539.tpdjo09v_2
Ceci a pour but qu'un même utilisateur soit toujours dirigé vers le même serveur pour conserver sa session. Mais si encore une fois de nombreux utilisateurs arrivent via cette même URL, ils seront tous dirigés vers le même noeud du cluster, entraînant un déséquilibre de charge.

Dans le cas d'un proxy qui fait aussi du cache, afin d'éviter ce genre de problèmes il faudrait pour bien faire refuser de mettre en cache les réponses dès lors qu'elles contiennent un en-tête "Set-cookie" puisqu'elle peuvent correspondre au moment de la création de session mais aussi repérer dans les requête les jsessionid pour ne pas non plus mettre en cache les résultats qui correspondront potentiellement au cas de l'utilisateur qui n'accepte pas les cookies.
On pourrait également imaginer filtrer les jsessionid directement dans les pages mais ce serait complexe et coûteux.

Il faut noter que dans le cas hypothétique ou un utilisateur s'amuserait réellement à naviguer sans cookie, il suffit d'un seul lien que les développeurs auraient oublié de réécrire (par exemple dans une application développée avec le framework Struts un lien en dur plutôt que généré par un tag Struts) pour que l'utilisateur perde sa session !

Conclusion

Le mécanisme de suivi de session par réécriture d'URL partait d'une bonne intention. Conçu à une époque ou certains navigateurs ne supportaient pas les cookies et ou certains utilisateurs les désactivaient volontairement.
De nos jours il semble impensable de naviguer sans cookie et d'ailleurs même sur les sites ou la réécriture fonctionne, il n'y en a pratiquement aucun pour lequel la réécriture soit systématique.
C'est donc devenu une mécanisme pratiquement inutile mais source de nombreux problèmes et qu'aucune option ne permet de désactiver.

En pratique certains serveurs proposent quand même de les désactiver même si ce n'est pas standardisé. Une autre solution est d'utiliser un Servlet Filter.
Pour les éditeurs de proxys, de caches ou de crawlers ça reste un gros problème.

mercredi 13 juin 2012

EsiGate 3.4 : nouveau système de cache

Dans la version 3.4 d'EsiGate, le système de cache développé spécifiquement (à l'époque ou il n'existait pas de cache HTTP en open source Java) a été remplacé par le cache qui est apparu depuis la version 4.1 de Apache HttpClient.

Cette migration en plus de réduire la base de code d'EsiGate d'environ 20% permet d'améliorer la qualité (le projet HttpClient Cache bénéficie d'une bonne communauté et une conception particulièrement soignée), de réduire le travail de maintenance coté EsiGate et apporte quelques fonctionnalités très intéressante dont :

  • support EhCache / Terracotta
  • support MemCached
  • revalidation des entrées de cache en tache de fond (très utile pour les site à forte charge avec un backend peu optimisé)
Plus de détails sur le blog EsiGate