Conseils et astuces Kubernetes : caractĂ©ristiques de l'exĂ©cution de l'arrĂȘt gracieux dans NGINX et PHP-FPM

Condition standard lors de la mise en Ɠuvre de CI/CD dans Kubernetes : l'application doit ĂȘtre capable de refuser de nouvelles requĂȘtes clients avant d'ĂȘtre complĂštement arrĂȘtĂ©e, mais surtout, elle doit ĂȘtre en mesure de terminer avec succĂšs les demandes existantes.

Conseils et astuces Kubernetes : caractĂ©ristiques de l'arrĂȘt en douceur dans NGINX et PHP-FPM

Respecter cette condition permet d'atteindre un temps d'arrĂȘt nul lors du dĂ©ploiement. Cependant, mĂȘme en utilisant des combinaisons trĂšs populaires (comme NGINX et PHP-FPM), il est possible de rencontrer des difficultĂ©s qui entraĂźneront une augmentation des erreurs Ă  chaque dĂ©ploiement


Théorie. Comment vit un pod

Nous avons dĂ©jĂ  publiĂ© des dĂ©tails sur le cycle de vie d'un pod. cet articleDans le contexte du sujet traitĂ©, nous nous intĂ©ressons au moment oĂč le pod passe Ă  l'Ă©tat Terminating, il ne reçoit plus de nouvelles requĂȘtes (le pod – il n'est utilisĂ© qu'une seule fois pour exĂ©cuter la commande et disparaĂźt ensuite ! Un employĂ© de la sĂ©curitĂ© de l'information surveillant la machine de la victime ne pourra pas dĂ©tecter est retirĂ© de la liste des endpoints pour le service). Ainsi, pour Ă©viter un temps d'arrĂȘt lors du dĂ©ploiement, il nous suffit de rĂ©soudre le problĂšme d'arrĂȘt correct de l'application.

Il convient Ă©galement de se rappeler que la pĂ©riode de grĂące est par dĂ©faut de 30 secondes: aprĂšs cela, le pod sera terminĂ© et l'application doit ĂȘtre en mesure de traiter toutes les requĂȘtes avant cette pĂ©riode. Remarque: bien que toute requĂȘte prenant plus de 5 Ă  10 secondes soit dĂ©jĂ  problĂ©matique, et qu'un arrĂȘt gracieux ne pourra plus l'aider


Pour mieux comprendre ce qui se passe lorsque le pod termine son travail, il suffit d'étudier le schéma suivant :

Conseils et astuces Kubernetes : caractĂ©ristiques de l'arrĂȘt en douceur dans NGINX et PHP-FPM

A1, B1 — RĂ©ception des changements d'Ă©tat du pod
A2 — Envoi de SIGTERM
B2 — Suppression du pod des endpoints
B3 — RĂ©ception des changements (la liste des endpoints a changĂ©)
B4 — Mise à jour des rùgles iptables

Remarque : la suppression de l'endpoint du pod et l'envoi de SIGTERM ne se font pas de maniĂšre sĂ©quentielle, mais en parallĂšle. Et en raison du fait que l'Ingress ne reçoit pas immĂ©diatement la liste mise Ă  jour des endpoints, de nouvelles requĂȘtes des clients seront envoyĂ©es au pod, ce qui entraĂźnera des erreurs 500 pendant la terminaison du pod. (nous avons traduit un matĂ©riel plus dĂ©taillĂ© Ă  ce sujet, qui traite). Pour rĂ©soudre ce problĂšme, il faut procĂ©der de la maniĂšre suivante :

  • Envoyer dans les en-tĂȘtes de rĂ©ponse Connection: close (si cela concerne une application HTTP).
  • S'il n'est pas possible d'apporter des modifications au code, cet article dĂ©crit une solution qui permettra de traiter les requĂȘtes jusqu'Ă  la fin de la pĂ©riode de grĂące.

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

NGINX

Commençons par NGINX, car tout est assez Ă©vident avec lui. En nous plongeant dans la thĂ©orie, nous dĂ©couvrons qu'NGINX possĂšde un processus maĂźtre et plusieurs « workers » — ce sont des processus enfants qui gĂšrent les requĂȘtes des clients. Il existe une fonctionnalitĂ© pratique : Ă  l'aide de la commande nginx -s nous pouvons terminer les processus soit en mode fast shutdown, soit en graceful shutdown. Évidemment, c'est le dernier choix qui nous intĂ©resse.

Ensuite, tout est simple : nous devons ajouter dans le hook preStop la commande qui enverra le signal pour un graceful shutdown. Cela peut ĂȘtre fait dans le Deployment, dans le bloc de conteneur :

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

Maintenant, au moment de l'arrĂȘt du pod, nous verrons dans les logs du conteneur NGINX ce qui suit :

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 exactement ce dont nous avons besoin : NGINX attend la fin de l'exĂ©cution des requĂȘtes, aprĂšs quoi il termine le processus. Cependant, ci-dessous, nous examinerons un problĂšme courant qui peut entraĂźner une interruption mĂȘme en prĂ©sence de la commande nginx -s quit le processus se termine de maniĂšre incorrecte.

À cette Ă©tape, nous avons terminĂ© avec NGINX : au moins, d'aprĂšs les logs, nous pouvons comprendre que tout fonctionne comme il se doit.

Qu'en est-il de PHP-FPM ? Comment gĂšre-t-il le graceful shutdown ? Analysons cela.

PHP-FPM

Dans le cas de PHP-FPM, les informations sont un peu moins nombreuses. Si nous nous basons sur le manuel officiel de PHP-FPM, il indique que les signaux POSIX suivants sont pris en compte :

  1. SIGINT, SIGTERM — fast shutdown ;
  2. SIGQUIT — graceful shutdown (ce dont nous avons besoin).

Les autres signaux ne sont pas nĂ©cessaires pour cette tĂąche, ainsi nous allons omettre leur analyse. Pour un arrĂȘt correct du processus, le hook preStop suivant devra ĂȘtre Ă©crit :

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

À premiĂšre vue, c'est tout ce qui est nĂ©cessaire pour effectuer un graceful shutdown dans les deux conteneurs. Cependant, la tĂąche est plus complexe qu'elle n'y paraĂźt. Deux cas oĂč le graceful shutdown n'a pas fonctionnĂ©, provoquant une indisponibilitĂ© temporaire du projet lors du dĂ©ploiement, sont examinĂ©s ci-dessous.

Pratique. ProblĂšmes possibles avec le graceful shutdown

NGINX

PremiĂšre chose Ă  retenir : en plus de l'exĂ©cution de la commande nginx -s quit Il existe une autre Ă©tape Ă  ne pas nĂ©gliger. Nous avons rencontrĂ© un problĂšme oĂč NGINX envoyait SIGTERM au lieu du signal SIGQUIT, ce qui empĂȘchait les requĂȘtes de se terminer correctement. On retrouve des cas similaires, par exemple, ici. Malheureusement, nous n'avons pas pu Ă©tablir la cause prĂ©cise de ce comportement : nous avons suspectĂ© certaines versions de NGINX, mais cela n'a pas Ă©tĂ© confirmĂ©. Les symptĂŽmes Ă©taient que dans les journaux du conteneur NGINX, nous observions des messages « open socket #10 left in connection 5 », aprĂšs quoi le pod s'arrĂȘtait.

Nous pouvons observer ce problÚme, par exemple, dans les réponses de notre Ingress :

Conseils et astuces Kubernetes : caractĂ©ristiques de l'arrĂȘt en douceur dans NGINX et PHP-FPM
Les indicateurs de statut au moment du déploiement

Dans ce cas, nous recevons le code d'erreur 503 de l'Ingress lui-mĂȘme : il ne peut pas accĂ©der au conteneur NGINX, car celui-ci n'est dĂ©jĂ  plus disponible. Si nous consultons les journaux du conteneur NGINX, nous y trouvons :

[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 : cela est confirmĂ© par le fait qu'aucune erreur 503 n'est plus observĂ©e.

Si vous ĂȘtes confrontĂ© Ă  un problĂšme similaire, il est logique de vĂ©rifier quel signal d'arrĂȘt est utilisĂ© dans le conteneur et comment le hook preStop est configurĂ©. Il est tout Ă  fait possible que la raison en soit.

PHP-FPM
 et pas seulement

Le problÚme avec PHP-FPM est décrit de maniÚre triviale : il ne attend pas la fin des processus fils, les termine, ce qui entraßne des erreurs 502 lors des déploiements et d'autres opérations. Des rapports d'erreurs existent depuis 2005 sur bugs.php.net (par exemple, ici et ici), décrivant ce problÚme. Vous ne verrez probablement rien dans les journaux : PHP-FPM annonçera la fin de son processus sans aucune erreur ni notification externe.

Il convient de prĂ©ciser que le problĂšme lui-mĂȘme peut varier en fonction de l'application et peut ne pas se manifester, par exemple, dans la surveillance. Si vous tombez sur ce problĂšme, le premier rĂ©flexe pourrait ĂȘtre d'appliquer un simple contournement : ajouter un hook preStop avec sleep(30). Cela permettra de terminer toutes les requĂȘtes qui ont eu lieu auparavant (et nous n'acceptons pas de nouvelles, puisque le pod est dĂ©jĂ  dans l'Ă©tat Terminating), et aprĂšs 30 secondes, le pod se terminera avec le signal SIGTERM.

Il en résulte qu'un seul image provenant d'un registre lent peut bloquer le déploiement le lifecycle pour le conteneur se présentera comme suit :

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

Cependant, à cause de l'indication des 30 secondes, cela sleep nous fortement nous allons augmenter le temps de déploiement, car chaque pod sera terminé minimum 30 secondes, ce qui n'est pas bon. Que pouvons-nous y faire ?

Nous allons nous adresser Ă  la partie responsable de l'exĂ©cution directe de l'application. Dans notre cas, c'est PHP-FPM, qui par dĂ©faut ne surveille pas l'exĂ©cution de ses processus enfants: le processus maĂźtre est terminĂ© immĂ©diatement. Ce comportement peut ĂȘtre modifiĂ© Ă  l'aide de la directive process_control_timeout, qui spĂ©cifie des limites temporelles pour l'attente des signaux des processus enfants du maĂźtre. Si une valeur de 20 secondes est dĂ©finie, cela couvrira la plupart des requĂȘtes exĂ©cutĂ©es dans le conteneur, et aprĂšs leur achĂšvement, le processus maĂźtre sera arrĂȘtĂ©.

Avec cette connaissance, revenons Ă  notre dernier problĂšme. Comme mentionnĂ© prĂ©cĂ©demment, Kubernetes n'est pas une plateforme monolithique : l'interaction entre ses diffĂ©rents composants prend un certain temps. Cela est particuliĂšrement pertinent lorsque nous examinons le fonctionnement des Ingress et d'autres composants connexes, car en raison de ce retard au moment du dĂ©ploiement, il est facile de rencontrer des pics d'erreurs 500. Par exemple, une erreur peut se produire lors de l'envoi d'une requĂȘte Ă  l'upstream, mais le « lag temporel » d'interaction entre les composants est en fait assez court — moins d'une seconde.

Par conséquent, dans l'ensemble avec la directive mentionnée ci-dessus process_control_timeout vous pouvez utiliser la structure suivante pour le lifecycle:

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

Dans ce cas, nous compensons le retard avec la commande sleep et nous n'augmentons pas beaucoup le temps de déploiement : aprÚs tout, il y a une différence entre 30 secondes et une ?.. En substance, c'est « le travail principal » qui est pris en charge par process_control_timeout, et le lifecycle est utilisé uniquement comme « sécurité » en cas de lag.

il est impossible d'obtenir une le comportement dĂ©crit et la solution correspondante ne concernent pas uniquement PHP-FPM. Une situation similaire peut survenir d'une maniĂšre ou d'une autre lors de l'utilisation d'autres langages/frameworks. Si d'autres moyens pour corriger l'arrĂȘt en douceur ne fonctionnent pas — par exemple, réécrire le code pour que l'application gĂšre correctement les signaux de terminaison — vous pouvez appliquer la mĂ©thode dĂ©crite. Ce n'est peut-ĂȘtre pas la plus Ă©lĂ©gante, mais cela fonctionne.

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

Le test de charge est l'un des moyens de vĂ©rifier le fonctionnement d'un conteneur, car cette procĂ©dure se rapproche des conditions rĂ©elles, lorsque des utilisateurs accĂšdent au site. Pour tester les recommandations ci-dessus, vous pouvez utiliser Yandex.Tank: il couvre parfaitement tous nos besoins. Ci-dessous se trouvent des conseils et recommandations pour effectuer les tests avec un exemple concret – grĂące aux graphiques de Grafana et de Yandex.Tank – tirĂ© de notre expĂ©rience.

L'essentiel ici est de vĂ©rifier les modifications par Ă©tapes. AprĂšs l'ajout d'un nouveau correctif, lancez les tests et observez si les rĂ©sultats ont changĂ© par rapport au lancement prĂ©cĂ©dent. Sinon, il sera difficile d'identifier les solutions inefficaces, et Ă  long terme, cela pourrait mĂȘme causer des problĂšmes (par exemple, augmenter le temps de dĂ©ploiement).

Un autre point Ă  prendre en compte est d'examiner les journaux du conteneur lors de sa terminaison. Y a-t-il des informations sur un arrĂȘt en douceur ? Y a-t-il des erreurs dans les journaux lors de l'accĂšs Ă  d'autres ressources (par exemple, au conteneur PHP-FPM voisin) ? Y a-t-il des erreurs provenant de l'application elle-mĂȘme (comme dans le cas mentionnĂ© ci-dessus avec NGINX) ? J'espĂšre que les informations prĂ©liminaires de cet article aideront Ă  mieux comprendre ce qui se passe avec le conteneur lors de sa terminaison.

Ainsi, le premier lancement du test s'est effectué sans le lifecycle et sans directives supplémentaires pour le serveur d'applications (process_control_timeout dans PHP-FPM). L'objectif de ce test était d'identifier le nombre approximatif d'erreurs (et s'il y en a). Il convient également de noter que le temps moyen de déploiement de chaque pod était d'environ 5 à 10 secondes avant d'atteindre un état de pleine disponibilité. Les résultats sont les suivants :

Conseils et astuces Kubernetes : caractĂ©ristiques de l'arrĂȘt en douceur dans NGINX et PHP-FPM

Sur le tableau de bord de Yandex.Tank, on observe un pic de 502 erreurs qui s'est produit au moment du dĂ©ploiement et a durĂ© en moyenne jusqu'Ă  5 secondes. On suppose que cela Ă©tait dĂ» Ă  l'interruption des requĂȘtes existantes vers l'ancien pod au moment de sa terminaison. Par la suite, des erreurs 503 sont apparues, ce qui Ă©tait le rĂ©sultat d'un conteneur NGINX arrĂȘtĂ©, qui a Ă©galement rompu les connexions Ă  cause du backend (ce qui a empĂȘchĂ© Ingress de se connecter).

Voyons comment process_control_timeout dans PHP-FPM nous aidera à attendre la fin des processus enfants, c'est-à-dire à corriger ces erreurs. Un redéploiement déjà avec cette directive :

Conseils et astuces Kubernetes : caractĂ©ristiques de l'arrĂȘt en douceur dans NGINX et PHP-FPM

Plus de 500 erreurs pendant le dĂ©ploiement ! Le dĂ©ploiement se dĂ©roule avec succĂšs, l'arrĂȘt en douceur fonctionne.

Cependant, il est important de se rappeler le moment avec les conteneurs Ingress, un petit pourcentage d'erreurs que nous pouvons rencontrer en raison d'un lag temporaire. Pour les éviter, il suffit d'ajouter une construction avec sleep et de répéter le déploiement. Néanmoins, dans notre cas particulier, aucun changement n'était visible (pas d'erreurs à nouveau).

Conclusion

Pour achever correctement le processus, nous attendons du programme le comportement suivant :

  1. Attendre quelques secondes, puis cesser d'accepter de nouvelles connexions.
  2. Attendre la fin de toutes les requĂȘtes et fermer toutes les connexions keepalive qui n'effectuent pas de requĂȘtes.
  3. Terminer son processus.

Cependant, toutes les applications ne fonctionnent pas de cette maniÚre. L'une des solutions au problÚme dans le contexte de Kubernetes est :

  • l'ajout d'un hook pre-stop qui attendra quelques secondes ;
  • l'examen du fichier de configuration de notre backend pour vĂ©rifier les paramĂštres appropriĂ©s.

L'exemple avec NGINX permet de comprendre qu'une application, mĂȘme celle censĂ©e gĂ©rer correctement les signaux de terminaison, peut ne pas le faire, donc il est crucial de vĂ©rifier la prĂ©sence d'erreurs 500 lors du dĂ©ploiement de l'application. Cela permet Ă©galement d'adopter une perspective plus large et d'Ă©viter de se concentrer uniquement sur un pod ou un conteneur particulier, mais de considĂ©rer l'ensemble de l'infrastructure.

Pour tester, vous pouvez utiliser Yandex.Tank en conjonction avec n'importe quel systĂšme de surveillance (dans notre cas, les donnĂ©es de test provenaient de Grafana avec un backend Prometheus). Les problĂšmes d'arrĂȘt en douceur sont bien visibles sous de fortes charges, que peut gĂ©nĂ©rer un benchmark, et la surveillance aide Ă  analyser plus en dĂ©tail la situation pendant ou aprĂšs le test.

En rĂ©ponse aux retours sur l'article : il convient de prĂ©ciser que les problĂšmes et leurs solutions dĂ©crits ici s'appliquent Ă  NGINX Ingress. Pour d'autres cas, il existe d'autres solutions que nous examinerons peut-ĂȘtre dans les futurs articles de cette sĂ©rie.

P.S.

Autre extrait de la série K8s tips & tricks :

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster