Kushti standard gjatë realizimit të CI/CD në Kubernetes: aplikacioni duhet të jetë në gjendje të mos pranojë kërkesa të reja nga klientët përpara se të ndalet plotësisht, dhe më e rëndësishmja – të përfundojë me sukses ato që ekzistojnë.

Respektimi i këtij kushti lejon arrirjen e një ndalimi të zero gjatë deploy-it. Megjithatë, edhe kur përdoren kombinime shumë të njohura (si NGINX dhe PHP-FPM), mund të hasni vështirësi, të cilat do të çojnë në një shpërthim gabimesh gjatë çdo deploy-i...
Teoria. Si jeton një pod
Kemi publikuar tashmë në detaje mbi ciklin e jetës së një podi . Në kontekst të temës së trajtuar, na intereson e siguiente: në momentin kur një pod kalon në gjendjen Terminating, nuk pranojnë kërkesa të reja (pod nga lista e endpoints për shërbimin). Kështu, për të shmangur ndalime gjatë deploy-it, nga ana jonë mjafton të zgjidhim problemin e ndalimit të duhur të aplikacionit.
Gjithashtu, duhet të kujtojmë se periudha e dëmshpërblimit sipas parazgjedhjes është : pas kësaj periudhe, pod-i do të përfundohet dhe aplikacioni duhet të arrijë të përpunojë të gjitha kërkesat para kësaj periudhe. Shënim: ndonëse çdo kërkesë që zgjat më shumë se 5-10 sekonda, tashmë është problematike, dhe ndalimi i butë nuk do ta ndihmojë atë...
Për të kuptuar më mirë se çfarë ndodh kur pod-i përfundon punën e tij, mjafton të shqyrtoni skemën e mëposhtme:

A1, B1 — Marrja e ndryshimeve mbi gjendjen e pod-it
A2 — Dërgimi i SIGTERM
B2 — Heqja e pod-it nga endpoints
B3 — Marrja e ndryshimeve (lista e endpoints u ndryshua)
B4 — Përditësimi i rregullave iptables
Kujdes: heqja e endpoint-it të pod-it dhe dërgimi i SIGTERM nuk ndodhin njëra pas tjetrës, por paralelisht. Dhe për shkak se Ingress nuk merr menjëherë listën e përditësuar të Endpoints, pod-i do të pranojë kërkesa të reja nga klientët, gjë që do të shkaktojë gabimet 500 gjatë përfundimit të pod-it (material më të detajuar për këtë çështje kemi ). Duhet të zgjidhet ky problem në këto mënyra:
- Dërgoni në headers përgjigjës Connection: close (nëse lidhet me aplikacionin HTTP).
- Nëse nuk ka mundësi për të bërë ndryshime në kod, atëherë në këtë artikull është përshkruar një zgjidhje që do t'u mundësojë përpunimin e kërkesave deri në fund të periudhës së ndalimit të butë.
Teoria. Si NGINX dhe PHP-FPM përfundojnë proceset e tyre
NGINX
Le të fillojmë me NGINX, pasi me të gjithçka duket mjaft e qartë. Duke u thelluar në teori, ne zbulojmë se NGINX ka një proces kryesor dhe disa "punëtorë" - këto janë procese dytësore që përpunojnë kërkesat e klientëve. Ka një mundësi të shkëlqyer: me anë të komandës nginx -s mund të përfundojmë proceset si në mënyrë fast shutdown, ashtu edhe në graceful shutdown. Sigurisht, ne jemi të interesuar për variantin e fundit.
Më pas gjithçka është e thjeshtë: duhet të shtojmë në komandën që do të dërgojë sinjalin për graceful shutdown. Kjo mund të bëhet në Deployment, në bllokun e kontejnerit:
lifecycle:
preStop:
exec:
command:
- /usr/sbin/nginx
- -s
- quitTani, në momentin e përfundimit të punës së pod-it, në logs e kontejnerit NGINX ne do të shohim sa vijon:
2018/01/25 13:58:31 [notice] 1#1: sinjali 3 (SIGQUIT) u pranuar, duke u mbyllur
2018/01/25 13:58:31 [notice] 11#11: duke u mbyllur me gjithë qetësi Dhe kjo do të thotë atë që na nevojitet: NGINX pret përfundimin e përpunimit të kërkesave, pas së cilës vret procesin. Megjithatë, më poshtë do të shqyrtohet një problem i zakonshëm, për shkak të të cilit edhe me komandën nginx -s quit procesi përfundon në mënyrë të pasaktë.
Dhe në këtë pikë kemi përfunduar me NGINX: si të paktën nga log-et, mund të kuptohet se gjithçka funksionon ashtu siç duhet.
Si qëndrojnë punët me PHP-FPM? Si e përpunon ai graceful shutdown? Le ta shqyrtojmë.
PHP-FPM
Në rastin e PHP-FPM, informacioni është pak më i kufizuar. Nëse orientoheni nga për PHP-FPM, do të thuhet se pranohen sinjale të mëposhtme POSIX:
-
SIGINT,SIGTERM— fast shutdown; -
SIGQUIT— graceful shutdown (kjo është ajo që na nevojitet).
Sinjalet e tjera në këtë detyrë nuk janë të nevojshme, prandaj do ta anashkalojmë shqyrtimin e tyre. Për një përfundim të saktë të procesit, do të nevojitet të shkruhet hook i mëposhtëm preStop:
lifecycle:
preStop:
exec:
command:
- /bin/kill
- -SIGQUIT
- "1"Në pamje të parë, kjo është gjithçka që nevojitet për të kryer graceful shutdown në të dy kontejnerët. Megjithatë, detyra është më e komplikuar se sa duket. Më poshtë shqyrtohen dy raste kur graceful shutdown nuk funksiononte dhe shkaktonte mungesë të përkohshme të projektit gjatë përmirësimit.
Praktika. Probleme të mundshme me graceful shutdown
NGINX
Para së gjithash, është e dobishme të mbahet mend: përveç ekzekutimit të komandës nginx -s quit është një tjetër fazë që merret parasysh. Kemi hasur në një problem kur NGINX dërgonte gjithmonë SIGTERM në vend të sinjalit SIGQUIT, çka bënte që kërkesat të mos përfundonin siç duhet. Rastet e ngjashme mund të gjejmë, për shembull, . Fatkeqësisht, nuk arritëm të përcaktonim shkakun e saktë të këtij sjelljeje: kishte dyshime për versionet e NGINX, por kjo nuk u konfirmua. Simptomat përfshinin se në logun e konteinerit NGINX shiheshin mesazhe «open socket #10 left in connection 5», pas së cilës pod-i ndalohej.
Mund të vëzhgojmë një problem të tillë, për shembull, në përgjigjet në Ingress-in tonë:

Të dhënat e kodit të statusit në momentin e shpërndarjes
Në këtë rast, marrim pikërisht kodin 503 të gabimit nga Ingressi: ai nuk mund të kontaktojë me konteinerin NGINX, pasi ai tashmë nuk është i disponueshëm. Nëse shohim në loget e konteinerit me NGINX, ato tregojnë:
[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13Pas ndryshimit të sinjalit të ndalimit, konteineri fillon të ndalet siç duhet: kjo konfirmohet nga fakti që nuk vërehet më gabimi 503.
Nëse ju jeni përballur me një problem të ngjashëm, ka kuptim të hetojmë se cili është sinjali i ndalimit në konteiner dhe si duket në të vërtetë preStop-hook. Është plotësisht e mundshme që arsyeja qëndron pikërisht këtu.
PHP-FPM… dhe jo vetëm
Problemi me PHP-FPM përshkruhet thjesht: ai nuk pret që proceset fëmijë të përfundojnë, i terminon ato, çka shkakton gabimet 502 gjatë shpërndarjeve dhe operacioneve të tjera. Në bugs.php.net nga viti 2005 ka disa mesazhe rreth këtij problemi (për shembull, dhe ), në të cilat përshkruhet ky problem. Ndërsa në loget, ju, shumë shëndoshë, nuk do të shihni asgjë: PHP-FPM do të shpallë përfundimin e procesit të tij pa asnjë gabim ose njoftime të jashtme.
Duhet të saktësohet se problemi vetë mund të varet në një shkallë më të vogël ose më të madhe nga vetë aplikacioni dhe të mos shfaqet, për shembull, në monitorim. Nëse megjithatë përballeni me të, ndihma e parë e thjeshtë që më vjen në mendje është: shtoni një preStop-hook me sleep(30). Ai do të lejojë përfundimin e të gjitha kërkesave që ishin deri tani (dhe nuk marrim të reja, pasi pod-i tashmë është në gjendje Terminating), dhe pas kalimit të 30 sekondave, vetë pod-i do të përfundojë me sinjalin SIGTERM.
Kështu që lifecycle për konteinerin do të duket si më poshtë:
lifecycle:
preStop:
exec:
command:
- /bin/sleep
- "30" Megjithatë, për shkak të treguesit 30-sekondësh fjetje ne do ta rritim shumë kohën e deploy-it, pasi çdo pod do të përfundojë minimum 30 sekonda, që nuk është mirë. Çfarë mund të bëjmë për këtë?
Le të referohemi te ana që është përgjegjëse për ekzekutimin e drejtpërdrejt të aplikacionit. Në rastin tonë kjo është PHP-FPM, i cili në mënyrë të paracaktuar nuk mbikëqyr ekzekutimin e proceseve të tij child: procesi master përfundohet menjëherë. Ky sjellje mund të ndryshohet nëpërmjet direktivës process_control_timeout, e cila përcakton kufijtë kohorë për pritjen e sinjaleve nga masteri nga proceset child. Nëse vendosni një vlerë prej 20 sekondash, kjo do të mbulojë shumicën e kërkesave që përpunohen në kontejner dhe pas përfundimit të tyre, procesi master do të ndalet.
Me këtë njohje, le të kthehemi te problemi ynë të fundit. Siç u përmend më parë, Kubernetes nuk është një platformë monolitike: ndërveprimi mes komponenteve të ndryshme merr ca kohë. Kjo është veçanërisht e rëndësishme kur shqyrtojmë funksionimin e Ingress-ave dhe komponentëve të tjerë, pasi për shkak të një vonese të tillë gjatë deploy-it, është lehtë të hasni një shpërthim të gabimeve 500. Për shembull, gabimi mund të ndodhë gjatë dërgimit të kërkesës te upstream-i, por vetë "vonesa temporale" e ndërveprimit mes komponenteve është mjaft e shkurtër - më pak se një sekondë.
Prandaj, në përmbledhje me direktivën e përmendur më parë process_control_timeout mund të përdoret ndërtimi më poshtë për lifecycle:
lifecycle:
preStop:
exec:
command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"] Në këtë rast, ne kompensojmë vonesën me komandën fjetje dhe nuk e rrisim shumë kohën e deploy-it: pasi ka një ndryshim të dukshëm midis 30 sekondave dhe një?.. Në thelb "punën kryesore" e merr në dorë vetë process_control_timeout, ndërsa lifecycle përdoret vetëm si "siguri" në rast të vonesave.
Në përgjithësi, sjellja e përshkruar dhe zgjidhja përkatëse nuk i referohen vetëm PHP-FPM. Një situatë e ngjashme mund të ndodhë në një farë mënyre kur përdoren gjuhë të tjera/programet. Nëse nuk arrini ta zgjidhni në mënyra të tjera mbylljen e qetë - për shembull, duke rishkruar kodin që aplikacioni të trajtojë siç duhet sinjalet e përfundimit, - mund të përdorim mënyrën e përshkruar. Le të mos jetë më e bukur, por funksionon.
Praktika. Testimi i ngarkesës për të verifikuar funksionimin e pod-it
Testimi i ngarkesës është një nga mënyrat për të verifikuar se si funksionon një kontenier, pasi kjo procedurë e afrohet me kushtet reale të përdorimit, kur përdoruesit hyjnë në faqe. Për testimin e rekomandimeve të mësipërme mund të shfrytëzohet : ai mbulon në mënyrë të shkëlqyer të gjitha nevojat tona. Më poshtë janë këshillat dhe rekomandimet për kryerjen e testimit me një shembull të qartë — falë grafikëve të Grafana dhe vetë Yandex.Tank — nga përvoja jonë.
Gj thingja më e rëndësishme këtu është të kontrolloni ndryshimet hap pas hapi. Pas shtimit të një rregullimi të ri, nisni testimin dhe shikoni nëse rezultatet kanë ndryshuar krahasuar me nisjen e kaluar. Në të kundërt, do të jetë e vështirë të identifikoni zgjidhjet e papërshtatshme, dhe në perspektivë mund të dëmtoni (p.sh., të rrisni kohën e deploy-it).
Një aspekt tjetër — shikoni log-et e kontenierit gjatë terminimit të tij. A regjistrohet ndonjë informacion mbi mbylljen e butë? A ka gabime në log kur lidhemi me burime të tjera (p.sh., me kontenierin për PHP-FPM)? Gabime të aplikacionit vetë (siç ishte rasti i mësipërm me NGINX)? Shpresoj se informacioni fillestar nga ky artikull do t'ju ndihmojë të kuptoni më mirë se çfarë ndodh me kontenierin gjatë terminimit të tij.
Pra, nisja e parë e testimit ndodhi pa lifecycle dhe pa drejtime të tjera për serverin e aplikacioneve (process_control_timeout në PHP-FPM). Qëllimi i këtij testi ishte identifikimi i numrit të afërt të gabimeve (dhe nëse ato ekzistojnë fare). Gjithashtu, nga informacioni shtesë duhet të dihet se koha mesatare e deploy-it për çdo pod ishte rreth 5-10 sekonda deri në një gjendje të plotë gatishmërie. Rezultatet janë si mëposhtë:

Në panelin informativ të Yandex.Tank, vihet re një shpërthim i gabimeve 502, i cili ndodhi në momentin e deploy-it dhe vazhdoi në mesatare deri në 5 sekonda. Supozohet se këto ishin kërkesat ekzistuese për pod-in e vjetër, kur ai po terminonte. Pas kësaj u shfaqën gabime 503, që erdhën si rezultat i ndërprerjes së kontenierit NGINX, i cili gjithashtu ndaloi lidhjet për shkak të backend-it (për shkak të të cilit Ingress nuk arriti të lidhej me të).
Le të shohim se si process_control_timeout në PHP-FPM do të na ndihmojë të presim përfundimin e proceseve të fëmijëve, dmth. të rregullojmë këto gabime. Deploy-i i përsëritur tashmë me përdorimin e kësaj drejtime:

Gjatë deploy-it nuk ka më gabime 500! Deploy-i kalon me sukses, ndalimi i butë funksionon.
Megjithatë, është e rëndësishme të kujtojmë momentin me kontejnerët Ingress, një përqindje e vogël gabimesh mund të ndodhin për shkak të një vonese të përkohshme. Për t'i evituar ato, mbetet të shtojmë një konstrukcion me fjetje dhe të përsërisim deploy-in. Megjithatë, në rastin tonë konkret, nuk ka pasur ndryshime (çfarë do thotë sërish pa gabime).
Përfundim
Për të mbyllur saktësisht procesin, presim nga aplikacioni sjelljen e mëposhtme:
- Të presim disa sekonda, pastaj të ndalojmë pranimin e lidhjeve të reja.
- Të presim për përfundimin e të gjitha kërkesave dhe të mbyllim të gjitha lidhjet keepalive, të cilat nuk janë duke përfunduar kërkesa.
- Të përfundojmë procesin tonë.
Megjithatë, jo të gjitha aplikacionet dinë të punojnë ashtu. Një nga zgjidhjet për problemin në realitetin Kubernetes është:
- shtimi i një hook pre-stop, i cili do të presë disa sekonda;
- kontrollimi i skedarit të konfigurimit të backend-it tonë për parametrat përkatës.
Shembulli me NGINX lejon të kuptohet se edhe ai aplikacion që në fillim duhet të reagonte saktë ndaj sinjaleve për përfundim, mund ta mos e bëjë këtë, prandaj është kritike të kontrollojmë për gabime 500 gjatë deploy-it të aplikacionit. Kjo gjithashtu mundëson një pamje më të gjerë të problemit dhe të mos përqendrohemi në një pod të veçantë ose kontejner, por të shohim të gjithë infrastrukturën në tërësi.
Si një mjet për testim, mund të përdoret Yandex.Tank në bashkëpunim me çdo sistem monitorimi (në rastin tonë, të dhënat u morën nga Grafana me backend në formë Prometheus). Problemet me ndalimin e butë shfaqen qartë gjatë ngarkesave të mëdha, të cilat mund të gjenerohen nga benchmark-u, ndërsa monitorimi ndihmon për të analizuar situatën në detaje gjatë ose pas testit.
Duke iu përgjigjur feedback-ut mbi artikullin: duhet theksuar se problemet dhe mënyrat për zgjidhjen e tyre përshkruhen në lidhje me NGINX Ingress. Për raste të tjera ka zgjidhje të tjera, të cilat ndoshta do t'i shqyrtojmë në materialet e ardhshme të ciklit.
P.S.
Diçka tjetër nga cikli K8s tips & tricks:
- «»;
- «»;
- «»;
- «».
Burimi: habr.com
