6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]

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:

6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]

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]

6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]

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 superkronický (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 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:

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

6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]

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:

    6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]

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

6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]

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:

  1. #5916;
  2. kubernetes #57345.

...posledný z nich sa týka PR v systemd: # 7811 (problém v systemd - # 7798).

Problém už neexistuje v Ubuntu 18.04, ale ak chcete pokračovať v používaní Ubuntu 16.04, naše riešenie na túto tému môže 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ť:

  1. Skúste použiť svoj Docker Registry priamo v klastri alebo priamo s klastrom (napríklad register GitLab, Nexus atď.);
  2. Využite pomôcky ako napr Kraken.

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]

A takto - po nehody:

6 zábavných systémových chýb v prevádzke Kubernetes [a ich riešenie]

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]

Zo snímok obrazovky je teda zrejmé, že:

  1. RAM na stroji je blízko konca;
  2. Došlo k prudkému skoku v spotrebe pamäte RAM, po ktorom je prístup k celému stroju náhle zakázaný;
  3. 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.

Toto správanie by sa mali zlepšiť 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 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:

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

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

PPS

Prečítajte si aj na našom blogu:

Zdroj: hab.com

Pridať komentár