Architectures distribuées et traitements asynchrones

Cela fait quelque temps que je réfléchis à l’idée d’écrire un article sur les architectures distribuées, et plus particulièrement sur les traitements asynchrones. J’en parlais déjà au Forum PHP en 2013, dans de ma conférence intitulée «De 0 à 10 millions de visiteurs uniques avec les moyens d’une startup» mais je vais entrer plus en profondeur dans le sujet.

Pour quoi faire ?

On peut vouloir faire des traitements asynchrones pour différentes raisons. Si on s’en tient au développement web, la principale raison est de répondre plus vite aux requêtes des visiteurs, en exécutant certaines actions de manière différée. L’exemple classique est lorsqu’une action sur votre site doit déclencher un appel sur l’API d’un service tiers : Cet appel d’API peut prendre plusieurs secondes, et il peut même échouer en cas de problème momentané du service en question ; dans ce cas, plutôt que de faire attendre notre internaute, il vaut mieux lui répondre immédiatement et traiter cet appel d’API de manière asynchrone. Pareil pour des envois d’emails (même si les serveurs SMTP opèrent déjà de cette manière), des traitements sur des fichiers audio/vidéo, des calculs lourds, etc.

Si on élargit le débat au-delà du web, c’est un bon moyen pour découpler les différentes briques qui composent un logiciel afin de mettre en place une architecture distribuée. C’est souvent un élément essentiel lorsqu’on a besoin de scalabilité et même de rapidité. Un système monolithique atteindra tôt ou tard ses limites, là où un système composé de plusieurs entités qui communiquent ensemble sera plus facile à faire évoluer − au prix d’une complexité initiale plus grande.
En éclatant le code applicatif en plusieurs morceaux, on peut distribuer son exécution sur plusieurs machines assez facilement, facilitant ainsi l’évolutivité de la plate-forme.

Concrètement, l’idée est d’avoir des entités qui s’envoient des messages au lieu de discuter entre elles directement. Quand un message est envoyé, on n’attend pas une réponse immédiate ; une fois que le message aura été traité, un autre message sera éventuellement envoyé en retour pour le notifier, à moins que le résultat ne soit par exemple stocké dans une base de données où il sera disponible pour de futurs besoins.

Le sujet des architectures distribuées n’est pas nouveau. L’idée de RPC (appel à procédure distante) a été décrite dès 1976. Dans les années 90, quand les langages orientés objet étaient à la mode, beaucoup d’énergie a été déployée dans les systèmes servant à exécuter des objets à distance. C’est dans ce contexte que sont apparus des ORB (object request brokers) comme CORBA dès 1991 et RMI (Remote Method Invocation) intégré à Java dès la version 1.1 en 1997.
D’ailleurs, la complexité de CORBA a facilité l’émergence de normes plus simples et basées sur des technos web, comme XML-RPC et SOAP (Simple Object Access Protocol) en 1998, elles-mêmes simplifiées avec l’apparition du REST en 2000 (qui s’est développé d’autant plus avec l’usage du JSON, sérialisation plus simple à manipuler que le XML).

Mais comme on va le voir, il ne suffit pas de considérer qu’il y a des procédures/méthodes/objets à appeler pour que le système soit robuste en toutes circonstances. On peut d’ailleurs remarquer que les illusions de l’informatique distribuée sont bien connues et listées depuis les années 90 :

  1. Le réseau est fiable.
  2. Le temps de latence est nul.
  3. La bande passante est infinie.
  4. Le réseau est sûr.
  5. La topologie du réseau ne change pas.
  6. Il y a un et un seul administrateur réseau.
  7. Le coût de transport est nul.
  8. Le réseau est homogène.

Effectivement, rien de tout cela n’est vrai. Découpler une application apporte beaucoup d’avantages, mais comporte aussi des contraintes et des risques qu’il faut connaître et évaluer.

Comment faire ?

Pour mettre en place ce type d’architecture, il existe un certain nombre de moyens techniques, depuis les plus simples jusqu’aux plus complexes. Je vais passer en revue quelques notions et solutions.

Quand on parle de traitements asynchrones, on a habituellement trois composants à mettre en place :

  • Une partie qui permet de donner des ordres de traitements, avec éventuellement des informations complémentaires (de quel type d’ordre il s’agit, avec parfois des données additionnelles, etc.). Cela prend habituellement la forme d’une simple bibliothèque logicielle, mais certaines technos imposent plus de contraintes − comment les ORB qui obligeaient à créer des stubs.
  • La pièce centrale du système, qui gère les messages qui sont envoyés, en les classant éventuellement par priorité ou en les rangeant dans différentes files. Cette partie peut être basée sur des technologies variées (bases de données, files de messages, tables de hachage, ou autre).
    Les technos utilisées, que ce soit pour gérer les messages ou pour communiquer entre les différentes briques de l’architecture, ont une influence directe sur les performances du système.
  • La dernière partie est constituée par des «workers», c’est-à-dire du code qui traite les messages et effectue le travail demandé. Là encore, il existe plusieurs manières de faire : soit les workers sont des programmes indépendants (qu’il faut alors gérer correctement), soit ils sont lancés par la brique centrale (au prix d’un coût d’instanciation qui ajoute de la latence dans les traitements).

Dans les notions qui me semblent importantes à avoir en tête, il y en a principalement deux :

  • La scalabilité : Est-ce que la solution retenue peut soutenir un très grand nombre de messages échangés, et est-il possible de répartir leur traitement sur plusieurs ordinateurs ?
  • La persistance : Si la partie centrale de l’architecture venait à tomber en panne, est-ce que les messages qui étaient en attente de traitement sont perdus ou bien sont-ils conservés pour être traités au redémarrage du système ?

Pour la mise en œuvre, je vais explorer 2 manières de faire, chacune avec 2 technos différentes :

  • L’utilisation d’une base de données dans laquelle on écrit les messages, avec un système de polling (“attente active” ou “scrutation” en français) pour instancier leurs traitements.
    1. De manière artisanale, avec l’utilisation de la crontab.
    2. Avec l’outil Resque.
  • L’utilisation d’un système intégrant une file de messages.
    1. Avec l’outil Gearman.
    2. Avec l’outil Beanstalkd.

Base de données ou file de messages ?

Parmi les quatre méthodes que je vais détailler, trois d’entre elles reposent d’une manière ou d’une autre sur l’utilisation d’une base de données. Continuer la lecture de « Architectures distribuées et traitements asynchrones »

OVH, je t’aime moi non plus

Je m’apprêtais à écrire un article sur l’utilisation que je fais de l’offre Public Cloud d’OVH. Mais ce matin (nous sommes le 29 janvier 2018 au moment où je commence à écrire ces lignes), l’hébergeur rencontre de gros soucis qui empêchent de se connecter à son interface utilisateur, et donc de gérer les machines. Je me suis dit que ça pourrait être une bonne occasion de revenir sur les récents problèmes techniques qui ont entaché la réputation de la licorne française de l’hébergement (la dernière fois que j’en avais parlé, c’était il y a presque 7 ans maintenant).

Pour faire un tour d’horizon un peu complet, je vais remonter au 29 juin 2017, il y a 7 mois exactement. Ce jour-là, OVH subissait une très grosse panne, qualifiée par son président comme étant le «pire incident depuis 2006».

29 juin 2017

Je vous invite à lire le ticket de travaux qui a été écrit par Octave Klaba, ainsi que le post-mortem publié sur le blog d’OVH, qui récapitule ce qu’il s’est passé à ce moment. En résumé, il y a eu une fuite de liquide dans le refroidissement liquide d’un serveur, qui a coulé sur une baie de disque qui s’est éteinte après avoir détecté un problème électrique. Cette baie n’aurait pas dû se trouver à côté d’équipements refroidis par eau. De plus, le système de détection de fuites d’eau n’a pas pu alerter les techniciens, car il avait été mis à jour le même jour, et cette mise-à-jour était boguée ; du coup, les techniciens sont intervenus tardivement.

Ce qui n’est pas dit clairement dans le post-mortem, mais qui était dit dans les premières communications effectuées par OVH ce jour-là (et depuis modifiées), c’est que les techniciens semblaient ne pas avoir conscience du problème hydraulique. Ils ont essayé de redémarrer la baie de disque, et ont incriminé le matériel.

L’impact de ce problème technique est la mise hors ligne d’un très grand nombre de sites web. La baie de disques était utilisée pour le stockage des bases de données de l’offre d’hébergement mutualisé ; sans base de données, la plupart des sites ne sont plus fonctionnels. On parle de cinquante mille sites impactés (cf. cet article sur ZDNet), qui plus est en période de soldes.

En urgence, une baie de secours a été acheminée de Roubaix à Paris pour tenter de réinstaller les disques durs, et en parallèle les sauvegardes quotidiennes ont été restaurées sur un autre système. La deuxième baie n’a semble-t-il pas fonctionné, et la restauration des données a duré jusqu’à la fin du jour suivant.

Au passage, la manière dont OVH a communiqué a été largement critiquée (le post-mortem tente d’expliquer tout ça).

Mon analyse tient en 4 points :
1) Le geste commercial est plutôt symbolique (2 mois d’hébergement offerts aux clients impactés), mais c’est un symbole fort. À 4 euros par mois d’hébergement mutualisé en moyenne, cela fait un réel manque à gagner pour OVH, mais qui reste une goutte d’eau pour une entreprise de cette taille.

2) Les clients qui font tourner leurs sites de e-commerce sur de l’hébergement mutualisé devraient avoir honte et ne pas trop s’étonner de rencontrer des problèmes. Faire tourner leur business sur une offre à 4€/mois sans garantie de service, c’est comme construire une boutique physique avec des murs en carton : on n’est pas stupéfait lorsque ça s’écroule sous la pluie.

3) Un système de monitoring et d’alerte en datacenter qui est mis à jour mais qui n’est pas testé, vérifié, validé ? Vraiment ?

4) Passons sur le fait que la baie n’aurait pas dû rester à côté du refroidissement liquide, ça a été bien expliqué par OVH. Mais les données ont été restaurées sur un système qui a été mis en place en une nuit ? Soit ça veut dire qu’ils auraient pu le faire bien plus tôt, soit ça implique qu’ils ont reconstruit un truc à la va-vite, qui ne respecte pas leur niveau de qualité habituel ; sauf que je crains que les sites mutualisés tournent encore sur ce système « temporaire »… Est-ce que ça pétera encore un de ces jours ?

État des lieux avant l’incident : Une installation temporaire (baie posée à côté de serveurs refroidis par eau) qui a été oubliée en l’état, même si elle n’était pas « au standard ».
État des lieux après l’incident : Une installation réalisée en quatrième vitesse, sans qu’on ait plus d’information sur le fait qu’elle soit « au standard ».

9 novembre 2017

Donc, l’incident de juin était censé être le pire vécu par OVH depuis plus de 10 ans. Manque de chance, 4 mois plus tard a lieu un incident dont l’ampleur est bien plus importante. En fait il s’agissait de deux incidents arrivés en même temps, dans deux datacenters différents. Résultat, il y a pas mal de littérature officielle sur le sujet :

À Strasbourg, le câble d’alimentation électrique a été endommagé, et les groupes électrogènes n’ont pas démarré, et donc tous les serveurs se sont éteints. Voilà, c’est aussi simple que ça. Ah oui, il faut savoir que la dernière fois que les groupes électrogènes avaient été testés, c’était au mois de mai. Soit 6 mois auparavant ! Enfin, pour être plus précis, les groupes électrogènes sont (censés être) testés « à vide » tous les mois ; ce qui datait du mois de mai, c’est le test d’alimentation des datacenters par l’alimentation de secours en conditions réelles. Après investigations, il s’avère que l’automate de démarrage des groupes électrogènes était bloqué en position verrouillée ; pas plus d’explication, mais on peut imaginer que cela datait du test du mois de mai (comme quoi il faut tester plus souvent en conditions réelles).

Il faut aussi savoir que la manière dont les datacenters de Strasbourg ont été construits n’est pas « au standard » d’un point de vue électrique. Certains datacenters ont une arrivée électrique simple (non redondée), d’autres encore ont été chaînés (entraînant un effet domino si le premier datacenter de la chaîne rencontre un souci).

À Roubaix, siège historique de l’entreprise qui représente un nœud d’interconnexion majeur, les 6 fibres optiques sont tombées d’un coup. La raison tient du fait que ces fibres sont gérées par deux équipements réseau identiques, qui ont perdu simultanément leurs configurations. Même en expliquant qu’il y a eu une cascade de bugs, démarrée par une surcharge CPU, je pense qu’on ne saura jamais le fin mot réel de l’histoire.

On peut toutefois remarquer qu’OVH avait initié deux semaines auparavant le remplacement des cartes contrôleurs, les nouvelles ayant deux fois plus de puissance CPU et deux fois plus de RAM que celles en place. Cela laisse penser que la surcharge CPU de ces cartes n’était pas un phénomène nouveau. Dans le ticket d’incident en date du 10 novembre, il est écrit «Nous avons reçu les 2 premières hier pour Roubaix»… Donc ils les ont reçues le jour de l’incident ; c’est dommage que ce remplacement n’ait pas eu lieu juste quelques jours plus tôt.

Le souci avec ces deux incidents est qu’en arrivant en même temps, ils ont créé le pire des scénarii. Si vous avez des serveurs dédiés et que vous redoutez qu’une panne survienne dans le datacenter où ils sont hébergés, vous pouvez louer des serveurs dans plusieurs datacenters différents − éventuellement chez plusieurs hébergeurs différents. Mais pour rediriger le trafic, vous allez avoir besoin de pouvoir configurer certaines choses : soit modifier les IP fail-over/load-balancing (dans le cas où vous avez des serveurs dans des datacenters OVH), soit modifier les DNS pour faire pointer sur des serveurs différents (dans le cas où vous passez par des hébergeurs différents).
Le bug ayant affecté Strasbourg a fait tomber des serveurs. Mais celui de Roubaix a rendu indisponible l’interface client d’OVH. Il était alors impossible de modifier la moindre configuration. C’est la double peine : vos serveurs sont HS, mais vous ne pouvez même pas rediriger les requêtes vers d’autres serveurs.

La communication d’OVH est aussi à prendre avec du recul. On nous dit que l’incident de Strasbourg a duré 4 heures, et que celui de Roubaix a duré 2 heures 20 minutes.

Pourtant, si on regarde dans le détail, 9 heures après le début de l’incident de Strasbourg, seuls 71% des serveurs avaient été redémarrés ; au bout de 30 heures, 99% des serveurs étaient redémarrés ; les derniers serveurs ont été redémarrés après 4 jours d’interruption. En effet, ce genre de problème électrique peut faire griller des serveurs, dont certaines parties physiques doivent alors être remplacées.
Et concernant Roubaix, il faut plutôt attendre 6h30 avant que la plate-forme soit complètement stable.

Dans tous les cas, il se pose la même question que pour l’incident du 29 juin : les solutions mises en place sont-elles pérennes ou bien sont-elles juste des bricolages effectués rapidement pour que les services soient en ligne ? Dans le deuxième cas, est-ce que le temps a été pris ensuite pour revenir dessus et tout remettre d’équerre ?

Mon analyse :
Là encore, on voit que la situation résulte de choses connues et anticipables par OVH. Que ce soit l’installation électrique pas « au standard » à Strasbourg, ou les cartes CPU qui devaient être changées à Roubaix. L’explication tient évidemment au fait qu’il s’agit d’investissements coûteux dans un cas (pour Strasbourg, Octave parle de 4-5 millions d’euros) et délicat dans l’autre cas (on ne touche pas l’équipement réseau d’un nœud aussi important que Roubaix sans quelques précautions).
Mais dans les deux cas, c’était identifié et important. Ils ne l’ont pas fait avant que ça leur pète à la gueule, c’est tout.

6 décembre 2017

Là ce n’est vraiment pas de chance. Suite à l’incident du 9 novembre, ils ont prévu une intervention pour corriger les failles (isolation des équipements réseau en trois groupes séparés, pour qu’un souci ne puisse plus impacter qu’un tiers du trafic dans le pire des cas). Sauf qu’en préparant cette opération de maintenance, tout est retombé de nouveau − de manière identique au 9 novembre.

Les liens d’info :

Le résultat, c’est 4 heures d’interruption de service de nouveau.

On peut se dire que c’est quelque chose qui peut arriver. Rien n’est parfait, on est d’accord. Mais là, ça reste étrange : soit la plate-forme était tellement instable qu’elle menaçait de s’écrouler à tout moment, soit cette intervention aurait dû être menée avec beaucoup plus de précautions. Peut-être qu’il aurait fallu être plus précautionneux, justement parce que la plate-forme était instable.

Est-ce que des équipements réseau identiques avaient été montés en labo, pour répéter l’opération avant de la tenter « pour de vrai » ? On ne le saura jamais. De la même manière qu’on ne sait pas si l’opération a été refaite avec succès depuis…

24 janvier 2018

Dans le datacenter de Gravelines, un onduleur est tombé en panne. L’onduleur se rendant compte de son propre défaut, il laisse passer le courant en se by-passant lui-même. Malheureusement, le souci électrique de cet onduleur était « trop important », ce qui fait que le tableau électrique a arrêté de l’alimenter ; et donc les serveurs qui étaient derrière l’onduleur n’étaient plus alimentés à leur tour.

À lire :

C’est quelque chose qui peut arriver. Sauf que justement, ce type d’incident devrait être bien géré. Là, 1150 serveurs sont tombés. Le problème s’est déclaré à 23h02 ; plus d’un millier de serveurs avaient été redémarrés à 5h00 du matin (soit une interruption de 6 heures), mais ils ont fini de redémarrer tous les serveurs impactés le lendemain à 19h37 (plus de 20 heures d’interruption). Mais il faut savoir que ces serveurs étaient sans protection électrique par l’onduleur, puisqu’à 21h45 le support indiquait «Nous avons sorti l’UPS7 de l’installation. Nous avons procédé à son remplacement par un modèle neuf. Le nouvel UPS7 va être mis en fonctionnement.»

Là encore, on ne sait pas quand il a été mis en fonctionnement. Mais surtout, on ne sait pas si ce genre d’incident peut se renouveler. Un onduleur qui déconne, encore une fois, ça peut arriver. Mais ça ne devrait pas avoir ce genre d’impact et prendre autant de temps à être corrigé.
Enfin, peut-être que je me trompe.

29 janvier 2018

Et donc, dernier gros incident en date, durant lequel il semblerait qu’un équipement réseau soit tombé en panne à 7h33. Malheureusement, le ticket d’incident est assez cryptique. J’en comprends que c’est un équipement qui travaillait en binôme («L’autre membre du couple p19-94-n6 est toujours fonctionnel») ; mais dans ce cas, pourquoi celui qui n’est pas tombé en panne n’a pas pris correctement la relève ? Est-ce que ces équipements sont sous-dimensionnés au point que même en étant redondés, un élément d’une paire ne peut pas faire le travail de la paire entière ?

Le résultat est que tous les hébergements mutualisés sur Paris sont devenus indisponibles. Mais aussi, l’interface de gestion est devenue indisponible (ainsi que l’API OVH qui permet de gérer les services sans l’interface web), exactement comme lors de l’incident de novembre à Roubaix. Ça devient un peu lourd, d’ailleurs ; que le souci soit à Paris ou à Roubaix, on subit les mêmes effets…

Toujours d’après le ticket, les services étaient de nouveau fonctionnels à 15h45, soit 8 heures d’interruption de service.

Sur cet incident, la communication a été particulièrement mauvaise (en tout cas pour le moment). Le ticket ne dit pas grand-chose, et les échanges sur Twitter se sont révélés très succincts.

D’ailleurs, lorsque des gens ont demandé sur Twitter s’il y aurait des indemnisations, la réponse était «c’est du mutualisé, donc pas de SLA, donc pas d’indemnisation». Je peux comprendre que ça commence à faire beaucoup d’indemnisation pour l’entreprise, mais si l’interruption du mois de juin avait duré 30 heures et entraîné 2 mois d’hébergement gratuit, c’est un peu dur de ne rien donner du tout cette fois-ci en échange d’une interruption de 8 heures.

Le manque de communication sur cet incident (pas de post-mortem notamment, mais on peut croiser les doigts qu’il arrive dans les jours ou semaines à venir) est étonnant et un peu problématique. On ne peut pas banaliser cette perturbation : si les hébergements mutualisés étaient importants en juin, ils le sont toujours aujourd’hui ; et le fait de ne pas pouvoir gérer ses serveurs et ses DNS est assez inadmissible.

Conclusion

C’est bien de soutenir l’innovation et de lever des millions (250 en 2016 et 400 en 2017), mais il faut que la base soit solide sinon tout s’écroule.

Le service DownDetector permet de faire quelques statistiques rapides et perfectibles, mais quand même intéressantes. Si on s’intéresse toujours à cette période de 7 mois comprise entre le 29 juin 2017 et le 29 janvier 2018 (soit 214 jours), on obtient :

  • OVH : 34 jours avec des incidents, soit 16%
  • AWS : 16 jours avec des incidents, soit 8%
  • Salesforce : 6 jours avec des incidents, soit 2,8%
  • Microsoft Azure : 3 jours avec des incidents, soit 1,4%
  • Rackspace : 3 jours avec des incidents, soit 1,4%

Ça laisse quand même à réfléchir.

On peut remarquer que plusieurs incidents sont le résultat de choses qui auraient dû être anticipées. Et que l’argent économisé en ne lançant pas les chantiers à temps est perdu en devant payer des jets privés pour dépêcher des équipes techniques en urgence (sans parler des coûts humains).

Mais faut-il quitter OVH pour autant ? À chacun de se faire sa propre idée, mais la réponse ne peut évidemment pas être binaire. Pour ma part, j’utilisais déjà OVH et AWS en parallèle, en choisissant les services chez l’un ou chez l’autre en fonction de ce qui me semblait pertinent. Et, comme je l’ai écrit en préambule, je comptais écrire un article au sujet de l’offre Public Cloud que je trouve pas mal du tout.

Pour le moment, ces incidents ne me font pas quitter OVH. Bien sûr, ils me rendent plus attentif, mais les offres restent d’une compétitivité incroyable. On peut se dire qu’il vaut mieux payer plus cher ailleurs pour avoir un service qui ne tombe absolument jamais, mais je n’en suis pas encore là ; les économies restent largement supérieures aux inconvénients. D’un autre côté, si un jour tout tombe en rade pendant une longue durée, je ne tiendrai pas le même discours…

Il est assez amusant de voir que le bilan que je tirais en 2011 reste complètement d’actualité. Il faut de toute façon se préparer à ce que les choses se passent mal, quel que soit l’hébergeur.

Edit du 31 janvier 2018 : Sur Twitter, le support OVH vient de me répondre, me disant qu’aucun post-mortem n’est prévu pour l’incident de cette semaine. C’est assez honteux.

Pour la petite histoire, je vous copie-colle un extrait du post-mortem qui avait été écrit suite à l’incident de juin :
«Cet épisode a révélé la nécessité de mieux coordonner les informations durant un incident, afin de fournir aux utilisateurs une explication qui ne soit pas seulement transparente, c’est le minimum, mais aussi cohérente, compréhensible par tous et régulièrement actualisée.»

C’est amusant de voir comment les bonnes résolutions sont vite oubliées.

La suite de ce paragraphe était : «Au-delà de la refonte prévue du site travaux.ovh.net, au profit d’une interface plus évoluée, et de la possibilité prochaine d’être alerté de façon personnalisée en cas d’incident impactant l’un de ses services, une équipe spécialisée sera mise en place pour délivrer aux utilisateurs une information dont la qualité est adaptée à ce type de situation.»

Les trois choses évoquées ici (nouveau site de suivi des incidents, alertes personnalisées et équipe dédiée à la communication de crise) ne sont toujours pas en place, 7 mois après. Malheureusement, durant cette période, OVH a traversé sa pire suite de problèmes techniques. Le truc, c’est que les clients − à commencer par moi − sont passablement patients et sympas tant que la communication reste au top ; là j’ai l’impression que ça se dégrade, alors qu’il faudrait au contraire augmenter l’effort.

Edit du 02 février 2018 : Et on recommence. Aujourd’hui, nouvelle impossibilité de se connecter à l’interface client (cf. ici, et encore ). Pas de ticket d’incident qui semble en parler sur le site travaux.ovh.net. Pas d’infos sur Twitter, à part une réponse succincte à l’un des messages que j’ai mis en lien et qui dit en gros «on va regarder», puis plus rien. Bref, le grand néant…

Dispak : Gestion et déploiement de tags/branches Git + serveurs/services

Après Arkiv (dont je vous avais parlé dans un précédent article), voici un nouvel outil que j’ai développé pour mes besoins et que je publie sous licence libre : Dispak

Mon besoin était :

  • Créer facilement des tags sur un repository Git, en effectuant un certain nombre de vérifications et actions automatiques (état du repo, minification de fichiers JS/CSS, envoi de fichiers statiques sur Amazon S3, …).
  • Déployer les tags sur mes serveurs, là aussi en automatisant des choses (génération de fichiers de configuration, migration de schéma de base de données, installation de crontab, installation de fichiers de configuration Apache, …).

Et je voulais deux choses en plus :

  • Afficher la liste des tags de manière plus pratique que ce qu’offre Git.
  • Gérer les branches : les créer (en local et en distant), les effacer, les merger sur le master, et backporter les nouveautés du master vers les branches.

Je voulais aussi pouvoir ajouter facilement des fonctionnalités supplémentaires, pour éviter d’avoir plein de scripts qui se baladent dans tous les coins (qu’il s’agisse d’ajouter un utilisateur dans ma base de données, générer de la documentation à partir du code, effacer des fichiers temporaires, redémarrer des services sous certaines conditions, etc.).

N’hésitez pas à aller lire la documentation complète sur GitHub et à installer Dispak.

La genèse

J’avais commencé à gérer tout ça avec un fichier Makefile. J’utilise beaucoup les Makefiles, et pas seulement pour compiler du code source. C’est très pratique dès que vous voulez faire un script shell qui peut prendre des options sur la ligne de commandes. Et encore plus pratique pour centraliser plusieurs actions qui seraient habituellement séparées en plusieurs scripts.
Mais assez rapidement, je me suis retrouvé avec un très gros fichier, et même si j’avais trouvé un moyen pour mutualiser le code (des règles « privées » qui sont appelées par plusieurs règles « publiques »), ça devenait un peu acrobatique. C’est là que je me suis dit qu’un vrai outil serait préférable.

J’ai défini le périmètre :

  • Je voulais faire un outil vraiment portable, donc pas développé dans un langage de programmation de haut niveau dont l’interpréteur n’est pas forcément présent. Naturellement, j’en suis venu au shell. Par contre, je me suis quand même autorisé à utiliser le bash qui offre pas mal de facilitées bien utiles tout en étant très répandu.
  • Je voulais pouvoir facilement ajouter de nouvelles fonctionnalités à l’outil, que ce soit de manière globale ou individuellement par projet de développement. Cela a conduit à le rendre modulaire, avec une attention particulière sur la simplicité d’écriture des modules.
  • Je me suis autorisé certains choix quant à la manière de gérer les types de plates-formes, la normalisation des tags, la manière de gérer les migrations de bases de données, ou encore les logiciels utilisés. Je vais revenir sur ces différents points.

Concepts-clés

Dispak utilise la gestion sémantique de versions et la numérotation impaire pour les versions instables.
Les tags sont donc nommés sous la forme X.Y.Z :

  • X : Numéro de version majeure. Dois être incrémenté en cas d’évolution importante ou lorsque la compatibilité descendante n’est plus assurée.
  • Y : Numéro de version mineure. Dois être incrémenté pour chaque évolution de fonctionnalité.
    • Les numéros pairs (0, 2, 4, …) sont utilisés pour les versions stables.
    • Les numéros impairs (1, 3, 5, …) sont utilisés pour les versions instables.
  • Z : Numéro de révision. Dois être incrémenté lors de résolutions de bugs.

Dispak gère 3 types de plates-formes :

  • dev : Poste de développement
  • test : Serveur de test/staging/pré-production
  • prod : Serveur de production

La plate-forme est détectée automatiquement à partir du nom de la machine, ou peut être indiquée dans un fichier de configuration. Si la machine est nommée ‘test‘ suivi de chiffres, elle est considérée comme un serveur de test ; si son nom commence par ‘prod‘, ‘web‘, ‘db‘, ‘cron‘, ‘worker‘, ‘front‘ ou ‘back‘ (suivi de chiffres), c’est un serveur de production ; autrement on la considère comme un poste de développement.

Seules les versions stables peuvent être installées en production. Les versions instables ne peuvent être installées qu’en test/pré-production uniquement. C’est une sécurité qui peut être très utile.
De la même manière, la fonctionnalité de copie des fichiers statiques vers Amazon S3 n’est exécutée que pour les tags stables (mais peut être forcée via le fichier de configuration).

Fonctionnalités de base

Affichage de l’aide

Cliquez pour agrandir

Lister les tags d’un projet en version succincte

Cliquez pour agrandir

Liste des tags en version détaillée

Cliquez pour agrandir

Création de tags

La création de tags suit plusieurs étapes (certaines étant optionnelles) :

  • Suggestion du numéro de version (ou vérification si un numéro est fourni).
  • Vérification de la branche et des fichiers non commités ou non poussés.
  • Exécution de scripts de pré-packaging.
  • Commit du fichier de migration de base de données.
  • Minification JS/CSS.
  • Envoi des fichiers statiques sur Amazon S3.
  • Exécution de scripts de post-packaging.

Déploiement de tags

L’installation d’une nouvelle version automatise le process suivant :

  • Vérification qu’un tag instable n’est pas déployé en production.
  • Exécution de scripts pré-installation.
  • Installation de fichier crontab.
  • Migration de schéma de base de données.
  • Installation de fichiers de configuration Apache.
  • Gestion des droits d’accès aux fichiers.
  • Exécution de scripts post-installation.

Gestion des branches

Dispak offre en plus quelques raccourcis par-dessus Git :

  • Création de branches : Les branches sont toujours créées à partir de la branche master, soit depuis son état courant, soit depuis un tag donné.
  • Effacement de branches
  • Merge : Pour rapatrier sur le master les évolutions d’une branche.
  • Backport : Pour récupérer sur une branche les dernières nouveautés du master.

Ajout de fonctionnalités

Il est très facile d’ajouter des modules supplémentaires, qui peuvent être soit utilisables pour tous les projets (en plaçant le module dans le répertoire d’installation de Dispak), soit juste pour un projet particulier (en mettant le module dans l’arborescence du projet).

Il suffit de créer un petit fichier en shell Bash dont la version minimale ne contient qu’une variable et deux fonctions (l’une pour l’affichage de l’aide, l’autre pour l’exécution du module).

Voici un exemple très rapide (détaillé dans la documentation) :

#!/bin/bash

# Rule's name.
RULE_NAME="minimal"

# Show help for this rule.
rule_help_minimal() {
	echo "   dpk $(ansi bold)minimal$(ansi reset)"
	echo "       $(ansi dim)Minimal rule that displays the current user login.$(ansi reset)"
}

# Execution of the rule
rule_exec_minimal() {
	echo "Current user login: " $(id -un)
}

Pour un usage plus avancé, le module peut attendre des paramètres sur la ligne de commande, qui peuvent être obligatoires ou optionnels ; Dispak se charge alors de vérifier que tous les paramètres obligatoires sont bien fournis, et qu’il n’y a aucun paramètre non attendu.

Une vingtaine de fonctions shell sont utilisables. Il y a des fonctions servant à l’affichage des données, d’autres facilitant les vérifications (on est sur une branche/le master, tous les fichiers ont été commités/poussés, …), la récupération de données (branche courante, tag courant, prochain numéro de version, …) ou encore le traitement général (affichage de message d’alerte, arrêt des traitements, etc.).

La documentation est assez complète, vous verrez qu’il est vraiment facile de créer des modules supplémentaires. Si vous n’êtes pas fan des scripts shell, vous pouvez faire un code minimal qui se contentera d’appeler vos programmes externes.

Configurer le nom d’un serveur

Un petit article technique sur quelque chose que j’ai longtemps considéré comme un point de détail, mais qui peut s’avérer important dans quelques rares cas.

Lorsqu’on configure un ordinateur, on lui donne un nom. Jusque-là, tout va bien.

Je pourrais disserter sur la manière de nommer les serveurs. Certaines personnes aiment donner des noms de planètes, des noms d’animaux, des noms de personnages issus de livres ou de films, ou simplement des prénoms ; personnellement, je préfère m’en tenir à des noms techniques qui retirent toute ambiguïté. (“db1”, “db2”… pour les bases de données ; “web1”, “web2”… pour les frontaux web ; et ainsi de suite). Mais ce n’est pas le but de cet article.

Pour l’exemple, on va dire que nous installons un serveur nommé “test1”, faisant partie du domaine “exemple.fr”. Lors de la configuration du serveur, il y a 2 fichiers à modifier (/etc/hostname et /etc/hosts). Leur contenu est moins évident qu’il n’y paraît, et non seulement on peut prendre en considération des documentations comme la RFC 1178, mais il semblerait que les choses soient différentes suivant le système Unix utilisé, voire même la distribution Linux… Je vais donner ici des infos pour les systèmes dérivés de Debian, comme Ubuntu par exemple.

Le fichier /etc/hostname doit contenir le nom de la machine, sans le nom de domaine. Dans notre exemple, ce sera simplement “test1”.
Pour s’assurer qu’il soit pris en compte sans attendre le redémarrage de la machine, il faut en plus exécuter la commande suivante (en tant que root) :

# hostname test1

On peut vérifier que le nom est bien pris en compte en lançant cette commande :

# hostname

Elle devrait afficher simplement :

test1

Le cas du fichier /etc/hosts est un peu plus délicat (enfin bon, n’exagérons rien). En fait, le système se base sur ce fichier pour connaître le nom complet (FQDN = Fully Qualified Domain Name) de la machine.
Donc, on début du fichier, on écrira les deux lignes suivantes :

127.0.0.1        test1.exemple.fr        test1
127.0.0.1        localhost.localdomain   localhost

On peut vérifier que tout est bon en exécutant :

# hostname --fqdn

On devrait alors obtenir :

test1.exemple.fr

Conclusion : Tout cela ne méritait peut-être pas un article. Mais dans les liens que j’ai fournis plus haut, on trouve des témoignages intéressants (un employé de Digg dit avoir rendu le site indisponible pendant 2 heures à cause d’une modification innocente du fichier /etc/hosts ; et il semblerait que certaines applications d’Oracle nécessitent une configuration différente), donc c’est mieux de savoir ce qu’on fait.

Arkiv : Sauvegarde de fichiers et bases MySQL + archivage sur Amazon S3 et Amazon Glacier

Pour mes besoins, j’ai créé un programme qui sert à faire des sauvegardes de fichiers et de bases de données MySQL. Ces sauvegardes sont stockées en local et peuvent être archivées sur Amazon S3 ainsi que sur Amazon Glacier.

Ce programme s’appelle Arkiv et son code source est disponible sur GitHub. Il est placé sous une licence libre très permissive.

Pourquoi avoir développé ce logiciel ?

J’utilisais depuis longtemps le programme Backup-Manager, qui me permettait de sauvegarder mes fichiers et mes bases de données en local et sur un serveur FTP, puis sur Amazon S3. Ce programme est très pratique ; il existe depuis plus de 10 ans (il me semble que je l’utilise depuis tout ce temps), il offre pas mal d’options de configuration, et il est disponible dans les paquets Ubuntu.
Mais avec le temps, mes besoins ont évolué, et certaines limites sont apparues : Il ne permet pas de faire plus d’une sauvegarde par jour, ne supporte pas tous les datacenters Amazon (les plus récents ne sont pas compatibles avec les anciennes versions de l’API), ne permet pas d’archiver sur Amazon Glacier et il manque de souplesse au niveau de la purge des sauvegardes.

Vous connaissez sûrement Amazon S3, mais Amazon Glacier est plus récent et moins connu. Ce sont deux services AWS (Amazon Web Services), l’offre de Cloud Computing d’Amazon.
Amazon S3 peut être vu comme un espace de stockage de fichiers de taille illimitée à haute disponibilité. Les données sont disponibles en temps réel. On paye suivant la quantité de données stockée, pour un coût assez faible (pour donner un ordre d’idée, stocker 500 GO de données dans le datacenter de Londres coûte environ 12$ par mois), auquel s’ajoutent des frais suivant le nombre d’échanges et la quantité de données échangées. Un très grand nombre de services stockent leurs fichiers sur Amazon S3 pour ne plus avoir à s’embarrasser de la problématique du stockage.
Amazon Glacier est lui aussi un espace de stockage de fichiers illimité. Sauf que contrairement à S3, il a été conçu spécifiquement pour les stockages à très longue durée, qui n’ont pas forcément besoin d’être lus en temps réel. Le coût est extrêmement faible (compter 2,25$ par mois pour stocker 500 GO de données à Londres), mais en contrepartie la récupération de données peut prendre entre quelques minutes et 12 heures (suivant le tarif de récupération choisi).

Mon souhait était de pouvoir fonctionner (pour mes serveurs personnels) de la manière suivante :

  • Sauvegarder tous les jours certains répertoires et certaines bases MySQL.
  • Écrire ces sauvegardes sur le disque local, mais les copier aussi sur Amazon S3 et sur Amazon Glacier.
  • Garder les sauvegardes sur le disque local pendant 3 jours avant de les effacer, pour que les versions les plus “fraîches” soient accessibles immédiatement en cas de problème.
  • Attendre 2 semaines avant d’effacer les archives stockées sur Amazon S3, pour permettre une récupération relativement facile de données assez récentes, sans pour autant payer le stockage de données périmées.
  • Garder sans limite de temps les archives stockées sur Amazon Glacier, pour avoir une trace de toutes les versions de mes fichiers.

Ça a l’air simple, vu comme ça, mais je n’ai pas trouvé d’outil me permettant de le faire simplement.
Et pour compliquer un peu les choses, mes besoins professionnels vont plus loin :

  • Sauvegarder toutes les heures certains répertoires et certaines bases MySQL.
  • Écrire ces sauvegardes en local, ainsi que sur Amazon S3 et Amazon Glacier.
  • Garder toutes les sauvegardes (donc 24 sauvegardes par jour) en local pendant 3 jours ; puis n’en garder qu’une sur six (donc 4 sauvegardes par jour) pendant 4 jours ; puis une par jour pendant une semaine.
  • Garder toutes les sauvegardes sur Amazon S3 pendant 2 semaines ; puis une sur quatre (6 sauvegardes par jour) pendant 2 semaines ; puis une par jour pendant un mois.
  • Stocker les archives sur Amazon Glacier sans limite de temps.

Donc là il faut être capable de faire une sauvegarde toutes les heures, mais aussi gérer les purges (en local et sur Amazon S3) de manière très fine.

J’ai donc pris la décision de créer mon propre script de sauvegarde et d’archivage. Les habitués de ce blog se souviennent que j’ai écrit récemment un article sur l’utilisation de mysqldump, fournissant même un script de sauvegarde de bases MySQL qui allait jusqu’à copier les sauvegardes sur Amazon S3. Je suis juste passé à l’échelle supérieure.

Pour un maximum de portabilité, j’ai développé le programme en shell Bash. C’est le shell le plus courant sur les systèmes Unix/Linux.  Ainsi, pas besoin d’interpréteur spécifique (ce qui serait le cas si je l’avais développé en PHP) et pas besoin de compilation (ce qui aurait été nécessaire si je l’avais codé en C).
Pour être le plus ergonomique possible − dans la mesure de ce qu’il est possible de faire en ligne de commande − j’ai pris le temps de faire deux choses :

  • La configuration est interactive. La génération du fichier de configuration se fait en répondant à des questions (dont une majorité proposant des réponses par défaut) et non pas en remplissant à la main un fichier vide. Et le script ajoute automatiquement (enfin, si vous le souhaitez uniquement) Arkiv en Crontab pour qu’il soit exécuté automatiquement à la fréquence que vous souhaitez.
  • J’utilise les capacités ANSI des terminaux pour faire un peu de mise-en-page des textes, avec de la couleur, des textes en gras ou en vidéo-inverse. Que ce soit pour la génération du fichier de configuration ou pour les logs d’exécution, cela facilite grandement la compréhension.

Et pour le nom du projet, j’ai choisi un mot qui veut dire “archive” dans plusieurs langues (notamment scandinaves ; un peu comme Skriv, qui veut dire “écrire” dans ces mêmes langues).

Comment installer Arkiv

La directive est assez simple, et expliquée sur la page GitHub du projet.
Pour commencer, il faut cloner le repository :

# git clone https://github.com/Amaury/Arkviv

Si vous souhaitez archiver sur Amazon S3 et Amazon Glacier : Avant de lancer le programme de configuration, il faut créer un “bucket” Amazon S3 et un “vault” Amazon Glacier (dans le même datacenter), puis créer un utilisateur IAM et lui donner les droits en lecture-écriture sur ce bucket et ce vault.

Ensuite on lance la configuration :

# cd Arkiv; ./arkiv config

Voici une copie d’écran d’un exemple d’installation :

Cliquez pour agrandir

(Dans cet exemple, vous pouvez voir que les sauvegardes sont faites toutes les heures, archivées sur Amazon S3 et Amazon Glacier, et que les purges sont configurées assez finement.)

Il est possible de faire en sorte que les sauvegardes ne se fassent pas tous les jours et toutes les heures, mais de choisir une fréquence à la carte. Et comme il est possible d’utiliser plusieurs fichiers de configuration différents (en fournissant leurs chemins en paramètre), il est possible de mettre en place des politiques de sauvegarde assez complexes, pour coller au plus près des besoins.

À la fin de ce processus, le fichier de configuration est créé, et le programme a été ajouté en Crontab pour s’exécuter automatiquement.

À l’exécution

Lorsque le programme s’exécute, il passe par plusieurs étapes :

  1. Démarrage
    1. Arkiv est lancé par la Crontab.
    2. Il crée un dossier sur le disque local, qui accueillera les fichiers sauvegardés.
  2. Sauvegarde
    1. Tous les chemins listés dans la configuration sont compressés et le résultat est enregistré dans le dossier dédié.
    2. Si la sauvegarde MySQL est activée, les bases de données sont dumpées et sauvegardées dans le même dossier.
    3. Des checksums sont calculés pour tous les fichiers sauvegardés.
  3. Archivage
    1. Si l’archivage sur Amazon Glacier a été activé, tous les fichiers de sauvegarde y sont copiés. Pour chacun d’eux, un fichier JSON est créé, contenant la réponse renvoyée par Amazon. Ces fichiers contiennent les identifiants nécessaires à la récupération des fichiers.
    2. Si l’archivage sur Amazon S3 a été activé, le dossier et tout ce qu’il contient (fichiers de sauvegarde, fichier de checksum, fichiers JSON d’Amazon Glacier) y sont copiés.
  4. Purge
    1. Effacement sur le disque local des fichiers de sauvegarde qui ont atteint l’âge défini.
    2. Si l’archivage sur Amazon S3 a été activé, les fichiers de sauvegarde qui ont atteint l’âge défini sont effacés. Les fichiers de checksum et les fichiers JSON d’Amazon Glacier ne sont pas effacés, afin de permettre la récupération des fichiers archivés sur Amazon Glacier et d’en vérifier l’intégrité.

Sauf en cas de problème, l’exécution du programme se déroule de manière silencieuse.

Voici un exemple de ce que vous pourrez trouver dans le fichier de log :

Cliquez pour agrandir

Vous pouvez voir que la « mise-en page » est là pour rendre les logs faciles à lire. Les éventuelles alertes et erreurs sont mises en avant grâce à de la couleur et des pictos.

Améliorations futures

Comme toujours, j’ai développé ce logiciel pour répondre à mes besoins, et je le rends accessible à tous sous licence libre parce qu’il pourra rendre des services à d’autres personnes que moi.
Je n’ai pas envie d’en étendre les fonctionnalités. L’archivage par FTP ou SCP, ou sur des plates-formes Cloud autres que celle d’Amazon, pourrait être pratique pour certains, mais cela complexifierait beaucoup le code. Arkiv ne se destine pas à être un outil obèse capable de tout gérer ; je préfère qu’il reste un outil avec des fonctionnalités réduites, mais qu’il adresse les bonnes fonctionnalités, et qu’il les accomplisse le mieux possible.
Donc on verra.

En attendant, n’hésitez pas à le tester et à me faire des retours. Si vous trouvez des bugs, vous pouvez utiliser la buglist fournie par GitHub.

Edit du 13 août : J’ai ajouté une fonctionnalité supplémentaire, l’encryption des fichiers (algorithme AES 256 bits par OpenSSL). Ça peut être utile pour les plus paranoïaques 😉

Edit du 29 août : J’ai ajouté le support des sauvegardes binaires de bases de données (utilisant l’outil xtrabackup), en global ou incrémental. Ainsi que l’écriture des logs dans syslog.

Mise-à-jour Ubuntu Server

Il y a un an, j’écrivais un article sur l’installation de serveur HTTP. J’expliquais qu’à l’époque j’avais utilisé un petit serveur virtuel sous Ubuntu 16.04.

Vous le savez sûrement, le cycle de sortie des versions de la distribution Ubuntu respecte un rythme qui fait qu’une version LTS (Long Term Support, support longue durée, avec des mises-à-jours pendant 5 ans) sort tous les deux ans, et des versions intermédiaires tous les six mois.
La version 16.04 était une version LTS, ce qui est plutôt bien pour un serveur. Mais si mettre à jour un serveur tous les 6 mois est assez contraignant, attendre 2 ans peut être embêtant si on a besoin de packages récents.

Donc comment faire pour mettre à jour une 16.04 en 17.04 ?

(toutes les commandes ci-dessous sont à exécuter en tant que root)
Pour commencer, on va s’assurer que le système est bien à jour :

# apt-get update
# apt-get dist-upgrade

Puis on va redémarrer le serveur (au cas où le kernel a été mis à jour, par exemple) :

# reboot

Une fois le serveur redémarré, on va s’assurer que le package “update-manager-core” est installé (il devrait l’être, mais on ne sait jamais) :

# apt-get install update-manager-core

On va ensuite éditer le fichier /etc/update-manager/release-upgrades car il contient une directive importante pour la mise-à-jour du système ; par défaut, l’option “Prompt” vaut “lts”, ce qui fait qu’on ne pourra mettre à jour notre système 16.04 que vers une prochaine LTS (ce sera la 18.04, qui sortira en avril 2018). Si on veut que le système se mette à jour vers n’importe quelle version (LTS ou intermédiaire), il faut que le fichier contienne la ligne suivante :

Prompt=normal

Avant de lancer la mise-à-jour, il faut savoir qu’en cas de problème les choses peuvent être un peu compliquées dans le cas où on met un serveur à jour à distance. Le programme de mise à jour va démarrer un second serveur SSH sur le port 1022 ; il faut donc d’abord ouvrir ce port dans votre firewall, ou bien l’ouvrir temporairement avec la commande :

# iptables -A INPUT -p tcp --dport 1022 -j ACCEPT

Je vous recommande aussi de lancer la mise-à-jour dans un terminal géré par le programme “screen” ; il sera ainsi possible de récupérer votre terminal même si vous vous faites déconnecter (à la condition de pouvoir rétablir une connexion SSH et que la machine n’a pas redémarré entretemps). Commencez par l’installer si nécessaire, puis exécutez-le :

# apt-get install screen
# screen

(il faut appuyer une deuxième fois sur la touche Entrée pour entrer dans le terminal)
On peut ensuite lancer la mise-à-jour :

# do-release-upgrade

Au fur et à mesure que les paquets logiciels seront mis à jour, on va vous demander si vous souhaitez mettre à jour les fichiers de configuration ou si vous souhaitez garder votre version locale. À vous de voir ce que vous préférez, mais si un logiciel change de version majeure, vos fichiers de configuration risquent de ne plus être compatibles ; il vaut peut-être mieux faire d’abord une copie de vos fichiers, d’accepter de les mettre à jour, puis de regarder les différences une à une.

Enfin, sachez que − pour cet exemple précis − la mise-à-jour se fait théoriquement de la version 16.04 vers la version 16.10 ; il faut donc renouveler l’opération une seconde fois. Toutefois, lors de mes tests sur plusieurs machines, une seule opération de mise-à-jour m’a amené directement en version 17.04.
Si vous n’êtes pas sûr, vous pouvez connaître la version installée de la manière suivante :

# more /etc/lsb-release

Autre méthode, qui fonctionne sur un grand nombre de distributions (toutes celles qui sont conformes aux spécifications de la Linux Standard Base) :

# lsb_release -a

 

Utilisation de MySQLDump

Il existe plusieurs moyens pour faire des sauvegardes de bases de données. Je ne vais pas parler ici de l’utilisation des logs binaires ni de la mise en place de réplication pour effectuer les sauvegardes sur un serveur esclave ; je vais me concentrer sur l’outil principal de sauvegarde lorsqu’on utilise MySQL : mysqldump

Je pense que tout le monde (en tout cas, une majorité des gens qui lisent cet article) sait à quoi sert cet outil. Il sert à écrire un fichier contenant toutes les directives SQL permettant de recréer une base de données à l’identique de l’état dans lequel elle se trouvait au moment de la sauvegarde.
Je ne vais pas m’étaler sur les aspects théoriques, mais plutôt vous expliquer comment je l’utilise.

Je me sers de mysqldump depuis une bonne quinzaine d’années, il me semble. À l’époque, l’utilisation par défaut (sans passer d’option particulière en ligne de commande) était assez pénible, surtout quand on voulait restaurer les données ; le code SQL généré contenait une commande INSERT pour chaque ligne de chaque table. Quand on injectait le fichier, le serveur mettait un temps infini pour exécuter toutes ces insertions et recalculer les index au fur et à mesure.
Ma vie a changé quand j’ai découvert l’option “–extended-insert”, qui regroupe plusieurs insertions dans une seule commande INSERT.

Les options à utiliser

Aujourd’hui, l’option “–opt” est activée par défaut sur les versions modernes de mysqldump. Cette option se contente d’activer par défaut les options suivantes :

  • –add-drop-table : Ajoute des directives pour effacer les tables avant de les recréer. Utile pour réinjecter un fichier en écrasant les anciennes versions des tables qui pourraient déjà traîner dans la base.
  • –add-locks : Ajoute des commandes pour verrouiller les tables pendant leur écriture ; il est alors impossible de lire ou d’écrire dedans en même temps que les données sont injectées, ce qui évite de ralentir l’injection.
  • –create-options : Ajoute les commandes de création des tables. On ne saurait s’en passer.
  • –disable-keys : Cette option accélère l’injection des données en faisant en sorte que les index sont créés qu’une fois que toutes les lignes sont injectées. Sauf que cela ne fonctionne qu’avec les index non uniques des tables MyISAM. Et comme vous êtes censés utiliser le moteur InnoDB à la place du MyISAM, on s’en moque un peu…
  • –extended-insert : Je viens d’en parler, c’est juste essentiel.
  • –lock-tables : Cette option a pour but de garantir l’intégrité des données qui sont sauvegardées, en verrouillant les tables durant la lecture. Je vais revenir sur ce point, car il est problématique.
  • –quick : Par défaut, quand mysqldump s’occupe d’une table, il tente de récupérer toutes les données en mémoire, avant d’en écrire le contenu. Sauf que si vous avez de très grosses tables, cela ne tiendra pas en RAM. L’option “–quick” empêche de tout charger en mémoire.
  • –set-charset : Demande l’ajout de la directive SET NAMES avec l’indication de l’encodage de caractères utilisés. Ça ne mange pas de pain.

Du coup, toutes ces options peuvent sembler utiles (et c’est sûrement pour ça qu’elles ont été regroupées sous l’option unique “–opt”, et que cette option est activée par défaut). Sauf qu’il y a un problème lorsqu’on exécute mysqldump en production avec ces options : Je vous ai dit que l’option “–lock-tables” verrouille les tables au moment où on les lit.

Encore une fois, la volonté derrière cela est assez simple. Si des écritures (ajouts ou modifications de données) ont lieu pendant la sauvegarde de la base de données, le fichier qui est généré n’a alors plus aucune cohérence. Vous pouvez avoir récupéré des données dont le début reflète un état différent de la fin, parce que les données ont bougé entretemps… Vous imaginez le bordel.
Sauf que si vous utilisez cette option sur votre serveur de production (et sans avoir mis en place une réplication qui vous permettrait de faire les sauvegardes sur un serveur esclave), cela veut dire que pendant toute la durée de l’exécution de mysqldump, votre application risque de vouloir manipuler les données… sans succès. Le pire, c’est que ces verrous sont posés par base ; donc si vous avez des données avec des liaisons qui se font entre des tables qui sont dans des bases différentes, cela ne sera pas suffisant de toute façon.

Continuer la lecture de « Utilisation de MySQLDump »

Utiliser GSuite pour envoyer des emails depuis un serveur

Imaginons la situation suivante :

  • Vous avez souscrit à l’offre GSuite de Google, et vous utilisez ainsi Gmail avec votre propre nom de domaine.
  • Vous avez un serveur Linux, depuis lequel vous souhaitez pouvoir envoyer des emails.

(Si vous n’êtes pas encore client GSuite, n’oubliez pas que j’ai proposé des coupons de réduction)

Il existe plusieurs raisons pour lesquelles vous avez tout intérêt à ce que les emails soient en fait envoyés par les serveurs de Google plutôt que par le votre : Dans la mesure où votre domaine est géré (au moins pour les emails) par Google, votre serveur n’apparaîtra pas comme étant légitime pour ce domaine, et donc les messages qu’il enverra risquent d’être classés en spam. De plus, vous pourrez retrouver la trace de tous les messages envoyés dans Gmail, ce qui peut être très pratique.

Côté serveur, j’ai l’habitude d’utiliser Exim. Historiquement, c’était le seul serveur SMTP qui pouvait intégrer complètement le filtre anti-spam SpamAssassin (pas en l’appelant comme un filtre externe, mais bien en l’intégrant à Exim, ce qui est bien plus efficace). J’ai gardé l’habitude de l’utiliser, car il est au moins aussi bon que Postfix ou Qmail.

Configuration GSuite

Pour commencer, nous allons créer un compte GSuite dédié à ces envois. Pour cela, il faut aller dans l’interface d’administration, puis dans Utilisateurs. Cliquez sur l’icône « Créer un utilisateur » en bas à droite. Enfin bon, si vous utilisez GSuite, vous devez savoir comment créer un utilisateur et comment lui assigner une ou plusieurs adresses email.

Continuer la lecture de « Utiliser GSuite pour envoyer des emails depuis un serveur »

Génération de tokens

Je suis en train de travailler sur une application qui nécessite l’utilisation de tokens. Certains doivent avoir une durée de vie assez courte, d’autres n’ont pas de péremption. Tout l’enjeu autour de ces tokens est de s’assurer qu’ils ne soient pas devinables facilement, ni qu’ils soient attaquable par force brute. Mais en même temps, certains tokens sont destinés à être copiés-collés, et il est alors préférable qu’ils puissent être saisis à la main par un être humain ; il faut donc qu’ils ne soient pas trop longs et qu’ils soient intelligibles.

À une époque, on aurait généré un hash MD5 à partir d’un nombre aléatoire, éventuellement en utilisant la date courante comme sel. En PHP ça donnait ça :

$token = md5(time() . mt_rand());

Simple et plutôt efficace. Sauf qu’il y a plusieurs soucis avec cette solution :

  • Le résultat est une chaîne de 32 caractères hexadécimaux, ce qui correspond à un message de 128 bits. C’est bien d’un point de vue cryptographique (plus le message est long, plus il est difficile à craquer par force brute), mais c’est un peu long à taper à la main.
  • MD5 est un algorithme aujourd’hui considéré comme insuffisamment sécurisé.

Pour le premier point, on peut changer la base du message ; en passant d’une base 16 (caractères hexadécimaux) à une base 36 (tous les caractères alphanumériques, aussi appelée base hexatridécimale, hexatrigésimale ou sexatrigésimale), on diminue la taille du token à 25 caractères.
Pour le second point, il est recommandé de passer à l’algorithme SHA256.

En PHP, cela donne ceci :

$token = base_convert(hash('sha256', time() . mt_rand()), 16, 36);

Cette fois-ci, on a un message de 256 bits, ce qui est encore mieux d’un point de vue cryptographique, généré par un algorithme bien plus robuste. Et grâce à la base 36, on a un message de 50 caractères au lieu de 64.
Il y a toutefois deux choses qui pourraient être améliorées. Le message est encore trop long à mon goût ; personne n’a envie de taper à la main un hash de 50 caractères, donc il faut trouver un moyen pour le raccourcir. Et il y a des caractères trop similaires (les lettres i et l, et le chiffre 1 ; le chiffre 0 et la lettre o) pour qu’un être humain normal puisse les recopier sans se poser la moindre question.

Continuer la lecture de « Génération de tokens »

Gestion des dates et heures dans les bases de données

Je vais vous parler d’une réflexion que j’ai en ce moment, causée par un développement sur un produit qui se veut à destination d’utilisateurs répartis autour du globe.

Jusqu’ici, je n’avais jamais vraiment été confronté à de véritables problèmes de gestion des dates dans mes applications. Je stockais les dates et heures en base de données dans des champs de type DATETIME (avec MySQL). Les dates enregistrées l’étaient en partant du principe qu’elles étaient valables en France, et donc au moment de lire les dates pour les afficher cela ne posait pas de soucis pour des utilisateurs situés en France là encore.

Notez bien que j’utilise MySQL, qui a la très mauvaise idée de stocker les dates sans la moindre information de timezone. Donc quand vous enregistrez la valeur ‘2011-04-12 11:04:12’, il l’enregistre telle quelle, sans plus d’information. À vous de vous débrouiller avec ça. Encore une fois, tant que vous êtes dans le même pays pour la lecture et l’écriture, cela ne pose aucun problème. Ça devient plus délicat quand ce n’est pas le cas.
Je vais m’expliquer en détail, mais sachez qu’avec d’autres moteurs de base de données, comme PostgreSQL par exemple (je ne connais pas les autres ; j’ai travaillé un peu avec Oracle il y a 10 ans, mais je n’ai pas de souvenir à ce niveau), il y a des champs permettant de gérer les timezones, ce qui évite bien des soucis la plupart du temps.

Le cas d’utilisation problématique apparaît lorsque vous devez lire une date et l’afficher pour un utilisateur situé dans un fuseau horaire différent de celui qui a été à l’origine de l’enregistrement.
Ce qui est normalement prévu, c’est de convertir la date depuis sa timezone de départ vers la timezone d’arrivée. Si on part du principe que la timezone utilisée par le serveur n’a pas été modifiée, on peut se baser dessus pour faire la conversion. Pour cela, on peut utiliser la fonction CONVERT_TZ(). Par exemple, pour lire une date enregistrée en France et l’afficher pour un utilisateur québécois, on peut écrire ceci :

SELECT CONVERT_TZ(date, @@session.time_zone, '-5:00') FROM MaTable;

Le résultat est que la date stockée dans le champ date est convertie, depuis la timezone définie par le système (ou éventuellement dans la configuration de MySQL), vers une date dont la timezone a 5 heures de retard sur le fuseau zéro.
Donc si la date en base est ‘2011-04-12 11:04:12’, que le système a une timezone par défaut configurée pour être celle de la France, qu’on est actuellement en hiver (donc la timezone serveur est ‘+01:00’), le résultat sera ‘2011-04-12 05:04:12’.

C’est parfait !

Oui mais non. En Europe, le passage à l’heure d’été se fait le dernier dimanche de mars, alors qu’en Amérique du Nord il se fait le deuxième dimanche de mars ; les passages à l’heure d’hiver se faisant respectivement le premier dimanche de novembre et le dernier dimanche d’octobre. Il y a donc 3 semaines dans l’année pendant lesquelles il n’y a plus que 5 heures de décalage horaire entre la France et le Québec, au lieu des 6 heures habituelles.

Imaginons que je convertisse la date ‘2010-03-21 15:27:10’, par la même manière que précédemment, que se passe-t-il ?

CONVERT_TZ('2010-03-21 15:27:10', @@session.time_zone, '-05:00')

Il faut comprendre que le premier paramètre est indépendant des deux suivants. Le deuxième paramètre prend la valeur de la timezone locale, définie par le système ; ce qui me donne ‘+01:00’ (décalage en France en hiver) ou ‘+02:00’ (décalage en France en été). Mais pour le troisième paramètre, j’ai donné une valeur fixe, qui va s’ajouter au paramètre précédent.
Donc, suivant que je fasse cette conversion en été ou en hiver (et donc suivant la valeur du second paramètre), le décalage horaire qui va être calculé sera de 6 ou de 7 heures.

Ce n’est pas bon du tout ! Il n’y a jamais 7 heures de décalage horaire entre la France et le Québec !

Il faudrait donc que je sache moduler le troisième paramètre, pour lui donner une valeur différente en fonction de la date en cours (‘-05:00’ en hiver et ‘-04:00’ en été). Ainsi, le décalage sera toujours bien de 6 heures…
Sauf que la date en question (21 mars) tombe pile sur l’un des créneaux pendant lesquels il n’y a plus que 5 heures de décalage horaire entre la France et le Québec ! Comment faire pour interpréter cela correctement, et obtenir la bonne valeur (‘2010-03-21 10:27:10’, pour info) ?

Après avoir pas mal cherché, il semblerait qu’il ne faille pas faire confiance à MySQL. Du tout. Il ne sait pas gérer les timezones et encore moins les passages heure d’été/heure d’hiver ; donc quand il enregistre une date, il la stocke telle quelle et il vous laissera vous débrouiller quand vous voudrez l’afficher.
Pour ne pas faire confiance à MySQL, le plus simple est de stocker toutes les dates telles qu’elles sont sur le temps UTC, donc sans décalage par rapport au temps universel. Au moment de la lecture, les dates doivent alors être récupérées de la même manière (temps UTC), puis converties au moment de l’affichage, en fonction des préférences locales de l’utilisateur.

Pour être plus clair, il suffit de faire la conversion au moment de l’écriture :

INSERT INTO MaTable SET date = CONVERT_TZ('2017-05-06 05:49:00', @@session.time_zone, '+00:00');

Au moment de la lecture, plus besoin de faire de conversion quand on lit les données en base. Par contre, il faudra convertir au moment de l’affichage, comme je l’ai dit plus haut.

Mais cette requête utilise la timezone locale du serveur, pour calculer le décalage horaire (ici ‘+01:00’ ou ‘+02:00’ si le serveur est en France, suivant qu’on soit en été ou en hiver au moment où la requête est exécutée). Si c’est un utilisateur qui fournit une date et heure, il va falloir récupérer sa timezone locale (‘-05:00’ ou ‘-04:00’ s’il est au Québec, suivant la période de l’année).

Par contre, si les seules dates que vous enregistrez sont les date et heure courantes, en utilisant la fonction NOW(), il peut être plus simple de configurer le serveur pour qu’il reste sur l’UTC.
Cela peut être fait en modifiant le fichier de configuration (‘/etc/mysql/my.cnf) :

default_time_zone = '+00:00'

Ou en modifiant le paramètre de la connexion en cours :

SET @@session.time_zone = '+00:00';

Quelques liens sur le sujet :