Sfaturi și trucuri Kubernetes: caracteristici de închidere grațioasă în NGINX și PHP-FPM

O condiție tipică la implementarea CI/CD în Kubernetes: aplicația trebuie să fie capabilă să nu accepte cereri noi de clienți înainte de a opri complet și, cel mai important, să le completeze cu succes pe cele existente.

Sfaturi și trucuri Kubernetes: caracteristici de închidere grațioasă în NGINX și PHP-FPM

Respectarea acestei condiții vă permite să obțineți un timp de nefuncționare zero în timpul implementării. Cu toate acestea, chiar și atunci când utilizați pachete foarte populare (cum ar fi NGINX și PHP-FPM), puteți întâmpina dificultăți care vor duce la un val de erori cu fiecare implementare...

Teorie. Cum trăiește pod

Am publicat deja în detaliu despre ciclul de viață al unui pod acest articol. În contextul subiectului luat în considerare, ne interesează următoarele: în momentul în care podul intră în stare terminator, solicitările noi nu mai sunt trimise către acesta (pod șters din lista de puncte finale pentru serviciu). Astfel, pentru a evita timpii de nefuncționare în timpul implementării, este suficient să rezolvăm problema opririi corecte a aplicației.

De asemenea, ar trebui să vă amintiți că perioada de grație implicită este 30 de secunde: după aceasta, pod-ul va fi încheiat și aplicația trebuie să aibă timp să proceseze toate cererile înainte de această perioadă. Nota: deși orice solicitare care durează mai mult de 5-10 secunde este deja problematică, iar oprirea grațioasă nu o va mai ajuta...

Pentru a înțelege mai bine ce se întâmplă când un pod se termină, priviți următoarea diagramă:

Sfaturi și trucuri Kubernetes: caracteristici de închidere grațioasă în NGINX și PHP-FPM

A1, B1 - Primirea modificărilor despre starea focarului
A2 - Plecare SIGTERM
B2 - Eliminarea unui pod de la punctele finale
B3 - Primirea modificărilor (lista de puncte finale s-a schimbat)
B4 - Actualizați regulile iptables

Vă rugăm să rețineți: ștergerea podului endpoint și trimiterea SIGTERM nu se întâmplă secvenţial, ci în paralel. Și datorită faptului că Ingress nu primește imediat lista actualizată de Endpoint-uri, noi solicitări de la clienți vor fi trimise către pod, ceea ce va provoca o eroare 500 în timpul rezilierii podului (pentru materiale mai detaliate despre această problemă, noi tradus). Această problemă trebuie rezolvată în următoarele moduri:

  • Trimitere conexiune: închideți anteturile de răspuns (dacă se referă la o aplicație HTTP).
  • Dacă nu este posibil să faceți modificări codului, atunci articolul următor descrie o soluție care vă va permite să procesați cererile până la sfârșitul perioadei de grație.

Teorie. Cum NGINX și PHP-FPM își încheie procesele

Nginx

Să începem cu NGINX, deoarece totul este mai mult sau mai puțin evident cu el. Scufundându-ne în teorie, aflăm că NGINX are un proces principal și mai mulți „lucrători” - acestea sunt procese copil care procesează cererile clienților. Este oferită o opțiune convenabilă: utilizarea comenzii nginx -s <SIGNAL> terminați procesele fie în modul de închidere rapidă, fie în modul de închidere grațioasă. Evident, aceasta din urmă variantă este cea care ne interesează.

Atunci totul este simplu: trebuie să adaugi la preStop-hook o comandă care va trimite un semnal de oprire grațios. Acest lucru se poate face în Deployment, în blocul container:

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

Acum, când podul se închide, vom vedea următoarele în jurnalele containerului 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

Și asta va însemna ceea ce avem nevoie: NGINX așteaptă ca cererile să se finalizeze și apoi oprește procesul. Cu toate acestea, mai jos vom lua în considerare și o problemă comună din cauza căreia, chiar și cu comanda nginx -s quit procesul se încheie incorect.

Și în această etapă am terminat cu NGINX: cel puțin din jurnale puteți înțelege că totul funcționează așa cum trebuie.

Care este treaba cu PHP-FPM? Cum se ocupă de oprirea grațioasă? Să ne dăm seama.

PHP-FPM

În cazul PHP-FPM, există puțin mai puține informații. Dacă te concentrezi asupra manualul oficial conform PHP-FPM, se va spune că sunt acceptate următoarele semnale POSIX:

  1. SIGINT, SIGTERM - oprire rapidă;
  2. SIGQUIT — închidere grațioasă (de ce avem nevoie).

Semnalele rămase nu sunt necesare în această sarcină, așa că vom omite analiza lor. Pentru a termina corect procesul, va trebui să scrieți următorul cârlig preStop:

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

La prima vedere, acesta este tot ceea ce este necesar pentru a efectua o oprire grațioasă în ambele containere. Cu toate acestea, sarcina este mai dificilă decât pare. Mai jos sunt două cazuri în care oprirea grațioasă nu a funcționat și a cauzat indisponibilitatea pe termen scurt a proiectului în timpul implementării.

Practică. Posibile probleme cu oprirea grațioasă

Nginx

În primul rând, este util de reținut: pe lângă executarea comenzii nginx -s quit Mai este o etapă căreia merită să fii atent. Am întâlnit o problemă în care NGINX ar trimite în continuare SIGTERM în loc de semnalul SIGQUIT, ceea ce face ca cererile să nu fie finalizate corect. Cazuri similare pot fi găsite, de exemplu, aici. Din păcate, nu am putut determina motivul specific al acestui comportament: a existat o suspiciune cu privire la versiunea NGINX, dar nu a fost confirmată. Simptomul a fost că mesajele au fost observate în jurnalele containerului NGINX: „deschideți priza nr. 10 rămasă în conexiunea 5”, după care păstaia s-a oprit.

Putem observa o astfel de problemă, de exemplu, din răspunsurile la Ingress de care avem nevoie:

Sfaturi și trucuri Kubernetes: caracteristici de închidere grațioasă în NGINX și PHP-FPM
Indicatori ai codurilor de stare la momentul implementării

În acest caz, primim doar un cod de eroare 503 de la Ingress însuși: acesta nu poate accesa containerul NGINX, deoarece nu mai este accesibil. Dacă te uiți la jurnalele containerului cu NGINX, acestea conțin următoarele:

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

După schimbarea semnalului de oprire, containerul începe să se oprească corect: acest lucru este confirmat de faptul că eroarea 503 nu mai este observată.

Dacă întâmpinați o problemă similară, este logic să vă dați seama ce semnal de oprire este utilizat în container și cum arată exact cârligul preStop. Este foarte posibil ca motivul să stea tocmai în asta.

PHP-FPM... și multe altele

Problema cu PHP-FPM este descrisă într-un mod banal: nu așteaptă finalizarea proceselor copil, ci le termină, motiv pentru care apar erori 502 în timpul implementării și a altor operațiuni. Există mai multe rapoarte de erori pe bugs.php.net din 2005 (de ex aici и aici), care descrie această problemă. Dar cel mai probabil nu veți vedea nimic în jurnale: PHP-FPM va anunța finalizarea procesului său fără erori sau notificări de la terți.

Merită clarificat faptul că problema în sine poate depinde într-o măsură mai mică sau mai mare de aplicația în sine și poate să nu se manifeste, de exemplu, în monitorizare. Dacă o întâlniți, vă vine în minte o soluție simplă: adăugați un cârlig preStop cu sleep(30). Vă va permite să finalizați toate solicitările care au fost înainte (și nu acceptăm altele noi, deoarece pod deja capabil de terminator), iar după 30 de secunde podul în sine se va termina cu un semnal SIGTERM.

Se pare că lifecycle căci containerul va arăta astfel:

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

Cu toate acestea, din cauza celor 30 de secunde sleep noi tare vom mări timpul de implementare, deoarece fiecare pod va fi terminat minim 30 de secunde, ceea ce este rău. Ce se poate face în privința asta?

Să apelăm la partea responsabilă cu executarea directă a cererii. În cazul nostru este PHP-FPMimplicit nu monitorizează execuția proceselor sale fii: Procesul principal este încheiat imediat. Puteți modifica acest comportament folosind directiva process_control_timeout, care specifică limitele de timp pentru ca procesele copil să aștepte semnale de la master. Dacă setați valoarea la 20 de secunde, aceasta va acoperi majoritatea interogărilor care rulează în container și va opri procesul principal odată ce acestea sunt finalizate.

Cu aceste cunoștințe, să revenim la ultima noastră problemă. După cum am menționat, Kubernetes nu este o platformă monolitică: comunicarea între diferitele sale componente durează ceva timp. Acest lucru este valabil mai ales când luăm în considerare funcționarea Ingress-urilor și a altor componente aferente, deoarece datorită unei astfel de întârzieri la momentul implementării, este ușor să obțineți o creștere de 500 de erori. De exemplu, o eroare poate apărea în etapa de trimitere a unei solicitări către un amonte, dar „decalajul” al interacțiunii dintre componente este destul de scurt - mai puțin de o secundă.

Prin urmare, In total cu directiva deja amintita process_control_timeout puteti folosi urmatoarea constructie pentru lifecycle:

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

În acest caz, vom compensa întârzierea cu comanda sleep și nu creșteți semnificativ timpul de implementare: la urma urmei, diferența dintre 30 de secunde și una este vizibilă?... De fapt, este process_control_timeoutȘi lifecycle folosit doar ca „plasă de siguranță” în caz de întârziere.

În general vorbind comportamentul descris și soluția corespunzătoare se aplică nu numai PHP-FPM. O situație similară poate apărea într-un fel sau altul atunci când utilizați alte limbi/cadre. Dacă nu puteți remedia oprirea grațioasă în alte moduri - de exemplu, prin rescrierea codului, astfel încât aplicația să proceseze corect semnalele de terminare - puteți utiliza metoda descrisă. Poate că nu este cel mai frumos, dar funcționează.

Practică. Testare de încărcare pentru a verifica funcționarea podului

Testarea de încărcare este una dintre modalitățile de a verifica cum funcționează containerul, deoarece această procedură îl aduce mai aproape de condițiile reale de luptă atunci când utilizatorii vizitează site-ul. Pentru a testa recomandările de mai sus, puteți utiliza Yandex.Tankom: Acoperă perfect toate nevoile noastre. Următoarele sunt sfaturi și recomandări pentru efectuarea testelor cu un exemplu clar din experiența noastră, datorită graficelor Grafana și Yandex.Tank în sine.

Cel mai important lucru aici este verificați modificările pas cu pas. După adăugarea unei noi remedieri, rulați testul și vedeți dacă rezultatele s-au schimbat în comparație cu ultima rulare. În caz contrar, va fi dificil să identifici soluții ineficiente, iar pe termen lung poate face doar rău (de exemplu, crește timpul de implementare).

O altă nuanță este să te uiți la jurnalele containerului în timpul încetării acestuia. Sunt înregistrate acolo informații despre închiderea grațioasă? Există erori în jurnalele la accesarea altor resurse (de exemplu, un container PHP-FPM vecin)? Erori în aplicația în sine (ca și în cazul NGINX descris mai sus)? Sper că informațiile introductive din acest articol vă vor ajuta să înțelegeți mai bine ce se întâmplă cu containerul în timpul încetării acestuia.

Deci, primul test a avut loc fără lifecycle și fără directive suplimentare pentru serverul de aplicații (process_control_timeout în PHP-FPM). Scopul acestui test a fost de a identifica numărul aproximativ de erori (și dacă există). De asemenea, din informații suplimentare, trebuie să știți că timpul mediu de implementare pentru fiecare pod a fost de aproximativ 5-10 secunde până când acesta a fost complet gata. Rezultatele sunt:

Sfaturi și trucuri Kubernetes: caracteristici de închidere grațioasă în NGINX și PHP-FPM

Panoul de informații Yandex.Tank arată un vârf de 502 erori, care au avut loc în momentul implementării și au durat în medie până la 5 secunde. Probabil că acest lucru s-a datorat faptului că cererile existente către vechiul pod au fost terminate atunci când acesta era terminat. După aceasta, au apărut 503 erori, care a fost rezultatul unui container NGINX oprit, care a scăpat și conexiunile din cauza backend-ului (care a împiedicat Ingress să se conecteze la acesta).

Să vedem cum process_control_timeout în PHP-FPM ne va ajuta să așteptăm finalizarea proceselor copil, de exemplu. corectează astfel de erori. Reinstalați folosind această directivă:

Sfaturi și trucuri Kubernetes: caracteristici de închidere grațioasă în NGINX și PHP-FPM

Nu mai există erori în timpul celei de-a 500-a implementări! Implementarea este reușită, oprirea grațioasă funcționează.

Cu toate acestea, merită să ne amintim problema cu containerele Ingress, un mic procent de erori în care putem primi din cauza unui decalaj de timp. Pentru a le evita, tot ce rămâne este să adăugați o structură cu sleep și repetați desfășurarea. Cu toate acestea, în cazul nostru particular, nu au fost vizibile modificări (din nou, fără erori).

Concluzie

Pentru a încheia procesul cu grație, ne așteptăm la următorul comportament din partea aplicației:

  1. Așteptați câteva secunde și apoi nu mai acceptați conexiuni noi.
  2. Așteptați ca toate solicitările să se finalizeze și închideți toate conexiunile keepalive care nu execută cereri.
  3. Încheiați-vă procesul.

Cu toate acestea, nu toate aplicațiile pot funcționa astfel. O soluție la problema din realitățile Kubernetes este:

  • adăugarea unui cârlig pre-stop care va aștepta câteva secunde;
  • studiind fișierul de configurare al backend-ului nostru pentru parametrii corespunzători.

Exemplul cu NGINX arată clar că chiar și o aplicație care ar trebui să proceseze inițial semnalele de terminare corect poate să nu facă acest lucru, așa că este esențial să verificați 500 de erori în timpul implementării aplicației. Acest lucru vă permite, de asemenea, să priviți problema mai larg și să nu vă concentrați pe un singur pod sau container, ci să priviți întreaga infrastructură în ansamblu.

Ca instrument de testare, puteți utiliza Yandex.Tank împreună cu orice sistem de monitorizare (în cazul nostru, datele au fost preluate de la Grafana cu un backend Prometheus pentru test). Problemele legate de oprirea grațioasă sunt vizibile în mod clar sub sarcini mari pe care le poate genera benchmark-ul, iar monitorizarea ajută la analiza mai detaliată a situației în timpul sau după test.

Ca răspuns la feedback-ul despre articol: merită menționat că problemele și soluțiile sunt descrise aici în legătură cu NGINX Ingress. Pentru alte cazuri, există și alte soluții, pe care le putem lua în considerare în următoarele materiale ale seriei.

PS

Altele din seria de sfaturi și trucuri K8s:

Sursa: www.habr.com

Adauga un comentariu