Tipy a triky Kubernetes: funkce elegantního vypnutí v NGINX a PHP-FPM

Typická podmínka při implementaci CI/CD v Kubernetes: aplikace musí umět nepřijímat nové požadavky klientů před úplným zastavením a hlavně úspěšně dokončit ty stávající.

Tipy a triky Kubernetes: funkce elegantního vypnutí v NGINX a PHP-FPM

Dodržení této podmínky umožňuje dosáhnout nulových prostojů během nasazení. I při použití velmi oblíbených balíčků (jako NGINX a PHP-FPM) se však můžete setkat s problémy, které povedou k nárůstu chyb při každém nasazení...

Teorie. Jak žije pod

O životním cyklu lusku jsme již podrobně publikovali tento článek. V kontextu zvažovaného tématu nás zajímá toto: v okamžiku, kdy lusk vstupuje do stavu Ukončení, přestanou se mu odesílat nové požadavky (pod odstraněny ze seznamu koncových bodů pro službu). Abychom se vyhnuli prostojům při nasazení, stačí, abychom správně vyřešili problém zastavení aplikace.

Měli byste také pamatovat na to, že výchozí doba odkladu je 30 sekund: poté bude modul ukončen a aplikace musí mít čas na zpracování všech požadavků před tímto obdobím. Poznámka: i když jakýkoli požadavek, který trvá déle než 5-10 sekund, je již problematický a ladné vypnutí už mu nepomůže...

Chcete-li lépe porozumět tomu, co se stane, když modul skončí, podívejte se na následující diagram:

Tipy a triky Kubernetes: funkce elegantního vypnutí v NGINX a PHP-FPM

A1, B1 - Příjem změn o stavu topeniště
A2 - Odlet SIGTERM
B2 – Odebrání modulu z koncových bodů
B3 - Příjem změn (seznam koncových bodů se změnil)
B4 - Aktualizace pravidel iptables

Poznámka: smazání podu koncového bodu a odeslání SIGTERM neprobíhá postupně, ale paralelně. A vzhledem k tomu, že Ingress okamžitě neobdrží aktualizovaný seznam koncových bodů, nové požadavky od klientů budou odeslány na modul, což způsobí chybu 500 při ukončení modulu. (pro podrobnější materiál k tomuto problému jsme přeloženo). Tento problém je třeba vyřešit následujícími způsoby:

  • Odeslat připojení: zavřít v hlavičkách odpovědi (pokud se jedná o HTTP aplikaci).
  • Pokud není možné provést změny v kódu, pak následující článek popisuje řešení, které vám umožní zpracovávat požadavky až do konce období odkladu.

Teorie. Jak NGINX a PHP-FPM ukončují své procesy

Nginx

Začněme s NGINX, protože s ním je vše víceméně zřejmé. Ponoříme-li se do teorie, zjistíme, že NGINX má jeden hlavní proces a několik „pracovníků“ - to jsou podřízené procesy, které zpracovávají požadavky klientů. K dispozici je pohodlná možnost: pomocí příkazu nginx -s <SIGNAL> ukončit procesy buď v režimu rychlého vypnutí, nebo v režimu elegantního vypnutí. Je zřejmé, že nás zajímá druhá možnost.

Pak je vše jednoduché: musíte přidat preStop-hák příkaz, který odešle ladný signál vypnutí. To lze provést v Deployment, v kontejnerovém bloku:

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

Nyní, když se modul vypne, uvidíme v protokolech kontejneru NGINX následující:

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

A to bude znamenat to, co potřebujeme: NGINX čeká na dokončení požadavků a poté proces ukončí. Níže však také zvážíme běžný problém, kvůli kterému i s příkazem nginx -s quit proces se nesprávně ukončí.

A v této fázi jsme s NGINX hotovi: alespoň z protokolů můžete pochopit, že vše funguje, jak má.

Jak je to s PHP-FPM? Jak zvládá ladné vypnutí? Pojďme na to přijít.

PHP-FPM

V případě PHP-FPM je informací o něco méně. Pokud se zaměříte na oficiální manuál podle PHP-FPM to řekne, že jsou akceptovány následující signály POSIX:

  1. SIGINT, SIGTERM — rychlé vypnutí;
  2. SIGQUIT — půvabné vypnutí (co potřebujeme).

Zbývající signály nejsou v této úloze vyžadovány, takže jejich analýzu vynecháme. Pro správné ukončení procesu budete muset napsat následující preStop hook:

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

To je na první pohled vše, co je potřeba k provedení ladného odstavení v obou kontejnerech. Úkol je však těžší, než se zdá. Níže jsou uvedeny dva případy, kdy ladné vypnutí nefungovalo a způsobilo krátkodobou nedostupnost projektu během nasazení.

Praxe. Možné problémy s plynulým vypnutím

Nginx

V první řadě je užitečné si zapamatovat: kromě provedení příkazu nginx -s quit Je tu ještě jedna etapa, která stojí za pozornost. Narazili jsme na problém, kdy NGINX stále posílal SIGTERM místo signálu SIGQUIT, což způsobovalo, že požadavky nebyly správně dokončeny. Podobné případy lze nalézt např. zde. Bohužel se nám nepodařilo zjistit konkrétní důvod tohoto chování: existovalo podezření na verzi NGINX, ale nepotvrdilo se. Příznakem bylo, že v protokolech kontejneru NGINX byly pozorovány zprávy: "otevřená zásuvka #10 vlevo ve spojení 5", načež se lusk zastavil.

Takový problém můžeme pozorovat například z odpovědí na Ingress, které potřebujeme:

Tipy a triky Kubernetes: funkce elegantního vypnutí v NGINX a PHP-FPM
Indikátory stavových kódů v době nasazení

V tomto případě obdržíme pouze chybový kód 503 od samotného Ingress: nemůže získat přístup ke kontejneru NGINX, protože již není přístupný. Pokud se podíváte na protokoly kontejnerů s NGINX, obsahují následující:

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

Po změně signálu zastavení se kontejner začne správně zastavovat: to je potvrzeno skutečností, že chyba 503 již není pozorována.

Pokud se setkáte s podobným problémem, má smysl zjistit, který signál zastavení se v kontejneru používá a jak přesně vypadá hák preStop. Je docela možné, že důvod spočívá právě v tom.

PHP-FPM... a další

Problém PHP-FPM je popsán triviálně: nečeká na dokončení podřízených procesů, ukončuje je, proto při nasazení a dalších operacích dochází k 502 chybám. Na bugs.php.net je od roku 2005 několik hlášení o chybách (např zde и zde), která tento problém popisuje. Ale s největší pravděpodobností nic v protokolech neuvidíte: PHP-FPM oznámí dokončení svého procesu bez jakýchkoli chyb nebo upozornění třetích stran.

Je vhodné upřesnit, že samotný problém může v menší či větší míře záviset na samotné aplikaci a nemusí se projevit například při sledování. Pokud se s tím setkáte, nejprve vás napadne jednoduché řešení: přidejte hák preStop s sleep(30). Umožní vám dokončit všechny požadavky, které byly dříve (a nové nepřijímáme, protože pod již schopný Ukončení) a po 30 sekundách se modul sám ukončí signálem SIGTERM.

Ukazuje se, že lifecycle protože kontejner bude vypadat takto:

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

Nicméně vzhledem k 30-ti vteřin sleep jsme silně prodloužíme dobu nasazení, protože každý modul bude ukončen minimum 30 sekund, což je špatné. Co se s tím dá dělat?

Obraťme se na stranu odpovědnou za přímé vyřízení žádosti. V našem případě ano PHP-FPMKterý ve výchozím nastavení nemonitoruje provádění svých podřízených procesů: Hlavní proces je okamžitě ukončen. Toto chování můžete změnit pomocí direktivy process_control_timeout, který specifikuje časové limity pro podřízené procesy, aby čekaly na signály od mastera. Pokud nastavíte hodnotu na 20 sekund, pokryje to většinu dotazů spuštěných v kontejneru a po jejich dokončení zastaví hlavní proces.

S těmito znalostmi se vraťme k našemu poslednímu problému. Jak již bylo zmíněno, Kubernetes není monolitická platforma: komunikace mezi jejími různými součástmi nějakou dobu trvá. To platí zejména, když vezmeme v úvahu provoz Ingresses a dalších souvisejících komponent, protože kvůli takovému zpoždění v době nasazení je snadné získat nárůst 500 chyb. Například může dojít k chybě ve fázi odesílání požadavku na upstream, ale „časová prodleva“ interakce mezi komponentami je poměrně krátká – méně než sekunda.

Tak, Dohromady s již zmíněnou směrnicí process_control_timeout můžete použít následující konstrukci lifecycle:

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

V tomto případě vyrovnáme zpoždění příkazem sleep a výrazně neprodlužujte dobu nasazení: je znatelný rozdíl mezi 30 sekundami a jednou?... Ve skutečnosti je to process_control_timeouta lifecycle slouží pouze jako „záchranná síť“ v případě zpoždění.

Obecně řečeno, popsané chování a odpovídající řešení platí nejen pro PHP-FPM. Podobná situace může tak či onak nastat při používání jiných jazyků/rámců. Pokud nemůžete ladné vypnutí opravit jinými způsoby - například přepsáním kódu tak, aby aplikace správně zpracovávala ukončovací signály - můžete použít popsanou metodu. Sice to není nejhezčí, ale funguje to.

Praxe. Zátěžový test pro kontrolu provozu podu

Zátěžové testování je jedním ze způsobů, jak zkontrolovat, jak kontejner funguje, protože tento postup jej přibližuje skutečným bojovým podmínkám, když uživatelé navštíví web. Chcete-li otestovat výše uvedená doporučení, můžete použít Yandex.Tankom: Dokonale pokrývá všechny naše potřeby. Následují tipy a doporučení pro provádění testování s jasným příkladem z naší zkušenosti díky grafům Grafana a samotného Yandex.Tank.

Nejdůležitější je zde zkontrolovat změny krok za krokem. Po přidání nové opravy spusťte test a zjistěte, zda se výsledky ve srovnání s posledním spuštěním změnily. V opačném případě bude obtížné identifikovat neefektivní řešení a z dlouhodobého hlediska to může pouze uškodit (například prodloužit dobu nasazení).

Další nuance je podívat se na protokoly kontejneru během jeho ukončení. Jsou tam zaznamenány informace o ladném vypnutí? Jsou nějaké chyby v protokolech při přístupu k jiným zdrojům (například k sousednímu kontejneru PHP-FPM)? Chyby v samotné aplikaci (jako ve výše popsaném případě s NGINX)? Doufám, že vám úvodní informace z tohoto článku pomohou lépe pochopit, co se děje s kontejnerem při jeho ukončení.

První zkušební provoz tedy proběhl bez lifecycle a bez dalších direktiv pro aplikační server (process_control_timeout v PHP-FPM). Účelem tohoto testu bylo zjistit přibližný počet chyb (a zda nějaké jsou). Z dalších informací byste také měli vědět, že průměrná doba nasazení každého modulu byla přibližně 5–10 sekund, dokud nebyl plně připraven. Výsledky jsou:

Tipy a triky Kubernetes: funkce elegantního vypnutí v NGINX a PHP-FPM

Informační panel Yandex.Tank ukazuje špičku 502 chyb, ke kterým došlo v době nasazení a trvaly v průměru až 5 sekund. Pravděpodobně to bylo proto, že stávající požadavky na starý modul byly ukončeny, když byl ukončen. Poté se objevilo 503 chyb, což bylo důsledkem zastaveného kontejneru NGINX, který také kvůli backendu zahodil spojení (což bránilo Ingress se k němu připojit).

Podívejme se jak process_control_timeout v PHP-FPM nám pomůže počkat na dokončení podřízených procesů, tzn. opravit takové chyby. Znovu nasaďte pomocí této směrnice:

Tipy a triky Kubernetes: funkce elegantního vypnutí v NGINX a PHP-FPM

Během 500. nasazení již nedochází k žádným chybám! Nasazení je úspěšné, ladné vypnutí funguje.

Nicméně stojí za to si připomenout problém s kontejnery Ingress, malé procento chyb, které můžeme obdržet kvůli časové prodlevě. Abyste se jim vyhnuli, zbývá pouze přidat strukturu s sleep a opakujte nasazení. V našem konkrétním případě však nebyly vidět žádné změny (opět žádné chyby).

Závěr

Aby bylo možné proces řádně ukončit, očekáváme od aplikace následující chování:

  1. Počkejte několik sekund a poté přestaňte přijímat nová připojení.
  2. Počkejte na dokončení všech požadavků a zavřete všechna udržovací připojení, která nevykonávají požadavky.
  3. Ukončete proces.

Ne všechny aplikace však mohou tímto způsobem fungovat. Jedno řešení problému v Kubernetes realitách je:

  • přidání předzastavovacího háku, který počká několik sekund;
  • prostudování konfiguračního souboru našeho backendu pro příslušné parametry.

Příklad s NGINX jasně ukazuje, že i aplikace, která by měla zpočátku správně zpracovávat ukončovací signály, to nemusí dělat, takže je důležité zkontrolovat 500 chyb během nasazení aplikace. To také umožňuje podívat se na problém šířeji a nezaměřovat se na jeden modul nebo kontejner, ale podívat se na celou infrastrukturu jako celek.

Jako testovací nástroj můžete použít Yandex.Tank ve spojení s jakýmkoli monitorovacím systémem (v našem případě byla data pro test převzata z Grafany s backendem Prometheus). Problémy s ladným vypnutím jsou jasně viditelné při velkém zatížení, které může benchmark generovat, a monitorování pomáhá analyzovat situaci podrobněji během testu nebo po něm.

V reakci na zpětnou vazbu k článku: stojí za zmínku, že problémy a řešení jsou zde popsány ve vztahu k NGINX Ingress. Pro jiné případy existují jiná řešení, která můžeme zvážit v následujících materiálech řady.

PS

Další ze série tipů a triků K8s:

Zdroj: www.habr.com

Přidat komentář