ProHoster > Blog > Uprava > 6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]
6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]
V letih uporabe Kubernetesa v produkciji smo nabrali veliko zanimivih zgodb o tem, kako so hrošči v različnih komponentah sistema vodili do neprijetnih in/ali nerazumljivih posledic, ki vplivajo na delovanje vsebnikov in podov. V tem članku smo izbrali nekaj najbolj pogostih ali zanimivih. Tudi če nikoli nimate sreče, da se srečate s takšnimi situacijami, je branje takšnih kratkih detektivskih zgodb – še posebej iz »prve roke« – vedno zanimivo, kajne?..
Zgodba 1. Supercronic in Docker visi
Na eni od gruč smo občasno prejemali zamrznjen Docker, ki je motil normalno delovanje gruče. Istočasno je bilo v dnevnikih Docker opaženo naslednje:
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
…
Pri tej napaki nas najbolj zanima sporočilo: pthread_create failed: No space left on device. Hitra študija dokumentacijo pojasnil, da Docker ni mogel razcepiti procesa, zato je občasno zamrznil.
Pri spremljanju dogajanju ustreza naslednja slika:
Težava je naslednja: ko se naloga izvaja v supercronicu, je proces, ki ga je sprožila ne more pravilno zaključiti, spreminjanje v zombi.
Obvestilo: Če smo natančnejši, procese sprožijo opravila cron, vendar supercronic ni init sistem in ne more »posvojiti« procesov, ki so jih ustvarili njegovi otroci. Ko se sprožijo signali SIGHUP ali SIGTERM, se ne prenesejo na podrejene procese, zaradi česar se podrejeni procesi ne zaključijo in ostanejo v statusu zombija. Več o vsem tem lahko preberete na primer v tak članek.
Obstaja nekaj načinov za reševanje težav:
Kot začasna rešitev - povečajte število PID-jev v sistemu naenkrat:
/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
Ali pa zaženite naloge v supercronicu ne neposredno, ampak z istim tini, ki lahko pravilno prekine procese in ne ustvari zombijev.
Zgodba 2. "Zombiji" pri brisanju cgroup
Kubelet je začel porabljati veliko procesorja:
To nikomur ne bo všeč, zato smo se oborožili perf in se začel ukvarjati s težavo. Rezultati preiskave so bili naslednji:
Kubelet porabi več kot tretjino svojega procesorskega časa za pridobivanje podatkov iz pomnilnika iz vseh skupin c:
Na poštnem seznamu razvijalcev jedra lahko najdete razprava o problemu. Skratka, poanta se spusti na tole: razne datoteke tmpfs in druge podobne zadeve niso popolnoma odstranjene iz sistema pri brisanju cgroup, t.i memcg zombi. Prej ali slej bodo izbrisani iz predpomnilnika strani, vendar je na strežniku veliko pomnilnika in jedro ne vidi smisla izgubljati časa z njihovim brisanjem. Zato se kar kopičijo. Zakaj se to sploh dogaja? To je strežnik s cron opravili, ki nenehno ustvarja nova delovna mesta in z njimi nove pode. Tako se ustvarijo nove cgroup za vsebnike v njih, ki se kmalu izbrišejo.
Zakaj cAdvisor v kubeletu izgublja toliko časa? To je enostavno videti z najpreprostejšo izvedbo time cat /sys/fs/cgroup/memory/memory.stat. Če na zdravem stroju operacija traja 0,01 sekunde, potem na problematičnem cron02 traja 1,2 sekunde. Stvar je v tem, da cAdvisor, ki zelo počasi bere podatke iz sysfs, poskuša upoštevati pomnilnik, ki se uporablja v zombi cgroups.
Za prisilno odstranitev zombijev smo poskusili počistiti predpomnilnike, kot je priporočeno v LKML: sync; echo 3 > /proc/sys/vm/drop_caches, - toda jedro se je izkazalo za bolj zapleteno in je sesulo avto.
Kaj storiti? Težava se odpravlja (zavezati, za opis pa glej sprosti sporočilo) posodobitev jedra Linuxa na različico 4.16.
Zgodovina 3. Systemd in njegova namestitev
Spet kubelet porablja preveč virov na nekaterih vozliščih, toda tokrat porablja preveč pomnilnika:
Izkazalo se je, da obstaja težava v systemd, ki se uporablja v Ubuntu 16.04, in se pojavi pri upravljanju namestitev, ki so ustvarjene za povezavo subPath iz ConfigMap ali Secret's. Ko je pod končal svoje delo storitev systemd in njen priklop storitve ostaneta v sistemu. Sčasoma se jih nabere ogromno. Obstajajo celo težave na to temo:
#!/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
... in teče vsakih 5 minut z uporabo prej omenjenega supercronica. Njegova datoteka Dockerfile je videti takole:
Ugotovljeno je bilo, da: če imamo enoto postavljeno na vozlišče in njeno sliko črpamo zelo dolgo, bo druga enota, ki je "zadela" isto vozlišče, preprosto ne začne vleči slike novega pod. Namesto tega počaka, da se potegne slika prejšnjega sklopa. Posledično bo pod, ki je bil že načrtovan in katerega sliko bi lahko prenesli v samo minuti, končal v statusu containerCreating.
Dogodki bodo izgledali nekako takole:
Normal Pulling 8m kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal pulling image "registry.example.com/infra/openvpn/openvpn:master"
Izkazalo se je, da ena sama slika iz počasnega registra lahko blokira uvajanje na vozlišče.
Na žalost ni veliko izhodov iz situacije:
Poskusite uporabiti svoj register Docker neposredno v gruči ali neposredno z gručo (na primer register GitLab, Nexus itd.);
Zgodba 5. Vozlišča visijo zaradi pomanjkanja pomnilnika
Med delovanjem različnih aplikacij smo naleteli tudi na situacijo, ko vozlišče popolnoma preneha biti dostopno: SSH se ne odziva, vsi nadzorni demoni odpadejo, nato pa v dnevnikih ni nič (ali skoraj nič) nepravilnega.
Povedal vam bom v slikah na primeru enega vozlišča, kjer je deloval MongoDB.
Tako izgleda vrh za nesreče:
In tako - po nesreče:
Pri spremljanju je tudi oster skok, pri katerem vozlišče preneha biti na voljo:
Tako je iz posnetkov zaslona jasno, da:
RAM na stroju je blizu konca;
Pride do močnega skoka porabe RAM-a, po katerem je dostop do celotnega stroja nenadoma onemogočen;
Na Mongo pride velika naloga, ki prisili proces DBMS, da uporabi več pomnilnika in aktivno bere z diska.
Izkazalo se je, da če Linuxu zmanjka prostega pomnilnika (nastane pritisk na pomnilnik) in ni zamenjave, za Ko pride morilec OOM, lahko pride do ravnovesja med metanjem strani v predpomnilnik strani in zapisovanjem nazaj na disk. To naredi kswapd, ki pogumno sprosti čim več pomnilniških strani za kasnejšo distribucijo.
Na žalost z veliko V/I obremenitvijo skupaj z majhno količino prostega pomnilnika, kswapd postane ozko grlo celotnega sistema, saj so na to vezani Vsi dodelitve (napake strani) pomnilniških strani v sistemu. To lahko traja zelo dolgo, če procesi ne želijo več uporabljati pomnilnika, ampak so pritrjeni na sam rob prepada OOM-killer.
Naravno vprašanje je: zakaj OOM morilec pride tako pozno? V svoji trenutni ponovitvi je ubijalec OOM izjemno neumen: uničil bo proces šele, ko poskus dodelitve pomnilniške strani ne uspe, tj. če napaka strani ne uspe. To se ne zgodi prav dolgo, ker kswapd pogumno osvobodi pomnilniške strani in vrže predpomnilnik strani (pravzaprav celoten V/I disk v sistemu) nazaj na disk. Podrobneje z opisom korakov, potrebnih za odpravo takšnih težav v jedru, lahko preberete tukaj.
V nekaterih grozdih, v katerih deluje res veliko podov, smo začeli opažati, da jih večina zelo dolgo »visi« v stanju Pending, čeprav se sami vsebniki Docker že izvajajo na vozliščih in je z njimi mogoče delati ročno.
Poleg tega v describe nič ni narobe:
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
Po nekaj kopanju smo domnevali, da kubelet preprosto nima časa za pošiljanje vseh informacij o stanju podov in testih živahnosti/pripravljenosti strežniku API.
In po študiju pomoči smo našli naslednje 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)
Kot je razvidno, privzete vrednosti so precej majhne, in v 90% pokrivajo vse potrebe... Vendar v našem primeru to ni bilo dovolj. Zato smo postavili naslednje vrednosti:
... in znova zagnali kubelets, nakar smo v grafih klicev na API strežnik videli naslednjo sliko:
... in ja, vse je začelo leteti!
PS
Za njihovo pomoč pri zbiranju hroščev in pripravi tega članka se globoko zahvaljujem številnim inženirjem našega podjetja, še posebej pa svojemu kolegu iz naše ekipe za raziskave in razvoj Andreju Klimentjevu (zuzze).