Cinque errori nel lancio della prima applicazione su Kubernetes

Cinque errori nel lancio della prima applicazione su KubernetesFallimento di Aris-Dreamer

Molti credono che sia sufficiente migrarle un'applicazione su Kubernetes (sia tramite Helm sia manualmente) per ottenere successo. Ma non è tutto così semplice.

Team Mail.ru Cloud Solutions Questo articolo è stato tradotto dall'ingegnere DevOps Juliane Gindi. Racconta quali insidie ha affrontato la sua azienda durante il processo di migrazione, affinché non possiate cadere negli stessi tranelli.

Primo passo: impostazione delle richieste del pod e dei limiti

Iniziamo a configurare un ambiente pulito in cui lavoreranno i nostri pod. Kubernetes gestisce magnificamente la pianificazione dei pod e la gestione degli stati di errore. Tuttavia, si è scoperto che il pianificatore a volte non riesce a collocare un pod quando ha difficoltà a valutare quante risorse siano necessarie per funzionare correttamente. È qui che entrano in gioco le richieste di risorse e i limiti. Ci sono molte discussioni sul miglior approccio per configurare le richieste e i limiti. A volte sembra davvero più un'arte che una scienza. Ecco il nostro approccio.

Richieste del pod (pod requests) sono il valore principale utilizzato dal pianificatore per collocare ottimamente un pod.

Da documentazione di Kubernetes: nella fase di filtraggio viene determinato un insieme di nodi in cui è possibile pianificare il pod. Ad esempio, il filtro PodFitsResources verifica se ci sono risorse sufficienti nel nodo per soddisfare le richieste specifiche del pod.

Utilizziamo le richieste delle applicazioni per stimare quante risorse in realtà sono necessarie all'applicazione per funzionare correttamente. In questo modo, il pianificatore può posizionare realisticamente i nodi. Inizialmente, volevamo impostare le richieste con un margine per garantire un'adeguata disponibilità di risorse per ogni pod, ma abbiamo notato che il tempo di pianificazione aumentava significativamente e alcuni pod non venivano mai completamente pianificati, come se non fossero state fatte richieste di risorse per loro.

In questo caso, il pianificatore spesso 'espelleva' i pod e non riusciva a ripianificarli perché il piano di controllo non sapeva quante risorse sarebbero state necessarie per l'applicazione, e questo è un componente chiave dell'algoritmo di pianificazione.

I limiti del pod (pod limits) rappresentano una restrizione più chiara per il pod. Essa costituisce il massimo volume di risorse che il cluster assegnerà al contenitore.

Ancora, da documentazione ufficiale: se per il contenitore è impostato un limite di memoria di 4 GiB, il kubelet (e l'ambiente di esecuzione del contenitore) lo forzerà. L'ambiente di esecuzione non consente al contenitore di utilizzare più del limite di risorse specificato. Ad esempio, quando un processo all'interno del contenitore tenta di utilizzare più della memoria consentita, il kernel del sistema termina quel processo con un errore «out of memory» (OOM).

Un contenitore può sempre utilizzare più risorse di quelle specificate nella richiesta di risorse, ma non può mai utilizzare più di quanto indicato nel limite. È difficile impostare correttamente questo valore, ma è molto importante.

Idealmente, desideriamo che i requisiti di risorse del pod cambino durante il ciclo di vita del processo, senza interferire con altri processi nel sistema: questo è l'obiettivo dell'impostazione dei limiti.

Sfortunatamente, non posso fornire indicazioni specifiche sui valori da impostare, ma noi stessi seguiamo queste linee guida:

  1. Utilizzando uno strumento di stress testing, modelliamo il livello base di traffico e osserviamo l'utilizzo delle risorse del pod (memoria e CPU).
  2. Impostiamo le richieste del pod a un valore arbitrariamente basso (con un limite delle risorse di circa 5 volte superiore al valore delle richieste) e osserviamo. Quando le richieste sono a un livello troppo basso, il processo non può avviarsi, il che spesso provoca errori misteriosi durante l'esecuzione di Go.

Desidero sottolineare che limiti delle risorse più elevati complicano la pianificazione, poiché il pod necessita di un nodo target con un numero sufficiente di risorse disponibili.

Immaginate una situazione in cui avete un server web leggero con un limite delle risorse molto elevato, ad esempio 4 GB di memoria. Probabilmente, questo processo dovrà essere scalato orizzontalmente e ogni nuovo modulo dovrà essere pianificato su un nodo con un volume di memoria disponibile di almeno 4 GB. Se non esiste un tale nodo, il cluster deve introdurre un nuovo nodo per gestire questo pod, il che potrebbe richiedere del tempo. È importante ottenere una differenza minima tra richieste di risorse e limiti, per garantire una scalabilità rapida e fluida.

Passo due: configurazione dei test di Liveness e Readiness

Questo è un altro argomento delicato che viene spesso discusso nella comunità di Kubernetes. È importante comprendere bene i test di vitalità (Liveness) e di prontezza (Readiness), poiché forniscono un meccanismo per il funzionamento affidabile del software e riducono al minimo i tempi di inattività. Tuttavia, possono avere un impatto significativo sulle prestazioni della tua applicazione se non configurati correttamente. Di seguito è riportato un riepilogo di cosa sono entrambi i controlli.

Liveness mostra se il contenitore è attivo. Se fallisce, kubelet uccide il contenitore e si attiva la politica di riavvio. Se il contenitore non è dotato di un controllo Liveness, lo stato predefinito sarà successo — così viene detto in documentazione di Kubernetes.

I controlli di Liveness devono essere economici, ovvero non consumare molte risorse, poiché vengono eseguiti frequentemente e devono informare Kubernetes che l'applicazione è in esecuzione.

Se imposti il parametro per eseguirlo ogni secondo, ciò porterà a 1 richiesta al secondo, quindi tieni presente che saranno necessari ulteriori risorse per gestire questo traffico.

Nella nostra azienda, i test di Liveness verificano i componenti principali dell'applicazione, anche quando i dati (ad esempio, da un database remoto o dalla cache) non sono completamente disponibili.

Abbiamo configurato nelle applicazioni un endpoint di «disponibilità» che restituisce semplicemente un codice di risposta 200. Questo indica che il processo è avviato e in grado di gestire richieste (ma non ancora il traffico).

Probe Readiness indica se il contenitore è pronto per gestire richieste. Se la prova di prontezza fallisce, il controller degli endpoint rimuove l'indirizzo IP del pod dagli endpoint di tutti i servizi corrispondenti al pod. Questo è anche indicato nella documentazione di Kubernetes.

Le probe di Readiness consumano più risorse, poiché devono accedere al backend in modo da mostrare la prontezza dell'applicazione a ricevere richieste.

Nel nostro settore ci sono molte discussioni sull'opportunità di rivolgersi direttamente al database. Considerando i costi (le verifiche vengono effettuate frequentemente, ma possono essere regolate), abbiamo deciso che per alcune applicazioni la disponibilità di servire il traffico viene considerata valida solo dopo aver verificato che il database restituisce registrazioni. Prove di prontezza ben pianificate hanno garantito un livello di disponibilità più elevato ed eliminato i tempi di inattività durante il deployment.

Se decidete di effettuare una richiesta al database per verificare la prontezza dell'applicazione, assicuratevi che sia il più economica possibile. Consideriamo la seguente richiesta:

SELECT small_item FROM table LIMIT 1

Ecco un esempio di come impostiamo questi due valori in Kubernetes:

livenessProbe: 
 httpGet:   
   path: /api/liveness    
   port: http 
readinessProbe:  
 httpGet:    
   path: /api/readiness    
   port: http  periodSeconds: 2

È possibile aggiungere alcuni parametri di configurazione aggiuntivi:

  • initialDelaySeconds — quanti secondi passeranno tra l'avvio del contenitore e l'inizio delle verifiche.
  • periodSeconds — intervallo tra le esecuzioni delle verifiche.
  • timeoutSeconds — numero di secondi dopo i quali il pod viene considerato in errore. Un timeout standard.
  • failureThreshold — numero di fallimenti dei test prima che il pod invii un segnale di riavvio.
  • successThreshold — numero di successi richiesti prima che il pod passi allo stato di prontezza (dopo un guasto, quando il pod viene avviato o ripristinato).

Passaggio tre: configurazione delle politiche di rete predefinite del pod

In Kubernetes, la topografia di rete è «flessibile», per impostazione predefinita tutti i pod interagiscono direttamente tra loro. In alcuni casi, questo non è auspicabile.

Un potenziale problema di sicurezza è che un aggressore potrebbe usare un'applicazione vulnerabile per inviare traffico a tutti i pod nella rete. Come in molti ambiti della sicurezza, qui si applica il principio del minimo privilegio. Idealmente, le politiche di rete dovrebbero specificare esplicitamente quali connessioni tra pod sono consentite e quali no.

Ad esempio, qui di seguito c'è una semplice politica che vieta tutto il traffico in ingresso per uno specifico namespace:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:  
 name: default-deny-ingress
spec:  
 podSelector: {}  
 policyTypes:  
   - Ingress

Visualizzazione di questa configurazione:

Cinque errori nel lancio della prima applicazione su Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Maggiore chiarezza qui.

Passo quattro: comportamento personalizzato con hook e contenitori init

Uno dei nostri obiettivi principali era garantire il deploy su Kubernetes senza interruzioni per gli sviluppatori. Questo è difficile perché ci sono molte opzioni per terminare le applicazioni e liberare le risorse utilizzate.

Difficoltà particolari si sono presentate con Nginx. Abbiamo notato che durante il deploy sequenziale di questi pod, le connessioni attive venivano interrotte prima del completamento con successo.

Dopo ampie ricerche su Internet, è emerso che Kubernetes non attende che le connessioni Nginx si esauriscano prima di terminare il pod. Con l'uso dell'hook pre-stop, abbiamo implementato questa funzionalità e completamente eliminato il downtime:

lifecycle: 
 preStop:
   exec:
     command: ["/usr/local/bin/nginx-killer.sh"]

Ecco nginx-killer.sh:

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
   echo "Waiting while shutting down nginx..."
   sleep 10
done

Un'altra importante pratica è l'uso di init-container per gestire l'avvio di applicazioni specifiche. Questo è particolarmente utile quando hai un processo di migrazione del database ad alta intensità che deve essere avviato prima dell'applicazione. Per questo processo, puoi anche specificare un limite di risorse più alto senza impostare un limite per l'applicazione principale.

Un'altra configurazione comune è l'accesso ai segreti nell'init-container, che fornisce queste credenziali al modulo principale, prevenendo accessi non autorizzati ai segreti dal modulo principale dell'applicazione.

Come al solito, una citazione dalla documentazione: gli init-container eseguono in modo sicuro codice o utility personalizzate che altrimenti comprometterebbero la sicurezza dell'immagine del container dell'applicazione. Tenendo separati gli strumenti non necessari, limiti la superficie di attacco dell'immagine del container dell'applicazione.

Passo cinque: configurazione del kernel

Infine, parleremo di una tecnica più avanzata.

Kubernetes è una piattaforma estremamente flessibile che consente di eseguire i carichi di lavoro come meglio credi. Abbiamo una serie di applicazioni ad alte prestazioni che richiedono risorse estremamente elevate. Dopo aver effettuato ampi test di carico, abbiamo scoperto che una delle applicazioni fatica a gestire il traffico previsto se sono attuali le impostazioni predefinite di Kubernetes.

Tuttavia, Kubernetes consente di avviare un contenitore privilegiato, che modifica i parametri del kernel solo per un determinato pod. Ecco cosa abbiamo utilizzato per cambiare il numero massimo di connessioni aperte:

initContainers:
  - name: sysctl
     image: alpine:3.10
     securityContext:
         privileged: true
      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

Questa è una tecnica più avanzata, che spesso non è necessaria. Ma se la tua applicazione fatica a gestire un carico elevato, puoi provare a configurare alcuni di questi parametri. Maggiori informazioni su questo processo e sulla configurazione di diversi valori — come sempre nella documentazione ufficiale.

In conclusione

Sebbene Kubernetes possa sembrare una soluzione pronta all'uso, è necessario intraprendere alcuni passaggi chiave per garantire il corretto funzionamento delle applicazioni.

Durante tutta la migrazione a Kubernetes, è fondamentale seguire il "ciclo di stress testing": avviare l'applicazione, testarla sotto carico, monitorare le metriche e il comportamento durante lo scaling, configurare le impostazioni in base a questi dati e ripetere nuovamente il ciclo.

Valuta realisticamente il traffico atteso e cerca di superarlo per vedere quali componenti si rompono per primi. Con questo approccio iterativo, potrebbe bastare solo qualche suggerimento per avere successo, oppure potrebbe essere necessaria una configurazione più approfondita.

Poniti sempre queste domande:

  1. Quante risorse consumano le applicazioni e come cambierà questo volume?
  2. Quali sono le reali esigenze di scalabilità? Quanto traffico gestirà mediamente l'applicazione? E per quanto riguarda i picchi di traffico?
  3. Con quale frequenza il servizio necessiterà di scalabilità orizzontale? Quanto velocemente devono essere messi in funzione nuovi pod per gestire il traffico?
  4. Quanto accuratamente vengono terminati i pod? È davvero necessario? È possibile ottenere un deployment senza downtime?
  5. Come minimizzare i rischi per la sicurezza e limitare i danni di eventuali pod compromessi? Ci sono permessi o accessi di cui alcuni servizi non hanno bisogno?

Kubernetes offre una piattaforma straordinaria che consente di applicare le migliori pratiche per il deployment di migliaia di servizi in un cluster. Tuttavia, ogni applicazione è diversa. A volte l'implementazione richiede un po' più di lavoro.

Fortunatamente, Kubernetes fornisce le configurazioni necessarie per raggiungere tutti gli obiettivi tecnici. Utilizzando una combinazione di richieste di risorse e limiti, probe Liveness e Readiness, init-container, politiche di rete e configurazioni personalizzate del kernel, è possibile ottenere elevate prestazioni insieme a resilienza e rapida scalabilità.

Cosa altro leggere:

  1. Le migliori pratiche e consigli per l'implementazione di container e Kubernetes in ambienti produttivi.
  2. 90+ strumenti utili per Kubernetes: deployment, gestione, monitoraggio, sicurezza e altro ancora.
  3. Il nostro canale Attorno a Kubernetes su Telegram.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster