![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/bed059552ed86580939aa18fbdf1553e.jpg)
Negli anni di utilizzo di Kubernetes in produzione, abbiamo accumulato molte storie interessanti su come bug in vari componenti di sistema portassero a conseguenze spiacevoli e/o incomprensibili, influenzando il funzionamento dei contenitori e dei pod. In questo articolo, abbiamo raccolto alcune delle più comuni o interessanti di esse. Anche se non ti capiterà mai di trovare situazioni simili, leggere di brevi detective stories di questo tipo — soprattutto 'da una fonte diretta' — è sempre interessante, non credi?..
Storia 1. Supercronic e Docker bloccato
Su uno dei cluster, abbiamo periodicamente riscontrato un Docker 'bloccato', che impediva il normale funzionamento del cluster. Nei log di Docker, era visibile 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
… In questo errore, ci interessa principalmente il messaggio: pthread_create failed: No space left on device. Uno sguardo veloce ha chiarito che Docker non riesce a forkare il processo, motivo per cui si 'bloccava' periodicamente.
La situazione nel monitoraggio è la seguente:
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/bd778052c87b338493bae54b26830ef3.jpg)
Situazioni simili sono osservate anche su altri nodi:
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/ef512532a95ca982e4342071115dbe9f.jpg)
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/43c32ebca78755dde348ed5e7ac75c79.jpg)
Su questi stessi nodi vediamo:
root@kube-node-1 ~ # ps auxfww | grep curl -c
19782
root@kube-node-1 ~ # ps auxfww | grep curl | head
root 16688 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 17398 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 16852 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 9473 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 4664 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 30571 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 24113 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 16475 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 7176 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]
root 1090 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl]Si è scoperto che questo comportamento è il risultato del funzionamento del pod con (uno strumento in Go che utilizziamo per eseguire compiti cron nei pod):
_ docker-containerd-shim 833b60bb9ff4c669bb413b898a5fd142a57a21695e5dc42684235df907825567 /var/run/docker/libcontainerd/833b60bb9ff4c669bb413b898a5fd142a57a21695e5dc42684235df907825567 docker-runc
| _ /usr/local/bin/supercronic -json /crontabs/cron
| _ /usr/bin/newrelic-daemon --agent --pidfile /var/run/newrelic-daemon.pid --logfile /dev/stderr --port /run/newrelic.sock --tls --define utilization.detect_aws=true --define utilization.detect_azure=true --define utilization.detect_gcp=true --define utilization.detect_pcf=true --define utilization.detect_docker=true
| | _ /usr/bin/newrelic-daemon --agent --pidfile /var/run/newrelic-daemon.pid --logfile /dev/stderr --port /run/newrelic.sock --tls --define utilization.detect_aws=true --define utilization.detect_azure=true --define utilization.detect_gcp=true --define utilization.detect_pcf=true --define utilization.detect_docker=true -no-pidfile
| _ [newrelic-daemon]
| _ [curl]
| _ [curl]
| _ [curl]
| _ [curl]
…Il problema è il seguente: quando un'attività viene avviata in supercronic, il processo generato da essa non può terminare correttamente, trasformandosi in .
Nota: A essere 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 vengono ricevuti i segnali SIGHUP o SIGTERM, questi non vengono trasmessi ai processi generati, il che porta i processi figli a non terminare, rimanendo quindi in stato di zombie. Maggiori dettagli possono essere trovati, ad esempio, in .
Ci sono un paio di modi per risolvere il problema:
- Come soluzione temporanea, aumentare il numero di PID nella sistema in un dato 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 fare l'avvio delle attività in supercronic non direttamente, ma utilizzando lo stesso , che è in grado di terminare correttamente i processi e non generare zombie.
Storia 2. "Zombie" durante l'eliminazione di cgroup
Kubelet ha iniziato a consumare un gran numero di CPU:
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/6140058330faaa3785b089dcba857056.jpg)
Questo non piacerà a nessuno, quindi ci siamo attrezzati con e abbiamo iniziato a indagare sul problema. I risultati dell'indagine sono stati i seguenti:
- Kubelet spende più di un terzo del tempo di processamento per estrarre dati sulla memoria da tutti i cgroup:
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20600%20241%22%3E%3C%2Fsvg%3E)
- Nella mailing list degli sviluppatori del kernel può essere trovato . In breve, il punto è che diversi file tmpfs e altre cose simili non vengono completamente eliminati dal sistema quando si elimina un cgroup — rimangono i cosiddetti zombie. Prima o poi verranno rimossi dalla page cache, tuttavia ci sono molte risorse nel server e il kernel non vede il senso di perdere tempo per rimuoverli. Pertanto continuano ad accumularsi. Perché ciò accade? Questo è un server con job cron che crea continuamente nuovi job e con essi, nuovi pod. In questo modo, vengono create nuove cgroup per i contenitori che presto vengono eliminate.
- Perché cAdvisor in kubelet impiega così tanto tempo? È facile vederlo eseguendo semplicemente
time cat /sys/fs/cgroup/memory/memory.stat. Se su una macchina sana l'operazione richiede 0,01 secondi, su cron02 problematica richiede 1,2 secondi. Il problema è che cAdvisor, leggendo lentamente i dati da sysfs, tenta di considerare la memoria utilizzata anche nelle cgroup zombie. - Per forzare la rimozione degli zombie, abbiamo provato a ripulire le cache, come raccomandato in LKML:
sync; echo 3 > /proc/sys/vm/drop_caches, – ma il kernel si è rivelato più complesso e ha bloccato la macchina.
Cosa fare? Il problema viene risolto (, e la descrizione si trova nel ) aggiornando il kernel Linux alla versione 4.16.
Storia 3. Systemd e le sue mount
Ancora una volta, kubelet consuma troppe risorse su alcuni nodi, ma questa volta – già memoria:
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/044c4e23a772c61a6206b9b20aa67c1d.jpg)
Si è scoperto che c'era un problema in systemd, utilizzato in Ubuntu 16.04, che si verifica durante la gestione dei mount creati per collegare subPath da ConfigMap o secret. Dopo il termine del lavoro del pod il servizio systemd e il suo mount residuale rimangono nel sistema. Con il tempo, si accumulano in grandissimo numero. Esistono anche problemi al riguardo:
- ;
- .
… nell'ultimo di questi fanno riferimento a un PR in systemd: (problema in systemd – ).
Il problema non esiste più in Ubuntu 18.04, ma se desideri continuare a utilizzare Ubuntu 16.04, potresti trovare utile il nostro workaround a riguardo.
Quindi, abbiamo creato il seguente DaemonSet:
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
labels:
app: systemd-slices-cleaner
name: systemd-slices-cleaner
namespace: kube-system
spec:
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
app: systemd-slices-cleaner
template:
metadata:
labels:
app: systemd-slices-cleaner
spec:
containers:
- command:
- /usr/local/bin/supercronic
- -json
- /app/crontab
Image: private-registry.org/systemd-slices-cleaner/systemd-slices-cleaner:v0.1.0
imagePullPolicy: Always
name: systemd-slices-cleaner
resources: {}
securityContext:
privileged: true
volumeMounts:
- name: systemd
mountPath: /run/systemd/private
- name: docker
mountPath: /run/docker.sock
- name: systemd-etc
mountPath: /etc/systemd
- name: systemd-run
mountPath: /run/systemd/system/
- name: lsb-release
mountPath: /etc/lsb-release-host
imagePullSecrets:
- name: antiopa-registry
priorityClassName: cluster-low
tolerations:
- operator: Exists
volumes:
- name: systemd
hostPath:
path: /run/systemd/private
- name: docker
hostPath:
path: /run/docker.sock
- name: systemd-etc
hostPath:
path: /etc/systemd
- name: systemd-run
hostPath:
path: /run/systemd/system/
- name: lsb-release
hostPath:
path: /etc/lsb-release… e viene utilizzato uno script del genere:
#!/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 viene eseguito ogni 5 minuti utilizzando il già menzionato supercronic. Il suo Dockerfile appare così:
FROM ubuntu:16.04
COPY rootfs /
WORKDIR /app
RUN apt-get update &&
apt-get upgrade -y &&
apt-get install -y gnupg curl apt-transport-https software-properties-common wget
RUN add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu xenial stable" &&
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - &&
apt-get update &&
apt-get install -y docker-ce=17.03.0*
RUN wget https://github.com/aptible/supercronic/releases/download/v0.1.6/supercronic-linux-amd64 -O
/usr/local/bin/supercronic && chmod +x /usr/local/bin/supercronic
ENTRYPOINT ["/bin/bash", "-c", "/usr/local/bin/supercronic -json /app/crontab"]Storia 4. Concorrenza durante la pianificazione dei pod
È stata notata una cosa: se un pod viene collocato su un nodo e la sua immagine viene scaricata molto lentamente, allora un altro pod che "capita" su quello stesso nodo non inizia a scaricare l'immagine del nuovo pod.Invece, aspetta che l'immagine del pod precedente sia completamente scaricata. Di conseguenza, un pod già programmato la cui immagine potrebbe essere scaricata in un minuto rimarrà a lungo nello stato containerCreating.
Negli eventi ci sarà qualcosa del genere:
Normal Pulling 8m kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal pulling image "registry.example.com/infra/openvpn/openvpn:master"Quindi, risulta che un'unica immagine da un registro lento può bloccare il deploy sul nodo.
Sfortunatamente, ci sono poche soluzioni a questo problema:
- Cerca di utilizzare il tuo Docker Registry direttamente nel cluster o direttamente con il cluster (ad esempio, GitLab Registry, Nexus, ecc.);
- Utilizza strumenti come .
Storia 5. Il blocco dei nodi in caso di mancanza di memoria
Durante l'utilizzo di diverse applicazioni, abbiamo anche riscontrato situazioni in cui un nodo smette completamente di essere accessibile: non risponde a SSH, tutti i demoni di monitoraggio si fermano e nei log non appare nulla (o quasi nulla) di anomalo.
Racconterò con delle immagini l'esempio di un nodo dove funzionava MongoDB.
Ecco come appare atop fino a incidenti:
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/5de916d270a862cbcbb5ed23c31f698e.jpg)
Ed ecco come — la data rientri nel PITR. incidenti:
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/0f32bf1113204cf19f4639a297e40348.jpg)
Nel monitoraggio si osserva anche un brusco picco, durante il quale il nodo smette di essere accessibile:
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/31e770cac5be32bb7f95cfbbc6b9f1ae.jpg)
Pertanto, dalle schermate si vede che:
- La memoria RAM sulla macchina è quasi esaurita;
- Si osserva un brusco incremento del consumo di memoria RAM, dopodiché l'accesso all'intera macchina viene disconnesso bruscamente;
- A Mongo arriva un grande compito, che costringe il processo di DBMS a utilizzare più memoria e a leggere attivamente dal disco.
Si scopre che, se in Linux finisce la memoria libera (si verifica la pressure di memoria) e non c'è swap, allora fino a l'arrivo dell'oom killer può portare a un equilibrio tra il caricamento delle pagine nella cache della pagina e il loro writeback sul disco. A occuparsene è kswapd, che coraggiosamente libera il maggior numero possibile di pagine di memoria per la successiva allocazione.
Sfortunatamente, sotto un elevato carico di I/O insieme a una scarsa disponibilità di memoria libera, kswapd diventa il collo di bottiglia dell'intero sistema, perché su di lui si concentrano tutti le allocazioni (page faults) delle pagine di memoria nel sistema. Questo può durare a lungo, se i processi non vogliono più utilizzare la memoria, ma si fissano sul bordo dell'abisso OOM killer.
Sorprende la domanda: perché l'oom killer arriva così tardi? Nella sua attuale iterazione, l'oom killer è estremamente stupido: ucciderà il processo solo quando fallirà un tentativo di allocazione di una pagina di memoria, cioè se il page fault si verifica con errore. Questo non accade per lungo tempo, perché kswapd coraggiosamente libera le pagine di memoria, resettando la cache della pagina (tutto l'I/O disco nel sistema, di fatto) sul disco. Maggiori dettagli, con una descrizione dei passaggi necessari per risolvere tali problemi nel kernel, possono essere letti .
Questo comportamento con il kernel Linux 4.6+.
Storia 6. I pod rimangono in stato di Pending
In alcuni cluster, dove funzionano davvero molti pod, abbiamo cominciato a notare che la maggior parte di essi resta a lungo "appesa" in stato Pending, sebbene i container Docker siano già in esecuzione sui nodi e possano essere gestiti manualmente.
Tuttavia, in describe non c'è nulla di sbagliato:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 1m default-scheduler Assegnato con successo sphinx-0 a ss-dev-kub07
Normal SuccessfulAttachVolume 1m attachdetach-controller AttachVolume.Attach riuscito per il volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normal SuccessfulMountVolume 1m kubelet, ss-dev-kub07 MountVolume.SetUp riuscito per il volume "sphinx-config"
Normal SuccessfulMountVolume 1m kubelet, ss-dev-kub07 MountVolume.SetUp riuscito per il volume "default-token-fzcsf"
Normal SuccessfulMountVolume 49s (x2 su 51s) kubelet, ss-dev-kub07 MountVolume.SetUp riuscito per il volume "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normal Pulled 43s kubelet, ss-dev-kub07 Immagine del container "registry.example.com/infra/sphinx-exporter/sphinx-indexer:v1" già presente sulla macchina
Normal Created 43s kubelet, ss-dev-kub07 Container creato
Normal Started 43s kubelet, ss-dev-kub07 Container avviato
Normal Pulled 43s kubelet, ss-dev-kub07 Immagine del container "registry.example.com/infra/sphinx/sphinx:v1" già presente sulla macchina
Normal Created 42s kubelet, ss-dev-kub07 Container creato
Normal Started 42s kubelet, ss-dev-kub07 Container avviatoEsaminando la situazione, abbiamo ipotizzato che kubelet semplicemente non riesca a inviare all'API-server tutte le informazioni sullo stato dei pod, sulle probe di liveness/readiness.
Esaminando l'help, abbiamo trovato i seguenti parametri:
--kube-api-qps - QPS da utilizzare mentre si comunica con l'apiserver di kubernetes (default 5)
--kube-api-burst - Burst da utilizzare mentre si comunica con l'apiserver di kubernetes (default 10)
--event-qps - Se > 0, limita le creazioni di eventi al secondo a questo valore. Se 0, illimitato. (default 5)
--event-burst - Dimensione massima di un record di eventi bursty, consente temporaneamente ai record di eventi di superare questo numero, senza superare l'event-qps. Utilizzato solo se --event-qps > 0 (default 10)
--registry-qps - Se > 0, limita il QPS di pull del registro a questo valore.
--registry-burst - Dimensione massima dei pull bursty, consente temporaneamente ai pull di superare questo numero, senza superare il registry-qps. Utilizzato solo se --registry-qps > 0 (default 10)Come si vede, i valori di default sono piuttosto piccoli, e nel 90% dei casi coprono tutte le esigenze… Tuttavia, nel nostro caso si sono rivelati insufficienti. Pertanto abbiamo impostato i seguenti valori:
--event-qps=30 --event-burst=40 --kube-api-burst=40 --kube-api-qps=30 --registry-qps=30 --registry-burst=40… e abbiamo riavviato i kubelet, dopo di che nei grafici delle richieste all'API-server abbiamo visto il seguente scenario:
![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/b2ae099729e55a686f6bec3012b96195.jpg)
… e sì, tutto ha cominciato a volare!
P.S.
Desidero esprimere una grande gratitudine ai numerosi ingegneri della nostra azienda per l'aiuto nella raccolta dei bug e nella preparazione dell'articolo, in particolare al collega del nostro team R&D, Andrey Klimentyev ().
P.P.S.
Leggi anche nel nostro blog:
- «».
- Ciclo di suggerimenti e trucchi per Kubernetes:
- «»;
- «»;
- «»;
- «».
Fonte: habr.com

![6 bug di sistema interessanti durante l'uso di Kubernetes [e le loro soluzioni]](/wp-content/uploads/2019/03/0d15d1de17cd6838fc1cad19615af218.jpg)