Vous êtes ici : Accueil / 2012 / Août / Scripts de mise en production sur une plateforme de haute disponibilité

Scripts de mise en production sur une plateforme de haute disponibilité

écrit le 21/08/2012 Par Gaël Le Mignot
Lorsqu'on administre une plateforme Web de haute disponibilité, la mise en production d'une nouvelle version du code est souvent délicate. Cet article présente un certain nombre de problèmes techniques et de solutions à ces problèmes, en prenant le cas particulier de la plateforme Web de Libération comme exemple.

Introduction

Lorsqu'un site Web nécessite une haute disponibilité et que toute coupure de service, même de courte durée et planifiée est problématique, la mise en production d'une nouvelle version du code est toujours délicate.

C'est en particulier le cas sur la plateforme du quotidien Libération, où toute coupure de service, que ce soit pour les visiteurs du site en quête d'information ou pour la rédaction du journal, est à éviter.

Cet article explique les différents problèmes auxquels nous avons été confrontés pour obtenir un processus de mise en production simple, automatisé le plus possible, mais n'entraînant aucune coupure de service. Ainsi que, bien sûr, les solutions que nous avons retenues.

L'architecture de la plateforme

Une architecture Web de haute disponibilité possède en général au minium trois composants :

  1. Une base de données, redondée.
  2. Un load-balancer, redondé.
  3. Des frontaux applicatifs, servant à la fois de répartition de charge et de tolérance de panne.

La manière de redonder la base de données et le load-balancer n'entrent pas en compte lorsqu'il s'agit de la mise en production.

Cependant, le load-balancer lui-même importe, nous verrons exactement comment. Le load-balancer que nous utilisons est haproxy.

Mode d'opération

Le mode d'opération pour une mise en production manuelle est le suivant :

  1. Effectuer la mise en production sur une plateforme de préproduction et vérifier que tout fonctionne parfaitement.
  2. Retirer certains frontaux du load-balancer. L'ensemble du trafic est transféré sur les autres frontaux.
  3. Effectuer la mise en production sur les frontaux qui ont été retirés du load-balancer.
  4. Vérifier que ces frontaux répondent convenablement.
  5. Basculer la charge sur les frontaux à jour.
  6. Effectuer la mise en production sur les autres frontaux.
  7. Remettre l'ensemble des frontaux dans le load-balancer.
  8. Célébrer la réussite de l'opération en consommant une quantité raisonnable de chocolat (noir, bien sûr).

Ce fonctionnement semble simple, mais il y a beaucoup de détails à prendre en compte. Par exemple, bien attendre que les requêtes en cours sur un frontal soient traitées avant de commencer à effectuer des mises en production dessus. Ou alors, ne pas oublier de couper les éventuels cron (tâches programmées de manière régulière) pendant la mise en production.

Effectuer toutes les opérations à la main est fastidieux, même avec notre deuxième meilleur ami historique du shell (le meilleur ami étant bien sûr grep). Et comme dans toute opération manuelle, il existe un risque d'erreur ou d'oubli. Il nous a donc fallu réaliser des scripts pour ces opérations.

Modularité des scripts

Afin de garder une possibilité d'effectuer des mises en production de manière partiellement manuelle si nécessaire, de simplifier les tests des scripts et de manière générale de garder une grande flexibilité, nous avons opté pour une architecture très modulaire :

  1. Un premier script effectue juste le cœur de la mise en production. Il met à jour le code source depuis le dépôt mercurial sur le tag indiqué, ainsi que les éventuels modules externes depuis leurs dépôts (mercurial, git ou subversion), si nécessaire. Il effectue aussi des opérations "métier", comme changer le préfixe de cache memcache.
  2. Un deuxième script (nommé "meta-script" ou "dfctl") s'occupe lui des opérations à effectuer sur un frontal donné :
  1. Vérifier qu'il n'est plus en train de répondre à des requêtes.
  2. Arrêter les cron et autre démons.
  3. Arrêter le service lui-même.
  4. Nettoyer les fichiers .pyc par mesure de sécurité.
  5. Lancer le premier script.
  6. Relancer le service et les cron/démons.
  1. Une série de scripts distincts s'occupent eux, sur le load-balancer, d'activer ou de désactiver des frontaux.
  2. Et enfin, un script principal (nommé "meta-meta-script" ou "djazctl") s'occupe de se connecter aux différentes machines (frontaux et load-balancer) et d'y lancer, dans le bon ordre, les différents scripts.

Détails de certaines opérations

Contrôle du load-balancer

La première consiste à pouvoir contrôler le load-balancer simplement, sans créer de coupure. Le load-balancer que nous utilisons, haproxy, possède un moyen de contrôle simple et efficace : une socket Unix qui permet de recevoir des commandes.

Il est possible d'activer/de désactiver des frontaux applicatifs avec des commandes simples du type :

echo "disable server webfarm/djazfront1" | socat stdio /var/run/haproxy.stat

Lorsqu'un frontal est désactivé ainsi, haproxy n'enverra plus de nouvelles requêtes vers le serveur en question. Les requêtes déjà envoyées vers ce frontal mais non encore traitées ne sont pas affectées, ce qui correspond parfaitement à nos besoins d'éviter toute coupure, mais nécessite une précaution supplémentaire.

Vérifier qu'un frontal est coupé

Puisque les requêtes déjà en cours ne sont pas affectées, il ne faut pas commencer la mise en production sur un frontal qui vient d'être retiré du load-balancer avant que toutes les requêtes en cours ne soient traitées.

La solution que nous avons retenue consiste à utiliser le programme netstat qui donne des informations sur la couche réseau (processus en écoute, socket ouvertes, etc.), et de vérifier qu'il n'y a plus de connexions actives (en état ESTABLISHED) vers le processus Apache, avec une commande du type :

netstat -anp | grep -q ESTABLISHED.*apache

Par mesure de sécurité, nous attendons que pendant trois secondes consécutives aucune connexion ne soit active vers le processus Apache, avant de considérer le frontal comme disponible.

Accès à distance : ssh et sudo

Afin de gérer l'accès à distance pour le meta-meta-script, nous utilisons les outils ssh et sudo. En particulier, nous utilisons des clés ssh pour permettre de se connecter sans mot de passe, et une configuration qui permet de lancer les autres scripts en root lorsque nécessaire, depuis un compte dédié aux mises en production, le tout sans effectuer de demande de mot de passe, mais uniquement pour ces commandes là.

Notre autre ami : screen

Parmi les nombreux amis de l'administrateur Unix, il en est un qui nous est critique pour ces mises en production : l'outil GNU screen.

Cet outil rend énormément de services, mais dans notre cas précis, il permet de s'assurer que même si la connexion Internet entre nos bureaux (ou éventuellement le domicile, voire la gare ou l'aéroport...) d'où nous effectuons la mise en production et le datacenter où se trouvent les serveurs venait à sauter, la mise en production se termine sans encombre.

L'astuce consiste à lancer le script à l'intérieur d'un screen, sur l'une des machines du parc de serveurs. Ainsi, si la connexion venait à sauter, les opérations continueraient, et il nous serait possible de récupérer le screen par la suite pour en vérifier le bon déroulement.

Subtilités d'implémentation du méta-méta-script

L'implémentation du méta-méta-script (celui qui se connecte partout et lance les opérations) possède quelques subtilités techniques qui sont intéressantes à évoquer.

Multiplexer les flux

Pour minimiser le temps de la mise en production (pendant lequel la plateforme, ne fonctionnant que sur la moitié de ses frontaux, n'est plus aussi robuste en cas de pic de charge), les opérations sont effectuées en parallèle sur plusieurs frontaux.

Il est donc nécessaire de multiplexer les flux, c'est à dire d'afficher sur le terminal les sorties des commandes sur plusieurs frontaux à la fois, en les préfixant du nom du frontal considéré.

La solution la plus simple pour effectuer ceci en Python est de passer par les threads : au lieu de lancer directement une commande, nous lançons un thread, qui lui exécute la commande, récupère la sortie, et l'écrit, préfixée du nom de la machine, à l'écran.

Une subtilité de Python est à noter ici : utiliser la syntaxe for line in file pose de problèmes de buffering, et provoque l'arrivée des lignes par bloc, au lieu de leur arrivée au fur et à mesure. Il est nécessaire de les lire plus manuellement, en utilisant la méthode readline des objets fichiers.

Traitement des erreurs

« Des erreurs ? Quelles erreurs ? Il n'y pas d'erreurs dans mon code ! » est hélas rarement applicable. Il faut donc gérer les erreurs. Qui peuvent survenir à tous les niveaux.

Il y a deux types d'erreur, au niveau système :

  1. Quand un programme se termine anormalement (code de retour différent de 0).
  2. Quand un message est affiché sur l'erreur standard (descripteur de fichier numéro 2).

Notre solution est de considérer les erreurs de type 1 (l'une des commandes se termine avec un code différent de 0) comme une erreur fatale, qui interrompt immédiatement le processus de mise en production, et demande une intervention manuelle. Sur un script shell, nous utilisons un set -x qui implémente ce comportement, sur un script Python, nous effectuons le test et l'arrêt nous-même.

Pour les erreurs de type 2, il peut s'agir d'un simple avertissement. Nous avons donc opté pour le mode de fonctionnement suivant : les erreurs affichées par les différents scripts et programmes sur l'erreur standard sont remontées comme les messages normaux, et n'interrompent pas le fonctionnement du programme.

En plus de les afficher au fur et à mesure, nous conservons les messages d'avertissement à part, et les affichons sous forme de récapitulatif à la fin de l'ensemble des opérations, pour être sûr que l'utilisateur du script les ait prises en compte.

Entre en scène le célèbre select

Et c'est là que tout se complique. Tant qu'on gère la sortie d'erreur comme la sortie standard, on peut fusionner les deux flux au niveau du module subprocess de Python, et les traiter de notre côté comme un seul fichier. Le thread est alors simple : il lit ce fichier, préfixe, écrit sur le terminal.

Gérer la sortie d'erreur séparément nous oblige à utiliser le module select de Python, qui est une très fine surcouche à la fonction select du C. Cette fonction prend en paramètre une liste de fichiers. Elle attend ensuite qu'au moins un de ces fichiers soit disponible et nous informe duquel (ou desquels) il s'agit.

En réalité elle prend trois listes de fichiers en paramètre pour attendre des conditions différentes (lecture, écriture et exceptions), mais nous n'utiliserons ici que la première liste : en lecture.

Nous devons donc effectuer un appel du type :

res, _, _ = select.select([child.stdout, child.stderr], [], [])

Et ensuite regarder ce qui se trouve dans res. Si nous y trouvons child.stdout, un message normal est disponible. Si nous y trouvons child.stderr un message d'erreur est disponible. Il est possible d'avoir les deux à la fois, si deux messages ont été mis très rapidement.

Conclusion

Nous avons évoqué les principaux problèmes et leurs solutions pour effectuer des scripts de mise en production sur une plateforme Web comme celle de Libération.

Mais nous sommes restés ici dans les cas simples : lorsqu'il n'y a aucune modification à effectuer sur le schéma de données. Gérer ces cas étant beaucoup plus délicat, nous n'aborderons pas le problème ici. Ils sont cependant rares, et peuvent donc être traités manuellement au cas par cas.

Actions sur le document