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:

6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

Podobno stanje opazimo na drugih vozliščih:

6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

Na istih 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] <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>

Izkazalo se je, da je to vedenje posledica dela stroka supercronic (pripomoček Go, ki ga uporabljamo za izvajanje opravil cron 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] <defunct>
|       _ [curl] <defunct>
|       _ [curl] <defunct>
|       _ [curl] <defunct>
…

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:

  1. 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
  2. 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:

6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

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:

    6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

  • 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:

6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

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:

  1. #5916;
  2. kubernetes #57345.

... zadnji se nanaša na PR v systemd: #7811 (težava v systemd - #7798).

Težava v Ubuntu 18.04 ne obstaja več, če pa želite še naprej uporabljati Ubuntu 16.04, se vam bo morda zdela naša rešitev za to temo koristna.

Tako 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

... in uporablja naslednji 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

... in teče vsakih 5 minut z uporabo prej omenjenega supercronica. Njegova datoteka Dockerfile je videti takole:

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

Zgodba 4. Konkurenčnost pri razporejanju pods

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:

  1. Poskusite uporabiti svoj register Docker neposredno v gruči ali neposredno z gručo (na primer register GitLab, Nexus itd.);
  2. Uporabite pripomočke, kot je npr Kraken.

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:

6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

In tako - po nesreče:

6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

Pri spremljanju je tudi oster skok, pri katerem vozlišče preneha biti na voljo:

6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

Tako je iz posnetkov zaslona jasno, da:

  1. RAM na stroju je blizu konca;
  2. Pride do močnega skoka porabe RAM-a, po katerem je dostop do celotnega stroja nenadoma onemogočen;
  3. 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.

To vedenje se mora izboljšati z jedrom Linux 4.6+.

Zgodba 6. Stroki se zataknejo v stanju čakanja

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:

--event-qps=30 --event-burst=40 --kube-api-burst=40 --kube-api-qps=30 --registry-qps=30 --registry-burst=40

... in znova zagnali kubelets, nakar smo v grafih klicev na API strežnik videli naslednjo sliko:

6 zabavnih sistemskih hroščev v delovanju Kubernetesa [in njihova rešitev]

... 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).

PPS

Preberite tudi na našem blogu:

Vir: www.habr.com

Dodaj komentar