Kubernetes tips och tricks: funktioner för graciös avstängning i NGINX och PHP-FPM

Ett typiskt tillstånd vid implementering av CI/CD i Kubernetes: applikationen måste kunna inte acceptera nya klientförfrågningar innan den stoppas helt, och viktigast av allt, framgångsrikt slutföra befintliga.

Kubernetes tips och tricks: funktioner för graciös avstängning i NGINX och PHP-FPM

Efterlevnad av detta villkor gör att du kan uppnå noll stillestånd under driftsättning. Men även när du använder mycket populära paket (som NGINX och PHP-FPM), kan du stöta på svårigheter som kommer att leda till ett stort antal fel med varje distribution...

Teori. Hur podden lever

Vi har redan publicerat i detalj om en pods livscykel denna artikel. Inom ramen för det aktuella ämnet är vi intresserade av följande: i det ögonblick då podden kommer in i staten avsluta, nya förfrågningar slutar skickas till den (pod raderade från listan över slutpunkter för tjänsten). För att undvika stillestånd under driftsättning räcker det alltså för oss att lösa problemet med att stoppa applikationen korrekt.

Du bör också komma ihåg att den förinställda respitperioden är 30 sekunder: efter detta kommer podden att avslutas och ansökan måste hinna behandla alla förfrågningar före denna period. Notera: även om varje begäran som tar mer än 5-10 sekunder redan är problematisk, och en elegant avstängning hjälper det inte längre...

För att bättre förstå vad som händer när en pod avslutas, titta bara på följande diagram:

Kubernetes tips och tricks: funktioner för graciös avstängning i NGINX och PHP-FPM

A1, B1 - Tar emot ändringar om härdens tillstånd
A2 - Avgång SIGTERM
B2 - Ta bort en pod från endpoints
B3 - Ta emot ändringar (listan över ändpunkter har ändrats)
B4 - Uppdatera iptables regler

Observera: att ta bort endpoint-podden och skicka SIGTERM sker inte sekventiellt utan parallellt. Och på grund av det faktum att Ingress inte omedelbart tar emot den uppdaterade listan med Endpoints, kommer nya förfrågningar från klienter att skickas till podden, vilket kommer att orsaka ett 500-fel under poddens avslutning (för mer detaljerat material om denna fråga, vi översatt). Detta problem måste lösas på följande sätt:

  • Skicka anslutning: stäng svarsrubriker (om detta gäller en HTTP-applikation).
  • Om det inte är möjligt att göra ändringar i koden, beskriver följande artikel en lösning som gör att du kan behandla förfrågningar till slutet av den graciösa perioden.

Teori. Hur NGINX och PHP-FPM avslutar sina processer

nginx

Låt oss börja med NGINX, eftersom allt är mer eller mindre självklart med det. När vi dyker in i teorin lär vi oss att NGINX har en huvudprocess och flera "arbetare" - det här är underordnade processer som behandlar klientförfrågningar. Ett bekvämt alternativ tillhandahålls: använda kommandot nginx -s <SIGNAL> avsluta processer antingen i snabb avstängning eller graciöst avstängningsläge. Uppenbarligen är det det senare alternativet som intresserar oss.

Då är allt enkelt: du måste lägga till preStop-krok ett kommando som skickar en graciös avstängningssignal. Detta kan göras i Deployment, i containerblocket:

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

Nu, när podden stängs av, kommer vi att se följande i NGINX containerloggar:

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

Och detta kommer att betyda vad vi behöver: NGINX väntar på att förfrågningar ska slutföras och dödar sedan processen. Men nedan kommer vi också att överväga ett vanligt problem på grund av vilket, även med kommandot nginx -s quit processen avslutas felaktigt.

Och i det här skedet är vi klara med NGINX: åtminstone från loggarna kan du förstå att allt fungerar som det ska.

Vad är grejen med PHP-FPM? Hur hanterar den graciös avstängning? Låt oss ta reda på det.

PHP-FPM

När det gäller PHP-FPM finns det lite mindre information. Om du fokuserar på officiella manual enligt PHP-FPM kommer det att säga att följande POSIX-signaler accepteras:

  1. SIGINT, SIGTERM — snabb avstängning;
  2. SIGQUIT — graciös avstängning (vad vi behöver).

De återstående signalerna krävs inte i denna uppgift, så vi kommer att utelämna deras analys. För att avsluta processen korrekt måste du skriva följande preStop-krok:

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

Vid första anblicken är detta allt som krävs för att utföra en graciös avstängning i båda behållarna. Uppgiften är dock svårare än den verkar. Nedan finns två fall där graciös avstängning inte fungerade och orsakade kortvarig otillgänglighet för projektet under driftsättningen.

Öva. Möjliga problem med graciös avstängning

nginx

Först och främst är det användbart att komma ihåg: förutom att utföra kommandot nginx -s quit Det finns ytterligare ett steg som är värt att uppmärksamma. Vi stötte på ett problem där NGINX fortfarande skickade SIGTERM istället för SIGQUIT-signalen, vilket gjorde att förfrågningar inte slutfördes korrekt. Liknande fall finns t.ex. här. Tyvärr kunde vi inte fastställa den specifika orsaken till detta beteende: det fanns en misstanke om NGINX-versionen, men den bekräftades inte. Symptomet var att meddelanden observerades i NGINX-behållarloggarna: "öppna uttag #10 kvar i anslutning 5", varefter podden stannade.

Vi kan observera ett sådant problem, till exempel från svaren på Ingress vi behöver:

Kubernetes tips och tricks: funktioner för graciös avstängning i NGINX och PHP-FPM
Indikatorer för statuskoder vid tidpunkten för driftsättning

I det här fallet får vi bara en 503-felkod från Ingress själv: den kan inte komma åt NGINX-behållaren, eftersom den inte längre är tillgänglig. Om du tittar på behållarloggarna med NGINX innehåller de följande:

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

Efter att ha ändrat stoppsignalen börjar behållaren stanna korrekt: detta bekräftas av det faktum att 503-felet inte längre observeras.

Om du stöter på ett liknande problem är det vettigt att ta reda på vilken stoppsignal som används i behållaren och exakt hur preStop-kroken ser ut. Det är mycket möjligt att orsaken ligger just i detta.

PHP-FPM... och mer

Problemet med PHP-FPM beskrivs på ett trivialt sätt: det väntar inte på slutförandet av underordnade processer, det avslutar dem, vilket är anledningen till att 502-fel uppstår under driftsättning och andra operationer. Det finns flera felrapporter på bugs.php.net sedan 2005 (t.ex här и här), som beskriver detta problem. Men du kommer med största sannolikhet inte att se något i loggarna: PHP-FPM kommer att meddela att processen är klar utan några fel eller meddelanden från tredje part.

Det är värt att klargöra att själva problemet kan bero i mindre eller större utsträckning på själva applikationen och kanske inte visar sig, till exempel vid övervakning. Om du stöter på det, kommer en enkel lösning först att tänka på: lägg till en preStop-krok med sleep(30). Det gör att du kan slutföra alla förfrågningar som fanns tidigare (och vi accepterar inte nya, eftersom pod redan kapabel att avsluta), och efter 30 sekunder kommer själva kapseln att avslutas med en signal SIGTERM.

Det visar sig att lifecycle för behållaren kommer att se ut så här:

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

Men på grund av 30-sekunder sleep vi starkt vi kommer att öka distributionstiden, eftersom varje pod kommer att avslutas minimum 30 sekunder, vilket är dåligt. Vad kan man göra åt detta?

Låt oss vända oss till den part som är ansvarig för det direkta utförandet av ansökan. I vårt fall är det så PHP-FPMSom som standard övervakar inte exekveringen av dess underordnade processer: Masterprocessen avslutas omedelbart. Du kan ändra detta beteende med hjälp av direktivet process_control_timeout, som anger tidsgränserna för underordnade processer att vänta på signaler från mastern. Om du ställer in värdet på 20 sekunder kommer detta att täcka de flesta av de frågor som körs i behållaren och kommer att stoppa huvudprocessen när de är klara.

Med denna kunskap, låt oss återgå till vårt sista problem. Som nämnts är Kubernetes inte en monolitisk plattform: kommunikation mellan dess olika komponenter tar lite tid. Detta är särskilt sant när vi överväger driften av Ingresses och andra relaterade komponenter, eftersom det på grund av en sådan fördröjning vid tidpunkten för implementering är lätt att få en ökning på 500 fel. Till exempel kan ett fel uppstå i skedet av att skicka en begäran till en uppströms, men "tidsfördröjningen" för interaktion mellan komponenter är ganska kort - mindre än en sekund.

därför Totalt med det redan nämnda direktivet process_control_timeout du kan använda följande konstruktion för lifecycle:

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

I det här fallet kommer vi att kompensera för förseningen med kommandot sleep och öka inte drifttiden avsevärt: finns det en märkbar skillnad mellan 30 sekunder och en?.. I själva verket är det process_control_timeoutOch lifecycle används endast som ett "skyddsnät" vid fördröjning.

Generellt sett det beskrivna beteendet och motsvarande lösning gäller inte bara PHP-FPM. En liknande situation kan på ett eller annat sätt uppstå när man använder andra språk/ramverk. Om du inte kan fixa graciös avstängning på andra sätt - till exempel genom att skriva om koden så att applikationen korrekt bearbetar avslutningssignaler - kan du använda den beskrivna metoden. Det kanske inte är det vackraste, men det fungerar.

Öva. Belastningstestning för att kontrollera poddens funktion

Lasttestning är ett av sätten att kontrollera hur behållaren fungerar, eftersom denna procedur för den närmare verkliga stridsförhållanden när användare besöker platsen. För att testa ovanstående rekommendationer kan du använda Yandex.Tankom: Den täcker alla våra behov perfekt. Följande är tips och rekommendationer för att utföra tester med ett tydligt exempel från vår erfarenhet tack vare graferna från Grafana och Yandex.Tank själv.

Det viktigaste här är kontrollera ändringar steg för steg. När du har lagt till en ny fix, kör testet och se om resultaten har ändrats jämfört med den senaste körningen. Annars blir det svårt att identifiera ineffektiva lösningar och i längden kan det bara göra skada (till exempel öka drifttiden).

En annan nyans är att titta på behållarloggarna när de avslutas. Finns information om graciös avstängning registrerad där? Finns det några fel i loggarna vid åtkomst till andra resurser (till exempel till en närliggande PHP-FPM-behållare)? Fel i själva applikationen (som i fallet med NGINX som beskrivs ovan)? Jag hoppas att den inledande informationen från den här artikeln hjälper dig att bättre förstå vad som händer med behållaren när den avslutas.

Så den första testkörningen ägde rum utan lifecycle och utan ytterligare direktiv för applikationsservern (process_control_timeout i PHP-FPM). Syftet med detta test var att identifiera det ungefärliga antalet fel (och om det finns några). Från ytterligare information bör du också veta att den genomsnittliga implementeringstiden för varje pod var cirka 5-10 sekunder tills den var helt klar. Resultaten är:

Kubernetes tips och tricks: funktioner för graciös avstängning i NGINX och PHP-FPM

Yandex.Tank-informationspanelen visar en topp på 502 fel, som inträffade vid tidpunkten för distributionen och varade i genomsnitt upp till 5 sekunder. Förmodligen berodde detta på att befintliga förfrågningar till den gamla podden avslutades när den avslutades. Efter detta dök 503 fel upp, vilket var resultatet av en stoppad NGINX-behållare, som också tappade anslutningar på grund av backend (vilket hindrade Ingress från att ansluta till den).

Låt oss se hur process_control_timeout i PHP-FPM kommer att hjälpa oss att vänta på slutförandet av underordnade processer, dvs. rätta till sådana fel. Distribuera om med detta direktiv:

Kubernetes tips och tricks: funktioner för graciös avstängning i NGINX och PHP-FPM

Det finns inga fler fel under den 500:e distributionen! Implementeringen är framgångsrik, graciös avstängning fungerar.

Det är dock värt att komma ihåg problemet med Ingress-behållare, en liten procentandel av fel som vi kan få på grund av en tidsfördröjning. För att undvika dem återstår bara att lägga till en struktur med sleep och upprepa distributionen. Men i vårt specifika fall var inga ändringar synliga (igen, inga fel).

Slutsats

För att avsluta processen på ett elegant sätt förväntar vi oss följande beteende från applikationen:

  1. Vänta några sekunder och sluta sedan acceptera nya anslutningar.
  2. Vänta tills alla förfrågningar har slutförts och stäng alla keepalive-anslutningar som inte utför förfrågningar.
  3. Avsluta din process.

Men inte alla applikationer kan fungera på detta sätt. En lösning på problemet i Kubernetes verklighet är:

  • lägga till en förstoppskrok som väntar några sekunder;
  • studerar konfigurationsfilen för vår backend för lämpliga parametrar.

Exemplet med NGINX gör det klart att även en applikation som initialt borde behandla avslutningssignaler korrekt kanske inte gör det, så det är viktigt att kontrollera efter 500 fel under applikationsdistributionen. Detta gör att du också kan se på problemet bredare och inte fokusera på en enda pod eller container, utan se på hela infrastrukturen som helhet.

Som ett testverktyg kan du använda Yandex.Tank i kombination med vilket övervakningssystem som helst (i vårt fall togs data från Grafana med en Prometheus-backend för testet). Problem med graciös avstängning är tydligt synliga under tunga belastningar som riktmärket kan generera, och övervakning hjälper till att analysera situationen mer i detalj under eller efter testet.

Som svar på feedback på artikeln: det är värt att nämna att problemen och lösningarna beskrivs här i relation till NGINX Ingress. För andra fall finns det andra lösningar som vi kan överväga i följande material i serien.

PS

Annat från K8s tips & tricks-serien:

Källa: will.com

Lägg en kommentar