Cinque errori da evitare nel deployment della prima applicazione su Kubernetes

Cinque errori da evitare nel deployment della prima applicazione su KubernetesFail di Aris-Dreamer

Molti credono che sia sufficiente trasferire l'applicazione su Kubernetes (sia con Helm che manualmente) e il successo sarà garantito. Ma non è così semplice.

Team Mail.ru Cloud Solutions Ho tradotto l'articolo dell'ingegnere DevOps Julian Gindi. Racconta quali insidie ha affrontato la sua azienda durante il processo di migrazione, affinché voi non incappiate negli stessi errori.

Primo passo: configurazione delle richieste del pod e dei limiti

Cominciamo con la configurazione di un ambiente pulito in cui i nostri pod lavoreranno. Kubernetes gestisce magistralmente la pianificazione dei pod e la gestione degli stati di errore. Tuttavia, è emerso che talvolta il pianificatore non riesce a posizionare un pod se fatica a stimare quante risorse siano necessarie per il suo corretto funzionamento. Qui entrano in gioco le richieste di risorse e i limiti. C'è un ampio dibattito su quale sia l'approccio migliore alla configurazione di richieste e 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 il posizionamento ottimale del pod.

Da documentazione Kubernetes: nel passo di filtraggio viene determinato l'insieme di nodi su cui è possibile programmare il pod. Ad esempio, il filtro PodFitsResources verifica se ci sono abbastanza risorse nel nodo per soddisfare le specifiche richieste di risorse del pod.

Utilizziamo le richieste delle applicazioni in modo da poter stimare quante risorse è davvero siano necessarie all'applicazione per funzionare normalmente. In questo modo il pianificatore può posizionare realisticamente i nodi. Inizialmente volevamo impostare le richieste con un margine, per garantire un numero adeguato di risorse per ogni pod, ma abbiamo notato che il tempo di pianificazione aumentava significativamente e alcuni pod non venivano completamente pianificati, quasi come se non avessero ricevuto affatto richieste di risorse.

In questo caso, il pianificatore spesso "spingeva" i pod e non riusciva a riprogrammarli perché la superficie di controllo non sapeva quante risorse sarebbero state necessarie all'applicazione, e questo è un componente chiave dell'algoritmo di pianificazione.

Limiti del pod (pod limits) sono un vincolo più chiaro per il pod. Rappresentano il volume massimo di risorse che il cluster assegnerà al contenitore.

Ancora una volta, da documentazione ufficiale: se per il contenitore è impostato un limite di memoria di 4 GiB, kubelet (e l'ambiente di esecuzione del contenitore) lo applicherà forzatamente. L'ambiente di esecuzione non consente al contenitore di utilizzare più di quanto specificato nel limite delle risorse. Ad esempio, quando un processo nel contenitore tenta di utilizzare più della quantità di memoria consentita, il kernel del sistema termina questo processo con un errore 'out of memory' (OOM).

Il 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 è estremamente 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 su quali valori impostare, ma noi stessi seguiamo le seguenti regole:

  1. Utilizzando uno strumento di test delle prestazioni, modelliamo una 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 circa 5 volte maggiore rispetto al valore delle richieste) e osserviamo. Quando le richieste sono troppo basse, il processo non può iniziare, il che spesso provoca errori misteriosi di runtime in Go.

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

Immagina una situazione in cui hai un server web leggero con un limite di 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 almeno 4 GB di memoria disponibile. Se non esiste un nodo del genere, il cluster deve introdurre un nuovo nodo per gestire questo pod, il che potrebbe richiedere del tempo. È importante ottenere una minima differenza tra le richieste di risorse e i limiti, per garantire uno scaling rapido e fluido.

Passo due: configurazione dei test di Liveness e Readiness

Questo è un altro tema delicato che viene spesso discusso nella comunità Kubernetes. È importante comprendere bene i test di vitalità (Liveness) e di prontezza (Readiness), poiché forniscono un meccanismo per un funzionamento sostenibile del software e minimizzano i tempi di inattività. Tuttavia, possono avere un impatto significativo sulle prestazioni della tua applicazione se non configurati correttamente. Di seguito è riportato un riassunto di cosa consistono entrambe le probe.

Liveness mostra se il contenitore funziona. Se fallisce, kubelet termina il contenitore e viene attivata la politica di riavvio. Se il contenitore non è dotato di una probe di Liveness, lo stato predefinito sarà successo — così viene indicato nel documentazione Kubernetes.

Le probe di Liveness devono essere economiche, ovvero non consumare molte risorse, poiché vengono eseguite frequentemente e devono informare Kubernetes che l'applicazione è attiva.

Se imposti il parametro per eseguire ogni secondo, questo aggiungerà 1 richiesta al secondo, quindi tieni presente che per gestire questo traffico serviranno risorse aggiuntive.

Nel nostro azienda, i test di Liveness controllano i componenti principali dell'applicazione, anche se i dati (ad esempio, da un database remoto o da una cache) non sono completamente disponibili.

Abbiamo configurato nelle applicazioni un endpoint di 'salute' che restituisce semplicemente un codice di risposta 200. Questo indica che il processo è in esecuzione e in grado di gestire le richieste (ma non ancora il traffico).

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

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

Nel settore si svolgono molte discussioni su se sia opportuno accedere direttamente al database. Considerando i costi generali (i controlli vengono eseguiti frequentemente, ma possono essere regolati), abbiamo deciso che per alcune applicazioni la disponibilità a gestire il traffico viene considerata solo dopo aver verificato che vengano restituiti record dal database. Prove di prontezza ben strutturate hanno offerto un livello più elevato di disponibilità ed eliminato interruzioni durante il deployment.

Se decidi di fare una richiesta al database per controllare la prontezza dell'applicazione, assicurati che sia il più economica possibile. Prendiamo, ad esempio, questa 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 passano tra l'avvio del contenitore e l'inizio delle prove.
  • periodSeconds — intervallo di attesa tra i lanci delle prove.
  • timeoutSeconds — numero di secondi dopo i quali un pod viene considerato in errore. Timeout standard.
  • failureThreshold — numero di fallimenti nei test prima che venga inviato un segnale di riavvio al pod.
  • successThreshold — numero di prove di successo richieste prima che il pod passi allo stato di prontezza (dopo un guasto, quando il pod viene avviato o ripristinato).

Passo tre: configurazione delle politiche di rete predefinite del pod

In Kubernetes, la topologia di rete è "piatta", per impostazione predefinita tutti i pod interagiscono direttamente tra loro. In alcuni casi, questo non è desiderabile.

Un potenziale problema di sicurezza è che un aggressore possa sfruttare un'unica 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 chiaramente quali connessioni tra i pod sono consentite e quali no.

Ad esempio, qui 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 da evitare nel deployment della prima applicazione su Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
In dettaglio qui.

Passo quattro: comportamento non standard tramite hook e init-container

Una delle nostre principali sfide era garantire deployment in Kubernetes senza tempi di inattività per gli sviluppatori. Questo è difficile a causa del fatto che ci sono molteplici modi per far terminare le applicazioni e liberare le risorse utilizzate.

Particolari difficoltà sono sorte con Nginx. Abbiamo notato che durante il deployment sequenziale di questi pod, le connessioni attive venivano interrotte prima del completamento riuscito.

Dopo una vasta ricerca su internet, è emerso che Kubernetes non attende che le connessioni Nginx si esauriscano prima di terminare il pod. Con l'implementazione di un hook pre-stop, abbiamo introdotto 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 paradigmi estremamente utili è l'uso di init-container per gestire l'avvio di applicazioni specifiche. Questo è particolarmente utile se hai un processo di migrazione del database che richiede molte risorse e deve essere eseguito prima dell'avvio dell'applicazione. Per questo processo, puoi anche specificare un limite di risorse più elevato senza impostare tale limite per l'applicazione principale.

Un altro schema comune è l'accesso ai segreti nell'init-container che fornisce queste credenziali al modulo principale, evitando quindi accessi non autorizzati ai segreti dal modulo principale dell'applicazione.

Come al solito, una citazione dalla documentazione: gli init-container eseguono in sicurezza codice utente o utilità che altrimenti comprometterebbero la sicurezza dell'immagine del container dell'applicazione. Conservando separatamente strumenti non necessari, si limita la superficie d'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 carichi di lavoro come ritieni opportuno. Abbiamo una serie di applicazioni ad alte prestazioni che richiedono quantità enormi di risorse. Dopo aver condotto un ampio test di carico, abbiamo scoperto che una delle applicazioni faticava a gestire il carico di traffico previsto se venivano applicate le impostazioni predefinite di Kubernetes.

Tuttavia, Kubernetes consente di avviare un contenitore privilegiato che modifica i parametri del kernel solo per un pod specifico. Ecco cosa abbiamo utilizzato per modificare 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 sta faticando a gestire un carico elevato, potresti provare a configurare alcuni di questi parametri. Maggiori dettagli su questo processo e sulla configurazione di diversi valori sono, come sempre, nella documentazione ufficiale.

In conclusione

Anche se Kubernetes può sembrare una soluzione pronta all'uso, per garantire il funzionamento senza interruzioni delle applicazioni sono necessari alcuni passaggi chiave.

Durante tutta la migrazione a Kubernetes, è importante seguire il «ciclo di test del carico»: avvii l'applicazione, la testi sotto carico, osservi le metriche e il comportamento durante la scalabilità, configuri le impostazioni in base a questi dati e poi ripeti questo ciclo.

Stima realisticamente il traffico previsto e prova a superarne i limiti per vedere quali componenti si guastano per primi. Con questo approccio iterativo, bastano solo alcuni dei consigli elencati per raggiungere il successo. Potrebbe anche essere necessaria una configurazione più approfondita.

Fai sempre queste domande:

  1. Quante risorse consumano le applicazioni e come cambierà questo volume?
  2. Quali sono i requisiti reali per la scalabilità? Quanto traffico in media gestirà l'applicazione? E per quanto riguarda il traffico di picco?
  3. Con che frequenza il servizio avrà bisogno di scalabilità orizzontale? Quanto velocemente è necessario mettere in funzione nuovi pod per accettare il traffico?
  4. Quanto bene vengono terminati i pod? È davvero necessario? È possibile realizzare un deployment senza downtime?
  5. Come minimizzare i rischi per la sicurezza e limitare i danni da eventuali pod compromessi? Alcuni servizi hanno autorizzazioni o accessi di cui non hanno bisogno?

Kubernetes offre una piattaforma incredibile che consente di utilizzare le migliori pratiche per implementare migliaia di servizi in un cluster. Tuttavia, tutte le applicazioni sono diverse. 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 di Liveness e Readiness, init-container, politiche di rete e configurazioni personalizzate del kernel, è possibile ottenere elevate prestazioni insieme a resilienza e rapida scalabilità.

Cosa leggere ancora:

  1. Le migliori pratiche e raccomandazioni per l'implementazione di container e Kubernetes negli ambienti di produzione.
  2. 90+ strumenti utili per Kubernetes: distribuzione, gestione, monitoraggio, sicurezza e altro ancora.
  3. Il nostro canale Intorno 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