Transformation numérique

Stabilité dans le pool de nœuds Kubernetes : Le défi avec les Cronjobs

Les tâches cron dans Kubernetes peuvent déstabiliser les pools de nœuds. Des stratégies d'optimisation sont appliquées, mais des problèmes ouverts persistent. Comment gère-t-on cela ?

11 mai 2023

Crédits photo : https://unsplash.com/photos/Bfxry3eVqwo
Crédits photo : https://unsplash.com/photos/Bfxry3eVqwo
Crédits photo : https://unsplash.com/photos/Bfxry3eVqwo
Crédits photo : https://unsplash.com/photos/Bfxry3eVqwo

Dans un environnement Kubernetes, le principe des conteneurs ne nous permet plus d'utiliser crontab pour les processus récurrents. À cet effet, des déploiements de tâches Cron spécialement conçus ont été créés. Cela fonctionne bien. Mais seulement tant que l'on a peu de tâches Cron qui veulent démarrer en même temps. Mais chez nous, nous avons des tâches Cron en double chiffre, qui veulent toutes démarrer en même temps, toutes les minutes ! Cela peut rendre les nœuds instables. Alors, comment pouvons-nous remédier à cela ?


Situation initiale

Notre produit SaaS tourne dans un cluster Kubernetes. Nous avons des déploiements séparés (que nous appelons espaces de travail) pour chacun de nos clients, et chaque espace de travail a trois tâches Cron :

  1. Une la nuit

  2. une à midi et

  3. une chaque minute

Cela fait au total > 100 déploiements dans un pool de nœuds. Notre pool de nœuds PROD a trois nœuds. En soi, cela ne devrait pas poser de problème, car nos déploiements ne consomment pas beaucoup de ressources. Ni en termes de CPU, ni en termes de RAM. Mais les tâches Cron chaque minute perturbent les nœuds. Lorsqu'un nombre de tâches Cron atteint les deux chiffres en même temps pour démarrer et effectuer leur travail, cela perturbe parfois un nœud et influence les autres conteneurs qui y tournent. La tâche Cron minute semble inoffensive au premier abord :


apiVersion: batch/v1
kind: CronJob
metadata:
  name: uhub-${WORKSPACE}-always-cron
spec:
  concurrencyPolicy: Forbid
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - command:
                - sh
                - -c
                - php bin/console uhub:check-post-statuses && php bin/console uhub:delete-obsolete-tasks
              env:
                - name: WORKSPACE
                  value: ${WORKSPACE}
                - name: DATABASE_URL
                  valueFrom:
                    secretKeyRef:
                      key: database_url
                      name: ${WORKSPACE}-db-credentials
              envFrom:
                - configMapRef:
                    name: symfony-config
                - secretRef:
                    name: ${WORKSPACE}-config
              image: gcr.io/${GCLOUD_PROJECT_ID}/php-fpm:${IMAGE_TAG}
              imagePullPolicy: IfNotPresent
              name: cron
              resources:
                requests:
                  cpu: "0.05"
                  memory: "50M"
              terminationMessagePath: /dev/termination-log
              terminationMessagePolicy: File
          dnsPolicy: ClusterFirst
          restartPolicy: Never
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
  schedule: '*/1 * * * *'
  successfulJobsHistoryLimit: 0
  suspend: false
apiVersion: batch/v1
kind: CronJob
metadata:
  name: uhub-${WORKSPACE}-always-cron
spec:
  concurrencyPolicy: Forbid
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - command:
                - sh
                - -c
                - php bin/console uhub:check-post-statuses && php bin/console uhub:delete-obsolete-tasks
              env:
                - name: WORKSPACE
                  value: ${WORKSPACE}
                - name: DATABASE_URL
                  valueFrom:
                    secretKeyRef:
                      key: database_url
                      name: ${WORKSPACE}-db-credentials
              envFrom:
                - configMapRef:
                    name: symfony-config
                - secretRef:
                    name: ${WORKSPACE}-config
              image: gcr.io/${GCLOUD_PROJECT_ID}/php-fpm:${IMAGE_TAG}
              imagePullPolicy: IfNotPresent
              name: cron
              resources:
                requests:
                  cpu: "0.05"
                  memory: "50M"
              terminationMessagePath: /dev/termination-log
              terminationMessagePolicy: File
          dnsPolicy: ClusterFirst
          restartPolicy: Never
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
  schedule: '*/1 * * * *'
  successfulJobsHistoryLimit: 0
  suspend: false
apiVersion: batch/v1
kind: CronJob
metadata:
  name: uhub-${WORKSPACE}-always-cron
spec:
  concurrencyPolicy: Forbid
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - command:
                - sh
                - -c
                - php bin/console uhub:check-post-statuses && php bin/console uhub:delete-obsolete-tasks
              env:
                - name: WORKSPACE
                  value: ${WORKSPACE}
                - name: DATABASE_URL
                  valueFrom:
                    secretKeyRef:
                      key: database_url
                      name: ${WORKSPACE}-db-credentials
              envFrom:
                - configMapRef:
                    name: symfony-config
                - secretRef:
                    name: ${WORKSPACE}-config
              image: gcr.io/${GCLOUD_PROJECT_ID}/php-fpm:${IMAGE_TAG}
              imagePullPolicy: IfNotPresent
              name: cron
              resources:
                requests:
                  cpu: "0.05"
                  memory: "50M"
              terminationMessagePath: /dev/termination-log
              terminationMessagePolicy: File
          dnsPolicy: ClusterFirst
          restartPolicy: Never
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
  schedule: '*/1 * * * *'
  successfulJobsHistoryLimit: 0
  suspend: false

Optimisation 1: Pool de nœuds dédié aux tâches cron

Nous avons donc attribué un pool de nœuds dédié aux tâches cron, les séparant ainsi de toutes les autres applications. Nous avons fait cela avec NodeSelectors. Les NodeSelectors veillent à ce que les conteneurs ne soient déployés que là où ils sont autorisés. Ils réagissent aux étiquettes des pools de nœuds. Nous avons donc défini une étiquette “Cronjob: autorisé” sur un nouveau pool de nœuds. Et les tâches cron ont reçu le NodeSelector suivant.

spécification:
 jobModèle:
  spécification:
   modèle:
    spécification:
     conteneurs:
      nodeSélecteur:
       travailPlanifié: autorisé

Ainsi, les cronjobs ne sont créés que sur les nœuds ayant l'étiquette « cronjob: allowed ». Sinon, rien ne s'y passe. Pourquoi? Parce que nous avons tous nos applications avec des NodeSelectors et les forçons à s'exécuter sur leurs pools de nœuds assignés. Un pool de nœuds pour les cronjobs, un pool de nœuds pour les clients et un pool de nœuds pour les applications nécessitant tous les mêmes ressources, telles que l'IAM, etc.

Cela a été utile et a apaisé la conscience. Mais rapidement, il a été clair que cela n'avait pas vraiment aidé. Les nœuds dans le pool de nœuds pour les cronjobs avaient toujours les mêmes problèmes et devenaient instables. La seule solution a été de nettoyer ces nœuds, de les supprimer et de permettre au pool de nœuds de générer automatiquement de nouveaux nœuds.

Pour éviter un travail manuel, nous devions penser à autre chose. La solution était évidente : nous devons abolir ces cronjobs !


Optimisation 2 : Plus de cronjobs !

Ne plus avoir de cronjobs serait agréable. Malheureusement, nous n'avons pas trouvé de solution pour supprimer les cronjobs. J'aurais aimé vous dire qu'un cronjob ” ” “ ” “ ” n'est plus nécessaire. Mais pour nous, il l'est toujours. Nous avons trouvé une solution pour que les cronjobs ne soient plus gérés par le cluster, ce qui élimine ainsi toute instabilité.

Nous avons une application centrale qui connaît tous les espaces de travail. Dès le début, nous avons eu cette gestion centrale. Maintenant, nous permettons à tous les espaces de travail de notifier qu'un cronjob doit être exécuté. Chaque espace de travail a une API qui lance le cronjob mentionné dans le shell. Cela augmente la charge sur l'espace de travail, mais la plupart du temps il ne se passe rien car il n'y a rien à faire.

Nous obtenons ainsi le même effet que avec des déploiements de cronjobs séparés. Cela permet d'éviter une charge excessive sur notre cluster due au démarrage fréquent de cronjobs.


Encore plus de potentiel d'optimisation

Malheureusement, cette solution a un inconvénient. Elle ne fonctionne que tant que nous avons moins de 300 clients : Une requête vers un espace de travail pour démarrer un cronjob prend environ 200ms. Dans une minute, nous pouvons donc démarrer jusqu'à 300 cronjobs. Ensuite, l'action de démarrer tous les cronjobs prend plus d'une minute et nous avons un décalage, un cronjob ne s'exécute plus toutes les minutes, mais un peu moins souvent.

Mais on peut aussi gérer cela… on peut exécuter les requêtes vers les espaces de travail dans des processus parallèles. Mais nous en parlerons une autre fois.


Sommes-nous les seuls à avoir des nœuds instables en raison des cronjobs ?

Comment gérez-vous les cronjobs lorsque de nombreuses applications veulent lancer un cronjob en même temps ?


Vous avez une question ou des commentaires? Envoyez-moi un e-mail

Rester en contact

Inscrivez-vous pour plus d'inspiration.