![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/bed059552ed86580939aa18fbdf1553e.jpg)
V letih delovanja Kubernetes v produkciji smo zbrali številne zanimive zgodbe o tem, kako so napake v različnih sistemskih komponentah povzročale neprijetne in/ali nerazumljive posledice, ki vplivajo na delovanje kontejnerjev in podov. V tem članku smo pripravili izbor nekaterih najbolj pogostih ali zanimivih primerov. Tudi če se nikoli ne srečate s takšnimi situacijami, je vedno zanimivo brati o teh kratkih detektivkah – še posebej „iz prvih rok“, kajne?...
Zgodba 1. Supercronic in zamrznjen Docker
Na enem izmed klasterjev smo občasno prejemali „zamrznjen“ Docker, kar je oviralo normalno delovanje klasterja. V logih Dockerja pa smo opazili 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
… V tej napaki nas najbolj zanima sporočilo: pthread_create failed: No space left on device. Hiter pregled je pojasnil, da Docker ne more forkniti procesa, zaradi česar je občasno „zamrznil“.
V monitoringu se kaže taka slika:
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/bd778052c87b338493bae54b26830ef3.jpg)
Podobna situacija je opažena tudi na drugih vozliščih:
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/ef512532a95ca982e4342071115dbe9f.jpg)
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/43c32ebca78755dde348ed5e7ac75c79.jpg)
Na teh vozliščih vidimo:
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]Izkazalo se je, da je to vedenje posledica delovanja poda z (orodje napisano v Go, ki ga uporabljamo za zagon cron nalog v podih):
_ 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]
…Težava je v naslednjem: ko se naloga zažene v supercronicu, proces, ki ga je ustvaril, ne more pravilno končati, kar vodi v .
Napomena: Natančneje, procesi se rodijo iz cron nalog, vendar supercronic ni init sistem in ne more »posvojiti« procesov, ki so jih ustvarili njegovi otroci. Ko pride do signalov SIGHUP ali SIGTERM, ti ne preidejo na podrejene procese, zaradi česar ti otroški procesi ne končajo, kar vodi v stanje zombie. Več o tem si lahko preberete v .
Obstaja nekaj načinov za reševanje težav:
- Kot začasna rešitev — povečanje števila PID-ov v sistemu v enem trenutku:
/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 naložite naloge v supercronic ne neposredno, ampak z uporabo istega , ki lahko pravilno končuje procese in ne ustvarja zombie.
Zgodba 2. „Zombi“ pri odstranjevanju cgroup
Kubelet je začel porabljati veliko CPU:
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/6140058330faaa3785b089dcba857056.jpg)
To nikomur ne bo všeč, zato smo se oborožili in se lotili težave. Rezultati preiskave so bili naslednji:
- Kubelet porabi več kot tretjino procesorskega časa za pridobivanje podatkov o pomnilniku iz vseh cgroup:
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](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)
- V okrožnici razvijalcev jedra lahko najdete . Na kratko, bistvo je, da različne tmpfs datoteke in druge podobne stvari niso popolnoma odstranjene iz sistema pri odstranjevanju cgroup — ostanejo tako imenovani zombie. Prej ali slej se bodo le odstranili iz predpomnilnika strani, toda prostora na strežniku je veliko in jedro ne vidi smisla, da bi izgubljalo čas z njihovim odstranjevanjem. Tako se še naprej kopičijo. Zakaj se to sploh dogaja? To je strežnik z cron opravili, ki nenehno ustvarja nove jobe, skupaj z njimi pa nove pod. Tako se za kontejnerje v njih ustvarjajo nove cgroup, ki se kmalu odstranijo.
- Zakaj cAdvisor v kubeletu porabi toliko časa? To je enostavno videti z najpreprostejšim izvajanjem
time cat /sys/fs/cgroup/memory/memory.stat. Če na zdravem računalniku operacija traja 0,01 sekunde, potem na problematičnem cron02 traja 1,2 sekunde. Gre za to, da cAdvisor zelo počasi bere podatke iz sysfs in poskuša upoštevati porabljeno pomnilnik ter v zombie cgroups. - Da bi nasilno odstranili zombije, smo poskusili očistiti predpomnilnike, kot so priporočali v LKML:
sync; echo 3 > /proc/sys/vm/drop_caches, — a se je izkazalo, da je jedro bolj zapleteno in je zrušilo računalnik.
Kaj storiti? Težavo odpravimo (, podrobnosti pa si oglejte v ) z nadgradnjo jedra Linux na različico 4.16.
Zgodba 3. Systemd in njegovo mount
Spet kubelet porabi preveč virov na nekaterih vozliščih, tokrat pa gre za pomnilnik:
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/044c4e23a772c61a6206b9b20aa67c1d.jpg)
Izkazalo se je, da je težava v systemd, ki se uporablja v Ubuntu 16.04, in se pojavi pri upravljanju mountov, ki se ustvarjajo za povezovanje subPath iz ConfigMapov ali secretov. Po končanju dela poda storitveni systemd in njegov službeni mount ostanejo v sistemu. S časom se jih nabere ogromno. O tem obstajajo celo težave:
- ;
- .
… v zadnjem se sklicujejo na PR v systemd: (težava v systemd — ).
Težave ni več v Ubuntu 18.04, vendar če želite še naprej uporabljati Ubuntu 16.04, vam lahko pride prav naš workaround v tej zadevi.
Torej smo naredili naslednji 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… и в нем используется такой скрипт:
#!/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… и он запускается каждые 5 минут с помощью уже упомянутого supercronic. Его Dockerfile выглядит так:
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"]История 4. Конкуренция при планировании pod’ов
Было замечено, что: если на узел помещается pod и его образ загружается очень долго, то другой pod, который «попал» на этот же узел, просто не начинает загружать образ нового pod’а. Вместо этого он ждет, пока загрузится образ предыдущего pod’а. В результате, pod, который уже был запланирован и образ которого мог бы загрузиться всего за минуту, окажется на продолжительное время в статусе containerCreating.
В событиях будет примерно следующее:
Normal Pulling 8m kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal pulling image "registry.example.com/infra/openvpn/openvpn:master"Получается, что один-единственный образ из медленного реестра может заблокировать деплой на узел.
К сожалению, выходов из ситуации не так много:
- Poskušajte uporabljati svoj Docker Registry neposredno v grozdu ali neposredno z grozdom (na primer, GitLab Registry, Nexus itd.);
- Uporabite takšne pripomočke, kot so .
Zgodovina 5. Zaseg vozlišč pri pomanjkanju pomnilnika
Med obratovanjem različnih aplikacij smo se tudi srečali s situacijo, ko vozlišče popolnoma preneha biti dostopno: ne odgovarja na SSH, vsi nadzorni daemoni odpovejo, v dnevnikih pa nato ni nič (ali skoraj nič) nenavadnega.
Prikazal bom na slikah z uporabo enega vozlišča, kjer je delovala MongoDB.
Tako izgleda atop do nesreče:
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/5de916d270a862cbcbb5ed23c31f698e.jpg)
Takole pa izgleda — po nesreče:
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/0f32bf1113204cf19f4639a297e40348.jpg)
V nadzoru se tudi opazi nenaden skok, pri katerem vozlišče preneha biti dostopno:
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/31e770cac5be32bb7f95cfbbc6b9f1ae.jpg)
Tako iz zaslonov lahko vidimo, da:
- Operativna pomnilnika na napravi je skoraj prazna;
- Opaziti je nenaden skok porabe operativne pomnilnika, po katerem se dostop do celotne naprave drastično prekine;
- Na Mongo pride velika naloga, ki prisili postopek DBMS, da porabi več pomnilnika in aktivno bere z diska.
Izkazuje se, da, če v Linuxu zmanjka prostega pomnilnika (nastopi memory pressure) in swap-a ni, do lahko pride do ravnotežja med metanjem strani v page cache in njihovim writeback-om nazaj na disk. S tem se ukvarja kswapd, ki pogumno osvobaja čim več strani pomnilnika za nadaljnjo dodelitev.
Žal, pri veliki obremenitvi vhod/izhod v kombinaciji z malo prostega pomnilnika, je kswapd vpetja steklenična grla celotnega sistema, ker se na njega vežejo vse dodelitve (page faults) strani pomnilnika v sistemu. To lahko traja zelo dolgo, če procesi ne želijo več uporabljati pomnilnika, ampak se ustavijo na robu OOM-u-killerja.
Zakonito vprašanje: zakaj OOM killer pride tako pozno? V svoji trenutni iteraciji je OOM killer izjemno neumen: ubil bo proces šele takrat, ko bo poskus dodelitve strani pomnilnika neuspešen, tj. če page fault uspe s napako. To dolgo ne prihaja, ker kswapd pogumno osvobaja strani pomnilnika s ponovnim nalaganjem page cache (vse diskovne I/O v sistemu, v osnovi) nazaj na disk. Podrobneje, s podrobnim opisom korakov, potrebnih za odpravo podobnih težav v jedru, lahko preberete .
To vedenje s jedrom Linux 4.6+.
Zgodovina 6. Pod’i se ustavljajo v stanju Pending
V nekaterih grozdih, kjer deluje res veliko pod’ov, smo začeli opažati, da večina od njih zelo dolgo "visí" v stanju Pending, čeprav so sami Docker kontejnerji že zagnani na vozlih in z njimi lahko ročno delamo.
Pri tem v describe ni nič slabega:
Tip Razlog Starost Iz Sporočilo
---- ------ ---- ---- -------
Normalno Načrtovano 1m privzeti-načrtovalec Uspešno dodeljeno sphinx-0 na ss-dev-kub07
Normalno Uspešno pripenjanje volumne 1m attachdetach-controller Pripni volumne je uspel za volumne "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normalno Uspešno montaža volumne 1m kubelet, ss-dev-kub07 Montaža volumne je uspela za volumne "sphinx-config"
Normalno Uspešno montaža volumne 1m kubelet, ss-dev-kub07 Montaža volumne je uspela za volumne "privzeti-token-fzcsf"
Normalno Uspešno montaža volumne 49s (x2 v 51s) kubelet, ss-dev-kub07 Montaža volumne je uspela za volumne "pvc-6aaad34f-ad10-11e8-a44c-52540035a73b"
Normalno Preneseno 43s kubelet, ss-dev-kub07 Slika kontejnerja "registry.example.com/infra/sphinx-exporter/sphinx-indexer:v1" že prisotna na napravi
Normalno Ustvarjeno 43s kubelet, ss-dev-kub07 Ustvarjen kontejner
Normalno Zagnano 43s kubelet, ss-dev-kub07 Zagnan kontejner
Normalno Preneseno 43s kubelet, ss-dev-kub07 Slika kontejnerja "registry.example.com/infra/sphinx/sphinx:v1" že prisotna na napravi
Normalno Ustvarjeno 42s kubelet, ss-dev-kub07 Ustvarjen kontejner
Normalno Zagnano 42s kubelet, ss-dev-kub07 Zagnan kontejnerPo raziskovanju smo sklepali, da kubelet preprosto ne more poslati API strežniku vseh informacij o stanju pod’ov, liveness/readiness preizkusih.
In po pregledu pomoči smo našli naslednje parametre:
--kube-api-qps - QPS za komunikacijo z kubernetes api strežnikom (privzeto 5)
--kube-api-burst - Burst za komunikacijo z kubernetes api strežnikom (privzeto 10)
--event-qps - Če > 0, omeji ustvarjanje dogodkov na sekundo na to vrednost. Če 0, brez omejitev. (privzeto 5)
--event-burst - Največja velikost dogodkov, ki se lahko preplavijo, začasno omogoča, da dogodki dosežejo to število, hkrati pa ne presežejo event-qps. Uporabljeno samo, če --event-qps > 0 (privzeto 10)
--registry-qps - Če > 0, omeji pull QPS registra na to vrednost.
--registry-burst - Največja velikost preplavitev pull, začasno omogoča, da pull preplavijo to število, hkrati pa ne presežejo registry-qps. Uporabljeno samo, če --registry-qps > 0 (privzeto 10)As you can see, privzete vrednosti — so precej majhne, in v 90 % pokrivajo vse potrebe… Vendar pa je v našem primeru tega bilo premalo. Zato smo nastavili naslednje vrednosti:
--event-qps=30 --event-burst=40 --kube-api-burst=40 --kube-api-qps=30 --registry-qps=30 --registry-burst=40… in znova zagnali kubelet’e, po čemer smo na grafikonih dostopov do API strežnika opazili naslednjo sliko:
![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/b2ae099729e55a686f6bec3012b96195.jpg)
… in ja, vse je začelo leteti!
P.S.
Za pomoč pri zbiranju napak in pripravi članka se zahvaljujem številnim inženirjem naše družbe, še posebej kolegu iz naše R&D ekipe Andreyju Klimentyevu ().
P.P.S.
Preberite tudi na našem blogu:
- «».
- Cikel Kubernetes nasvetov in trikov:
- «»;
- «»;
- «»;
- «».
Izvor: habr.com

![6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]](/wp-content/uploads/2019/03/0d15d1de17cd6838fc1cad19615af218.jpg)