Ordonnancement distribué : la planification est la partie facile
Analyser des expressions cron est un problème résolu. Cela devient difficile quand un runner disparaît en pleine tâche. Ce que la construction de Croniq nous a appris sur les exécutions orphelines, la reprise d'identité et le filtrage par calendrier.
Quand on construit un ordonnanceur, on se trompe d'abord sur la partie difficile. Analyser des expressions cron et calculer la prochaine échéance est un problème résolu, avec des bibliothèques dans tous les langages. Le difficile commence ensuite : quand plusieurs machines prennent en charge le travail et que l'une d'elles tombe.
Avec Croniq nous construisons un ordonnanceur distribué en Rust, actuellement en v0.31.0, Apache-2.0. Les problèmes intéressants des dernières versions n'avaient rien à voir avec les plannings.
Tirer plutôt que pousser
La première décision d'architecture a été que les runners vont chercher le travail au lieu de le recevoir. Les runners demandent au serveur, via HTTP long-poll, s'il y a quelque chose pour eux. Le serveur connaît les capacités de chaque runner et ne distribue que les tâches compatibles.
Cela coûte un peu de latence et économise beaucoup d'exploitation. Les runners n'ont besoin ni d'adresse joignable, ni de règles de pare-feu entrantes, ni d'enregistrement préalable. On démarre un container de plus et il prend du travail. La montée en charge devient une question du nombre de processus, pas de la configuration de la répartition.
Ce qui se passe quand un runner disparaît
C'est là que se trouve le vrai travail. Un runner prend une tâche, la marque comme réservée, puis meurt. Le processus n'existe plus, l'enregistrement dit "en cours". Sans contre-mesure, cette tâche reste dans cet état pour toujours et, pour les tâches singleton, bloque toute exécution ultérieure.
La solution demande trois éléments qui doivent fonctionner ensemble :
- Récupérer les exécutions orphelines. Les exécutions réservées sans runner vivant doivent être détectées et libérées, pour qu'un autre runner puisse les reprendre.
- Reprendre l'identité au redémarrage. Quand le même runner revient, il doit retrouver son ancienne identité au lieu de se présenter comme nouveau. Sinon la liste des runners grossit à chaque redémarrage.
- Détecter le battement. Un runner qui disparaît et revient toutes les minutes est un symptôme. Ces reprises sont journalisées et les compteurs correspondants se trouvent sur le point de terminaison des métriques, pour qu'une alerte puisse réagir.
Le troisième point est celui que l'on oublie à la première conception. Un système qui se répare sans le signaler cache précisément l'information dont on a besoin en production.
Le filtrage par calendrier cache un piège discret
Les tâches peuvent être liées à un calendrier : uniquement les jours ouvrés, pas les jours fériés, seulement sur certaines périodes. L'implémentation naïve calcule la prochaine échéance selon le planning et l'écarte si le calendrier l'interdit.
La conséquence : sur une période de blocage prolongée, une tâche épuise ses candidats puis ne se déclenche plus du tout. Le comportement correct consiste à avancer le déclencheur jusqu'au prochain instant autorisé plutôt que de l'écarter. La différence n'apparaît pas dans les tests, car les calendriers de test ont des blocages courts. Elle apparaît à Noël.
Quand cela vaut la peine
Un ordonnanceur maison est rarement la bonne réponse. Qui a une poignée de tâches cron sur un serveur n'en a pas besoin. Cela devient pertinent dans une bande étroite : environ vingt à deux cents tâches planifiées, réparties sur plusieurs machines, sans équipe plateforme dédiée souhaitant exploiter un gros moteur de workflow.
Croniq couvre exactement cette bande : un binaire ou un container, SQLite comme backend par défaut, un tableau de bord pour les tâches et les journaux d'exécution. L'essentiel est moins l'outil que le constat derrière. En exécution distribuée, la charge ne se trouve pas dans la planification, mais dans la question de ce que fait le système quand une de ses parties cesse de répondre.