Trucs et astuces Kubernetes : fonctionnalités d'arrêt progressif dans NGINX et PHP-FPM

Une condition typique lors de la mise en œuvre de CI/CD dans Kubernetes : l'application doit être capable de ne pas accepter de nouvelles demandes de clients avant de s'arrêter complètement et, surtout, de terminer avec succès celles existantes.

Trucs et astuces Kubernetes : fonctionnalités d'arrêt progressif dans NGINX et PHP-FPM

Le respect de cette condition vous permet d'obtenir un temps d'arrêt nul pendant le déploiement. Cependant, même en utilisant des bundles très populaires (comme NGINX et PHP-FPM), vous pouvez rencontrer des difficultés qui entraîneront une recrudescence d'erreurs à chaque déploiement...

Théorie. Comment vit le pod

Nous avons déjà publié en détail sur le cycle de vie d'un pod cet article. Dans le cadre du sujet considéré, nous nous intéressons à ce qui suit : au moment où le pod entre dans l'état Terminer, les nouvelles requêtes cessent de lui être envoyées (pod supprimé dans la liste des points de terminaison du service). Ainsi, pour éviter les temps d'arrêt lors du déploiement, il nous suffit de résoudre correctement le problème de l'arrêt de l'application.

N'oubliez pas également que le délai de grâce par défaut est 30 secondes: passé ce délai, le pod sera résilié et l'application devra avoir le temps de traiter toutes les requêtes avant ce délai. Noter: bien que toute demande qui prend plus de 5 à 10 secondes soit déjà problématique, et un arrêt progressif ne l'aidera plus...

Pour mieux comprendre ce qui se passe lorsqu'un pod se termine, il suffit de regarder le schéma suivant :

Trucs et astuces Kubernetes : fonctionnalités d'arrêt progressif dans NGINX et PHP-FPM

A1, B1 - Recevoir des changements sur l'état du foyer
A2 - Départ SIGTERM
B2 - Supprimer un pod des points de terminaison
B3 - Réception des modifications (la liste des endpoints a changé)
B4 - Mettre à jour les règles iptables

Attention : la suppression du pod de point de terminaison et l'envoi de SIGTERM ne se font pas séquentiellement, mais en parallèle. Et comme Ingress ne reçoit pas immédiatement la liste mise à jour des points de terminaison, les nouvelles requêtes des clients seront envoyées au pod, ce qui provoquera une erreur 500 lors de la terminaison du pod. (pour des informations plus détaillées sur cette question, nous traduit). Ce problème doit être résolu des manières suivantes :

  • Envoyer la connexion : fermez les en-têtes de réponse (s'il s'agit d'une application HTTP).
  • S'il n'est pas possible d'apporter des modifications au code, alors l'article suivant décrit une solution qui vous permettra de traiter les demandes jusqu'à la fin du délai de grâce.

Théorie. Comment NGINX et PHP-FPM terminent leurs processus

Nginx

Commençons par NGINX, puisque tout est plus ou moins évident avec. En plongeant dans la théorie, nous apprenons que NGINX a un processus maître et plusieurs « travailleurs » - ce sont des processus enfants qui traitent les demandes des clients. Une option pratique est proposée : en utilisant la commande nginx -s <SIGNAL> terminer les processus soit en mode d'arrêt rapide, soit en mode d'arrêt progressif. C’est évidemment cette dernière option qui nous intéresse.

Alors tout est simple : il faut ajouter à crochet preStop une commande qui enverra un signal d’arrêt gracieux. Cela peut être fait dans Déploiement, dans le bloc conteneur :

       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit

Désormais, lorsque le pod s'arrêtera, nous verrons ce qui suit dans les journaux du conteneur NGINX :

2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down

Et cela signifiera ce dont nous avons besoin : NGINX attend que les requêtes soient terminées, puis tue le processus. Cependant, ci-dessous, nous examinerons également un problème courant à cause duquel, même avec la commande nginx -s quit le processus se termine incorrectement.

Et à ce stade, nous en avons fini avec NGINX : au moins à partir des journaux, vous pouvez comprendre que tout fonctionne comme il se doit.

Quel est le problème avec PHP-FPM ? Comment gère-t-il l’arrêt progressif ? Voyons cela.

PHP FPM

Dans le cas de PHP-FPM, il y a un peu moins d'informations. Si vous vous concentrez sur manuel officiel selon PHP-FPM, il dira que les signaux POSIX suivants sont acceptés :

  1. SIGINT, SIGTERM — arrêt rapide ;
  2. SIGQUIT — arrêt progressif (ce dont nous avons besoin).

Les signaux restants ne sont pas nécessaires dans cette tâche, nous omettrons donc leur analyse. Pour terminer le processus correctement, vous devrez écrire le hook preStop suivant :

        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"

À première vue, c’est tout ce qui est nécessaire pour effectuer un arrêt en douceur dans les deux conteneurs. Toutefois, la tâche est plus ardue qu’il n’y paraît. Vous trouverez ci-dessous deux cas dans lesquels un arrêt progressif n'a pas fonctionné et a entraîné une indisponibilité à court terme du projet pendant le déploiement.

Pratique. Problèmes possibles avec l'arrêt progressif

Nginx

Tout d’abord, il est utile de rappeler : en plus d’exécuter la commande nginx -s quit Il y a une autre étape à laquelle il convient de prêter attention. Nous avons rencontré un problème où NGINX envoyait toujours SIGTERM au lieu du signal SIGQUIT, ce qui empêchait les requêtes de se terminer correctement. Des cas similaires peuvent être trouvés, par exemple, ici. Malheureusement, nous n'avons pas pu déterminer la raison précise de ce comportement : il y avait un soupçon sur la version NGINX, mais il n'a pas été confirmé. Le symptôme était que des messages étaient observés dans les journaux du conteneur NGINX : "ouvrir la prise n°10 à gauche dans la connexion 5", après quoi le pod s'est arrêté.

Nous pouvons observer un tel problème, par exemple, à partir des réponses sur l'Ingress dont nous avons besoin :

Trucs et astuces Kubernetes : fonctionnalités d'arrêt progressif dans NGINX et PHP-FPM
Indicateurs de codes d'état au moment du déploiement

Dans ce cas, nous recevons simplement un code d'erreur 503 d'Ingress lui-même : il ne peut pas accéder au conteneur NGINX, car il n'est plus accessible. Si vous consultez les journaux du conteneur avec NGINX, ils contiennent les éléments suivants :

[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13

Après avoir modifié le signal d'arrêt, le conteneur commence à s'arrêter correctement : ceci est confirmé par le fait que l'erreur 503 n'est plus observée.

Si vous rencontrez un problème similaire, il est logique de déterminer quel signal d'arrêt est utilisé dans le conteneur et à quoi ressemble exactement le crochet preStop. Il est fort possible que la raison réside précisément dans cela.

PHP-FPM... et plus encore

Le problème avec PHP-FPM est décrit de manière triviale : il n'attend pas la fin des processus enfants, il les termine, c'est pourquoi des erreurs 502 se produisent lors du déploiement et d'autres opérations. Il existe plusieurs rapports de bogues sur bugs.php.net depuis 2005 (par exemple ici и ici), qui décrit ce problème. Mais vous ne verrez probablement rien dans les journaux : PHP-FPM annoncera la fin de son processus sans aucune erreur ni notification de tiers.

Il convient de préciser que le problème lui-même peut dépendre plus ou moins de l'application elle-même et peut ne pas se manifester, par exemple lors de la surveillance. Si vous le rencontrez, une solution de contournement simple vous vient d'abord à l'esprit : ajoutez un hook preStop avec sleep(30). Cela vous permettra de compléter toutes les demandes précédentes (et nous n'en acceptons pas de nouvelles, puisque le pod déjà capable de Terminer), et après 30 secondes, le pod lui-même se terminera par un signal SIGTERM.

Il s'avère que lifecycle pour le conteneur ressemblera à ceci :

    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"

Cependant, en raison des 30 secondes sleep nous fortement nous augmenterons le temps de déploiement, puisque chaque pod sera terminé minimum 30 secondes, ce qui est mauvais. Que peut-on faire à ce sujet ?

Passons-nous à la partie responsable de l'exécution directe de la demande. Dans notre cas c'est PHP FPMQui par défaut, ne surveille pas l'exécution de ses processus enfants: Le processus maître est immédiatement terminé. Vous pouvez modifier ce comportement en utilisant la directive process_control_timeout, qui spécifie les délais pendant lesquels les processus enfants attendent les signaux du maître. Si vous définissez la valeur sur 20 secondes, cela couvrira la plupart des requêtes exécutées dans le conteneur et arrêtera le processus maître une fois qu'elles seront terminées.

Fort de ces connaissances, revenons à notre dernier problème. Comme mentionné, Kubernetes n’est pas une plateforme monolithique : la communication entre ses différents composants prend un certain temps. Cela est particulièrement vrai lorsque l'on considère le fonctionnement des entrées et d'autres composants associés, car en raison d'un tel retard au moment du déploiement, il est facile d'obtenir une augmentation de 500 erreurs. Par exemple, une erreur peut survenir au stade de l'envoi d'une requête vers un amont, mais le « décalage » d'interaction entre les composants est assez court - moins d'une seconde.

Ainsi, Au total avec la directive déjà mentionnée process_control_timeout vous pouvez utiliser la construction suivante pour lifecycle:

lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]

Dans ce cas, nous compenserons le retard avec la commande sleep et n'augmente pas beaucoup le temps de déploiement : y a-t-il une différence notable entre 30 secondes et une ?.. En fait, c'est le process_control_timeoutEt lifecycle utilisé uniquement comme « filet de sécurité » en cas de décalage.

D'une manière générale, le comportement décrit et la solution de contournement correspondante ne s'appliquent pas uniquement à PHP-FPM. Une situation similaire peut se produire d’une manière ou d’une autre lors de l’utilisation d’autres langages/frameworks. Si vous ne pouvez pas corriger l'arrêt progressif par d'autres moyens - par exemple, en réécrivant le code afin que l'application traite correctement les signaux de terminaison - vous pouvez utiliser la méthode décrite. Ce n'est peut-être pas le plus beau, mais ça marche.

Pratique. Test de charge pour vérifier le fonctionnement du pod

Les tests de charge sont l'un des moyens de vérifier le fonctionnement du conteneur, car cette procédure le rapproche des conditions de combat réelles lorsque les utilisateurs visitent le site. Pour tester les recommandations ci-dessus, vous pouvez utiliser Yandex.Tankom: Il couvre parfaitement tous nos besoins. Voici des conseils et des recommandations pour effectuer des tests avec un exemple clair tiré de notre expérience grâce aux graphiques de Grafana et Yandex.Tank lui-même.

La chose la plus importante ici est vérifier les modifications étape par étape. Après avoir ajouté un nouveau correctif, exécutez le test et voyez si les résultats ont changé par rapport à la dernière exécution. Dans le cas contraire, il sera difficile d’identifier les solutions inefficaces et, à long terme, cela ne pourra que nuire (par exemple, augmenter le temps de déploiement).

Une autre nuance est de consulter les journaux du conteneur lors de sa résiliation. Des informations sur l'arrêt progressif y sont-elles enregistrées ? Y a-t-il des erreurs dans les journaux lors de l'accès à d'autres ressources (par exemple, à un conteneur PHP-FPM voisin) ? Des erreurs dans l'application elle-même (comme dans le cas de NGINX décrit ci-dessus) ? J'espère que les informations introductives de cet article vous aideront à mieux comprendre ce qui arrive au conteneur lors de sa terminaison.

Ainsi, le premier test a eu lieu sans lifecycle et sans directives supplémentaires pour le serveur d'applications (process_control_timeout en PHP-FPM). Le but de ce test était d'identifier le nombre approximatif d'erreurs (et s'il y en a). De plus, à partir d'informations supplémentaires, vous devez savoir que le temps de déploiement moyen de chaque pod était d'environ 5 à 10 secondes jusqu'à ce qu'il soit complètement prêt. Les résultats sont :

Trucs et astuces Kubernetes : fonctionnalités d'arrêt progressif dans NGINX et PHP-FPM

Le panneau d'information Yandex.Tank affiche un pic de 502 erreurs, survenu au moment du déploiement et qui a duré en moyenne jusqu'à 5 secondes. Vraisemblablement, cela était dû au fait que les requêtes existantes adressées à l'ancien pod étaient terminées au moment où il était terminé. Après cela, 503 erreurs sont apparues, résultat d'un conteneur NGINX arrêté, qui a également interrompu les connexions à cause du backend (ce qui a empêché Ingress de s'y connecter).

Voyons comment process_control_timeout en PHP-FPM nous aidera à attendre la fin des processus enfants, c'est-à-dire corriger de telles erreurs. Redéployez à l'aide de cette directive :

Trucs et astuces Kubernetes : fonctionnalités d'arrêt progressif dans NGINX et PHP-FPM

Il n'y a plus d'erreurs lors du 500ème déploiement ! Le déploiement est réussi, l'arrêt progressif fonctionne.

Cependant, il convient de rappeler le problème des conteneurs Ingress, un petit pourcentage d'erreurs que nous pouvons recevoir en raison d'un décalage temporel. Pour les éviter, il ne reste plus qu'à ajouter une structure avec sleep et répétez le déploiement. Cependant, dans notre cas particulier, aucun changement n’était visible (encore une fois, aucune erreur).

Conclusion

Pour terminer le processus en douceur, nous attendons le comportement suivant de la part de l'application :

  1. Attendez quelques secondes, puis arrêtez d'accepter de nouvelles connexions.
  2. Attendez que toutes les requêtes soient terminées et fermez toutes les connexions keepalive qui n'exécutent pas de requêtes.
  3. Terminez votre processus.

Cependant, toutes les applications ne peuvent pas fonctionner de cette façon. Une solution au problème dans les réalités de Kubernetes est :

  • ajout d'un crochet de pré-arrêt qui attendra quelques secondes ;
  • étudier le fichier de configuration de notre backend pour les paramètres appropriés.

L'exemple avec NGINX montre clairement que même une application qui devrait initialement traiter correctement les signaux de terminaison peut ne pas le faire. Il est donc essentiel de vérifier 500 erreurs lors du déploiement de l'application. Cela vous permet également d'examiner le problème de manière plus large et de ne pas vous concentrer sur un seul pod ou conteneur, mais d'examiner l'ensemble de l'infrastructure dans son ensemble.

En tant qu'outil de test, vous pouvez utiliser Yandex.Tank avec n'importe quel système de surveillance (dans notre cas, les données ont été extraites de Grafana avec un backend Prometheus pour le test). Les problèmes d'arrêt progressif sont clairement visibles sous les fortes charges que le benchmark peut générer, et la surveillance permet d'analyser la situation plus en détail pendant ou après le test.

En réponse aux commentaires sur l'article : il convient de mentionner que les problèmes et les solutions sont décrits ici en relation avec NGINX Ingress. Pour d’autres cas, il existe d’autres solutions, que nous pouvons envisager dans les documents suivants de la série.

PS

Autres trucs et astuces de la série K8s :

Source: habr.com

Ajouter un commentaire