6 underholdende systemfejl i driften af Kubernetes [og deres løsning]
Gennem årene med brug af Kubernetes i produktionen har vi samlet mange interessante historier om, hvordan fejl i forskellige systemkomponenter førte til ubehagelige og/eller uforståelige konsekvenser, der påvirker driften af containere og pods. I denne artikel har vi lavet et udvalg af nogle af de mest almindelige eller interessante. Selvom du aldrig er så heldig at støde på sådanne situationer, er det altid interessant at læse om sådanne korte detektivhistorier - især "førstehånds" - ikke?
Historie 1. Supercronic og Docker hængende
På en af klyngerne modtog vi med jævne mellemrum en frossen Docker, som forstyrrede den normale funktion af klyngen. På samme tid blev følgende observeret i Docker-loggene:
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
…
Det, der interesserer os mest ved denne fejl, er meddelelsen: pthread_create failed: No space left on device. Hurtig undersøgelse dokumentation forklarede, at Docker ikke kunne forgrene en proces, hvorfor den med jævne mellemrum frøs.
Ved overvågning svarer følgende billede til, hvad der sker:
Problemet er dette: når en opgave køres i supercronic, er processen affødt af den kan ikke afsluttes korrekt, bliver til zombie.
Bemærk: For at være mere præcis er processer affødt af cron-opgaver, men supercronic er ikke et init-system og kan ikke "adoptere" processer, som dets børn affødte. Når SIGHUP- eller SIGTERM-signaler hæves, videregives de ikke til underordnede processer, hvilket resulterer i, at underordnede processer ikke afsluttes og forbliver i zombiestatus. Alt dette kan du læse mere om, for eksempel i sådan en artikel.
Der er et par måder at løse problemer på:
Som en midlertidig løsning - øg antallet af PID'er i systemet på et enkelt tidspunkt:
/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
Eller start opgaver i supercronic ikke direkte, men ved hjælp af det samme kar, som er i stand til at afslutte processer korrekt og ikke afføde zombier.
Historie 2. "Zombies" når du sletter en cgroup
Kubelet begyndte at forbruge en masse CPU:
Ingen vil kunne lide dette, så vi bevæbnede os perf og begyndte at håndtere problemet. Resultaterne af undersøgelsen var som følger:
Kubelet bruger mere end en tredjedel af sin CPU-tid på at trække hukommelsesdata fra alle cgroups:
I kerneudviklernes mailingliste kan du finde diskussion af problemet. Kort sagt kommer pointen ned til dette: forskellige tmpfs-filer og andre lignende ting fjernes ikke fuldstændigt fra systemet ved sletning af en cgroup, den såkaldte memcg Zombie. Før eller siden vil de blive slettet fra sidecachen, men der er meget hukommelse på serveren, og kernen ser ikke meningen med at spilde tid på at slette dem. Det er derfor, de bliver ved med at hobe sig op. Hvorfor sker det overhovedet? Dette er en server med cron-jobs, der konstant skaber nye jobs, og med dem nye pods. Der oprettes således nye cgroups til containere i dem, som snart slettes.
Hvorfor spilder cAdvisor i kubelet så meget tid? Dette er let at se med den enkleste udførelse time cat /sys/fs/cgroup/memory/memory.stat. Hvis operationen på en sund maskine tager 0,01 sekunder, så tager den på den problematiske cron02 1,2 sekunder. Sagen er, at cAdvisor, som læser data fra sysfs meget langsomt, forsøger at tage højde for den hukommelse, der bruges i zombie cgroups.
For at fjerne zombier med magt prøvede vi at rydde caches som anbefalet i LKML: sync; echo 3 > /proc/sys/vm/drop_caches, - men kernen viste sig at være mere kompliceret og styrtede bilen.
Hvad skal man gøre? Problemet er ved at blive rettet (begå, og for en beskrivelse se frigive besked) opdatering af Linux-kernen til version 4.16.
Historie 3. Systemd og dets montering
Igen bruger kubelet for mange ressourcer på nogle noder, men denne gang bruger den for meget hukommelse:
Det viste sig, at der er et problem i systemd brugt i Ubuntu 16.04, og det opstår ved håndtering af monteringer, der er oprettet til forbindelse subPath fra ConfigMap's eller secret's. Efter at poden har afsluttet sit arbejde systemd-tjenesten og dens servicemontering forbliver i system. Over tid akkumuleres et stort antal af dem. Der er endda problemer om dette emne:
...den sidste refererer til PR i systemd: #7811 (problem i systemd - #7798).
Problemet eksisterer ikke længere i Ubuntu 18.04, men hvis du vil fortsætte med at bruge Ubuntu 16.04, kan du finde vores løsning på dette emne nyttig.
#!/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
... og den kører hvert 5. minut ved hjælp af den tidligere nævnte supercronic. Dens Dockerfile ser sådan ud:
Historie 4. Konkurrenceevne ved planlægning af pods
Det blev bemærket, at: hvis vi har en pod placeret på en node, og dens billede pumpes ud i meget lang tid, så vil en anden pod, der "hitter" den samme node simpelthen begynder ikke at trække billedet af den nye pod. I stedet venter den, indtil billedet af den forrige pod er trukket. Som et resultat vil en pod, der allerede var planlagt, og hvis billede kunne være blevet downloadet på blot et minut, ende i status som containerCreating.
Begivenhederne kommer til at se sådan ud:
Normal Pulling 8m kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal pulling image "registry.example.com/infra/openvpn/openvpn:master"
Det viser sig, at et enkelt billede fra en langsom registreringsdatabasen kan blokere implementeringen pr. node.
Desværre er der ikke mange veje ud af situationen:
Prøv at bruge din Docker Registry direkte i klyngen eller direkte med klyngen (for eksempel GitLab Registry, Nexus osv.);
Historie 5. Noder hænger på grund af manglende hukommelse
Under driften af forskellige applikationer stødte vi også på en situation, hvor en node helt ophører med at være tilgængelig: SSH reagerer ikke, alle overvågningsdæmoner falder af, og så er der intet (eller næsten intet) unormalt i logfilerne.
Jeg fortæller dig i billeder ved at bruge eksemplet på en knude, hvor MongoDB fungerede.
Sådan ser det ud på toppen til ulykker:
Og sådan her - efter ulykker:
I overvågning er der også et skarpt spring, hvor noden ophører med at være tilgængelig:
Fra skærmbillederne er det således klart, at:
RAM på maskinen er tæt på enden;
Der er et kraftigt spring i RAM-forbruget, hvorefter adgangen til hele maskinen brat deaktiveres;
En stor opgave ankommer til Mongo, som tvinger DBMS-processen til at bruge mere hukommelse og aktivt læse fra disk.
Det viser sig, at hvis Linux løber tør for ledig hukommelse (hukommelsestrykket sætter ind), og der ikke er nogen swap, så til Når OOM-morderen ankommer, kan der opstå en balancegang mellem at smide sider ind i sidecachen og skrive dem tilbage til disken. Dette gøres af kswapd, som modigt frigør så mange hukommelsessider som muligt til efterfølgende distribution.
Desværre, med en stor I/O-belastning kombineret med en lille mængde ledig hukommelse, kswapd bliver flaskehalsen i hele systemet, fordi de er bundet til det alle allokeringer (sidefejl) af hukommelsessider i systemet. Dette kan fortsætte i meget lang tid, hvis processerne ikke ønsker at bruge hukommelsen længere, men er fastgjort helt på kanten af OOM-dræberens afgrund.
Det naturlige spørgsmål er: hvorfor kommer OOM-morderen så sent? I sin nuværende iteration er OOM-morderen ekstremt dum: den dræber kun processen, når forsøget på at tildele en hukommelsesside mislykkes, dvs. hvis sidefejlen fejler. Dette sker ikke ret længe, fordi kswapd modigt frigør hukommelsessider og dumper sidecachen (faktisk hele disk I/O i systemet) tilbage til disken. Mere detaljeret, med en beskrivelse af de nødvendige trin for at eliminere sådanne problemer i kernen, kan du læse her.
Historie 6. Pods sidder fast i afventende tilstand
I nogle klynger, hvor der er rigtig mange bælg, der opererer, begyndte vi at bemærke, at de fleste af dem "hænger" i meget lang tid i staten Pending, selvom selve Docker-beholderne allerede kører på noderne og kan arbejdes med manuelt.
På samme tid, i describe der er intet galt:
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
Efter lidt gravning antog vi, at kubelet simpelthen ikke har tid til at sende alle oplysninger om pods tilstand og liveness/readiness-test til API-serveren.
Og efter at have studeret hjælp fandt vi følgende parametre:
--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)
Som set, standardværdierne er ret små, og i 90% dækker de alle behov... Men i vores tilfælde var dette ikke nok. Derfor sætter vi følgende værdier:
... og genstartede kubelets, hvorefter vi så følgende billede i graferne af kald til API-serveren:
... og ja, alt begyndte at flyve!
PS
For deres hjælp til at indsamle fejl og forberede denne artikel, udtrykker jeg min dybe taknemmelighed til de mange ingeniører i vores virksomhed, og især til min kollega fra vores R&D-team Andrey Klimentyev (zuzzas).