Pouvoir forcer le node qui prend le CRON_LOCK dans un cluster / pour palier à un problème de réaffectation en boucle du CRON_LOCK quand les ping sont impossibles entre nodes

Request description

Nous avons relevé un problème de cohérence entre les règles de déploiement Kubernetes- que nous avons définies (ou pas) et la gestion du CRON_LOCK via pf_nodes. Je pose quelques éléments de contexte pour expliquer la situation et identifier d’éventuels problèmes de configuration de nos services.

Nous déployons au sein d’un même namespace (au sens Kubernetes) plusieurs services dimensionnés de manières dédiées à des cas d’usage spécifiques :

  • un service api dédié aux usages via nos API mappées avec plusieurs réplicas (plusieurs pods Kubernetes)
  • un service app dédié aux usages UI avec plusieurs réplicas
  • un service cron dédié aux batch uniques avec un unique réplica

Tous ces services s’appuient sur la même instance CloudSQL.

Dans cette situation, j’ai constaté qu’au gré des déploiements, la table pf_node est alimentée puis vidée de telle sorte que les fonctionnalités gérant le CRON_LOCK ne se comportent pas du tout comme attendu (les services s’approprient tous progressivement le CRON_LOCK au gré des restarts ou déploiements). Je pense que ce n’est que la partie visible de l’iceberg et j’imagine que d’autres problèmes se posent dans cette situation.

En faisant quelques tests en mode bash dans les conteneurs, j’ai constaté que les autres pods ne sont pas joignables par curl ou ping en utilisant leur hostname (ex. rfi-70649-cron-75d7fd4b5d-4ffmp) mais qu’en utilisant leur IP les curl ou ping fonctionnent. J’ai l’impression que ce problème de connectivité via le hostname est la cause racine.

La solution la plus propre serait de pouvoir rendre possible la communication réseau entre les pods des différents services mais ça dépasse mes connaissances (j’ai demandé à mes supports DevOps). En attendant de pouvoir mettre en place une telle solution, est-il envisageable que Simplicité n’utilise que l’adresse IP de chaque pod à la place du hostname afin de sécuriser cette hypothèse si le projet qui déploie n’a pas prévu la network policy qui va bien ?

Steps to reproduce

This request concerns an up-to-date Simplicité instance
and these are the steps to reproduce it:

Technical information

Instance /health
---paste the content of your-instance.com/health---
Simplicité logs
---paste the content of the **relevant** server-side logs---
Browser logs
---paste content of the **relevant** browser-side logs---
Other relevant information

----E.g. type of deployment, browser vendor and version, etc.----

Utiliser le hostname plutôt que l’IP est une stratégie qui permet de résister à des changements d’IP à hostname constant.

Sur une infra totalement dynamique comme K8S, cette résistance ne sert effectivement pas à grand chose…

A mon avis le pb sur votre infra est avant tout un pb de DNS ou autre subtilité réseau. Je n’ai pas connaissance de problème de ce genre sur d’autres déploiements K8S peut être plus basiques…

Bref, je serais d’avis de creuser d’abord la raison pour laquelle les noms ne se resolvent pas plutôt qu’envisager un mécanisme permettant de dire d’utiliser l’IP + le port + le scheme plutôt que directement l’URL du endpoint (à la lecture du code je pense qu’on a bien ces 4 infos, mais il manque le mécanisme pour dire d’utiliser l’un ou l’autre).

Merci beaucoup pour ton retour rapide.
Oui je relance mes DevOps pour tenter de résoudre le problème à la source avec une network policy adaptée.

Pour compléter la réflexion,

On pourrait imaginer un mode on/off pour que chaque cron ne fasse pas de polling du serveur sensé lancer les jobs uniques et reprenne la main si ping ko.

“Off” signifiant que ce ping / is alive, sera de la responsabilité du l’infra client pour détecter que plus personne ne lance les jobs uniques, et forcer un des nodes à le faire.

De notre côté Simplicité pourrait aussi persister l’ip port context… dans la table m_pf_node (en fallback du ping ko de l’url avec nommage dns…).

1 Like

Bonjour François, je suis dans une impasse avec nos DevOps (pas de solution pour garantir la résolution de hostname/ip au sein du cluster déployé). Si l’implémentation du ping pouvait en effet s’appuyer sur l’IP des endpoints enregistrés dans m_pf_node je pense que ça résoudrait mon problème.

Utiliser une URL basée sur l’IP nécessite donc bien une évolution:

  • soit dans une logique exclusive comme je le proposais => un param système pour dire d’utiliser l’IP plutôt que le hostname pour construire l’URL du node
  • soit dans une logique fallback comme le propose @Francois => on stocker le hostname et l’IP et on essaie d’abord sur l’URL construite avec le hostname, si erreur ou timeout, on réessaye avec l’URL construite avec l’IP
  • soit un mix des 2 => on stocke le hostname et l’IP et un param système choisit laquelle utiliser

Je suis assez pour la 3ème option qui sera plus souple/evolutive.

Cela dit je reste prudent sur la capacité de Tomcat de déterminer la “bonne” adresse IP = celle pour se faire appeler par les autres Tomcat du cluster (une VM ou un noeud pouvant avoir plusieurs IP en fct de la manière dont le réseau est configuré) => peut être faudrait il donc prévoir, en plus, un mécanisme externe (via variable d’environnement ou argument JVM) pour dire à un noeud qu’il doit se référencer en tant que telle ou telle URL au lieu de celle(s) qu’il arrive à déterminer

Bonsoir David,

merci beaucoup à toi aussi pour ton support.

voici les éléments que j’ai pu récupérer pour illustrer mon cas de déploiement:

pod bca-68521-api-585df6cd8f-6p5sn

Annotations du pod :

...
cni.projectcalico.org/podIP: 100.88.195.186/32
cni.projectcalico.org/podIPs: 100.88.195.186/32
...

contenu du fichier /etc/hosts

[root@bca-68521-api-585df6cd8f-6p5sn tomcat]# grep bca-68521-api-585df6cd8f-6p5sn /etc/hosts
100.88.195.186  bca-68521-api-585df6cd8f-6p5sn

lecture des adresses IP affectées au conteneur (depuis un bash dans le pod) :

[root@bca-68521-api-585df6cd8f-6p5sn tomcat]# hostname -I
100.88.195.186

A priori, je reste dans une configuration avec une seule adresse IP, cette adresse IP est retrouvée dans la console GCP, dans le fichier /etc/hosts et retournée par la commande hostname -I. Je pense que c’est la bonne adresse à considérer.

Je reviens sur votre architecture :
Si vous avez une instance dédiée (et donc j’imagine supervisée/disponible…) pour exécuter les cron uniques, il faudrait plutôt simplement :

  • pouvoir arrêter le mécanisme de Simplicité de réaffectation à un tomcat alive si celui qui a le verrou CRON_LOCK ne répond pas (le on/off dont je parlai)
  • et pouvoir forcer dans les paramètres de démarrage d’un noeud spécifique (il faut pouvoir l’identifier avec une ip ou hostname fixe, via param system ou variable java…) celui qui doit prendre la main sur ce CRON_LOCK (à date c’est le premier noeud qui démarre) pour que son tomcat comprenne que c’est lui et personne d’autre.
  • pouvoir arrêter le mécanisme de Simplicité de réaffectation → oui
  • pouvoir forcer dans les paramètres de démarrage d’un noeud spécifique → oui aussi

Je pense que ça répondrait à mon besoin de garantir l’exécution des cronjob uniques sur un pod identifié (mon pod cron).

Après, il reste cette dette de résolution impossible des hostnames notre mode de déploiement qui rend inopérant le ping des nodes. Je pense qu’il faudrait quand même considérer les solutions alternatives de ping sur hostname ou IP selon un paramètre fourni lors du déploiement ainsi que la bascule sur le ping/ip si le ping/hostname échoue (tel que décrit par David).

D’ailleurs, j’ai l’impression que cette dette de résolution impossible des hostnames n’est pas spécifiquement liée à nos configurations mais à l’aspect dynamique des déploiements dans Kubernetes avec des pods qui disparaissent/apparaissent avec des hostnames qui changent sans que les pods “survivants” ne soient informés des hostnames des nouveaux (cf. /etc/hosts read-only, statique dans les pods déployés, ne contenant que le hostname du pod courant)…

En gros, il manque un service DNS commun au niveau du cluster kube qui tient à jour les couples hostname/ip au gré des déploiements. Il faudra simplement gérer le contenu du fichier /etc/resolv.conf tout comme le contenu du fichier /etc/hosts l’est déjà.

Je ne comprends pas que ce genre de problématiques soit laissée à la charge des projets.

Bonjour,
Pouvez-vous me donner un peu de visibilité sur ce qui peut être envisagé et dans quels délais ?
Merci beaucoup pour votre support.

Si les URL sont inaccesibles niveau Simplicité/Tomcat vous devrez niveau infra :

  • gérer la haute dispo du noeud qui lance les cron uniques
  • gérer le clear cache global de chaque noeud : à voir comment vous gérez les upgrade socle et/ou métier à chaud (sinon arret/relance avec suppression du répertoire cache)
  • gérer le logout général d’un user qui perd ses droits, à défaut sa session éventuelle expirera en fonction de la config des timeout de session ou user token

Comme indiqué à très court terme, on peut surement forcer le noeud qui a le CRON_LOCK (il faut un algo qui garantisse le “c’est moi” donc on en revient forcement à une adresse statique/identifiable par tomcat qq part) et donc inhiber le failover géré niveau Simplicité.

Pour le besoin d’avoir des URL secondaires (basées sur d’autres hostnames ou ip) pour les ping/logout/clearcache…, il faudra une autre évolution du méta-modèle (table m_pf_node) ou pouvoir aussi le spécifier au démarrage de l’instance.

Merci François pour ton retour.

Le besoin de forcer le cron lock sur un nœud identifié est le plus important pour nous. Cela évitera de mettre en œuvre des moyens compliqués pour compenser la réaffectation automatique. Effectivement, nous devrons assurer la haute disponibilité de ce nœud.

Le besoin d’avoir des URL secondaires basées dans notre cas sur les adresses IP est une conséquence de la mise en œuvre de l’algorithme de réaffectation du cron lock. La connectivité entre les nœuds (capacité de ping node) est en effet un prérequis pour que cet algorithme fonctionne bien. Si on débraye cet algorithme, plus besoin de chercher des solutions alternatives pour qu’il fonctionne dans notre cas de figure.

Enfin, tu mentionnes deux autres fonctionnalités indépendantes de la gestion du cron lock : la distribution du clear cache du logout user. Je ne les ai pas mentionnées à l’origine car je ne les connais pas vraiment.

Si je comprends bien, la capacité de ping node permettrait de distribuer de manière quasi synchrone les clear cache et les logout user. Ces fonctionnalités sont évidemment très intéressantes et remettent du grain dans le moulin des URL secondaires pour disposer d’un ping node alternatif. Ce besoin est cependant moins important que le premier inhérent à la gestion du cron lock.

Ok effectivement les autres propagations au fil de l’eau ne sont pas bloquantes (logout forcé, clear cache…), d’autres mécanismes sont en places pour le faire par polling.

On va faire l’évolution rapidement pour inhiber le fonctionnement actuel de la (re)prise du CRON_LOCK des jobs uniques :

  • Ajout d’un paramètre optionnel CRON_LOCK_OWNER qui permet de forcer/savoir qui a toujours le lock
  • Au démarrage, le noeud qui prend la verrou doit vérifier CRON_LOCK_OWNER = mon adresse
  • Ensuite au runtime, les autres serveurs ne feront aucun ping de failover si un CRON_LOCK_OWNER est forcé/renseigné

L’algo n’est pas complexe, mais pour ne pas translater le problème actuel, tout est dans ce test d’unicité, je propose de vérifier que le paramètre est égal Platform.getEndPointURL() mais pas sur qu’entre vos différents clusters ce soit statique et unique pour le node des cron.

@bmo
Merci de nous confirmer ce que vous pouvez mettre dans ce paramètre pour identifier le noeud des batchs uniques.

Ce sera livré en 5.3.41+

  • Il faudra créer un paramètre système CRON_LOCK_OWNER = endpoint URL de votre noeud cron unique = à rendre statique ou à changer en base à chaque renommage
  • Et redémarrer tous les nodes

Les logs de démarrage de chaque node permettent de vérifier qui va bosser ou pas suivant la présence/valeur du paramètre.

Si Platform.getEndPointURL() construit à partir des infos système de InetAddress ne fonctionne pas, il faudra un autre moyen de savoir qui doit bosser.

Bonjour François,

merci beaucoup pour cette très bonne nouvelle.
J’ai patché tous mes environnements et tout semble OK sur la 5.3.41.

Par contre, sur la 6.0.14 ça ne semble pas fonctionner (les endpointURL autres que celui désigné dans CRON_LOCK_OWNER préemptent systématiquement le lock à la place du owner quelles que soient les séquences de démarrage des pods.

Bizarre car le code a bien été reporté sur chaque branche.

C’est peut être une erreur de packaging docker de la 6.0.14 restée en 13 (David est absent jusqu’à lundi, il regardera ça)

Peux tu regarder dans les logs de demarrage des pods, le start des cron locale ont des logs qui permettraient de voir ce qui se passe :

// si parametre CRON_LOCK_OWNER présent
AppLog.info("The CRON_LOCK_OWNER for unique cron-jobs is forced to " + forcedOwner, g);
AppLog.info("This endpoint will "+(lock ? "always" : "never")+" get the CRON_LOCK for unique cron-jobs.", g);

// ou si pas de parametre
AppLog.info("No forced CRON_LOCK_OWNER is defined for unique cron-jobs.", g);
AppLog.info("The CRON_LOCK for unique cron-jobs will be held by the first endpoint to start, or by one of the available endpoint.", g);

Si tu n’as aucun de ces logs, c’est que la 6.0.14 ne s’est pas bien buildée (c’est encore une 13).
Sinon c’est que le test basée sur le getEndpointURL ne fonctionne pas.
Ou que le nom/valeur du paramètre n’est pas le bon en base…

Tu peux faire un test unitaire comme suit :

Grant g = Grant.getSystemAdmin();
String endpoint = Platform.getEndPointURL();
String forcedOwner = g.getParameter(SystemParameters.CRON_LOCK_OWNER);
boolean winner = forcedOwner!=null && forcedOwner.equals(endpoint);

Bonjour François,

Voici le résultat du test demandé (passé sur un pod de chaque type de service) :

endpoint=http://bca-71077-cron-7bc57876b5-kmlf6:8080 
forcedOwner=http://bca-71077-cron-7bc57876b5-kmlf6:8080 
winner=true

endpoint=http://bca-71077-app-6479b4b984-mbk2v:8080 
forcedOwner=http://bca-71077-cron-7bc57876b5-kmlf6:8080 
winner=false

endpoint=http://bca-71077-api-68966b9b-vlj2w:8080 
forcedOwner=http://bca-71077-cron-7bc57876b5-kmlf6:8080 
winner=false

Depuis ce matin, le CRON_LOCK reste bien attaché au owner paramétré.
Je pense que mes conditions initiales de test n’étaient pas bonnes. Comme on déploie en mode bleu/vert, les anciens pods ne se terminent qu’après que les nouveaux aient complètement démarré. De ce fait, il est probable que les anciens pods qui faisaient tourner la version précédente ont eu le temps de kick le lock avant d’être terminés.

Je surveille ça de près sur plusieurs environnements. Si j’ai le moindre doute, je reviens vers toi.