ProHoster > blog > amministrazione > 6 divertenti bug di sistema nel funzionamento di Kubernetes [e la loro soluzione]
6 divertenti bug di sistema nel funzionamento di Kubernetes [e la loro soluzione]
Nel corso degli anni di utilizzo di Kubernetes in produzione, abbiamo accumulato molte storie interessanti su come i bug in vari componenti del sistema abbiano portato a conseguenze spiacevoli e/o incomprensibili che hanno influenzato il funzionamento di container e pod. In questo articolo abbiamo fatto una selezione di alcuni tra quelli più comuni o interessanti. Anche se non si ha mai la fortuna di imbattersi in situazioni del genere, leggere storie poliziesche così brevi - soprattutto "in prima persona" - è sempre interessante, non è vero?
Storia 1. Supercronic e Docker impiccati
Su uno dei cluster ricevevamo periodicamente un Docker congelato, che interferiva con il normale funzionamento del cluster. Allo stesso tempo, nei log Docker è stato osservato quanto segue:
level=error msg="containerd: start init process" error="exit status 2: "runtime/cgo: pthread_create failed: No space left on device
SIGABRT: abort
PC=0x7f31b811a428 m=0
goroutine 0 [idle]:
goroutine 1 [running]:
runtime.systemstack_switch() /usr/local/go/src/runtime/asm_amd64.s:252 fp=0xc420026768 sp=0xc420026760
runtime.main() /usr/local/go/src/runtime/proc.go:127 +0x6c fp=0xc4200267c0 sp=0xc420026768
runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2086 +0x1 fp=0xc4200267c8 sp=0xc4200267c0
goroutine 17 [syscall, locked to thread]:
runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2086 +0x1
…
Ciò che ci interessa di più di questo errore è il messaggio: pthread_create failed: No space left on device. Studio veloce documentazione ha spiegato che Docker non poteva creare un fork di un processo, motivo per cui si bloccava periodicamente.
Nel monitoraggio, la seguente immagine corrisponde a ciò che sta accadendo:
Si è scoperto che questo comportamento è una conseguenza del funzionamento del pod supercronico (un'utilità Go che utilizziamo per eseguire lavori cron nei pod):
Il problema è questo: quando un'attività viene eseguita in modalità supercronica, il processo viene generato da essa non può terminare correttamente, trasformandosi in zombie.
Nota: Per essere più precisi, i processi vengono generati dalle attività cron, ma supercronic non è un sistema init e non può "adottare" i processi generati dai suoi figli. Quando i segnali SIGHUP o SIGTERM vengono generati, non vengono trasmessi ai processi figli, con il risultato che i processi figli non terminano e rimangono nello stato zombie. Puoi leggere di più su tutto questo, ad esempio, in un articolo del genere.
Esistono un paio di modi per risolvere i problemi:
Come soluzione temporanea: aumentare il numero di PID nel sistema in un singolo momento:
/proc/sys/kernel/pid_max (since Linux 2.5.34)
This file specifies the value at which PIDs wrap around (i.e., the value in this file is one greater than the maximum PID). PIDs greater than this value are not allo‐
cated; thus, the value in this file also acts as a system-wide limit on the total number of processes and threads. The default value for this file, 32768, results in the
same range of PIDs as on earlier kernels
Oppure avvia attività in supercronico non direttamente, ma utilizzando lo stesso tini, che è in grado di terminare correttamente i processi e di non generare zombie.
Storia 2. "Zombies" durante l'eliminazione di un cgroup
Kubelet ha iniziato a consumare molta CPU:
Questo non piacerà a nessuno, quindi ci siamo armati perf e cominciò ad affrontare il problema. I risultati dell’indagine sono stati i seguenti:
Kubelet impiega più di un terzo del tempo della CPU estraendo dati di memoria da tutti i cgroup:
Nella mailing list degli sviluppatori del kernel puoi trovare discussione del problema. In breve il punto si riduce a questo: vari file tmpfs e altre cose simili non vengono completamente rimossi dal sistema quando si elimina un cgroup, il cosiddetto memcg zombie. Prima o poi verranno eliminati dalla cache delle pagine, ma c'è molta memoria sul server e il kernel non vede il motivo di perdere tempo nell'eliminarli. Ecco perché continuano ad accumularsi. Perché sta succedendo questo? Questo è un server con lavori cron che crea costantemente nuovi lavori e con essi nuovi pod. Pertanto, vengono creati nuovi cgroup per i contenitori al loro interno, che vengono presto eliminati.
Perché cAdvisor in kubelet perde così tanto tempo? Questo è facile da vedere con l'esecuzione più semplice time cat /sys/fs/cgroup/memory/memory.stat. Se su una macchina sana l'operazione richiede 0,01 secondi, su un cron02 problematico richiede 1,2 secondi. Il fatto è che cAdvisor, che legge i dati da sysfs molto lentamente, cerca di tenere conto della memoria utilizzata nei cgroup zombie.
Per rimuovere forzatamente gli zombie, abbiamo provato a svuotare le cache come consigliato in LKML: sync; echo 3 > /proc/sys/vm/drop_caches, - ma il kernel si è rivelato più complicato e ha fatto schiantare l'auto.
Cosa fare? Il problema è stato risolto (commettere, e per una descrizione vedere messaggio di rilascio) aggiornando il kernel Linux alla versione 4.16.
Storia 3. Systemd e il suo supporto
Ancora una volta, il kubelet sta consumando troppe risorse su alcuni nodi, ma questa volta sta consumando troppa memoria:
Si è scoperto che c'è un problema nel systemd utilizzato in Ubuntu 16.04 e si verifica durante la gestione dei montaggi creati per la connessione subPath da ConfigMap o da segreti. Dopo che il pod ha completato il suo lavoro il servizio systemd e il relativo montaggio del servizio rimangono nel sistema. Nel corso del tempo, un numero enorme di essi si accumula. Ci sono anche problemi su questo argomento:
...l'ultimo dei quali si riferisce al PR in systemd: #7811 (problema in systemd - #7798).
Il problema non esiste più in Ubuntu 18.04, ma se vuoi continuare a utilizzare Ubuntu 16.04, potresti trovare utile la nostra soluzione alternativa su questo argomento.
#!/bin/bash
# we will work only on xenial
hostrelease="/etc/lsb-release-host"
test -f ${hostrelease} && grep xenial ${hostrelease} > /dev/null || exit 0
# sleeping max 30 minutes to dispense load on kube-nodes
sleep $((RANDOM % 1800))
stoppedCount=0
# counting actual subpath units in systemd
countBefore=$(systemctl list-units | grep subpath | grep "run-" | wc -l)
# let's go check each unit
for unit in $(systemctl list-units | grep subpath | grep "run-" | awk '{print $1}'); do
# finding description file for unit (to find out docker container, who born this unit)
DropFile=$(systemctl status ${unit} | grep Drop | awk -F': ' '{print $2}')
# reading uuid for docker container from description file
DockerContainerId=$(cat ${DropFile}/50-Description.conf | awk '{print $5}' | cut -d/ -f6)
# checking container status (running or not)
checkFlag=$(docker ps | grep -c ${DockerContainerId})
# if container not running, we will stop unit
if [[ ${checkFlag} -eq 0 ]]; then
echo "Stopping unit ${unit}"
# stoping unit in action
systemctl stop $unit
# just counter for logs
((stoppedCount++))
# logging current progress
echo "Stopped ${stoppedCount} systemd units out of ${countBefore}"
fi
done
... e funziona ogni 5 minuti utilizzando il supercronico menzionato in precedenza. Il suo Dockerfile assomiglia a questo:
Storia 4. Competitività nella pianificazione dei pod
Si è notato che: se abbiamo un pod posizionato su un nodo e la sua immagine viene pompata per molto tempo, allora un altro pod che “colpisce” lo stesso nodo semplicemente non inizia a estrarre l'immagine del nuovo pod. Attende invece finché non viene estratta l'immagine del pod precedente. Di conseguenza, un pod già programmato e la cui immagine avrebbe potuto essere scaricata in appena un minuto finirà nello stato di containerCreating.
Gli eventi saranno simili a questi:
Normal Pulling 8m kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal pulling image "registry.example.com/infra/openvpn/openvpn:master"
Si scopre che una singola immagine da un registro lento può bloccare la distribuzione per nodo.
Sfortunatamente, non ci sono molte vie d'uscita dalla situazione:
Prova ad utilizzare il tuo Docker Registry direttamente nel cluster o direttamente con il cluster (ad esempio GitLab Registry, Nexus, ecc.);
Storia 5. I nodi si bloccano per mancanza di memoria
Durante il funzionamento di varie applicazioni, abbiamo anche riscontrato una situazione in cui un nodo cessa completamente di essere accessibile: SSH non risponde, tutti i demoni di monitoraggio cadono e quindi non c'è nulla (o quasi nulla) di anomalo nei log.
Te lo dirò in immagini usando l'esempio di un nodo in cui ha funzionato MongoDB.
Questo è ciò che appare in cima a incidenti:
E così - dopo incidenti:
Nel monitoraggio si verifica anche un brusco salto in cui il nodo cessa di essere disponibile:
Quindi, dagli screenshot è chiaro che:
La RAM della macchina è quasi esaurita;
Si verifica un forte aumento del consumo di RAM, dopodiché l'accesso all'intera macchina viene improvvisamente disabilitato;
Su Mongo arriva un compito di grandi dimensioni, che costringe il processo DBMS a utilizzare più memoria e a leggere attivamente dal disco.
Si scopre che se Linux esaurisce la memoria libera (si verifica una pressione di memoria) e non c'è scambio, allora a Quando arriva il killer OOM, potrebbe sorgere un atto di bilanciamento tra gettare le pagine nella cache delle pagine e riscriverle su disco. Questo viene fatto da kswapd, che coraggiosamente libera quante più pagine di memoria possibile per la successiva distribuzione.
Sfortunatamente, con un grande carico di I/O abbinato a una piccola quantità di memoria libera, kswapd diventa il collo di bottiglia dell'intero sistema, perché sono legati ad esso tutti allocazioni (errori di pagina) delle pagine di memoria nel sistema. Questo può durare molto a lungo se i processi non vogliono più utilizzare la memoria, ma sono fissati sull'orlo dell'abisso killer OOM.
La domanda naturale è: perché il killer dell’OOM arriva così tardi? Nella sua attuale iterazione, l'OOM killer è estremamente stupido: ucciderà il processo solo quando il tentativo di allocare una pagina di memoria fallisce, cioè se l'errore di pagina fallisce. Ciò non accade per molto tempo, perché kswapd libera coraggiosamente le pagine di memoria, scaricando la cache delle pagine (l'intero I/O del disco nel sistema, in effetti) sul disco. Più in dettaglio, con una descrizione dei passaggi necessari per eliminare tali problemi nel kernel, puoi leggere qui.
Storia 6. I pod rimangono bloccati nello stato In sospeso
In alcuni cluster, in cui i pod operativi sono davvero tanti, abbiamo cominciato a notare che la maggior parte di essi “si blocca” per molto tempo nello stato Pending, sebbene i contenitori Docker stessi siano già in esecuzione sui nodi e possano essere utilizzati manualmente.
Con questo in describe non c'è niente di sbagliato:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 1m default-scheduler Successfully assigned sphinx-0 to ss-dev-kub07
Normal SuccessfulAttachVolume 1m attachdetach-controller AttachVolume.Attach succeeded for volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normal SuccessfulMountVolume 1m kubelet, ss-dev-kub07 MountVolume.SetUp succeeded for volume "sphinx-config"
Normal SuccessfulMountVolume 1m kubelet, ss-dev-kub07 MountVolume.SetUp succeeded for volume "default-token-fzcsf"
Normal SuccessfulMountVolume 49s (x2 over 51s) kubelet, ss-dev-kub07 MountVolume.SetUp succeeded for volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normal Pulled 43s kubelet, ss-dev-kub07 Container image "registry.example.com/infra/sphinx-exporter/sphinx-indexer:v1" already present on machine
Normal Created 43s kubelet, ss-dev-kub07 Created container
Normal Started 43s kubelet, ss-dev-kub07 Started container
Normal Pulled 43s kubelet, ss-dev-kub07 Container image "registry.example.com/infra/sphinx/sphinx:v1" already present on machine
Normal Created 42s kubelet, ss-dev-kub07 Created container
Normal Started 42s kubelet, ss-dev-kub07 Started container
Dopo alcune ricerche, abbiamo ipotizzato che kubelet semplicemente non abbia il tempo di inviare tutte le informazioni sullo stato dei pod e sui test di attività/prontezza al server API.
E dopo aver studiato l'aiuto, abbiamo trovato i seguenti parametri:
--kube-api-qps - QPS to use while talking with kubernetes apiserver (default 5)
--kube-api-burst - Burst to use while talking with kubernetes apiserver (default 10)
--event-qps - If > 0, limit event creations per second to this value. If 0, unlimited. (default 5)
--event-burst - Maximum size of a bursty event records, temporarily allows event records to burst to this number, while still not exceeding event-qps. Only used if --event-qps > 0 (default 10)
--registry-qps - If > 0, limit registry pull QPS to this value.
--registry-burst - Maximum size of bursty pulls, temporarily allows pulls to burst to this number, while still not exceeding registry-qps. Only used if --registry-qps > 0 (default 10)
Come puoi vedere i valori predefiniti sono piuttosto piccoli, e nel 90% coprono tutte le esigenze... Ma nel nostro caso questo non è bastato. Pertanto impostiamo i seguenti valori:
... e riavviato i kubelet, dopodiché abbiamo visto la seguente immagine nei grafici delle chiamate al server API:
...e sì, tutto ha cominciato a volare!
PS
Per il loro aiuto nella raccolta dei bug e nella preparazione di questo articolo, esprimo la mia profonda gratitudine ai numerosi ingegneri della nostra azienda, e in particolare al mio collega del nostro team di ricerca e sviluppo Andrey Klimentyev (zuzzas).