Kubernetes tips og triks: funksjoner i grasiøs nedleggelse i NGINX og PHP-FPM

En typisk tilstand ved implementering av CI/CD i Kubernetes: applikasjonen må ikke kunne akseptere nye klientforespørsler før den stopper helt, og viktigst av alt, fullføre eksisterende.

Kubernetes tips og triks: funksjoner i grasiøs nedleggelse i NGINX og PHP-FPM

Overholdelse av denne betingelsen lar deg oppnå null nedetid under distribusjon. Men selv når du bruker veldig populære pakker (som NGINX og PHP-FPM), kan du støte på problemer som vil føre til en bølge av feil med hver distribusjon...

Teori. Hvordan poden lever

Vi har allerede publisert i detalj om livssyklusen til en pod denne artikkelen. I sammenheng med emnet som vurderes, er vi interessert i følgende: i øyeblikket når poden kommer inn i staten Avslutte, nye forespørsler slutter å bli sendt til den (pod fjernet fra listen over endepunkter for tjenesten). For å unngå nedetid under distribusjon, er det derfor nok for oss å løse problemet med å stoppe applikasjonen på riktig måte.

Du bør også huske at standard utsettelsesperiode er 30 sekunder: etter dette vil poden bli avsluttet og søknaden må ha tid til å behandle alle forespørsler før denne perioden. Note: selv om enhver forespørsel som tar mer enn 5-10 sekunder allerede er problematisk, og en grasiøs avslutning vil ikke lenger hjelpe det...

For bedre å forstå hva som skjer når en pod avsluttes, se bare på følgende diagram:

Kubernetes tips og triks: funksjoner i grasiøs nedleggelse i NGINX og PHP-FPM

A1, B1 - Mottar endringer om tilstanden til ildstedet
A2 - Avgang SIGTERM
B2 - Fjerne en pod fra endepunkter
B3 - Motta endringer (listen over endepunkter er endret)
B4 - Oppdater iptables-regler

Vennligst merk: sletting av endepunktpoden og sending av SIGTERM skjer ikke sekvensielt, men parallelt. Og på grunn av det faktum at Ingress ikke umiddelbart mottar den oppdaterte listen over endepunkter, vil nye forespørsler fra klienter bli sendt til poden, noe som vil forårsake en 500-feil under podavslutning (for mer detaljert materiale om dette problemet, vi oversatt). Dette problemet må løses på følgende måter:

  • Send tilkobling: lukk svarhoder (hvis dette gjelder en HTTP-applikasjon).
  • Hvis det ikke er mulig å gjøre endringer i koden, beskriver følgende artikkel en løsning som lar deg behandle forespørsler til slutten av den grasiøse perioden.

Teori. Hvordan NGINX og PHP-FPM avslutter prosessene sine

Nginx

La oss starte med NGINX, siden alt er mer eller mindre åpenbart med det. Når vi dykker ned i teorien, lærer vi at NGINX har én hovedprosess og flere "arbeidere" - dette er underordnede prosesser som behandler klientforespørsler. Et praktisk alternativ er gitt: bruk av kommandoen nginx -s <SIGNAL> avslutte prosesser enten i rask avslutning eller grasiøs avslutningsmodus. Det er åpenbart det siste alternativet som interesserer oss.

Da er alt enkelt: du må legge til preStop-krok en kommando som vil sende et grasiøst avstengningssignal. Dette kan gjøres i Deployment, i containerblokken:

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

Nå, når poden slås av, vil vi se følgende i NGINX-beholderloggene:

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

Og dette vil bety det vi trenger: NGINX venter på at forespørsler skal fullføres, og dreper deretter prosessen. Men nedenfor vil vi også vurdere et vanlig problem på grunn av dette, selv med kommandoen nginx -s quit prosessen avsluttes feil.

Og på dette stadiet er vi ferdige med NGINX: i det minste fra loggene kan du forstå at alt fungerer som det skal.

Hva er greia med PHP-FPM? Hvordan takler den grasiøs nedleggelse? La oss finne ut av det.

PHP-FPM

Når det gjelder PHP-FPM, er det litt mindre informasjon. Hvis du fokuserer på offisiell manual i følge PHP-FPM vil det si at følgende POSIX-signaler er akseptert:

  1. SIGINT, SIGTERM — rask avslutning;
  2. SIGQUIT — grasiøs nedleggelse (det vi trenger).

De resterende signalene er ikke nødvendige i denne oppgaven, så vi vil utelate analysen deres. For å avslutte prosessen på riktig måte, må du skrive følgende preStop-krok:

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

Ved første øyekast er dette alt som kreves for å utføre en grasiøs nedstenging i begge beholderne. Oppgaven er imidlertid vanskeligere enn det ser ut til. Nedenfor er to tilfeller der en elegant nedleggelse ikke fungerte og forårsaket kortsiktig utilgjengelighet av prosjektet under distribusjon.

Øve på. Mulige problemer med grasiøs nedleggelse

Nginx

Først av alt er det nyttig å huske: i tillegg til å utføre kommandoen nginx -s quit Det er et stadium til som er verdt å være oppmerksom på. Vi støtt på et problem der NGINX fortsatt ville sende SIGTERM i stedet for SIGQUIT-signalet, noe som førte til at forespørsler ikke ble fullført riktig. Lignende tilfeller kan finnes, f.eks. her. Dessverre var vi ikke i stand til å fastslå den spesifikke årsaken til denne oppførselen: det var en mistanke om NGINX-versjonen, men den ble ikke bekreftet. Symptomet var at meldinger ble observert i NGINX-beholderloggene: "åpne kontakt #10 igjen i forbindelse 5", hvoretter poden stoppet.

Vi kan observere et slikt problem, for eksempel fra svarene på Ingressen vi trenger:

Kubernetes tips og triks: funksjoner i grasiøs nedleggelse i NGINX og PHP-FPM
Indikatorer for statuskoder på tidspunktet for distribusjon

I dette tilfellet mottar vi bare en 503-feilkode fra Ingress selv: den kan ikke få tilgang til NGINX-beholderen, siden den ikke lenger er tilgjengelig. Hvis du ser på beholderloggene med NGINX, inneholder de følgende:

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

Etter å ha endret stoppsignalet, begynner beholderen å stoppe riktig: dette bekreftes av det faktum at 503-feilen ikke lenger observeres.

Hvis du støter på et lignende problem, er det fornuftig å finne ut hvilket stoppsignal som brukes i beholderen og nøyaktig hvordan preStop-kroken ser ut. Det er godt mulig at årsaken ligger nettopp i dette.

PHP-FPM... og mer

Problemet med PHP-FPM er beskrevet på en triviell måte: den venter ikke på fullføringen av underordnede prosesser, den avslutter dem, og det er grunnen til at 502-feil oppstår under distribusjon og andre operasjoner. Det er flere feilrapporter på bugs.php.net siden 2005 (f her и her), som beskriver dette problemet. Men du vil mest sannsynlig ikke se noe i loggene: PHP-FPM vil kunngjøre fullføringen av prosessen uten noen feil eller tredjepartsvarsler.

Det er verdt å presisere at selve problemet kan avhenge i mindre eller større grad av selve applikasjonen og kanskje ikke manifestere seg, for eksempel i overvåking. Hvis du støter på det, dukker du først opp en enkel løsning: legg til en preStop-krok med sleep(30). Det vil tillate deg å fullføre alle forespørsler som var før (og vi godtar ikke nye, siden pod allerede i stand til Avslutte), og etter 30 sekunder vil selve poden avsluttes med et signal SIGTERM.

Det viser seg at lifecycle for beholderen vil se slik ut:

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

Men på grunn av 30-sekunders sleep vi sterk vi vil øke utrullingstiden, siden hver pod vil bli avsluttet minimum 30 sekunder, noe som er dårlig. Hva kan gjøres med dette?

La oss henvende oss til den parten som er ansvarlig for den direkte gjennomføringen av søknaden. I vårt tilfelle er det det PHP-FPMSom som standard overvåker ikke utførelsen av sine underordnede prosesser: Masterprosessen avsluttes umiddelbart. Du kan endre denne virkemåten ved å bruke direktivet process_control_timeout, som spesifiserer tidsgrensene for underordnede prosesser for å vente på signaler fra masteren. Hvis du setter verdien til 20 sekunder, vil dette dekke de fleste spørringene som kjører i beholderen og vil stoppe hovedprosessen når de er fullført.

Med denne kunnskapen, la oss gå tilbake til vårt siste problem. Som nevnt er ikke Kubernetes en monolitisk plattform: kommunikasjon mellom de forskjellige komponentene tar litt tid. Dette gjelder spesielt når vi vurderer driften av Ingresses og andre relaterte komponenter, siden på grunn av en slik forsinkelse på tidspunktet for distribusjon er det lett å få en økning på 500 feil. For eksempel kan det oppstå en feil på stadiet for å sende en forespørsel til en oppstrøms, men "tidsforsinkelsen" for interaksjon mellom komponentene er ganske kort - mindre enn et sekund.

derfor, Totalt med det allerede nevnte direktivet process_control_timeout du kan bruke følgende konstruksjon til lifecycle:

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

I dette tilfellet vil vi kompensere for forsinkelsen med kommandoen sleep og ikke øke utplasseringstiden betydelig: er det en merkbar forskjell mellom 30 sekunder og ett?.. Faktisk er det process_control_timeoutOg lifecycle brukes kun som et "sikkerhetsnett" i tilfelle etterslep.

Generelt sett den beskrevne oppførselen og den tilsvarende løsningen gjelder ikke bare for PHP-FPM. En lignende situasjon kan på en eller annen måte oppstå ved bruk av andre språk/rammer. Hvis du ikke kan fikse grasiøs nedleggelse på andre måter - for eksempel ved å omskrive koden slik at applikasjonen behandler termineringssignaler korrekt - kan du bruke den beskrevne metoden. Det er kanskje ikke det vakreste, men det fungerer.

Øve på. Lasttesting for å sjekke funksjonen til poden

Lasttesting er en av måtene å sjekke hvordan containeren fungerer, siden denne prosedyren bringer den nærmere virkelige kampforhold når brukere besøker nettstedet. For å teste anbefalingene ovenfor, kan du bruke Yandex.Tankom: Den dekker alle våre behov perfekt. Følgende er tips og anbefalinger for å utføre testing med et tydelig eksempel fra vår erfaring takket være grafene til Grafana og Yandex.Tank selv.

Det viktigste her er sjekk endringene trinn for trinn. Etter å ha lagt til en ny rettelse, kjør testen og se om resultatene har endret seg sammenlignet med forrige kjøring. Ellers vil det være vanskelig å identifisere ineffektive løsninger, og i det lange løp kan det bare gjøre skade (for eksempel øke utplasseringstiden).

En annen nyanse er å se på beholderloggene under avslutningen. Er informasjon om grasiøs nedleggelse registrert der? Er det noen feil i loggene ved tilgang til andre ressurser (for eksempel til en nærliggende PHP-FPM-beholder)? Feil i selve applikasjonen (som i tilfellet med NGINX beskrevet ovenfor)? Jeg håper at den innledende informasjonen fra denne artikkelen vil hjelpe deg å bedre forstå hva som skjer med beholderen under oppsigelsen.

Så den første prøvekjøringen fant sted uten lifecycle og uten tilleggsdirektiver for applikasjonsserveren (process_control_timeout i PHP-FPM). Hensikten med denne testen var å identifisere det omtrentlige antallet feil (og om det er noen). Ut fra tilleggsinformasjon bør du også vite at den gjennomsnittlige utplasseringstiden for hver pod var omtrent 5-10 sekunder før den var helt klar. Resultatene er:

Kubernetes tips og triks: funksjoner i grasiøs nedleggelse i NGINX og PHP-FPM

Yandex.Tank-informasjonspanelet viser en topp på 502 feil, som oppstod på tidspunktet for distribusjon og varte i gjennomsnitt opptil 5 sekunder. Antagelig var dette fordi eksisterende forespørsler til den gamle poden ble avsluttet da den ble avsluttet. Etter dette dukket det opp 503 feil, som var et resultat av en stoppet NGINX-beholder, som også droppet tilkoblinger på grunn av backend (som hindret Ingress i å koble seg til den).

La oss se hvordan process_control_timeout i PHP-FPM vil hjelpe oss å vente på fullføringen av underordnede prosesser, dvs. rette opp slike feil. Distribuer på nytt ved å bruke dette direktivet:

Kubernetes tips og triks: funksjoner i grasiøs nedleggelse i NGINX og PHP-FPM

Det er ingen flere feil under den 500. utplasseringen! Utrullingen er vellykket, grasiøs nedleggelse fungerer.

Det er imidlertid verdt å huske problemet med Ingress-beholdere, en liten prosentandel av feil som vi kan motta på grunn av en tidsforsinkelse. For å unngå dem gjenstår det bare å legge til en struktur med sleep og gjenta distribusjonen. Men i vårt spesielle tilfelle var ingen endringer synlige (igjen ingen feil).

Konklusjon

For å avslutte prosessen elegant, forventer vi følgende oppførsel fra applikasjonen:

  1. Vent noen sekunder og slutt deretter å godta nye tilkoblinger.
  2. Vent til alle forespørsler er fullført og lukk alle keepalive-tilkoblinger som ikke utfører forespørsler.
  3. Avslutt prosessen.

Imidlertid kan ikke alle applikasjoner fungere på denne måten. En løsning på problemet i Kubernetes-realiteter er:

  • legge til en pre-stop krok som vil vente noen sekunder;
  • studerer konfigurasjonsfilen til vår backend for de riktige parameterne.

Eksemplet med NGINX gjør det klart at selv en applikasjon som i utgangspunktet skal behandle avslutningssignaler riktig, kanskje ikke gjør det, så det er viktig å se etter 500 feil under applikasjonsdistribusjon. Dette lar deg også se på problemet bredere og ikke fokusere på en enkelt pod eller container, men se på hele infrastrukturen som en helhet.

Som et testverktøy kan du bruke Yandex.Tank i forbindelse med et hvilket som helst overvåkingssystem (i vårt tilfelle ble data hentet fra Grafana med en Prometheus-backend for testen). Problemer med grasiøs nedleggelse er godt synlige under tunge belastninger som benchmark kan generere, og overvåking hjelper til med å analysere situasjonen mer detaljert under eller etter testen.

Som svar på tilbakemelding på artikkelen: det er verdt å nevne at problemene og løsningene er beskrevet her i forhold til NGINX Ingress. For andre tilfeller er det andre løsninger som vi kan vurdere i følgende materialer i serien.

PS

Annet fra K8s tips og triks-serien:

Kilde: www.habr.com

Legg til en kommentar