Nota di traduzione.: gli autori di questo articolo sono ingegneri di una piccola azienda ceca chiamata pipetail. Sono riusciti a raccogliere un elenco straordinario di [a volte banali, ma ancora] problemi e malintesi così attuali legati all'uso dei cluster Kubernetes.

Negli anni in cui abbiamo utilizzato Kubernetes, abbiamo lavorato con un gran numero di cluster (sia gestiti che non gestiti - su GCP, AWS e Azure). Col tempo, abbiamo cominciato a notare che alcuni errori si ripetono costantemente. Tuttavia, non c'è nulla di cui vergognarsi: siamo noi stessi a commettere la maggior parte di essi!
L'articolo raccoglie gli errori più comuni e menziona anche come correggerli.
1. Risorse: richieste e limiti
Questo punto merita sicuramente la massima attenzione e il primo posto nella lista.
La richiesta di CPU è solitamente o non è affatto impostata, oppure ha un valore molto basso (per collocare il maggior numero possibile di pod su ogni nodo). Di conseguenza, i nodi diventano sovraccarichi. Durante i picchi di carico, le capacità del processore del nodo sono completamente utilizzate e il carico di lavoro specifico riceve solo ciò che "ha richiesto" tramite il throttling della CPU. Questo porta a un aumento della latenza nell'applicazione, timeout e altre conseguenze sgradevoli. (Leggi di più su questo nel nostro recente altro articolo tradotto: «» — nota del traduttore.)
BestEffort (fortemente non consigliato):
resources: {}Una richiesta di CPU estremamente bassa (fortemente non consigliato):
resources:
Requests:
cpu: "1m"D'altra parte, avere un limite di CPU può portare a un'ingiustificata perdita di ciclo da parte dei pod, anche se il processore del nodo non è completamente carico. Anche in questo caso, ciò può portare a un aumento della latenza. Ci sono discussioni in corso riguardo al parametro quota CPU CFS nel kernel Linux e al throttling della CPU in base ai limiti impostati, così come alla disabilitazione della quota CFS... Purtroppo, i limiti della CPU possono causare più problemi di quanti ne risolvano. Puoi saperne di più al link qui sotto.
Un'eccessiva allocazione (overcommiting) della memoria può portare a problemi più gravi. Raggiungere il limite della CPU comporta la perdita di cicli, mentre raggiungere il limite della memoria comporta l'"uccisione" di un pod. Hai mai osservato OOMkill? Да, речь идет именно о нем.
Vuoi ridurre al minimo la probabilità di questo evento? Non allocare volumi eccessivi di memoria e utilizza Guaranteed QoS (Quality of Service), impostando la richiesta di memoria uguale ai limiti (come nell'esempio qui sotto). Puoi leggere di più in (ingegnere principale di Zalando).
Burstable (maggiore probabilità di ricevere OOMkilled):
resources:
requests:
memory: "128Mi"
cpu: "500m"
limits:
memory: "256Mi"
cpu: 2Guaranteed:
resources:
requests:
memory: "128Mi"
cpu: 2
limits:
memory: "128Mi"
cpu: 2Cosa può potenzialmente aiutare nella configurazione delle risorse?
Utilizzando metrics-server può mostrarti il consumo attuale di risorse CPU e l'utilizzo della memoria da parte dei pod (e dai container al loro interno). Probabilmente già lo stai utilizzando. Esegui semplicemente i seguenti comandi:
kubectl top pods
kubectl top pods --containers
kubectl top nodesTuttavia, mostrano solo l'utilizzo attuale. Possono darti un'idea approssimativa dell'ordine di grandezza, ma alla fine sarà necessario una cronologia delle metriche nel tempo (per rispondere a domande come: «Qual era il carico massimo della CPU?», «Qual era il carico ieri mattina?» — ecc.). Puoi utilizzare Prometheus, DataDog e altri strumenti. Ottengono semplicemente le metriche dal metrics-server e le archiviano, mentre l'utente può richiederle e costruire i relativi grafici.
consente automatizzare questo processo. Monitora la cronologia dell'uso della CPU e della memoria e regola le nuove richieste e limiti sulla base di queste informazioni.
Un uso efficiente delle capacità di calcolo è una sfida. È come giocare a Tetris costantemente. Se paghi troppo per le capacità di calcolo con un consumo medio basso (diciamo, ~10 %), ti consigliamo di dare un'occhiata ai prodotti basati su AWS Fargate o Virtual Kubelet. Sono costruiti su un modello di fatturazione serverless/pay-per-usage, che in queste condizioni potrebbe risultare più economico.
2. Controlli di liveness e readiness
Per impostazione predefinita, i controlli di stato di liveness e readiness in Kubernetes non sono abilitati. E talvolta si dimentica di attivarli...
Ma come si può altrimenti avviare il riavvio del servizio in caso di un errore irrisolvibile? E come fa il bilanciatore di carico a sapere che un certo pod è pronto a ricevere traffico? O che è in grado di gestire un carico di traffico maggiore?
Spesso questi controlli vengono confusi tra loro:
- Liveness — controllo della "vitalità", che riavvia il pod in caso di fallimento;
- Prontezza — controllo della prontezza, che disconnette il pod dal servizio Kubernetes in caso di fallimento (questo può essere verificato con
kubectl get endpoints) e il traffico non arriva fino a quando il successivo controllo non viene completato con successo.
Entrambi questi controlli VengONO ESEGUITI PER L'INTERA DURATA DEL CICLO DI VITA DEL POD. Questo è molto importante.
C'è una comune malinteso che i controlli di prontezza vengano eseguiti solo all'avvio, affinché il bilanciatore possa sapere che il pod è pronto (Pronto) e può iniziare a ricevere traffico. Tuttavia, questa è solo una delle loro applicazioni.
Un'altra è quella di sapere che il traffico sul pod è eccessivo e lo sovraccarica (o il pod sta eseguendo calcoli intensivi in termini di risorse). In questo caso, il controllo di prontezza aiuta a ridurre il carico sul pod e a "raffreddarlo".Il completamento con successo del controllo di prontezza in futuro permette di aumentare nuovamente il carico sul pod.In questo caso (in caso di fallimento del test di prontezza) il fallimento del controllo di vitalità sarebbe molto controproducente. Perché riavviare un pod che è sano e sta lavorando duramente?
Pertanto, in alcuni casi la completa assenza di controlli è meglio che averli attivati con parametri configurati in modo errato. Come detto sopra, se il controllo di vitalità copia il controllo di prontezza, allora sei in grossi guai. Una possibile opzione è configurare , ma Entrambi i tipi di controlli non devono fallire in caso di caduta delle dipendenze comuni, altrimenti questo porterà a un guasto a cascata (avalanche) di tutti i pod. In altre parole,
non danneggiare te stesso .
Probabilmente hai nel tuo cluster servizi HTTP che vorresti esporre al mondo esterno.
Se apri un servizio come
type: LoadBalancer , il suo controller (a seconda del provider) fornirà e gestirà un LoadBalancer esterno (non necessariamente funzionante su L7, piuttosto su L4), e questo può influire sui costi (indirizzo statico IPv4 esterno, potenza di calcolo, tariffazione per secondo) a causa della necessità di creare molte risorse simili.In questo caso ha molto più senso utilizzare un solo bilanciatore di carico esterno, aprendo i servizi come
type: NodePort. O, ancor meglio, implementare qualcosa comenginx-ingress-controller traefik (o traefik), che fungerà da unico NodePort endpoint, collegato a un bilanciatore di carico esterno, e instraderà il traffico all'interno del cluster utilizzando ingress-risorse Kubernetes.
Altri (micro)servizi intra-cluster, che interagiscono tra loro, possono "comunicare" tramite servizi di tipo ClusterIP e un meccanismo di scoperta dei servizi incorporato tramite DNS. È importante non utilizzare i loro DNS/IP pubblici, poiché ciò potrebbe influenzare la latenza e portare a un aumento dei costi dei servizi cloud.
4. Autoscaling del cluster senza considerare le sue peculiarità
Quando si aggiungono e si rimuovono nodi dal cluster, non si dovrebbe fare affidamento su alcune metriche di base come l'uso della CPU su questi nodi. La pianificazione dei pod deve avvenire tenendo conto di molteplici limitazioni, come l'affinità tra pod/nodi, taints e tolerations, richieste di risorse, QoS, ecc. L'uso di un autoscaler esterno, che non considera queste sfumature, può portare a problemi.
Immaginate che un certo pod debba essere pianificato, ma tutte le risorse CPU disponibili siano già richieste/allocate e il pod rimane bloccato in uno stato Pending. L'autoscaler esterno vede il carico medio attuale della CPU (e non quello richiesto) e non avvia l'espansione (scale-out) — non aggiunge un altro nodo. Di conseguenza, il pod non sarà pianificato.
D'altra parte, la riduzione della scalabilità (scale-in) — rimuovere un nodo dal cluster — è sempre più difficile da realizzare. Immaginate di avere un pod stateful (con uno storage permanente connesso). I Persistent-volume appartengono di solito a una certa zona di disponibilità e non vengono replicati nella regione. Pertanto, se l'autoscaler esterno rimuove un nodo con questo pod, il pianificatore non sarà in grado di pianificare il pod su un altro nodo, poiché ciò può essere fatto solo nella zona di disponibilità in cui si trova lo storage permanente. Il pod rimarrà bloccato in uno stato Pending.
Nel community di Kubernetes è molto popolare . Funziona all'interno del cluster, supporta API dai principali fornitori di servizi cloud, tiene conto di tutte le limitazioni e può scalare nei casi sopra menzionati. È anche in grado di effettuare scale-in mantenendo tutti i limiti imposti, risparmiando così denaro (che altrimenti sarebbe stato speso per risorse non richieste).
5. Ignorare le capacità IAM/RBAC
Fate attenzione a utilizzare utenti IAM con segreti permanenti per macchine e applicazioni. Organizza l'accesso temporaneo utilizzando ruoli e account di servizio (account di servizio).
Ci troviamo spesso di fronte al fatto che le chiavi di accesso (e i segreti) siano 'hardcodati' nella configurazione dell'applicazione, oltre a trascurare la rotazione dei segreti nonostante l'accesso a Cloud IAM. Utilizza i ruoli IAM e gli account di servizio invece degli utenti, dove appropriato.

Dimentica kube2iam e passa direttamente ai ruoli IAM per gli account di servizio (come descritto nella Štěpán Vraný):
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com\/role-arn: arn:aws:iam::123456789012:role\/my-app-role
name: my-serviceaccount
namespace: defaultUna annotazione. Non così difficile, vero?
Inoltre, non concedere agli account di servizio e ai profili degli istanze privilegi di admin e cluster-admin, a meno che non ne abbiano bisogno. Questo è un po' più difficile da realizzare, specialmente in RBAC K8s, ma ne vale sicuramente la pena.
6. Non affidarti all'anti-affinità automatica per i pod
Immagina di avere tre repliche di un certo deployment su un nodo. Il nodo va giù e con esso tutte le repliche. Situazione sgradevole, giusto? Ma perché tutte le repliche erano su un solo nodo? Kubernetes non dovrebbe fornire alta disponibilità (HA)?!
Purtroppo, il pianificatore Kubernetes non rispetta autonomamente le regole di esistenza separata (anti-affinità) per i pod. Devono essere esplicitamente definiti:
// опущено для краткости
labels:
app: zk
// опущено для краткости
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- zk
topologyKey: "kubernetes.io/hostname" Ecco fatto. Ora i pod saranno pianificati su nodi diversi (questa condizione viene verificata solo durante la pianificazione, non durante il loro funzionamento — ecco perché ed requiredDuringSchedulingIgnoredDuringExecution).
Qui parliamo di podAntiAffinity su nodi diversi: topologyKey: "kubernetes.io\/hostname", — e non su diverse zone di disponibilità. Per implementare una vera HA, sarà necessario approfondire questo tema.
7. Ignorare i PodDisruptionBudget
Immagina di avere un carico di lavoro in produzione in un cluster Kubernetes. Periodicamente, i nodi e il cluster stesso devono essere aggiornati (o ritirati). Il PodDisruptionBudget (PDB) è un po' come un accordo di servizio tra gli amministratori del cluster e gli utenti.
Il PDB aiuta a evitare interruzioni nei servizi causate dalla mancanza di nodi:
apiVersion: policy\/v1beta1
kind: PodDisruptionBudget
metadata:
name: zk-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: zookeeperIn questo esempio, tu, come utente del cluster, dici agli amministratori: «Ehi, ho un servizio zookeeper, e indipendentemente da ciò che fate, vorrei che almeno 2 repliche di questo servizio fossero sempre disponibili».
Puoi leggere di più al riguardo .
8. Più utenti o ambienti in un cluster comune
Spazi dei nomi di Kubernetes (namespaces) non forniscono un'isolamento forte.
C'è un comune malinteso che se si distribuisce un carico non prod in uno spazio dei nomi, e un carico prod in un altro, essi non influenzeranno l'uno sull'altro… Tuttavia, un certo livello di isolamento può essere raggiunto tramite richieste/limitazioni delle risorse, impostando quote, e assegnando priorityClass. Una sorta di isolamento «fisico» nel data plane è garantito da affinities, tolerations, taints (o nodeselectors), tuttavia tale separazione è piuttosto complessa da realizzare.
Coloro che devono combinare entrambi i tipi di carico di lavoro in un unico cluster dovranno convivere con la complessità. Se non c'è una tale necessità, e puoi permetterti di creare un altro cluster (diciamo, in un cloud pubblico), è meglio farlo. Questo permetterà di raggiungere un livello di isolamento molto più elevato.
9. externalTrafficPolicy: Cluster
Spesso osserviamo che tutto il traffico all'interno del cluster arriva tramite un servizio di tipo NodePort, per il quale per impostazione predefinita è prevista la politica externalTrafficPolicy: Cluster. Questo significa che NodePort è aperto su ogni nodo nel cluster, e si può usare qualsiasi di essi per interagire con il servizio desiderato (insieme di pod).

Tuttavia, i reali pod collegati a questo servizio NodePort di cui sopra sono solitamente presenti solo in un sottoinsieme di questi nodi. In altre parole, se mi connetto a un nodo che non ha il pod necessario, questo reindirizzerà il traffico a un altro nodo, aggiungendo un hop e aumentando la latenza (se i nodi si trovano in diverse zone di disponibilità/data center, la latenza può essere piuttosto elevata; inoltre, aumenteranno i costi per il traffico in uscita).
D'altra parte, se per un certo servizio Kubernetes è stata impostata la politica externalTrafficPolicy: Local, allora il NodePort è aperto solo sui nodi in cui sono effettivamente in esecuzione i pod necessari. Quando si utilizza un bilanciatore di carico esterno che controlla lo stato (healthchecking) degli endpoint (come fa AWS ELB), esso invierà traffico solo ai nodi necessari, che avrà un effetto positivo su latenza, esigenze computazionali, costi per egress (e il buon senso dettato lo stesso).
È molto probabile che tu stia già utilizzando qualcosa come traefik o traefik come punto finale NodePort (o LoadBalancer, che utilizza anche NodePort) per instradare il traffico HTTP ingress, e impostare questa opzione può ridurre significativamente la latenza per tali richieste.
In puoi scoprire maggiori dettagli su externalTrafficPolicy, i suoi vantaggi e svantaggi.
10. Non vincolarti a cluster e non abusare del control plane.
In passato, i server venivano comunemente chiamati con nomi propri: , HAL9000 e Colossus… Oggi sono stati sostituiti da identificatori generati casualmente. Tuttavia, l'abitudine è rimasta, e ora i nomi propri vengono assegnati ai cluster.
Una storia tipica (basata su eventi reali): tutto è iniziato come una prova di concetto, quindi il cluster portava con orgoglio il nome testing… Sono passati anni, e viene ANCORA utilizzato in produzione, e tutti hanno paura di toccarlo.
Non c'è niente di divertente nel vedere i cluster trasformarsi in animali domestici, quindi ti consigliamo di eliminarli periodicamente, esercitandoti nel ripristino dopo i guasti (in questo ti aiuterà — nota del traduttore.)). Inoltre, non sarebbe male occuparsi anche del layer di controllo (control plane). La paura di toccarlo non è un buon segno. Etcd è morto? Ragazzi, siete veramente nei guai!
Dall'altra parte, non dovresti esagerare con le manipolazioni. Col tempo il layer di controllo può diventare lento. È probabile che ciò sia dovuto all'elevato numero di oggetti creati senza rotazione (una situazione comune quando si utilizza Helm con impostazioni predefinite, che non aggiornano il suo stato in configmap\/segreti — come risultato, migliaia di oggetti si accumulano nel layer di controllo) o alla modifica costante degli oggetti kube-api (per il dimensionamento automatico, per CI\/CD, per il monitoraggio, registrazione eventi, controller, ecc.).
Inoltre, ti consigliamo di controllare gli accordi SLA\/SLO con il fornitore di Kubernetes gestito e prestare attenzione alle garanzie. Il fornitore può garantire la disponibilità del layer di controllo (o delle sue sotto-componenti), ma non la latenza p99 delle richieste che gli invii. In altre parole, puoi introdurre kubectl get nodes, e ricevere la risposta solo dopo 10 minuti, e questo non costituirà una violazione delle condizioni del contratto di servizio.
11. Bonus: utilizzo del tag latest
E questo è già un classico. Ultimamente ci imbattiamo in tecniche simili meno frequentemente, poiché molti, insegnati dall'amara esperienza, hanno smesso di usare il tag :latest e hanno iniziato a fissare (pin) le versioni. Evviva!
ECR ; ti consigliamo di familiarizzare con questa interessante caratteristica.
Riepilogo
Non aspettarti che tutto funzioni con un semplice gesto: Kubernetes non è una panacea. Un'applicazione scadente (e, forse, diventerà ancora peggiore). La negligenza porterà a una complessità eccessiva, a un funzionamento lento e teso dello strato di gestione. Inoltre, rischi di rimanere senza una strategia di ripristino di emergenza. Non contare sul fatto che Kubernetes «out of the box» garantirà isolamento e alta disponibilità. Dedica del tempo a rendere la tua applicazione realmente cloud native.
Puoi conoscere le esperienze negative di diversi team in di Henning Jacobs.
Gli interessati a integrare l'elenco degli errori presentati in questo articolo possono contattarci su Twitter (, ).
P.S. dal traduttore
Leggi anche nel nostro blog:
- «»;
- «»;
- «» (panoramica e video della presentazione);
- «».
Fonte: habr.com
