Cinque errori durante la distribuzione della prima applicazione su Kubernetes

Cinque errori durante la distribuzione della prima applicazione su KubernetesFallimento di Aris-Dreamer

Molte persone credono che sia sufficiente migrare l'applicazione su Kubernetes (utilizzando Helm o manualmente) e saranno contenti. Ma non è così semplice.

Squadra Mail.ru soluzioni cloud ha tradotto un articolo dell'ingegnere DevOps Julian Gindi. Racconta quali insidie ​​la sua azienda ha incontrato durante il processo di migrazione in modo da non calpestare lo stesso rastrello.

Passaggio uno: impostazione delle richieste e dei limiti dei pod

Iniziamo configurando un ambiente pulito in cui verranno eseguiti i nostri pod. Kubernetes svolge un ottimo lavoro di pianificazione dei pod e di gestione delle condizioni di errore. Ma si è scoperto che lo scheduler a volte non può posizionare un pod se è difficile stimare quante risorse sono necessarie per funzionare con successo. È qui che emergono richieste di risorse e limiti. Si discute molto sull’approccio migliore per stabilire richieste e limiti. A volte sembra davvero che si tratti più di arte che di scienza. Ecco il nostro approccio.

Richieste pod - Questo è il valore principale utilizzato dallo scheduler per posizionare in modo ottimale il pod.

Di Documentazione Kubernetes: la fase di filtraggio determina l'insieme di nodi in cui è possibile pianificare il pod. Ad esempio, il filtro PodFitsResources controlla se un nodo dispone di risorse sufficienti per soddisfare le richieste di risorse specifiche di un pod.

Utilizziamo le richieste delle applicazioni in modo che possano essere utilizzate per stimare quante risorse infatti L'applicazione ne ha bisogno per funzionare correttamente. In questo modo lo scheduler può posizionare i nodi in modo realistico. Inizialmente volevamo impostare le richieste con un margine per garantire che ciascun pod avesse un numero sufficientemente elevato di risorse, ma abbiamo notato che i tempi di pianificazione aumentavano notevolmente e alcuni pod non venivano mai completamente pianificati, come se per loro non fosse stata ricevuta alcuna richiesta di risorse.

In questo caso, lo scheduler spesso eliminava i pod e non era in grado di riprogrammarli perché il piano di controllo non aveva idea di quante risorse l'applicazione avrebbe richiesto, un componente chiave dell'algoritmo di pianificazione.

Limiti dei pod - questo è un limite più chiaro per il pod. Rappresenta la quantità massima di risorse che il cluster allocherà al contenitore.

Ancora una volta, da documentazione ufficiale: se un contenitore ha un limite di memoria impostato di 4 GiB, allora kubelet (e il runtime del contenitore) lo applicheranno. Il runtime non consente al contenitore di utilizzare più del limite di risorse specificato. Ad esempio, quando un processo in un contenitore tenta di utilizzare una quantità di memoria superiore a quella consentita, il kernel del sistema termina il processo con un errore di "memoria esaurita" (OOM).

Un contenitore può sempre utilizzare più risorse di quanto specificato nella richiesta di risorse, ma non può mai utilizzare più di quanto specificato nel limite. Questo valore è difficile da impostare correttamente, ma è molto importante.

Idealmente, vogliamo che i requisiti di risorse di un pod cambino nel corso del ciclo di vita di un processo senza interferire con altri processi nel sistema: questo è l'obiettivo di stabilire dei limiti.

Purtroppo non posso dare istruzioni specifiche su quali valori impostare, ma noi stessi ci atteniamo alle seguenti regole:

  1. Utilizzando uno strumento di test del carico, simuliamo un livello di traffico di base e monitoriamo l'utilizzo delle risorse del pod (memoria e processore).
  2. Impostiamo le richieste pod su un valore arbitrariamente basso (con un limite di risorse di circa 5 volte il valore delle richieste) e osserviamo. Quando le richieste sono troppo basse, il processo non può avviarsi, causando spesso misteriosi errori di runtime Go.

Tieni presente che limiti di risorse più elevati rendono la pianificazione più difficile perché il pod necessita di un nodo di destinazione con risorse sufficienti disponibili.

Immagina una situazione in cui disponi di un server Web leggero con un limite di risorse molto elevato, ad esempio 4 GB di memoria. Questo processo dovrà probabilmente scalare orizzontalmente e ogni nuovo modulo dovrà essere schedulato su un nodo con almeno 4 GB di memoria disponibile. Se non esiste alcun nodo di questo tipo, il cluster deve introdurre un nuovo nodo per elaborare quel pod, l'operazione potrebbe richiedere del tempo. È importante mantenere al minimo la differenza tra richieste e limiti di risorse per garantire una scalabilità rapida e fluida.

Passaggio due: impostazione dei test di attività e prontezza

Questo è un altro argomento delicato che viene spesso discusso nella comunità Kubernetes. È importante avere una buona conoscenza dei test di attività e disponibilità poiché forniscono un meccanismo che consente al software di funzionare senza problemi e ridurre al minimo i tempi di inattività. Tuttavia, se non configurati correttamente, possono causare un grave calo delle prestazioni dell'applicazione. Di seguito è riportato un riepilogo di come sono entrambi i campioni.

Vitalità mostra se il contenitore è in esecuzione. Se fallisce, kubelet uccide il contenitore e per esso viene abilitata una policy di riavvio. Se il contenitore non è dotato di una sonda Liveness, lo stato predefinito sarà Successo: questo è ciò che dice Documentazione Kubernetes.

Le sonde di attività dovrebbero essere economiche, nel senso che non dovrebbero consumare molte risorse, perché vengono eseguite frequentemente e devono informare Kubernetes che l'applicazione è in esecuzione.

Se imposti l'opzione per l'esecuzione ogni secondo, verrà aggiunta 1 richiesta al secondo, quindi tieni presente che saranno necessarie risorse aggiuntive per gestire questo traffico.

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

Abbiamo configurato le app con un endpoint "salute" che restituisce semplicemente un codice di risposta pari a 200. Ciò indica che il processo è in esecuzione ed è in grado di elaborare le richieste (ma non ancora il traffico).

prova prontezza indica se il contenitore è pronto per servire le richieste. Se l'analisi di disponibilità fallisce, il controller dell'endpoint rimuove l'indirizzo IP del pod dagli endpoint di tutti i servizi corrispondenti al pod. Ciò è indicato anche nella documentazione di Kubernetes.

Le sonde di disponibilità consumano più risorse perché devono essere inviate al back-end in modo da indicare che l'applicazione è pronta ad accettare le richieste.

Nella comunità si discute molto sull'opportunità di accedere direttamente al database. Dato il sovraccarico (i controlli vengono eseguiti frequentemente, ma possono essere modificati), abbiamo deciso che per alcune applicazioni la disponibilità a servire il traffico viene conteggiata solo dopo aver verificato che i record vengano restituiti dal database. Prove di preparazione ben progettate hanno garantito livelli più elevati di disponibilità ed eliminato i tempi di inattività durante l'implementazione.

Se decidi di interrogare il database per testare la disponibilità della tua applicazione, assicurati che sia il più economico possibile. Prendiamo questa richiesta:

SELECT small_item FROM table LIMIT 1

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

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

Puoi aggiungere alcune opzioni di configurazione aggiuntive:

  • initialDelaySeconds — quanti secondi passeranno tra il lancio del contenitore e l'inizio dei campioni.
  • periodSeconds — intervallo di attesa tra le analisi dei campioni.
  • timeoutSeconds — il numero di secondi dopo i quali l'unità viene considerata in emergenza. Timeout regolare.
  • failureThreshold — il numero di test falliti prima che venga inviato un segnale di riavvio al pod.
  • successThreshold — il numero di sonde riuscite prima che il pod passi allo stato pronto (dopo un errore, quando il pod si avvia o si ripristina).

Passaggio tre: impostazione delle policy di rete predefinite per il pod

Kubernetes ha una topografia di rete “piatta”; per impostazione predefinita, tutti i pod comunicano direttamente tra loro. In alcuni casi questo non è auspicabile.

Un potenziale problema di sicurezza è che un utente malintenzionato potrebbe utilizzare una singola applicazione vulnerabile per inviare traffico a tutti i pod sulla rete. Come in molti ambiti della sicurezza, anche in questo caso si applica il principio del privilegio minimo. Idealmente, le policy di rete dovrebbero specificare esplicitamente quali connessioni tra pod sono consentite e quali no.

Ad esempio, di seguito è riportata una semplice policy che nega tutto il traffico in entrata per uno spazio dei nomi specifico:

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

Visualizzazione di questa configurazione:

Cinque errori durante la distribuzione della prima applicazione su Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Maggiori dettagli qui.

Passaggio quattro: comportamento personalizzato utilizzando hook e contenitori init

Uno dei nostri obiettivi principali era fornire distribuzioni a Kubernetes senza tempi di inattività per gli sviluppatori. Ciò è difficile perché esistono molte opzioni per chiudere le applicazioni e liberare le risorse utilizzate.

Particolari difficoltà sono sorte con Nginx. Abbiamo notato che quando questi pod venivano distribuiti in sequenza, le connessioni attive venivano interrotte prima del completamento corretto.

Dopo approfondite ricerche online, si scopre che Kubernetes non aspetta che le connessioni Nginx si esauriscano prima di terminare il pod. Utilizzando un gancio pre-stop, abbiamo implementato le seguenti funzionalità ed eliminato completamente i tempi di inattività:

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

Ma 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 altro paradigma estremamente utile è l'uso dei contenitori init per gestire l'avvio di applicazioni specifiche. Ciò è particolarmente utile se si dispone di un processo di migrazione del database che utilizza molte risorse e che deve essere eseguito prima dell'avvio dell'applicazione. È inoltre possibile specificare un limite di risorse più elevato per questo processo senza impostare tale limite per l'applicazione principale.

Un altro schema comune consiste nell'accedere ai segreti in un contenitore init che fornisce tali credenziali al modulo principale, impedendo l'accesso non autorizzato ai segreti dal modulo dell'applicazione principale stesso.

Come al solito, cito dalla documentazione: i contenitori Init eseguono in modo sicuro codice personalizzato o utilità che altrimenti ridurrebbero la sicurezza dell'immagine del contenitore dell'applicazione. Mantenendo separati gli strumenti non necessari, si limita la superficie di attacco dell'immagine del contenitore dell'applicazione.

Passaggio cinque: configurazione del kernel

Infine, parliamo di una tecnica più avanzata.

Kubernetes è una piattaforma estremamente flessibile che ti consente di eseguire i carichi di lavoro nel modo che ritieni opportuno. Disponiamo di numerose applicazioni ad alte prestazioni che richiedono un utilizzo estremamente intensivo di risorse. Dopo aver condotto test di carico approfonditi, abbiamo scoperto che un'applicazione faticava a gestire il carico di traffico previsto quando erano attive le impostazioni Kubernetes predefinite.

Tuttavia, Kubernetes ti consente di eseguire 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 ha difficoltà a far fronte a un carico pesante, puoi provare a modificare alcune di queste impostazioni. Maggiori dettagli su questo processo e impostazione di valori diversi, come sempre nella documentazione ufficiale.

insomma

Sebbene Kubernetes possa sembrare una soluzione già pronta, ci sono alcuni passaggi chiave che devi eseguire per mantenere le tue applicazioni funzionanti senza intoppi.

Durante la migrazione a Kubernetes, è importante seguire il "ciclo di test di carico": avvia l'applicazione, testala di carico, osserva le metriche e il comportamento di ridimensionamento, regola la configurazione in base a tali dati, quindi ripeti nuovamente il ciclo.

Sii realistico riguardo al traffico previsto e prova ad andare oltre per vedere quali componenti si rompono per primi. Con questo approccio iterativo, solo alcune delle raccomandazioni elencate potrebbero essere sufficienti per raggiungere il successo. Oppure potrebbe richiedere una personalizzazione più profonda.

Ponetevi sempre queste domande:

  1. Quante risorse consumano le applicazioni e come cambierà questo volume?
  2. Quali sono i reali requisiti di scalabilità? Quanto traffico gestirà in media l'app? E il traffico di punta?
  3. Con quale frequenza il servizio dovrà scalare orizzontalmente? Quanto velocemente è necessario portare online i nuovi pod per ricevere traffico?
  4. Come si spengono correttamente i pod? È davvero necessario? È possibile ottenere una distribuzione senza tempi di inattività?
  5. Come puoi ridurre al minimo i rischi per la sicurezza e limitare i danni derivanti da eventuali pod compromessi? Ci sono servizi che dispongono di autorizzazioni o accessi che non richiedono?

Kubernetes fornisce un'incredibile piattaforma che ti consente di sfruttare le migliori pratiche per la distribuzione di migliaia di servizi in un cluster. Tuttavia, ogni applicazione è diversa. A volte l’implementazione richiede un po’ più di lavoro.

Fortunatamente, Kubernetes fornisce la configurazione necessaria per raggiungere tutti gli obiettivi tecnici. Utilizzando una combinazione di richieste e limiti di risorse, sonde di attività e disponibilità, contenitori di inizializzazione, policy di rete e ottimizzazione del kernel personalizzata, puoi ottenere prestazioni elevate insieme a tolleranza agli errori e rapida scalabilità.

Cos'altro leggere:

  1. Best practice e best practice per l'esecuzione di contenitori e Kubernetes negli ambienti di produzione.
  2. Oltre 90 strumenti utili per Kubernetes: distribuzione, gestione, monitoraggio, sicurezza e altro ancora.
  3. Il nostro canale Around Kubernetes in Telegram.

Fonte: habr.com

Aggiungi un commento