Suggerimenti e trucchi Kubernetes: funzionalità di spegnimento ordinato in NGINX e PHP-FPM

Una condizione tipica quando si implementa CI/CD in Kubernetes: l'applicazione deve essere in grado di non accettare nuove richieste del client prima di arrestarsi completamente e, soprattutto, completare con successo quelle esistenti.

Suggerimenti e trucchi Kubernetes: funzionalità di spegnimento ordinato in NGINX e PHP-FPM

Il rispetto di questa condizione consente di ottenere tempi di inattività pari a zero durante la distribuzione. Tuttavia, anche quando si utilizzano bundle molto popolari (come NGINX e PHP-FPM), è possibile incontrare difficoltà che porteranno a un'ondata di errori ad ogni implementazione...

Teoria. Come vive il baccello

Abbiamo già pubblicato in dettaglio il ciclo di vita di un pod questo articolo. Nel contesto dell'argomento in esame, siamo interessati a quanto segue: nel momento in cui il pod entra nello stato Terminare, le nuove richieste non vengono più inviate ad esso (pod rimosso dall'elenco degli endpoint per il servizio). Pertanto, per evitare tempi di inattività durante la distribuzione, è sufficiente che risolviamo il problema di arrestare correttamente l'applicazione.

Dovresti anche ricordare che il periodo di grazia predefinito è 30 secondi: dopodiché il pod verrà terminato e l'applicazione dovrà avere il tempo di elaborare tutte le richieste prima di questo periodo. Nota: sebbene qualsiasi richiesta che richieda più di 5-10 secondi sia già problematica e lo spegnimento graduale non aiuterà più...

Per capire meglio cosa succede quando un pod termina, basta guardare il seguente diagramma:

Suggerimenti e trucchi Kubernetes: funzionalità di spegnimento ordinato in NGINX e PHP-FPM

A1, B1 - Ricezione di cambiamenti sullo stato del focolare
A2 - Partenza SIGTERM
B2: rimozione di un pod dagli endpoint
B3 - Ricezione delle modifiche (l'elenco degli endpoint è cambiato)
B4 - Aggiorna le regole di iptables

Nota: l'eliminazione del pod endpoint e l'invio di SIGTERM non avvengono in sequenza, ma in parallelo. Inoltre, poiché Ingress non riceve immediatamente l'elenco aggiornato degli endpoint, le nuove richieste dei client verranno inviate al pod, il che causerà un errore 500 durante la chiusura del pod (per materiale più dettagliato su questo tema, noi tradotto). Questo problema deve essere risolto nei seguenti modi:

  • Invia connessione: chiudi nelle intestazioni di risposta (se si tratta di un'applicazione HTTP).
  • Se non è possibile apportare modifiche al codice, il seguente articolo descrive una soluzione che consentirà di elaborare le richieste fino alla fine del periodo di grazia.

Teoria. Come NGINX e PHP-FPM terminano i loro processi

Nginx

Cominciamo con NGINX, poiché con esso tutto è più o meno ovvio. Immergendoci nella teoria, apprendiamo che NGINX ha un processo principale e diversi "lavoratori": si tratta di processi secondari che elaborano le richieste dei clienti. Viene fornita un'opzione conveniente: utilizzare il comando nginx -s <SIGNAL> terminare i processi in modalità di arresto rapido o di arresto regolare. Ovviamente è quest’ultima opzione che ci interessa.

Allora tutto è semplice: devi aggiungere gancio preStop un comando che invierà un grazioso segnale di spegnimento. Questo può essere fatto in Deployment, nel blocco contenitore:

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

Ora, quando il pod si spegne, vedremo quanto segue nei log del contenitore NGINX:

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à ciò di cui abbiamo bisogno: NGINX attende il completamento delle richieste e quindi interrompe il processo. Tuttavia, di seguito considereremo anche un problema comune a causa del quale, anche con il comando nginx -s quit il processo termina in modo errato.

E a questo punto abbiamo finito con NGINX: almeno dai log si capisce che tutto funziona come dovrebbe.

Qual è il problema con PHP-FPM? Come gestisce lo spegnimento regolare? Scopriamolo.

PHP-FPM

Nel caso di PHP-FPM ci sono un po' meno informazioni. Se ti concentri su manuale ufficiale secondo PHP-FPM, dirà che sono accettati i seguenti segnali POSIX:

  1. SIGINT, SIGTERM — spegnimento rapido;
  2. SIGQUIT - spegnimento regolare (ciò di cui abbiamo bisogno).

I restanti segnali non sono richiesti in questo compito, quindi ometteremo la loro analisi. Per terminare correttamente il processo sarà necessario scrivere il seguente hook preStop:

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

A prima vista, questo è tutto ciò che serve per eseguire uno spegnimento regolare in entrambi i contenitori. Tuttavia il compito è più arduo di quanto sembri. Di seguito sono riportati due casi in cui l'arresto regolare non ha funzionato e ha causato l'indisponibilità a breve termine del progetto durante la distribuzione.

Pratica. Possibili problemi con lo spegnimento regolare

Nginx

Innanzitutto è utile ricordare: oltre a eseguire il comando nginx -s quit C'è un'altra fase a cui vale la pena prestare attenzione. Abbiamo riscontrato un problema per cui NGINX inviava comunque SIGTERM invece del segnale SIGQUIT, causando il mancato completamento delle richieste. Casi simili si possono riscontrare ad es. qui. Purtroppo non siamo riusciti a determinare il motivo specifico di questo comportamento: c'era un sospetto sulla versione NGINX, ma non è stato confermato. Il sintomo era che venivano osservati messaggi nei log del contenitore NGINX "socket aperto n. 10 lasciato nella connessione 5", dopodiché il pod si è fermato.

Possiamo osservare un problema del genere, ad esempio, dalle risposte su Ingress di cui abbiamo bisogno:

Suggerimenti e trucchi Kubernetes: funzionalità di spegnimento ordinato in NGINX e PHP-FPM
Indicatori dei codici di stato al momento della distribuzione

In questo caso, riceviamo semplicemente un codice di errore 503 da Ingress stesso: non può accedere al contenitore NGINX, poiché non è più accessibile. Se guardi i log del contenitore con NGINX, contengono quanto segue:

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

Dopo aver cambiato il segnale di stop, il contenitore comincia a fermarsi correttamente: ciò è confermato dal fatto che l'errore 503 non viene più osservato.

Se si riscontra un problema simile, è logico capire quale segnale di arresto viene utilizzato nel contenitore e che aspetto ha esattamente il gancio preStop. È del tutto possibile che la ragione risieda proprio in questo.

PHP-FPM... e altro ancora

Il problema con PHP-FPM è descritto in modo banale: non attende il completamento dei processi figli, li termina, motivo per cui si verificano errori 502 durante la distribuzione e altre operazioni. Ci sono diverse segnalazioni di bug su bugs.php.net dal 2005 (es qui и qui), che descrive questo problema. Ma molto probabilmente non vedrai nulla nei log: PHP-FPM annuncerà il completamento del suo processo senza errori o notifiche di terze parti.

È bene chiarire che il problema stesso può dipendere in misura maggiore o minore dall'applicazione stessa e potrebbe non manifestarsi, ad esempio, nel monitoraggio. Se lo incontri, ti viene in mente prima una semplice soluzione alternativa: aggiungi un hook preStop con sleep(30). Ti consentirà di completare tutte le richieste precedenti (e non ne accettiamo di nuove, poiché pod già capace Terminare), e dopo 30 secondi il pod stesso si spegnerà con un segnale SIGTERM.

Si scopre che lifecycle per il contenitore sarà simile a questo:

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

Tuttavia, a causa dei 30 secondi sleep noi fortemente aumenteremo il tempo di distribuzione, poiché ogni pod verrà terminato minimo 30 secondi, il che è brutto. Cosa si può fare a riguardo?

Rivolgiamoci al soggetto responsabile dell'esecuzione diretta della domanda. Nel nostro caso lo è PHP-FPMChe per impostazione predefinita non monitora l'esecuzione dei suoi processi figli: Il processo master viene terminato immediatamente. È possibile modificare questo comportamento utilizzando la direttiva process_control_timeout, che specifica i limiti di tempo per i processi secondari per attendere i segnali dal master. Se imposti il ​​valore su 20 secondi, ciò coprirà la maggior parte delle query in esecuzione nel contenitore e interromperà il processo principale una volta completate.

Con questa consapevolezza, torniamo al nostro ultimo problema. Come accennato, Kubernetes non è una piattaforma monolitica: la comunicazione tra i suoi diversi componenti richiede del tempo. Ciò è particolarmente vero se consideriamo il funzionamento degli Ingress e di altri componenti correlati, poiché a causa di tale ritardo al momento della distribuzione è facile ottenere un aumento di 500 errori. Ad esempio, potrebbe verificarsi un errore nella fase di invio di una richiesta a monte, ma il "ritardo" di interazione tra i componenti è piuttosto breve, meno di un secondo.

Così, nell'aggregato con la già citata direttiva process_control_timeout è possibile utilizzare la seguente costruzione per lifecycle:

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

In questo caso compenseremo il ritardo con il comando sleep e non aumentare significativamente il tempo di schieramento: dopo tutto, la differenza tra 30 secondi e uno si nota?.. Infatti, è il process_control_timeoutE lifecycle utilizzato solo come “rete di sicurezza” in caso di ritardo.

In generale, il comportamento descritto e la soluzione alternativa corrispondente si applicano non solo a PHP-FPM. Una situazione simile può verificarsi in un modo o nell'altro quando si utilizzano altri linguaggi/framework. Se non è possibile correggere l'arresto regolare in altri modi, ad esempio riscrivendo il codice in modo che l'applicazione elabori correttamente i segnali di terminazione, è possibile utilizzare il metodo descritto. Potrebbe non essere il più bello, ma funziona.

Pratica. Test di carico per verificare il funzionamento del pod

Il test di carico è uno dei modi per verificare il funzionamento del container, poiché questa procedura lo avvicina alle condizioni di combattimento reali quando gli utenti visitano il sito. Per testare i consigli di cui sopra, è possibile utilizzare Yandex.Tankom: Copre perfettamente tutte le nostre esigenze. Di seguito sono riportati suggerimenti e raccomandazioni per condurre test con un chiaro esempio tratto dalla nostra esperienza grazie ai grafici di Grafana e Yandex.Tank stesso.

La cosa più importante qui è controllare le modifiche passo dopo passo. Dopo aver aggiunto una nuova correzione, esegui il test e verifica se i risultati sono cambiati rispetto all'ultima esecuzione. Altrimenti, sarà difficile identificare soluzioni inefficaci e, a lungo termine, ciò potrà solo causare danni (ad esempio, aumentare i tempi di implementazione).

Un'altra sfumatura è guardare i log del contenitore durante la sua terminazione. Le informazioni sullo spegnimento regolare sono registrate lì? Sono presenti errori nei log quando si accede ad altre risorse (ad esempio, un contenitore PHP-FPM vicino)? Errori nell'applicazione stessa (come nel caso sopra descritto con NGINX)? Spero che le informazioni introduttive di questo articolo ti aiutino a capire meglio cosa succede al contenitore durante la sua chiusura.

Quindi, il primo giro di prova è avvenuto senza lifecycle e senza direttive aggiuntive per il server delle applicazioni (process_control_timeout in PHP-FPM). Lo scopo di questo test era identificare il numero approssimativo di errori (e se ce ne sono). Inoltre, da ulteriori informazioni, dovresti sapere che il tempo medio di distribuzione per ciascun pod è stato di circa 5-10 secondi prima che fosse completamente pronto. I risultati sono:

Suggerimenti e trucchi Kubernetes: funzionalità di spegnimento ordinato in NGINX e PHP-FPM

Il pannello informativo di Yandex.Tank mostra un picco di 502 errori, che si è verificato al momento dell'implementazione ed è durato in media fino a 5 secondi. Presumibilmente ciò era dovuto al fatto che le richieste esistenti al vecchio pod venivano terminate nel momento in cui veniva terminato. Successivamente sono comparsi 503 errori, che erano il risultato di un container NGINX arrestato, che ha anche interrotto le connessioni a causa del backend (che ha impedito a Ingress di connettersi ad esso).

Vediamo come process_control_timeout in PHP-FPM ci aiuterà ad attendere il completamento dei processi figli, ad es. correggere tali errori. Ridistribuire utilizzando questa direttiva:

Suggerimenti e trucchi Kubernetes: funzionalità di spegnimento ordinato in NGINX e PHP-FPM

Non ci sono più errori durante la 500a distribuzione! La distribuzione ha esito positivo e l'arresto regolare funziona.

Tuttavia, vale la pena ricordare il problema con i contenitori Ingress, una piccola percentuale di errori che potremmo ricevere a causa di un ritardo. Per evitarli non resta che aggiungere una struttura con sleep e ripetere la distribuzione. Tuttavia, nel nostro caso particolare, non sono state riscontrate modifiche visibili (anche in questo caso, nessun errore).

conclusione

Per terminare il processo con garbo, ci aspettiamo il seguente comportamento dall'applicazione:

  1. Attendi qualche secondo e poi smetti di accettare nuove connessioni.
  2. Attendi il completamento di tutte le richieste e chiudi tutte le connessioni keepalive che non stanno eseguendo richieste.
  3. Termina il processo.

Tuttavia, non tutte le applicazioni possono funzionare in questo modo. Una soluzione al problema nelle realtà Kubernetes è:

  • aggiungendo un gancio di pre-stop che attenderà qualche secondo;
  • studiando il file di configurazione del nostro backend per i parametri appropriati.

L'esempio con NGINX chiarisce che anche un'applicazione che dovrebbe inizialmente elaborare correttamente i segnali di terminazione potrebbe non farlo, quindi è fondamentale verificare la presenza di 500 errori durante la distribuzione dell'applicazione. Ciò consente anche di guardare al problema in modo più ampio e di non concentrarsi su un singolo pod o contenitore, ma di guardare all’intera infrastruttura nel suo insieme.

Come strumento di test, puoi utilizzare Yandex.Tank insieme a qualsiasi sistema di monitoraggio (nel nostro caso, i dati sono stati presi da Grafana con un backend Prometheus per il test). I problemi con lo spegnimento regolare sono chiaramente visibili sotto i carichi pesanti che il benchmark può generare, e il monitoraggio aiuta ad analizzare la situazione in modo più dettagliato durante o dopo il test.

In risposta al feedback sull'articolo: vale la pena ricordare che i problemi e le soluzioni sono descritti qui in relazione a NGINX Ingress. Per gli altri casi esistono altre soluzioni, che potremmo considerare nei seguenti materiali della serie.

PS

Altro dalla serie di suggerimenti e trucchi K8:

Fonte: habr.com

Aggiungi un commento