3 février 2010
Imprimer ce billet

Tomcat load balancing – mod_proxy vs mod_jk le match

Dans notre article sur l'utilisation de HTTPS avec Tomcat en production, nous avons étudié les solutions reposant sur la mise en place d'un reverse proxy HTTP. Nous n'avons pas oublié pour autant le protocole AJP. Ce protocole est né pour faciliter et accélérer les communications entre un serveur web frontal et le serveur d'application JServ en back-end d'Apache. Avec le temps, Tomcat a remplacé Apache JServ mais AJP est resté. Jusqu'en 2003, AJP était la seule solution viable permettant de placer le serveur d'application derrière un serveur Apache. Avec la maturation de la fonctionnalité Proxy dans Apache est née la solution tout HTTP. Nous avons donc décidé d'organiser un match opposant la solution AJP à la solution HTTP.

Un peu d'histoire

Tout commence en 1997 avec la création d'Apache JServ. A l'époque, il s'agit d'un serveur de Servlet qui supporte uniquement le protocole AJP créé pour l'occasion. Dans l'architecture initiale, c'est Apache 1.1 qui fournit le serveur web et transfère les requêtes par socket au moteur de Servlet. C'est la naissance du protocole AJP dans sa première version implémentée par le mod_jserv. L'Apache JServ Protocol fonctionne au départ comme un proxy qui redirige le flux vers JServ. Le protocole est en texte clair et utilise un caractère en début de ligne pour distinguer les différents éléments de la requête.

Rapidement, le protocole initial est considéré comme trop limité car il fonctionne uniquement en loopback et possède une authentification pauvre. En 1998, le protocole AJP 1.1 permet à JServ de tourner sur une autre machine que le serveur web et d'assurer une authentification forte basée sur md5. Avec le développement de JServ appparaît un problème de performance important : le coût d'ouverture d'une socket et la vitesse des réseaux. Ils sont considérés à l'époque comme les principaux goulets d'étranglement. Pour résoudre ces problèmes, le protocole passe à la version 1.2 dont vous trouverez le draft initial ici. AJP 1.2 est un protocole binaire orienté paquets qui permet de recycler la ou les sockets connectées à JServ. Le passage au binaire permet aussi d'améliorer les performances car il diminue la taille des données qui transitent et simplifie le traitement.

En 1999, Sun offre son implémentation de référence des Servlets à la fondation Apache. C'est le point de départ des projets Tomcat et Ant. Commence alors une période de transition qui finira par l'abandon d'Apache JServ. Pendant cette transition, AJP sera porté sur Tomcat qui bénéficiera d'emblée d'une facilité d'interconnexion avec Apache. Aux alentours de 2000, le mod_jk est développé pour étendre AJP qui pourra supporter le transport des données SSL. C'est la version 1.3 que l'on retrouve aujourd'hui supportée par les dernières générations de Tomcat. Après toutes ces années de développement, le mod_jk est maintenu par le projet Tomcat Connectors et n'a jamais été intégré aux projets Apache.

La création d'AJP résulte donc de la simplicité et de la rapidité de développement souhaitées par les auteurs. Il fallait aller vite, et implémenter un proxy pleinement compatible HTTP aurait été trop long. Le module mod_proxy existe depuis 1996 dans Apache 1.1. Mais il s'agissait d'une fonctionnalité expérimentale qui n'offrait ni performance ni stabilité. A la sortie d'Apache 2, le proxy a même été dé-scopé car il ne fonctionnait plus du tout. Les développeurs vont pourtant rapidement le corriger et le réintégrer comme solution pour faire des reverse proxies. Pour la sortie d'Apache 2.2, le mod_proxy est entièrement réécrit pour supporter le load-balancing. Il offre, pour la première fois dans Apache, une solution capable de concurrencer le mod_jk en performance et en scalabilité. Cerise sur le gâteau, Apache a décidé de supporter nativement le protocole AJP en ajoutant un mod_proxy_ajp à la solution mod_proxy.

Installation

mod_proxy

Le mod_proxy fait partie de la distribution standard d'Apache HTTPD. Il est livré avec le mod_proxy_http, le mod_proxy_ajp et le mod_proxy_balancer. Il suffit donc de s'assurer que ces modules sont bien chargés au démarrage d'Apache.

mod_jk

Si mod_jk était autrefois très délicat à installer avec la compilation du code sur un serveur similaire au serveur cible, la situation s'est grandement simplifiée. Le projet Apache Tomcat Connector fournit désormais les binaires pour les principales plateformes (Linux, Windows, Free BSD, Mac, Solaris, AIX, etc). Il faut donc télécharger le binaire du module et le copier dans le répertoire contenant les modules Apache.

Configuration

Pour mieux comparer les deux solutions, nous avons choisi de prendre comme exemple l'utilisation d'Apache en front desservant deux Tomcat en load-balancing. Nous ne nous intéresserons ici qu'à la configuration d'Apache HTTPD. La configuration AJP de Tomcat étant déjà largement documentée sur le web et celle via HTTP dans nos articles précédents, nous n'aborderons pas ces problématiques.

mod_proxy_http & mod_proxy_balancer

La configuration du mod_proxy consiste d'abord à s'assurer que les modules soient bien chargés avec les directives LoadModule. Nous activons ensuite le server-status ainsi que le balancer-manager pour obtenir une interface de surveillance / administration du load-balancer. Enfin, nous avons configuré le Proxy balancer nommé my-application-cluster pour contenir nos 2 serveurs Tomcat. La dernière ligne de configuration active le reverse proxy pour que les requêtes sur /my-application soient redirigées vers le /my-application du load-balancer.

Configuration avec mod_proxy_http & mod_proxy_balancer - httpd.conf

# LOAD MODULES
LoadModule proxy_module libexec/apache2/mod_proxy.so
LoadModule proxy_http_module libexec/apache2/mod_proxy_http.so
LoadModule proxy_balancer_module libexec/apache2/mod_proxy_balancer.so

# STATUS AND MONITORING
# Display proxy balancer status in /server-status page

ProxyStatus On
<Location /server-status>
    SetHandler server-info
    Order deny,allow
    Deny from all
    Allow from localhost
</Location>
<Location /balancer-manager>
    SetHandler balancer-manager
    Order Deny,Allow
    Deny from all
    Allow from localhost
</Location>

# APPLICATIONS CONFIGURATION

<Proxy balancer://my-application-cluster>
   BalancerMember      http://node-1:8080 route=node-1 disablereuse=On
   # ...
   BalancerMember      http://node-n:8081 route=node-n disablereuse=On
</Proxy>

ProxyPreserveHost On
ProxyPass /my-application balancer://my-application-cluster/my-application stickysession=JSESSIONID

Vous pouvez le constater, la configuration est parfaitement intégrée à la configuration standard d'Apache HTTPD. Les équipes de production n'auront à priori pas de grandes difficultés à prendre en main ce type de configuration qui nécessite seulement de savoir parcourir la documentation Apache déjà bien connue des administrateurs.

mod_jk

La première étape consiste à configurer le serveur Apache pour qu'il utilise le mod_jk. Tout commence par le chargement du module avec la directive LoadModule. Ensuite nous fournissons le chemin du deuxième fichier de configuration définissant les workers. La directive JkMount permet ensuite d'associer un worker du mod_jk à un pattern d'url du serveur. Pour chaque requête dont l'URL correspond au pattern, Apache va déléguer le traitement au mod_jk. Nous montons d'abord le worker jkstatus sur /jkmanager en autorisant l'accès uniquement depuis le système local. C'est ensuite au tour du loadbalancer qui servira notre application.

Configuration avec mod_jk - httpd.conf

# LOAD MODULES

LoadModule jk_module libexec/apache2/mod_jk.so

# MOD_JK CONFIGURATION FILE
JkWorkersFile /etc/apache2/other/workers.properties

# MOD_JK PROPRIETARY LOG FILE
JkLogFile     /var/log/apache2/mod_jk.log

# NEEDED ON MAC SNOW LEOPARD
JkShmFile     /var/log/apache2/

# STATUS AND MONITORING
JkMount /jkmanager/* jkstatus
<Location /jkmanager>
    Order deny,allow
    Deny from all
    Allow from localhost
</Location>

# APPLICATIONS CONFIGURATION
JkMount /my-application/* loadbalancer

Il faut maintenant configurer le mod_jk proprement dit dans son fichier de configuration spécifique. Il s'agit d'une liste de propriétés commençant toujours par worker.. La propriété worker.list fournit les noms des workers actifs. Le nom est ensuite utilisé pour paramétrer le worker avec des clés de la forme worker.nomWorker.parametre. Le premier worker activé jkstatus utilise le type spécial status qui correspond à l'interface de suivi et de gestion du mod_jk. Le deuxième worker loadbalancer est en fait chargé de répartir les sessions entre le worker1 et le worker2 via le type lb. Ce sont finalement les workers 1 à n qui assurent le transport des requêtes sur AJP1.3 vers le port 8009 d'un Tomcat distant.
Configuration avec mod_jk - workers.properties

# WORKERS AND PSEUDO WORKER
worker.list=jkstatus, loadbalancer

# STATUS AND MANAGEMENT PSEUDO WORKER
worker.jkstatus.type=status

# WORKER 1 TO N
worker.worker1.type=ajp13
worker.worker1.host=node-1
worker.worker1.port=8009

# ...

worker.worker2.port=8009
worker.worker2.host=node-n
worker.worker2.type=ajp13

# LOAD BALANCER PSEUDO WORKER
worker.loadbalancer.type=lb
worker.loadbalancer.balance_workers=worker1,worker2

Avec ses deux fichiers de configurations séparés et la syntaxe "rustique" du worker.properties, la configuration est définitivement le point faible du mod_jk. L'un des seuls avantages réside dans le nombre impressionnant de paramètres supportés qui permet un paramétrage fin si le besoin s'en fait sentir. Si seulement nous n'étions pas forcés à tant de verbosité !

Interfaces graphiques

Les deux modules fournissent des interfaces web pour assurer la supervision du load-balancer. Cette fois, le mod_proxy se contente de fournir des statistiques réduites dans une interface minimaliste. L'interface liste principalement le statut de chaque nœud du load-balancer ainsi que la quantité de données envoyée et reçue par nœud.

screenshot-apache-server-status--proxy-balancer

Outre le suivi des statistiques du load-balancer, le Balancer Manager permet aussi de faire des modifications à chaud, éditer les workers, les passer en offline, voire même changer la méthode de répartition utilisée.

screenshot-apache-server-status--balancer-manager

De son côté, le mod_jk prouve sa maturité avec son interface rustique elle aussi, mais plus voire trop complète. Elle est composée de plusieurs pages dont une page d'accueil similaire en tout point à l'interface du mod_proxy. L'avantage réside dans la fourniture d'une page détaillant l'état complet pour chaque nœud.

Mais le mod_jk ne se contente pas de cela : il fournit aussi une page permettant de modifier à chaud la configuration du load-balancer. Il est parfaitement envisageable pour le déploiement en production d'une nouvelle version de l'application de couper les nœuds un à un au moment de leur mise à jour puis de les réactiver sans interruption du service.

Attention toutefois, car les modifications faites par ce biais sont uniquement enregistrées en mémoire, la nouvelle configuration sera perdue au prochain redémarrage d'Apache.

screenshot-apache-jkmanager

La mince différence vient probablement de la jeunesse de la solution de load-balancing du mod_proxy face à la longue expérience de production du mod_jk. Mais elle ne saurait justifier une préférence pour l'un des deux modules, qui sur ce point sont très proches.

LoadBalancing et gestion d'erreur

Outre la configuration et l'interface graphique, les deux solutions disposent de quelques fonctions avancées notamment en ce qui concerne la gestion d'erreur et la gestion des sockets réseaux. Les deux modules utilisent des pools de connexion rangés par Thread du serveur Apache et par membres du cluster.
Avec le mod_proxy, il est possible de choisir parmi trois algorithmes de répartition de charge :

  • Par requête : la charge est répartie pour chaque requête entrante en fonction de la session si elle existe.
  • Par trafic réseau : les requêtes sont envoyées vers le membre du cluster ayant reçu le moins de trafic.
  • Par taux d'occupation : la charge est répartie en fonction du nombre de requêtes en cours de traitement ou en attente.

En ce qui concerne la gestion d'erreur, il n'y a pas grand chose à dire car il n'y a pas grand chose de fait. Si un timeout claque, un certain nombre de tentatives de reconnexion seront réalisées jusqu'à ce que le noeud soit marqué en erreur et n'en décolle plus.

C'est sur la gestion d'erreur que le mod_jk possède une plus grande sophistication que son récent concurrent.
Tout d'abord le protocole AJP fournit un mécanisme de health check (CPing/CPong) qui permet de tester l'état du lien entre le serveur frontal et un membre du cluster. Cependant, c'est beaucoup plus limité que les heart beat des load balancer hardware, il n'est pas possible de tester une url pour détecter des indisponibilités applicatives (échec de démarrage de la web app, web app KO à cause de l'indisponibilité d'un backend clef).
D'autre part, le module fait une distinction entre les erreurs locales temporaires qui sont sans impact particulier, un code d'erreur HTTP par exemple, et les erreurs globales qui indiquent que le serveur est clairement en erreur et ne recevra plus de nouvelles requêtes. Il existe un système d'escalade qui, en cas de répétition trop fréquente d'erreurs locales sur un noeud se charge de le passer en erreur globale. Le module se charge de réactiver le noeud en mode recovery après un délai paramétré.

Côté répartition de charge, mod_jk apporte un dernier algorithme de répartition reposant sur le nombre de sessions HTTP en cours par serveur. Cette méthode est relativement récente puisqu'elle est apparue dans la version 1.2.20 du mod_jk, actuellement en version 1.2.29. Elle est recommandée pour les applications chargeant fortement la session et de ce fait, supportant un nombre limité de sessions par serveur ; ce cas d'utilisation est assez marginal.
Dans les deux modules, l'algorithme de répartition est pondéré par un facteur nommé "lbfactor", qui permet d'appliquer des quotas de travail aux serveurs. Ce système s'avère utile si les serveurs sont de puissances différentes par exemple.

Le mod_jk marque ici un petit point sur le mod_proxy grâce à sa gestion d'erreur plus fine. Attention aux effets de bord, le retry sur timeout en a surpris plus d'un et l'éviction de serveurs tomcat pour cause de répétition d'erreur est un beau sujet de déni de service :-)

Pour le reste, le mod_proxy colle au fonctionnement du mod_jk, ce qui ne manquera pas de faciliter son utilisation aux habitués du mod_jk.

Exploitation et diagnostic des problèmes

mod_proxy_http présente sur mod_jk le très grand avantage d'utiliser un protocole standard, HTTP, connu de tous les acteurs d'un système d'information alors que mod_jk repose sur le protocole AJP que quasiment personne ne connait. Les administrateurs systèmes et réseaux sont habitués à HTTP et notamment à ses connections persistantes (aka HTTP keepAlive) ; ils savent ajuster leurs algorithmes de load balancing, les timeouts des firewalls et le dimensionnement des piles tcp/ip des serveurs (ulimit, tcp_keepalive_intvl, tcp_tw_bucket, etc).

Un autre atout de mod_proxy est la facilité de diagnostic. N'importe quel acteur du système d'information peut utiliser curl, wget, telnet, elinks voire wireshark pour troubleshooter un problème de communication HTTP; ce n'est pas le cas avec le protocole AJP qui n'est pas human readable et encore moins human writable. Choisir AJP mérite de former les administrateurs systèmes et réseaux et de prévoir des outils de tests permettant de requêter un connecteur AJP mais ce n'est hélas que très rarement fait.

En cas de problème réseau avec HTTP comme avec AJP, n'oubliez pas que désactiver les connections persistantes simplifie grandement les investigations et n'a rien de scandaleux en 2010 (cf HAProxy) ; c'est "disablereuse=On" pour mod_proxy_http et "JkOptions +DisableReuse" pour mod_jk :-)

Conclusion

Avec sa simplicité de configuration, sa répartition de charge calquée sur le mod_jk, le mod_proxy conviendra parfaitement dans la grande majorité des cas. Il s'accommode de clusters répartissant la charge sur plusieurs noeuds ou bien en tant que simple reverse proxy. La cerise sur le gateau étant l'utilisation du protocole HTTP qui permet de garantir la portabilité des services et facilite grandement l'analyse du trafic.

A nos yeux, avec l'intégration native de mod_proxy_http et mod_proxy_balancer à Apache, il n'y a plus de justification à ajouter le module additionnel mod_jk ni d'introduire le protocole méconnu AJP. Les optimisations d'AJP ne sont plus de mise aujourd'hui et ne justifient donc pas l'utilisation d'un mod_jk.

Il reste, dans les deux cas, quelques efforts à faire pour permettre de plus facilement monter des serveurs à chaud dans un cluster. Pour la haute disponibilité sans perte, il faudra mettre en place un cluster Tomcat assurant la réplication des sessions utilisateur entre les serveurs. Encore faut-il en avoir vraiment besoin ...

Attention, la recommandation de Tomcat est encore le mod_jk et, il est important de garder fonctionnel ce qui marche déjà. Donc, quoiqu'il arrive, si vous avez déjà une solution fonctionnelle avec le mod_jk, inutile de migrer. Pour ce qui est de l'interface et de la gestion d'erreur, nous parions sur l'avenir du mod_proxy qui est en développement intensif et bénéficie des corrections de bug de son ainé. Reste un nouveau venu dans le paysage développé par Jboss qui attire déjà notre attention c'est le mod_cluster actuellement, il n'est utilisable qu'avec Jboss AS. L'avantage de cette nouvelle solution en devenir est de suivre le cycle de vie des applications via un protocole spécifique MCMP reposant tout de même sur HTTP.