6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]

6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]

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 dokumenti 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]

Podobna situacija je opažena tudi na drugih vozliščih:

6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]

6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]

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 supercronic (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 zombie.

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 tem članku.

Obstaja nekaj načinov za reševanje težav:

  1. 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
  2. Ali pa naložite naloge v supercronic ne neposredno, ampak z uporabo istega tini, 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]

To nikomur ne bo všeč, zato smo se oborožili flamescope 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]

  • V okrožnici razvijalcev jedra lahko najdete razpravo o tej težavi. Na kratko, bistvo je, da različne tmpfs datoteke in druge podobne stvari niso popolnoma odstranjene iz sistema pri odstranjevanju cgroup — ostanejo tako imenovani memcg 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 (commit, podrobnosti pa si oglejte v sporočilu o izdaji) 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]

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:

  1. kops #5916;
  2. kubernetes #57345.

… v zadnjem se sklicujejo na PR v systemd: #7811 (težava v systemd — #7798).

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"

Получается, что один-единственный образ из медленного реестра может заблокировать деплой на узел.

К сожалению, выходов из ситуации не так много:

  1. Poskušajte uporabljati svoj Docker Registry neposredno v grozdu ali neposredno z grozdom (na primer, GitLab Registry, Nexus itd.);
  2. Uporabite takšne pripomočke, kot so kraken.

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]

Takole pa izgleda — po nesreče:

6 zanimljivih sistemskih grešaka pri korištenju Kubernetes [i njihovo rješenje]

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]

Tako iz zaslonov lahko vidimo, da:

  1. Operativna pomnilnika na napravi je skoraj prazna;
  2. Opaziti je nenaden skok porabe operativne pomnilnika, po katerem se dostop do celotne naprave drastično prekine;
  3. 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 ovde.

To vedenje bi se moralo izboljšati 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 kontejner

Po 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]

… 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 (zuzzas).

P.P.S.

Preberite tudi na našem blogu:

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster