Kubernetesi nipid ja trikid: graceful shutdown'i teostamine NGINX-is ja PHP-FPM-is

Standard requirement when implementing CI/CD in Kubernetes: the application must be able to stop accepting new client requests before full shutdown, and, importantly, successfully complete any existing requests.

Kubernetes'i nÀpunÀited ja nipid: graceful shutdown'i teostamise eripÀrad NGINX-is ja PHP-FPM-is

Compliance with this condition allows for zero downtime during deployment. However, even when using very popular stacks (like NGINX and PHP-FPM), one can encounter difficulties that lead to a spike in errors during each deployment


Theory. How a pod lives

We have already published details about the lifecycle of a pod seda artiklit. In the context of the topic at hand, we are interested in the following: at the moment when the pod transitions to a state Termineeritud, it stops receiving new requests (the pod – see kasutatakse ainult korra kĂ€su kĂ€ivitamiseks ja siis kaob! Infoturbe töötaja, kes jĂ€lgib ohvrimasinat, ei suuda tuvastada is removed from the endpoint list for the service). Thus, to avoid downtime during deployment, it is sufficient for us to resolve the issue of correctly shutting down the application.

It is also important to remember that the grace period by default is 30 seconds: after this, the pod will be terminated and the application must complete all requests within this period. MĂ€rkus: however, any request that takes longer than 5-10 seconds is already problematic, and graceful shutdown won't help it


To better understand what happens when a pod shuts down, one can study the following diagram:

Kubernetes'i nÀpunÀited ja nipid: graceful shutdown'i teostamise eripÀrad NGINX-is ja PHP-FPM-is

A1, B1 — Receiving changes about the pod's state
A2 — Sending SIGTERM
B2 — Removing the pod from the endpoints
B3 — Receiving changes (the list of endpoints has changed)
B4 — Updating iptables rules

Note: the removal of the pod's endpoint and sending SIGTERM do not occur sequentially, but in parallel. Because the Ingress does not immediately receive the updated list of Endpoints, new requests from clients will be sent to the pod, resulting in 500 errors during the pod's termination. (we have translated more detailed material on this issue) . This problem needs to be addressed in the following ways:)Sending Connection: close in response headers (if it is related to an HTTP application).

  • If it is not possible to make changes to the code, then the article describes a solution that will allow requests to be fully processed until the end of the graceful period.
  • Theory. How NGINX and PHP-FPM terminate their processes

Teooria. Kuidas NGINX ja PHP-FPM oma protsesse lÔpetavad

NGINX

Alustame NGINXiga, kuna selle kasutamine on enam-vĂ€hem selge. Vaatame teooriat, et teada saada, et NGINX-il on ĂŒks meistrikĂ€sk ja mitu „töötajat“ — need on alamprotsessid, mis töötlevad kliendi pĂ€ringuid. On olemas mugav vĂ”imalus: kĂ€su abil nginx -s protsesside lĂ”petamiseks, kas kiire lĂ”ppemise reĆŸiimis vĂ”i sujuvas lĂ”petamises. Ilmselgelt huvitab meid viimane variant.

Edasi on kĂ”ik lihtne: tuleb lisada preStop-hĂŒppamine kĂ€sk, mis saadab signaali sujuvaks lĂ”petamiseks. Seda saab teha paigutuses, konteineri plokis:

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

NĂŒĂŒd, kui pod lĂ”petab töö, nĂ€eme NGINX konteineri logides jĂ€rgmist:

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

Ja see tÀhendaks, et kÔik on nagu peab: NGINX ootab pÀringute tÀitmist, mille jÀrel lÔpetab protsessi. Siiski, allpool kÀsitletakse tavalist probleemi, mille tÔttu isegi kÀsu nginx -s quit protsess ei lÔpe Ôigesti.

NĂŒĂŒd oleme NGINX-iga Ă€rajÀÀnud; logidest on vĂ€hemalt selge, et kĂ”ik töötab nagu peab.

Kuidas on asjad PHP-FPM-iga? Kuidas see teostab sujuvat lÔpetamist? Uurime lÀhemalt.

PHP-FPM

PHP-FPM-i puhul on teave veidi vÀhem. Kui toetuda ametlikule kÀsiraamatule PHP-FPM-i kohta, siis seal rÀÀgitakse, et aktsepteeritakse jÀrgmisi POSIX-signaale:

  1. SIGINT, SIGTERM — kiire lĂ”petamine;
  2. SIGQUIT — sujuv lĂ”petamine (see, mis meid huvitab).

ÜlejÀÀnud signaale ei ole siin vaja, seega jĂ€tame need kĂ€sitlemata. Protsessi korrektseks lĂ”petamiseks on vajalik kirjutada jĂ€rgmine preStop-hĂŒppamine:

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

Esmapilgul nĂ€ib, et see on kĂ”ik, mis vajalik sujuva lĂ”petamise teostamiseks mĂ”lemas konteineris. Siiski, ĂŒlesanne on keerulisem, kui tundub. Edasi vaatame kahte juhtumit, kus sujuv lĂ”petamine ei toiminud ja pĂ”hjustas projekti ajutise kĂ€ttesaamatuse juurutamise ajal.

Praktika. VÔimalikud probleemid sujuva lÔpetamisega

NGINX

Esiteks on kasulik meeles pidada: lisaks kĂ€su tĂ€itmisele nginx -s quit on veel ĂŒks etapp, millele tasub tĂ€helepanu pöörata. Oleme kokku puutunud probleemiga, kus NGINX saatis SIGTERM signaali hoopis SIGQUIT'i asemel, mistĂ”ttu pĂ€ringud ei lĂ”petatud Ă”igesti. Selliseid juhtumeid vĂ”ib leida, nĂ€iteks, siin. Kahjuks ei Ă”nnestunud meil selle kĂ€itumise konkreetset pĂ”hjust tuvastada: kahtlustati NGINX'i versioonides, kuid see ei leidnud kinnitust. SĂŒmptomaatika seisnes selles, et NGINX-i konteineri logides ilmusid jĂ€rgmised teated «open socket #10 left in connection 5», pĂ€rast mida pod peatati.

Sellist probleemi vÔime jÀlgida nÀiteks meie vajaliku Ingress'i vastustes:

Kubernetes'i nÀpunÀited ja nipid: graceful shutdown'i teostamise eripÀrad NGINX-is ja PHP-FPM-is
Oleku koodide nÀitajad juurutamise hetkel

Antud juhul saame 503 veakoodi Ingress'ilt: see ei saa pöörduda NGINX-i konteineri poole, kuna see on juba mittesaadav. Kui vaadata NGINX-i konteineri logisid, on seal jÀrgmine:

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

PÀrast stopp-signaali muutmist hakkab konteiner Ôigesti peatuma: seda tÔendab, et 503 viga ei kordu enam.

Kui olete sarnase probleemiga silmitsi seisnud, on mĂ”ttekas vĂ€lja selgitada, milline stopp-signaal kasutatakse konteineris ja kuidas tĂ€pselt preStop-hĂŒĂŒk vĂ€lja nĂ€eb. TĂ”enĂ€oliselt peitub probleem just selles.

PHP-FPM
 ja mitte ainult

PHP-FPM-i probleem on kirjeldatud triviaalsetena: see ei oota lasteprotsesside lĂ”petamist, terminiseerib need, mistĂ”ttu tekivad juurutamise ja muude toimingute kĂ€igus 502 vead. bugs.php.net lehelt on alates 2005. aastast mitmeid veateateid (nĂ€iteks, siin ja siin), mis kirjeldavad seda probleemi. Logides, tĂ”enĂ€oliselt, midagi ei nĂ€e: PHP-FPM teatab oma protsessi lĂ”petamisest ilma igasuguste vigadeta vĂ”i kĂŒlgnevate teavitusteta.

Tuleb tĂ€psustada, et probleem ise vĂ”ib sĂ”ltuda vĂ€hemal vĂ”i suuremal mÀÀral rakendusest ning ei pruugi nĂ€iteks jĂ€lgimises vĂ€lja paista. Kui te siiski kokku puutute, siis tuleb kĂ”igepealt meelde lihtne lahendus: lisada preStop-hĂŒĂŒk koos sleep(30). See vĂ”imaldab lĂ”petada kĂ”ik eelnevad pĂ€ringud (ja me ei aktseptee uusi, kuna pod juba on seisundis Termineeritud), ja 30 sekundi möödudes lĂ”petab pod end signaaliga SIGTERM.

Selgub, et lifecycle konteineri jaoks nÀeb vÀlja jÀrgmiselt:

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

Kuid 30-sekundilise aja mÀÀramine sleep meie suuresti pikendab juurutamise aega, kuna iga pod lÔpetatakse minimaalselt 30 sekundi pÀrast, mis on halb. Mida sellega teha?

Suundume poole, kes vastutab rakenduse otsese tÀitmise eest. Meie puhul on see PHP-FPM, mis vaikimisi ei jÀlgi oma child-protsesse: peaprotsess lÔpetatakse kohe. Seda kÀitumist saab muuta juhisega process_control_timeout, mis mÀÀrab ajapiirangud signaalide ootamiseks peast alaealistele protsessidele. Kui seada vÀÀrtus 20 sekundile, katab see enamiku kaasasolevatest pÀringutest konteineris, ja pÀrast nende lÔpetamist lÔpetatakse peaprotsess.

Selle teadmisega pöördume tagasi meie viimase probleemi juurde. Nagu juba mainitud, ei ole Kubernetes monoliitne platvorm: erinevate komponentide vahelise suhtlemise jaoks kulub aega. See kehtib eriti siis, kui vaatame Ingresside ja teiste seotud komponentide tööd, kuna juurutamise ajal vĂ”ib isegi tekkida 500-ndate vigu. NĂ€iteks vĂ”ib viga tekkida pĂ€ringu saatmisel upstream'i, kuid tegelik 'ajalugu' komponentide vahelise suhtlemise vahel on ĂŒsna lĂŒhike — vĂ€hem kui sekund.

Seega, kokkuvÔttes koos juba mainitud juhisega process_control_timeout vÔib kasutada jÀrgmist konstruktsiooni lifecycle:

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

Sellisel juhul kompenseerime viivituse kĂ€sklusega sleep ja ei pikenda juurutamise aega: kas pole vahe 30 sekundi ja ĂŒhe vahel mĂ€rgatav? PĂ”himĂ”tteliselt on "peamine töö" just see, process_control_timeout, vaid lifecycle on lihtsalt kasutatud "kindlustuseks" viivituse korral.

Üldiselt kĂ€itumine ja vastav lahendus ei puuduta ainult PHP-FPM. Sarnane olukord vĂ”ib tekkida ka teiste programmikeelte/raamistikudega. Kui teised meetodid ei aita graceful shutdown'i parandada — nĂ€iteks, kui kood on ĂŒmber kirjutatud nii, et rakendus töötleb lĂ”petamis signaale Ă”igesti — vĂ”ib rakendada kirjeldatud viisi. Olgu see mitte kĂ”ige ilusam, kuid see töötab.

Praktika. Koormustestimine podi töö kontrollimiseks

Koormustestimine on ĂŒks meetod, millega kontrollitakse, kuidas konteiner töötab, kuna see protseduur simuleerib reaalseid lahingutingimusi, kui saidile pÀÀsevad juurde kasutajad. Ülaltoodud soovituste testimiseks saab kasutada Yandex.Tank: see katab suurepĂ€raselt kĂ”ik meie vajadused. Edasi antakse nĂ€punĂ€iteid ja soovitusi testimise lĂ€biviimiseks koos selge — tĂ€nu Grafana diagrammidele ja Yandex.Tanki nĂ€idenditele — nĂ€itega meie kogemusest.

Peamine siin on kontrollida muutusi etappidena. PÀrast uue paranduse lisamist kÀivitage testimine ja vaadake, kas tulemused on vÔrreldes eelmise kÀivitamisega muutunud. Vastasel juhul on raske tuvastada ebaefektiivseid lahendusi ja tulevikus vÔivad need isegi kahju tekitada (nt suurendades juurutamise aega).

Teine nĂŒanss on vaadata konteineri logisid selle lĂ”petamise ajal. Kas seal fikseeritakse teavet graceful shutdowni kohta? Kas logides on vigu, kui pöörduda teistele ressurssidele (nt naaberkonteiner PHP-FPM)? Kas rakenduse enda vead (nagu ĂŒlalpool kirjeldatud NGINX-i puhul)? Loodan, et selle artikli sissejuhatav teave aitab paremini mĂ”ista, mis siis juhtub konteineriga selle lĂ”petamise ajal.

Nii et esimene testimine toimus ilma lifecycle ja ilma tĂ€iendavate direktiivide jaoks rakendusserveris (process_control_timeout PHP-FPM-is). Selle testi eesmĂ€rk oli tuvastada ligikaudne vigade arv (ja kas neid ĂŒldse on). Samuti tuleb tĂ€iendavast teabest teada, et iga podi juurutamise keskmine aeg oli umbes 5-10 sekundit kuni tĂ€ieliku valmiduseni. Tulemused on jĂ€rgmised:

Kubernetes'i nÀpunÀited ja nipid: graceful shutdown'i teostamise eripÀrad NGINX-is ja PHP-FPM-is

Yandex.Tanki teabepaneelil on nĂ€ha 502 vigade tĂ”us, mis toimus juurutamise hetkel ja kesta keskmiselt kuni 5 sekundit. Eeldatakse, et see katkestas olemasolevad pĂ€ringud vanale pod’ile, kui see lĂ”petati. PĂ€rast seda ilmnesid 503 vead, mis tulenesid peatatud NGINX-konteinerist, mis samuti katkestas ĂŒhendused pĂ”hiteenusega (mille tĂ”ttu ei saanud Ingress sellele ĂŒhenduda).

Vaadakem, kuidas process_control_timeout PHP-FPM aitab meil oodata child-protsesside lÔpetamist, s.t. sellised vead parandada. Kordusjuurutamine juba selle direktiivi kasutamisega:

Kubernetes'i nÀpunÀited ja nipid: graceful shutdown'i teostamise eripÀrad NGINX-is ja PHP-FPM-is

500-nd vigu ajal vigu enam ei ole! Deployimine lÀheb edukalt ja graceful shutdown toimib.

Siiski tasub meenutada hetke Ingress-konteineritega, kus me vÔime saada vÀikese protsendi vigu ajutise viivituse tÔttu. Nende vÀltimiseks jÀÀb alles struktuuri lisamine, sleep ja deployimine kordamine. Kuid meie konkreetses olukorras ei olnud muutusi nÀha (vigu ei ole jÀlle).

KokkuvÔte

Protsessi korrektselt lÔpetamiseks ootame rakenduselt jÀrgmist kÀitumist:

  1. Oodata paar sekundi, pĂ€rast mida lĂ”petada uute ĂŒhenduste vastuvĂ”tmine.
  2. Oodata, kuni kĂ”ik pĂ€ringud on lĂ”petatud ning sulgeda kĂ”ik keepalive-ĂŒhendused, mis ei tĂ€ida pĂ€ringuid.
  3. LÔpetada oma protsess.

Kuid mitte kĂ”ik rakendused ei oska nii töötada. Ühe lahendusena Kuberneteses on:

  • pre-stop hooki lisamine, mis ootab paar sekundit;
  • meie backend'i konfiguratsioonifaili uurimine vastavate parameetrite osas.

NÀide NGINX-ist aitab mÔista, et isegi rakendus, mis algselt peaks korrektselt lÔpetamissignaale töötlema, ei pruugi seda teha, seega on kriitiline kontrollida 500 vigu rakenduse deployimise ajal. Samuti vÔimaldab see probleemile laiemalt vaadata ja mitte keskenduda eraldi pod'ile vÔi konteinerile, vaid vaadata kogu infrastruktuuri tervikuna.

Testimise tööriigina vĂ”ib kasutada Yandex.Tank'i koos mis tahes monitooringusĂŒsteemiga (meie juhul kasutati testimiseks andmeid Grafanast, mille backend oli Prometheus). Probleemid graceful shutdowniga on selgelt nĂ€htavad suurte koormate korral, mida vĂ”ib genereerida benchmark, ja monitooring aitab olukorda tĂ€psemalt analĂŒĂŒsida testi ajal vĂ”i pĂ€rast seda.

Vastates artikli tagasisidele: tuleb mÀrkida, et probleemid ja nende lahendused on siin kirjeldatud NGINX Ingress'i kontekstis. Teiste juhtumite jaoks on olemas teised lahendused, mida me vÔib-olla kÀsitleme jÀrgmistes materjalides.

P.S.

Teine osa K8s nÀpunÀidetest ja nippidest:

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster