6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]
Za roky používania Kubernetes vo výrobe sme nazhromaždili veľa zaujímavých príbehov o tom, ako chyby v rôznych systémových komponentoch viedli k nepríjemným a/alebo nepochopiteľným následkom ovplyvňujúcim fungovanie kontajnerov a podov. V tomto článku sme urobili výber tých najbežnejších alebo najzaujímavejších. Aj keď nikdy nemáte to šťastie stretnúť sa s takýmito situáciami, čítanie o takýchto krátkych detektívkach – najmä „z prvej ruky“ – je vždy zaujímavé, však?...
Príbeh 1. Supercronic a Docker zavesenie
Na jednom z klastrov sme pravidelne dostávali zamrznutý Docker, ktorý zasahoval do normálneho fungovania klastra. Zároveň sa v protokoloch Docker pozorovalo nasledovné:
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
…
Čo nás na tejto chybe najviac zaujíma, je správa: pthread_create failed: No space left on device. Rýchla štúdia dokumentáciu vysvetlil, že Docker nemôže rozdeliť proces, a preto pravidelne zamrzol.
Pri monitorovaní zodpovedá tomu, čo sa deje, nasledujúci obrázok:
Problém je tento: keď je úloha spustená v superkronike, proces splodil to nemôže správne ukončiť, mení sa na zombie.
Poznámka: Presnejšie povedané, procesy sú vytvárané úlohami cron, ale supercronic nie je init systém a nemôže „prijať“ procesy, ktoré splodili jeho deti. Keď sa aktivujú signály SIGHUP alebo SIGTERM, neprenesú sa do podriadených procesov, čo vedie k tomu, že podriadené procesy sa neukončia a zostanú v stave zombie. Viac sa o tom všetkom dočítate napríklad v takýto článok.
Existuje niekoľko spôsobov, ako vyriešiť problémy:
Ako dočasné riešenie - zvýšte počet PID v systéme v jednom okamihu:
/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
Alebo spúšťajte úlohy v superkronike nie priamo, ale pomocou toho istého tini, ktorý je schopný správne ukončiť procesy a nesplodiť zombíkov.
Príbeh 2. „Zombies“ pri odstraňovaní cgroup
Kubelet začal spotrebovávať veľa CPU:
Toto sa nebude páčiť nikomu, tak sme sa vyzbrojili výkon a začal riešiť problém. Výsledky vyšetrovania boli nasledovné:
Kubelet trávi viac ako tretinu svojho procesorového času sťahovaním pamäťových dát zo všetkých cgroups:
V adresári vývojárov jadra nájdete diskusiu o probléme. V skratke ide o toto: rôzne súbory tmpfs a iné podobné veci nie sú úplne odstránené zo systému pri odstraňovaní cgroup, tzv memcg zombie. Skôr či neskôr budú vymazané z vyrovnávacej pamäte stránok, ale na serveri je veľa pamäte a jadro nevidí dôvod strácať čas ich odstraňovaním. Preto sa stále hromadia. Prečo sa to vôbec deje? Toto je server s úlohami cron, ktorý neustále vytvára nové pracovné miesta a s nimi aj nové moduly. Pre kontajnery v nich sa teda vytvárajú nové cgroups, ktoré sa čoskoro vymažú.
Prečo cAdvisor v kubelet stráca toľko času? To je ľahko vidieť pri najjednoduchšom prevedení time cat /sys/fs/cgroup/memory/memory.stat. Ak na zdravom stroji trvá operácia 0,01 sekundy, tak na problematickom cron02 to trvá 1,2 sekundy. Ide o to, že cAdvisor, ktorý číta dáta zo sysfs veľmi pomaly, sa snaží brať do úvahy pamäť používanú v zombie cgroups.
Aby sme násilne odstránili zombie, pokúsili sme sa vyčistiť vyrovnávaciu pamäť, ako sa odporúča v LKML: sync; echo 3 > /proc/sys/vm/drop_caches, - ale jadro sa ukázalo byť komplikovanejšie a havarovalo auto.
Čo robiť? Problém sa rieši (zaviazať sa, a popis viď uvoľniť správu) aktualizácia linuxového jadra na verziu 4.16.
História 3. Systemd a jeho pripojenie
Opäť platí, že kubelet spotrebúva príliš veľa zdrojov na niektorých uzloch, ale tentoraz spotrebúva príliš veľa pamäte:
Ukázalo sa, že existuje problém v systemd používanom v Ubuntu 16.04 a vyskytuje sa pri správe pripojení, ktoré sú vytvorené na pripojenie subPath z ConfigMap alebo tajných. Keď modul dokončí svoju prácu služba systemd a jej pripojenie služby zostávajú v systéme. Postupom času sa ich nahromadí obrovské množstvo. Na túto tému sú dokonca problémy:
#!/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
... a beží každých 5 minút pomocou vyššie uvedeného supercronic. Jeho Dockerfile vyzerá takto:
Všimli sme si, že: ak máme modul umiestnený na uzle a jeho obraz je veľmi dlhý čas čerpaný, potom ďalší modul, ktorý „zasiahne“ rovnaký uzol, jednoducho nezačne ťahať obraz nového modulu. Namiesto toho čaká, kým sa nevytiahne obrázok predchádzajúceho modulu. Výsledkom je, že modul, ktorý už bol naplánovaný a ktorého obrázok bolo možné stiahnuť len za minútu, skončí v stave containerCreating.
Udalosti budú vyzerať asi takto:
Normal Pulling 8m kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal pulling image "registry.example.com/infra/openvpn/openvpn:master"
Ukazuje sa, že jeden obrázok z pomalého registra môže blokovať nasadenie na uzol.
Bohužiaľ, nie je veľa spôsobov, ako sa zo situácie dostať:
Skúste použiť svoj Docker Registry priamo v klastri alebo priamo s klastrom (napríklad register GitLab, Nexus atď.);
Počas prevádzky rôznych aplikácií sme sa stretli aj so situáciou, kedy uzol úplne prestane byť prístupný: SSH nereaguje, padnú všetky monitorovacie démony a v protokoloch potom nie je nič (alebo takmer nič) anomálne.
Poviem vám to na obrázkoch na príklade jedného uzla, kde fungoval MongoDB.
Takto vyzerá vrch na nehody:
A takto - po nehody:
Pri monitorovaní dochádza aj k prudkému skoku, pri ktorom uzol prestáva byť dostupný:
Zo snímok obrazovky je teda zrejmé, že:
RAM na stroji je blízko konca;
Došlo k prudkému skoku v spotrebe pamäte RAM, po ktorom je prístup k celému stroju náhle zakázaný;
Na Mongo prichádza veľká úloha, ktorá núti proces DBMS využívať viac pamäte a aktívne čítať z disku.
Ukazuje sa, že ak Linuxu dôjde voľná pamäť (nastaví sa tlak na pamäť) a nedôjde k výmene, potom na Keď príde zabijak OOM, môže nastať rovnováha medzi vyhodením stránok do vyrovnávacej pamäte stránok a ich zapísaním späť na disk. Robí to kswapd, ktorý odvážne uvoľní čo najviac pamäťových stránok pre následnú distribúciu.
Bohužiaľ, s veľkým zaťažením I/O spojeným s malým množstvom voľnej pamäte, kswapd sa stáva prekážkou celého systému, pretože sú na to viazaní všetko alokácie (chyby stránok) pamäťových stránok v systéme. Toto môže pokračovať veľmi dlho, ak procesy už nechcú používať pamäť, ale sú fixované na samom okraji priepasti OOM-killer.
Prirodzená otázka znie: prečo OOM zabijak prichádza tak neskoro? Vo svojej súčasnej iterácii je zabíjač OOM extrémne hlúpy: proces zabije len vtedy, keď zlyhá pokus o pridelenie pamäťovej stránky, t.j. ak chyba stránky zlyhá. To sa nestane pomerne dlho, pretože kswapd statočne uvoľňuje pamäťové stránky a ukladá vyrovnávaciu pamäť stránok (v skutočnosti celý diskový I/O systém) späť na disk. Podrobnejšie s popisom krokov potrebných na odstránenie takýchto problémov v jadre si môžete prečítať tu.
Príbeh 6. Moduly sa zaseknú v stave Čaká sa na vyriešenie
V niektorých zhlukoch, v ktorých funguje naozaj veľa modulov, sme si začali všímať, že väčšina z nich „visí“ veľmi dlho v štáte Pending, hoci samotné kontajnery Docker už bežia na uzloch a dá sa s nimi pracovať manuálne.
Navyše v describe na tom nie je nič zlé:
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 nejakom kopaní sme dospeli k predpokladu, že kubelet jednoducho nestihne odoslať všetky informácie o stave modulov a testoch životnosti/pripravenosti na server API.
A po preštudovaní pomoci sme našli nasledujúce 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)
Ako je vidieť, predvolené hodnoty sú pomerne malé, a v 90% pokrývajú všetky potreby... V našom prípade to však nestačilo. Preto nastavíme nasledujúce hodnoty:
... a reštartovali sme kubelety, po čom sme v grafoch volaní na server API videli nasledujúci obrázok:
... a áno, všetko začalo lietať!
PS
Za ich pomoc pri zbere chýb a príprave tohto článku vyjadrujem svoju hlbokú vďaku mnohým inžinierom našej spoločnosti a najmä môjmu kolegovi z nášho tímu R&D Andrey Klimentyev (zuzzas).