![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/bed059552ed86580939aa18fbdf1553e.jpg)
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 vysvetlil, že Docker nemôže rozdeliť proces, a preto pravidelne zamrzol.
Pri monitorovaní zodpovedá tomu, čo sa deje, nasledujúci obrázok:
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/bd778052c87b338493bae54b26830ef3.jpg)
Podobná situácia je pozorovaná na iných uzloch:
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/ef512532a95ca982e4342071115dbe9f.jpg)
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/43c32ebca78755dde348ed5e7ac75c79.jpg)
V rovnakých uzloch vidíme:
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] <defunct>
root 17398 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl] <defunct>
root 16852 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl] <defunct>
root 9473 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl] <defunct>
root 4664 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl] <defunct>
root 30571 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl] <defunct>
root 24113 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl] <defunct>
root 16475 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl] <defunct>
root 7176 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl] <defunct>
root 1090 0.0 0.0 0 0 ? Z Feb06 0:00 | _ [curl] <defunct>Ukázalo sa, že toto správanie je dôsledkom práce s modulom (nástroj Go, ktorý používame na spúšťanie úloh cron v moduloch):
_ 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] <defunct>
| _ [curl] <defunct>
| _ [curl] <defunct>
| _ [curl] <defunct>
…Problém je tento: keď je úloha spustená v superkronike, proces splodil to nemôže správne ukončiť, mení sa na .
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 .
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 , 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:
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/6140058330faaa3785b089dcba857056.jpg)
Toto sa nebude páčiť nikomu, tak sme sa vyzbrojili 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:
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20241'%3E%3C/svg%3E)
- V adresári vývojárov jadra nájdete . 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 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 (, a popis viď ) aktualizácia jadra Linux až po 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:
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/044c4e23a772c61a6206b9b20aa67c1d.jpg)
Ukázalo sa, že problém bol so systemd používaným v Ubuntu 16.04 a vyskytuje sa pri správe pripojení, ktoré sú vytvorené pre 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:
- ;
- .
...posledný z nich sa týka PR v systemd: (problém v systemd - ).
Problém už nie je tam Ubuntu 18.04, ale ak chcete pokračovať v používaní Ubuntu 16.04, naše riešenie tejto témy by vám mohlo byť užitočné.
Takže sme vytvorili nasledujúcu 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...a používa nasledujúci skript:
#!/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:
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"]Príbeh 4. Súťaživosť pri plánovaní modulov
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ď.);
- Využite pomôcky ako napr .
Príbeh 5. Uzly visia kvôli nedostatku pamäte
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:
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/5de916d270a862cbcbb5ed23c31f698e.jpg)
A takto - po nehody:
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/0f32bf1113204cf19f4639a297e40348.jpg)
Pri monitorovaní dochádza aj k prudkému skoku, pri ktorom uzol prestáva byť dostupný:
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/31e770cac5be32bb7f95cfbbc6b9f1ae.jpg)
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 v Linux dôjde voľná pamäť (vyskytne sa tlak na pamäť) a nedochádza k žiadnemu swapu, 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ť .
Toto správanie s jadrom Linux 4.6 +.
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 containerPo 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:
--event-qps=30 --event-burst=40 --kube-api-burst=40 --kube-api-qps=30 --registry-qps=30 --registry-burst=40... a reštartovali sme kubelety, po čom sme v grafoch volaní na server API videli nasledujúci obrázok:
![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/b2ae099729e55a686f6bec3012b96195.jpg)
... 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 ().
PPS
Prečítajte si aj na našom blogu:
- «".
- Slučka tipov a trikov Kubernetes:
- «»;
- «»;
- «»;
- «".
Zdroj: hab.com

![6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]](/wp-content/uploads/2019/03/0d15d1de17cd6838fc1cad19615af218.jpg)