Condizione standard nella realizzazione di CI/CD in Kubernetes: l'applicazione deve essere in grado di non accettare nuove richieste dei clienti prima di fermarsi completamente e, cosa più importante, completare con successo quelle già esistenti.

Il rispetto di questa condizione consente di raggiungere un downtime zero durante il deployment. Tuttavia, anche utilizzando combinazioni molto popolari (come NGINX e PHP-FPM), è possibile imbattersi in difficoltà che porteranno a un aumento degli errori ad ogni deployment...
Teoria. Come vive un pod
Abbiamo già pubblicato dettagli sul ciclo di vita di un pod . Nel contesto del tema trattato, ci interessa sapere: nel momento in cui il pod passa allo stato Terminating, non vengono più inviati nuovi richieste (il pod dalla lista degli endpoints per il servizio). Così, per evitare periodi d'inattività durante il deployment, da parte nostra è sufficiente risolvere il problema dello shutdown corretto dell'applicazione.
Occorre anche ricordare che il grace period di default è di : dopo questo periodo, il pod verrà terminato e l'applicazione deve elaborare tutte le richieste entro questo termine. Nota: sebbene qualsiasi richiesta che richiede più di 5-10 secondi rappresenti già un problema, e un arresto normale non possa più aiutare…
Per comprendere meglio cosa accade quando un pod termina la sua esecuzione, è sufficiente studiare il seguente schema:

A1, B1 — Ricezione delle modifiche sullo stato del pod
A2 — Invio di SIGTERM
B2 — Rimozione del pod dagli endpoint
B3 — Ricezione delle modifiche (l'elenco degli endpoint è cambiato)
B4 — Aggiornamento delle regole iptables
Si noti: la rimozione dell'endpoint del pod e l'invio di SIGTERM avvengono non in sequenza, ma in parallelo. E poiché l'Ingress non riceve immediatamente l'elenco aggiornato degli endpoint, verranno inviati nuovi richieste al pod dai client, il che provocherà errori 500 durante la terminazione del pod (materiale più dettagliato su questo argomento lo ). Per risolvere questo problema, sono necessari i seguenti metodi:
- Inviare nel’intestazione di risposta Connection: close (se si tratta di un'applicazione HTTP).
- Se non è possibile apportare modifiche al codice, nell'articolo è descritta una soluzione che permette di elaborare le richieste fino alla fine del periodo di arresto normale.
Teoria. Come NGINX e PHP-FPM terminano i loro processi
NGINX
Iniziamo da NGINX, perché con esso è tutto abbastanza chiaro. Approfondendo la teoria, scopriamo che NGINX ha un processo master e diversi "worker" — questi sono processi figli che gestiscono le richieste dei client. È prevista una comoda opzione: tramite il comando nginx -s è possibile terminare i processi sia in modalità fast shutdown che in graceful shutdown. Ovviamente, ci interessa proprio quest'ultima opzione.
Successivamente, è semplice: occorre aggiungere nel un comando che invierà il segnale per il graceful shutdown. Questo può essere fatto nel Deployment, nel blocco del contenitore:
lifecycle:
preStop:
exec:
command:
- /usr/sbin/nginx
- -s
- quitOra, al momento della chiusura del pod, nei log del contenitore NGINX vedremo quanto segue:
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 E questo significherà esattamente ciò di cui abbiamo bisogno: NGINX aspetta il completamento delle richieste, dopo di che termina il processo. Tuttavia, verrà poi esaminato un problema comune, che causa la chiusura errata del processo anche in presenza del comando nginx -s quit il processo si termina in modo non corretto.
E a questo punto abbiamo concluso con NGINX: almeno dai log si può capire che tutto funziona come dovrebbe.
Qual è la situazione con PHP-FPM? Come gestisce il graceful shutdown? Approfondiamo.
PHP-FPM
Nel caso di PHP-FPM, le informazioni sono un po' meno. Se ci orientiamo al per PHP-FPM, verrà indicato che vengono accettati i seguenti segnali POSIX:
-
SIGINT,SIGTERM— fast shutdown; -
SIGQUIT— graceful shutdown (quello di cui abbiamo bisogno).
Gli altri segnali non sono necessari per questo compito, quindi ne tralasciamo l'analisi. Per un corretto completamento del processo, sarà necessario scrivere il seguente preStop hook:
lifecycle:
preStop:
exec:
command:
- /bin/kill
- -SIGQUIT
- "1"A prima vista, questo è tutto ciò che è necessario per eseguire il graceful shutdown in entrambi i contenitori. Tuttavia, il compito è più complesso di quanto sembri. Di seguito vengono discussi due casi in cui il graceful shutdown non ha funzionato e ha causato una temporanea indisponibilità del progetto durante il deployment.
Pratica. Possibili problemi con il graceful shutdown
NGINX
Innanzitutto, è utile ricordare: oltre all'esecuzione del comando nginx -s quit c'è un altro aspetto a cui prestare attenzione. Ci siamo imbattuti nel problema in cui NGINX, invece di inviare il segnale SIGQUIT, invia comunque SIGTERM, il che causa il mancato completamento corretto delle richieste. Casi simili possono essere trovati, ad esempio, . Sfortunatamente, non siamo riusciti a determinare la causa specifica di questo comportamento: si sospettava una problematica con le versioni di NGINX, ma non è stata confermata. I sintomi consistevano nel fatto che nei log del contenitore NGINX apparivano messaggi «open socket #10 left in connection 5», dopo di che il pod si fermava.
Possiamo osservare questo problema, ad esempio, dalle risposte all'Ingress di nostro interesse:

I codici di stato al momento del deployment
In questo caso, riceviamo esattamente il codice di errore 503 dall'Ingress stesso: non riesce a contattare il contenitore NGINX, poiché quest'ultimo non è più disponibile. Se guardiamo nei log del contenitore NGINX, troviamo quanto segue:
[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13Dopo aver cambiato il segnale di stop, il contenitore inizia a fermarsi correttamente: questo è confermato dal fatto che non osserviamo più l'errore 503.
Se hai riscontrato un problema simile, è utile approfondire quale segnale di stop viene utilizzato nel contenitore e come appare esattamente il preStop-hook. È molto probabile che la causa sia proprio questa.
PHP-FPM... e non solo
Il problema con PHP-FPM può sembrare banale: non aspetta che i processi figlio siano completati e li termina, causando errori 502 durante il deploy e altre operazioni. Sin dal 2005, ci sono stati diversi report su bugs.php.net (per esempio, e ), che descrivono questo problema. Tuttavia, nei log probabilmente non vedrai nulla: PHP-FPM comunicherà la fine del suo processo senza errori o notifiche esterne.
È importante notare che il problema stesso può dipendere in misura minore o maggiore dall'applicazione e non manifestarsi, ad esempio, nel monitoraggio. Se ti imbatti comunque in esso, il primo workaround che viene in mente è semplicemente aggiungere un preStop-hook con sleep(30). Questo permetterà di completare tutte le richieste precedenti (e non accettiamo nuove, dato che il pod ha già è in stato Terminating), e dopo 30 secondi il pod verrà terminato con il segnale SIGTERM.
Ciò significa che lifecycle per il contenitore avrà il seguente aspetto:
lifecycle:
preStop:
exec:
command:
- /bin/sleep
- "30" Tuttavia, a causa dell'impostazione di 30 secondi sleep noi fortemente aumenterà il tempo di distribuzione, poiché ogni pod verrà terminato un minimo di 30 secondi, il che è problematico. Cosa si può fare al riguardo?
Rivolgiamoci a quella parte che si occupa dell'esecuzione diretta dell'applicazione. Nel nostro caso si tratta di PHP-FPM, che di default non monitora l'esecuzione dei propri processi figlio: il processo master termina immediatamente. È possibile modificare questo comportamento tramite la direttiva process_control_timeout, che specifica i limiti di tempo per attendere i segnali dal master dai processi figlio. Se si imposta un valore di 20 secondi, questo coprirà la maggior parte delle richieste eseguite nel contenitore e, dopo il loro completamento, il processo master verrà arrestato.
Con questa consapevolezza, torniamo al nostro ultimo problema. Come già accennato, Kubernetes non è una piattaforma monolitica: ci vuole del tempo per l'interazione tra i diversi componenti. Questo è particolarmente rilevante quando consideriamo il funzionamento degli Ingress e di altri componenti correlati, poiché a causa di tale ritardo, al momento del deploy, è facile riscontrare un picco di errori 500. Ad esempio, l'errore può verificarsi durante l'invio della richiesta all'upstream, ma il "lag" temporale dell'interazione tra i componenti è piuttosto breve — meno di un secondo.
Pertanto, nel complesso con la direttiva già menzionata process_control_timeout si può utilizzare la seguente struttura per lifecycle:
lifecycle:
preStop:
exec:
command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"] In questo modo compenseremo il ritardo con il comando sleep senza aumentare significativamente il tempo di deploy: c'è una notevole differenza tra trenta secondi e uno? In sostanza, il "lavoro principale" è svolto proprio da process_control_timeout, e lifecycle è usato solo come "copertura" nel caso di un lag.
In generale, il comportamento descritto e la relativa soluzione alternativa riguardano non solo PHP-FPM. Situazioni simili possono verificarsi in vari linguaggi di programmazione o framework. Se non si riesce a risolvere il problema del graceful shutdown in altri modi — ad esempio, riscrivendo il codice affinché l’applicazione gestisca correttamente i segnali di terminazione — si può applicare il metodo descritto. Anche se non è il più elegante, funziona.
Pratica. Test di carico per verificare il funzionamento del pod.
Il test di carico è uno dei modi per controllare come funziona un container, poiché questa procedura simula condizioni di utilizzo reale, quando gli utenti accedono al sito. Per testare le raccomandazioni sopra elencate, si può utilizzare : copre perfettamente tutte le nostre esigenze. Di seguito sono riportati suggerimenti e raccomandazioni per effettuare i test, con esempi pratici — grazie ai grafici di Grafana e di Yandex.Tank — tratti dalla nostra esperienza.
La cosa più importante qui è verificare le modifiche passo dopo passo.. Dopo aver aggiunto una nuova correzione, esegui i test e verifica se i risultati sono cambiati rispetto all'ultima esecuzione. Altrimenti, sarà difficile identificare soluzioni inefficaci e, a lungo termine, si può anche danneggiare (ad esempio, aumentando il tempo di deployment).
Un altro aspetto da considerare è controllare i log del container durante la sua terminazione. Viene registrata un'informazione sul 'graceful shutdown'? Ci sono errori nei log durante l'accesso ad altre risorse (ad esempio, al container PHP-FPM adiacente)? Errori dell'applicazione stessa (come nel caso descritto sopra con NGINX)? Spero che le informazioni fornite in questo articolo aiutino a comprendere meglio cosa succede al container durante la sua terminazione.
Quindi, il primo avvio del test è avvenuto senza lifecycle e senza direttive aggiuntive per il server applicativo (process_control_timeout in PHP-FPM). L'obiettivo di questo test era determinare il numero approssimativo di errori (e se ci sono). Inoltre, è utile sapere che il tempo medio di deployment di ogni pod era di circa 5-10 secondi fino a raggiungere lo stato di completa prontezza. I risultati sono i seguenti:

Nel pannello di controllo di Yandex.Tank è visibile un picco di errori 502, verificatosi durante il deployment e durato in media fino a 5 secondi. Presumibilmente, questo è stato causato dall'interruzione di richieste esistenti al vecchio pod durante la sua terminazione. Successivamente, sono apparsi errori 503, risultato di un contenitore NGINX fermo, che ha anche interrotto le connessioni a causa del backend (il che ha impedito il collegamento a Ingress).
Diamo un'occhiata a come process_control_timeout PHP-FPM può aiutarci ad attendere il completamento dei processi figlio, cioè a risolvere tali errori. Un nuovo deployment utilizzando questa direttiva:

Durante il deployment non ci sono più errori 500! Il deployment avviene con successo, lo shutdown graduale funziona.
Tuttavia, è importante ricordare il problema con i contenitori Ingress, da cui possiamo ricevere una piccola percentuale di errori a causa di un lag temporaneo. Per evitarli, resta da aggiungere una costruzione con sleep e ripetere il deployment. Tuttavia, nel nostro particolare caso non ci sono stati cambiamenti visibili (nessun errore di nuovo).
Conclusione
Per una corretta conclusione del processo, ci aspettiamo dal'applicazione il seguente comportamento:
- Aspettare alcuni secondi, dopo di che smettere di accettare nuove connessioni.
- Aspettare il completamento di tutte le richieste e chiudere tutte le connessioni keepalive che non stanno elaborando richieste.
- Terminare il proprio processo.
Tuttavia, non tutte le applicazioni riescono a funzionare in questo modo. Una delle soluzioni al problema nel contesto di Kubernetes è:
- l'aggiunta di un hook pre-stop che attenderà alcuni secondi;
- esaminare il file di configurazione del nostro backend per eventuali parametri pertinenti.
Un esempio con NGINX permette di capire che anche un'applicazione che inizialmente dovrebbe gestire correttamente i segnali di terminazione potrebbe non farlo, quindi è fondamentale controllare la presenza di errori 500 durante il deployment dell'applicazione. Questo permette anche di guardare il problema in una prospettiva più ampia e non concentrarsi su un singolo pod o contenitore, ma considerare l'intera infrastruttura nel suo complesso.
Come strumento di test si può utilizzare Yandex.Tank insieme a qualsiasi sistema di monitoraggio (nel nostro caso, sono stati utilizzati i dati di Grafana con backend Prometheus). I problemi con il graceful shutdown sono ben visibili sotto carichi elevati, che possono essere generati da un benchmark, e il monitoraggio aiuta a esaminare più a fondo la situazione durante o dopo il test.
Rispondendo ai feedback sull'articolo: è importante sottolineare che i problemi e le soluzioni qui descritti si riferiscono a NGINX Ingress. Per altri casi ci sono soluzioni diverse, che potrebbero essere trattate nei prossimi materiali della serie.
P.S.
Altro dal ciclo K8s tips & tricks:
- «»;
- «»;
- «»;
- «».
Fonte: habr.com
