Ciao a tutti! Mi chiamo Oleg Sidorenkov, lavoro presso DomClick come capo del team delle infrastrutture. Utilizziamo Kubik in produzione da più di tre anni e durante questo periodo abbiamo vissuto con esso molti momenti interessanti. Oggi ti dirò come, con il giusto approccio, puoi ottenere ancora più prestazioni da Vanilla Kubernetes per il tuo cluster. Pronti partenza via!
Sapete tutti molto bene che Kubernetes è un sistema open source scalabile per l'orchestrazione dei container; beh, o 5 binari che fanno magie gestendo il ciclo di vita dei tuoi microservizi in un ambiente server. Inoltre, è uno strumento abbastanza flessibile che può essere assemblato come i Lego per la massima personalizzazione per compiti diversi.
E tutto sembra andare bene: getta i server nel cluster come legna da ardere in un focolare e non conoscerai alcun dolore. Ma se sei a favore dell’ambiente, penserai: “Come posso mantenere il fuoco acceso e risparmiare la foresta?” In altre parole, come trovare modi per migliorare le infrastrutture e ridurre i costi.
1. Monitorare le risorse del team e dell'applicazione
Uno dei metodi più comuni ma efficaci è l'introduzione di richieste/limiti. Dividi le applicazioni per spazi dei nomi e gli spazi dei nomi per team di sviluppo. Prima della distribuzione, impostare i valori dell'applicazione per il consumo di tempo del processore, memoria e spazio di archiviazione temporaneo.
Attraverso l'esperienza, siamo giunti alla conclusione: non dovresti gonfiare le richieste dai limiti più del doppio. Il volume del cluster viene calcolato in base alle richieste e se fornisci alle applicazioni una differenza di risorse, ad esempio 5-10 volte, immagina cosa accadrà al tuo nodo quando sarà pieno di pod e riceverà improvvisamente un carico. Niente di buono. Come minimo, con la limitazione, e come massimo, dirai addio al lavoratore e otterrai un carico ciclico sui nodi rimanenti dopo che i pod inizieranno a muoversi.
Inoltre, con l'aiuto limitranges All'inizio, puoi impostare i valori delle risorse per il contenitore: minimo, massimo e predefinito:
➜ ~ kubectl describe limitranges --namespace ops
Name: limit-range
Namespace: ops
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu 50m 10 100m 100m 2
Container ephemeral-storage 12Mi 8Gi 128Mi 4Gi -
Container memory 64Mi 40Gi 128Mi 128Mi 2
Non dimenticare di limitare le risorse dello spazio dei nomi in modo che un team non possa assumere tutte le risorse del cluster:
Come si può vedere dalla descrizione resourcequotas, se il team operativo desidera distribuire pod che consumeranno altre 10 CPU, lo scheduler non lo consentirà e genererà un errore:
Per risolvere un problema del genere, puoi scrivere uno strumento, ad esempio, come questo, in grado di memorizzare e impegnare lo stato delle risorse di comando.
2. Scegli l'archiviazione ottimale dei file
Qui vorrei toccare l'argomento dei volumi persistenti e del sottosistema disco dei nodi di lavoro Kubernetes. Spero che nessuno utilizzi il “Cubo” su un HDD in produzione, ma a volte un normale SSD non è più sufficiente. Abbiamo riscontrato un problema per cui i log uccidevano il disco a causa di operazioni di I/O e non esistono molte soluzioni:
Utilizza SSD ad alte prestazioni o passa a NVMe (se gestisci il tuo hardware).
Ridurre il livello di registrazione.
Effettua il bilanciamento “intelligente” dei pod che violentano il disco (podAntiAffinity).
La schermata sopra mostra cosa succede sotto nginx-ingress-controller sul disco quando la registrazione access_logs è abilitata (~12 mila log/sec). Questa condizione, ovviamente, può portare al degrado di tutte le applicazioni su questo nodo.
Per quanto riguarda il fotovoltaico, ahimè, non ho provato tutto tipi Volumi persistenti. Utilizza l'opzione migliore adatta a te. Storicamente, nel nostro Paese è successo che una piccola parte dei servizi richiedesse volumi RWX e molto tempo fa hanno iniziato a utilizzare lo storage NFS per questo compito. Economico e...abbastanza. Certo, lui e io abbiamo mangiato merda, Dio ti benedica, ma abbiamo imparato a ignorarlo e la testa non mi fa più male. E se possibile, passa allo storage di oggetti S3.
3. Raccogli immagini ottimizzate
È meglio utilizzare immagini ottimizzate per il contenitore in modo che Kubernetes possa recuperarle più velocemente ed eseguirle in modo più efficiente.
Ottimizzato significa che le immagini:
contenere una sola applicazione o eseguire una sola funzione;
di piccole dimensioni, perché le immagini di grandi dimensioni vengono trasmesse peggio sulla rete;
disporre di endpoint di integrità e disponibilità che consentano a Kubernetes di intervenire in caso di tempi di inattività;
utilizzare sistemi operativi container-friendly (come Alpine o CoreOS), che sono più resistenti agli errori di configurazione;
utilizzare build a più fasi in modo da poter distribuire solo le applicazioni compilate e non i sorgenti associati.
Esistono molti strumenti e servizi che ti consentono di controllare e ottimizzare le immagini al volo. È importante mantenerli sempre aggiornati e testati per la sicurezza. Di conseguenza ottieni:
Carico di rete ridotto sull'intero cluster.
Riduzione del tempo di avvio del contenitore.
Dimensioni più piccole dell'intero registro Docker.
4. Utilizza la cache DNS
Se parliamo di carichi elevati, la vita è piuttosto pessima senza la messa a punto del sistema DNS del cluster. Una volta gli sviluppatori Kubernetes supportavano la loro soluzione kube-dns. È stato implementato anche qui, ma questo software non è stato particolarmente ottimizzato e non ha prodotto le prestazioni richieste, anche se sembrava essere un compito semplice. Poi è apparso coredns, a cui siamo passati e non abbiamo avuto problemi; in seguito è diventato il servizio DNS predefinito in K8s. Ad un certo punto siamo cresciuti fino a 40mila rps nel sistema DNS e anche questa soluzione è diventata insufficiente. Ma, per fortuna, è uscito Nodelocaldns, ovvero la cache locale del nodo, alias NodeLocal DNSCache.
Perché lo usiamo? C'è un bug nel kernel Linux che, quando più chiamate tramite conntrack NAT su UDP, portano a una condizione di competizione per le voci nelle tabelle conntrack e parte del traffico attraverso NAT viene perso (ogni viaggio attraverso il servizio è NAT). Nodelocaldns risolve questo problema eliminando NAT e aggiornando la connessione a TCP al DNS upstream, nonché memorizzando nella cache locale le query DNS upstream (inclusa una breve cache negativa di 5 secondi).
5. Ridimensiona automaticamente i pod orizzontalmente e verticalmente
Potete affermare con certezza che tutti i vostri microservizi sono pronti per un aumento del carico da due a tre volte? Come allocare correttamente le risorse alle tue applicazioni? Mantenere un paio di pod in esecuzione oltre il carico di lavoro può essere ridondante, ma mantenerli uno dopo l'altro comporta il rischio di tempi di inattività dovuti a un improvviso aumento del traffico verso il servizio. Servizi come Scalabilità automatica del pod orizzontale и Scalatore automatico pod verticale.
VPA ti consente di aumentare automaticamente le richieste/limiti dei tuoi contenitori nel pod in base all'utilizzo effettivo. Come può essere utile? Se disponi di pod che non possono essere ridimensionati orizzontalmente per qualche motivo (che non è del tutto affidabile), puoi provare ad affidare le modifiche alle relative risorse a VPA. La sua caratteristica è un sistema di raccomandazioni basato su dati storici e attuali provenienti dal metric-server, quindi se non vuoi modificare automaticamente richieste/limiti, puoi semplicemente monitorare le risorse consigliate per i tuoi contenitori e ottimizzare le impostazioni per risparmiare CPU e memoria nel cluster.
Immagine tratta da https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231
Lo scheduler in Kubernetes si basa sempre sulle richieste. Qualunque sia il valore inserito, lo scheduler cercherà un nodo adatto in base ad esso. I valori limite servono al cubetto per capire quando rallentare o uccidere il pod. E poiché l'unico parametro importante è il valore delle richieste, VPA funzionerà con esso. Ogni volta che si ridimensiona verticalmente un'applicazione, si definisce quali dovrebbero essere le richieste. Cosa accadrà allora ai limiti? Anche questo parametro verrà scalato proporzionalmente.
Ad esempio, ecco le consuete impostazioni del pod:
Il motore di raccomandazione determina che la tua applicazione richiede 300 milioni di CPU e 500 Mi per funzionare correttamente. Otterrai le seguenti impostazioni:
Come accennato in precedenza, si tratta di un ridimensionamento proporzionale basato sul rapporto richieste/limiti nel manifest:
CPU: 200m → 300m: rapporto 1:1.75;
Memoria: 250Mi → 500Mi: rapporto 1:2.
Per quanto riguarda HPA, allora il meccanismo di funzionamento è più trasparente. I parametri come CPU e memoria hanno una soglia e, se la media di tutte le repliche supera la soglia, l'applicazione viene ridimensionata di +1 sub finché il valore non scende al di sotto della soglia o finché non viene raggiunto il numero massimo di repliche.
Immagine tratta da https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231
Oltre alle solite metriche come CPU e memoria, puoi impostare soglie sulle tue metriche personalizzate da Prometheus e lavorare con esse se ritieni che sia l'indicazione più precisa su quando ridimensionare la tua applicazione. Una volta che l'applicazione si stabilizza al di sotto della soglia del parametro specificato, HPA inizierà a ridimensionare i pod fino al numero minimo di repliche o fino a quando il carico non raggiunge la soglia specificata.
6. Non dimenticare l'affinità dei nodi e l'affinità dei pod
Non tutti i nodi vengono eseguiti sullo stesso hardware e non tutti i pod devono eseguire applicazioni ad alta intensità di calcolo. Kubernetes ti consente di impostare la specializzazione di nodi e pod utilizzando Affinità nodale и Affinità del baccello.
Se disponi di nodi adatti per operazioni ad alta intensità di calcolo, per la massima efficienza è meglio collegare le applicazioni ai nodi corrispondenti. Per fare questo utilizzare nodeSelector con un'etichetta di nodo.
Diciamo che hai due nodi: uno con CPUType=HIGHFREQ e un gran numero di core veloci, un altro con MemoryType=HIGHMEMORY più memoria e prestazioni più veloci. Il modo più semplice è assegnare la distribuzione a un nodo HIGHFREQaggiungendo alla sezione spec questo selettore:
…
nodeSelector:
CPUType: HIGHFREQ
Un modo più costoso e specifico per farlo è utilizzare nodeAffinity nel campo affinity sezione spec. Ci sono due opzioni:
requiredDuringSchedulingIgnoredDuringExecution: impostazione rigida (lo scheduler distribuirà i pod solo su nodi specifici (e in nessun altro posto));
preferredDuringSchedulingIgnoredDuringExecution: impostazione soft (lo scheduler proverà a eseguire la distribuzione su nodi specifici e, se fallisce, proverà a eseguire la distribuzione sul successivo nodo disponibile).
È possibile specificare una sintassi specifica per la gestione delle etichette dei nodi, ad esempio In, NotIn, Exists, DoesNotExist, Gt o Lt. Tuttavia, ricorda che metodi complessi in lunghi elenchi di etichette rallenteranno il processo decisionale in situazioni critiche. In altre parole, mantienilo semplice.
Come accennato in precedenza, Kubernetes ti consente di impostare l'affinità dei pod attuali. Cioè, puoi assicurarti che determinati pod funzionino insieme ad altri pod nella stessa zona di disponibilità (rilevante per i cloud) o nodi.
В podAffinity поля affinity sezione spec sono disponibili gli stessi campi del caso di nodeAffinity: requiredDuringSchedulingIgnoredDuringExecutionи preferredDuringSchedulingIgnoredDuringExecution. L'unica differenza è questa matchExpressions collegherà i pod a un nodo che sta già eseguendo un pod con quell'etichetta.
Kubernetes offre anche un campo podAntiAffinity, che, al contrario, non vincola il pod ad un nodo con pod specifici.
A proposito di espressioni nodeAffinity Si può dare lo stesso consiglio: cercare di mantenere le regole semplici e logiche, non cercare di sovraccaricare le specifiche del pod con un insieme complesso di regole. È molto semplice creare una regola che non corrisponda alle condizioni del cluster, creando un carico non necessario sullo scheduler e riducendo le prestazioni complessive.
7. Inquinamenti e tolleranze
Esiste un altro modo per gestire lo scheduler. Se disponi di un cluster di grandi dimensioni con centinaia di nodi e migliaia di microservizi, è molto difficile non consentire l'hosting di determinati pod su determinati nodi.
Il meccanismo delle contaminazioni – che vietano le regole – aiuta in questo. Ad esempio, in determinati scenari puoi vietare a determinati nodi di eseguire i pod. Per applicare la contaminazione a un nodo specifico è necessario utilizzare l'opzione taint in kubectl. Specificare la chiave e il valore e quindi contaminare come NoSchedule o NoExecute:
Vale anche la pena notare che il meccanismo di contaminazione supporta tre effetti principali: NoSchedule, NoExecute и PreferNoSchedule.
NoSchedulesignifica che per ora non ci sarà alcuna voce corrispondente nelle specifiche del pod tolerations, non potrà essere distribuito sul nodo (in questo esempio node10).
PreferNoSchedule - versione semplificata NoSchedule. In questo caso, lo scheduler proverà a non allocare i pod che non hanno una voce corrispondente tolerations per nodo, ma questa non è una limitazione rigida. Se non sono presenti risorse nel cluster, i pod inizieranno a essere distribuiti su questo nodo.
NoExecute- questo effetto innesca l'evacuazione immediata dei pod che non hanno un'entrata corrispondente tolerations.
È interessante notare che questo comportamento può essere annullato utilizzando il meccanismo delle tolleranze. Questo è conveniente quando c'è un nodo “proibito” e su di esso è necessario posizionare solo i servizi infrastrutturali. Come farlo? Sono consentiti solo i baccelli per i quali esiste una tolleranza adeguata.
Ciò non significa che la prossima ridistribuzione ricadrà su questo particolare nodo, questo non è il meccanismo di affinità del nodo e nodeSelector. Ma combinando diverse funzionalità, puoi ottenere impostazioni di pianificazione molto flessibili.
8. Imposta la priorità di distribuzione dei pod
Solo perché hai pod assegnati ai nodi non significa che tutti i pod debbano essere trattati con la stessa priorità. Ad esempio, potresti voler distribuire alcuni pod prima di altri.
Kubernetes offre diversi modi per configurare la priorità e la prelazione dei pod. L'ambientazione è composta da più parti: oggetto PriorityClasse descrizioni dei campi priorityClassNamenelle specifiche del pod. Diamo un'occhiata ad un esempio:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 99999
globalDefault: false
description: "This priority class should be used for very important pods only"
Noi creiamo PriorityClass, assegnagli un nome, una descrizione e un valore.Più alto value, maggiore è la priorità. Il valore può essere qualsiasi numero intero a 32 bit inferiore o uguale a 1. I valori più alti sono riservati ai pod di sistema mission-critical che generalmente non possono essere anticipati.Lo spostamento avverrà solo se un pod ad alta priorità non ha spazio per girarsi, quindi alcuni dei pod da un determinato nodo verranno evacuati. Se questo meccanismo è troppo rigido per te, puoi aggiungere l'opzione preemptionPolicy: Never, e quindi non ci sarà alcuna prelazione, il pod rimarrà per primo in coda e attenderà che lo scheduler trovi risorse gratuite per esso.
Successivamente, creiamo un pod in cui indichiamo il nome priorityClassName:
apiVersion: v1
kind: Pod
metadata:
name: static-web
labels:
role: myrole
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort: 80
protocol: TCP
priorityClassName: high-priority
Puoi creare tutte le classi di priorità che desideri, anche se è consigliabile non lasciarsi trasportare da questo (ad esempio, limitarsi a priorità bassa, media e alta).
Pertanto, se necessario, è possibile aumentare l’efficienza della distribuzione di servizi critici come nginx-ingress-controller, coredns, ecc.
9. Ottimizzare il cluster ETCD
L'ETCD può essere definito il cervello dell'intero cluster. È molto importante mantenere il funzionamento di questo database ad un livello elevato, poiché la velocità delle operazioni in Cube dipende da questo. Una soluzione abbastanza standard e allo stesso tempo buona sarebbe quella di mantenere il cluster ETCD sui nodi master in modo da avere un ritardo minimo verso kube-apiserver. Se non puoi farlo, posiziona l’ETCD il più vicino possibile, con una buona larghezza di banda tra i partecipanti. Prestare inoltre attenzione a quanti nodi di ETCD possono cadere senza danni al cluster
Tieni presente che aumentare eccessivamente il numero di membri in un cluster può aumentare la tolleranza agli errori a scapito delle prestazioni, tutto dovrebbe essere moderato.
Se parliamo di configurazione del servizio, ci sono alcuni consigli:
Avere un buon hardware, in base alla dimensione del cluster (puoi leggere qui).
Modifica alcuni parametri se hai distribuito un cluster tra una coppia di controller di dominio o se la tua rete e i dischi lasciano molto a desiderare (puoi leggere qui).
conclusione
Questo articolo descrive i punti che il nostro team cerca di rispettare. Questa non è una descrizione dettagliata delle azioni, ma delle opzioni che potrebbero essere utili per ottimizzare il sovraccarico del cluster. È chiaro che ogni cluster è unico a modo suo e le soluzioni di configurazione possono variare notevolmente, quindi sarebbe interessante ricevere il tuo feedback su come monitori il tuo cluster Kubernetes e su come ne migliori le prestazioni. Condividi la tua esperienza nei commenti, sarà interessante saperlo.