Nove suggerimenti sulle prestazioni di Kubernetes

Nove suggerimenti sulle prestazioni di Kubernetes

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

Nove suggerimenti sulle prestazioni di Kubernetes

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.

resources:
   requests:
     memory: 2Gi
     cpu: 250m
   limits:
     memory: 4Gi
     cpu: 500m

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:

➜  ~ kubectl describe resourcequotas --namespace ops
Name:                   resource-quota
Namespace:              ops
Resource                Used          Hard
--------                ----          ----
limits.cpu              77250m        80
limits.memory           124814367488  150Gi
pods                    31            45
requests.cpu            53850m        80
requests.memory         75613234944   150Gi
services                26            50
services.loadbalancers  0             0
services.nodeports      0             0

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:

Error creating: pods "nginx-proxy-9967d8d78-nh4fs" is forbidden: exceeded quota: resource-quota, requested: limits.cpu=5,requests.cpu=5, used: limits.cpu=77250m,requests.cpu=53850m, limited: limits.cpu=10,requests.cpu=10

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

Nove suggerimenti sulle prestazioni di Kubernetes

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

Nove suggerimenti sulle prestazioni di Kubernetes

È 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:

  1. Carico di rete ridotto sull'intero cluster.

  2. Riduzione del tempo di avvio del contenitore.

  3. Dimensioni più piccole dell'intero registro Docker.

4. Utilizza la cache DNS

Nove suggerimenti sulle prestazioni di Kubernetes

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

Nove suggerimenti sulle prestazioni di Kubernetes

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.

Nove suggerimenti sulle prestazioni di KubernetesImmagine 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:

resources:
   requests:
     memory: 250Mi
     cpu: 200m
   limits:
     memory: 500Mi
     cpu: 350m

Il motore di raccomandazione determina che la tua applicazione richiede 300 milioni di CPU e 500 Mi per funzionare correttamente. Otterrai le seguenti impostazioni:

resources:
   requests:
     memory: 500Mi
     cpu: 300m
   limits:
     memory: 1000Mi
     cpu: 525m

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.

Nove suggerimenti sulle prestazioni di KubernetesImmagine 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

Nove suggerimenti sulle prestazioni di Kubernetes

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:

$ kubectl taint nodes node10 node-role.kubernetes.io/ingress=true:NoSchedule

Vale anche la pena notare che il meccanismo di contaminazione supporta tre effetti principali: NoSchedule, NoExecute и PreferNoSchedule.

  • NoSchedule significa 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.

Ecco come apparirebbero le specifiche del pod:

spec:
   tolerations:
     - key: "node-role.kubernetes.io/ingress"
        operator: "Equal"
        value: "true"
        effect: "NoSchedule"

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 PriorityClass e descrizioni dei campi priorityClassName nelle 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

Nove suggerimenti sulle prestazioni di Kubernetes

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

Nove suggerimenti sulle prestazioni di Kubernetes

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:

  1. Avere un buon hardware, in base alla dimensione del cluster (puoi leggere qui).

  2. 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.

Fonte: habr.com