6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

De-a lungul anilor de utilizare a Kubernetes în producție, am acumulat multe povești interesante despre modul în care erorile din diferite componente ale sistemului au dus la consecințe neplăcute și/sau de neînțeles care afectează funcționarea containerelor și podurilor. În acest articol am făcut o selecție a unora dintre cele mai comune sau interesante. Chiar dacă nu ești niciodată suficient de norocos să întâlnești astfel de situații, a citi despre astfel de povești scurte despre detectivi - în special „de prima mână” - este întotdeauna interesant, nu-i așa?...

Povestea 1. Supercronic și Docker agățat

Pe unul dintre clustere, am primit periodic un Docker înghețat, care a interferat cu funcționarea normală a clusterului. În același timp, în jurnalele Docker s-au observat următoarele:

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

…

Ceea ce ne interesează cel mai mult la această eroare este mesajul: pthread_create failed: No space left on device. Studiu rapid documentație a explicat că Docker nu a putut întrerupe un proces, motiv pentru care a înghețat periodic.

În monitorizare, următoarea imagine corespunde cu ceea ce se întâmplă:

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

O situație similară se observă și pe alte noduri:

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

La aceleași noduri vedem:

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>

S-a dovedit că acest comportament este o consecință a lucrului cu podul supercronic (un utilitar Go pe care îl folosim pentru a rula joburi cron în pod-uri):

 _ 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>
…

Problema este aceasta: atunci când o sarcină este rulată în supercronic, procesul este generat de aceasta nu se poate termina corect, a se transforma in zombie.

Nota: Pentru a fi mai precis, procesele sunt generate de sarcini cron, dar supercronic nu este un sistem init și nu poate „adopta” procesele pe care le-au generat copiii. Când semnalele SIGHUP sau SIGTERM sunt ridicate, acestea nu sunt transmise proceselor copil, ceea ce duce la neterminarea proceselor copil și rămânând în starea zombie. Puteți citi mai multe despre toate acestea, de exemplu, în un astfel de articol.

Există câteva moduri de a rezolva problemele:

  1. Ca o soluție temporară - creșteți numărul de PID-uri din sistem la un moment dat:
           /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. Sau lansați sarcini în supercronic nu direct, ci folosind același lucru tini, care este capabil să încheie procesele corect și să nu dea zombi.

Povestea 2. „Zombi” când ștergeți un cgroup

Kubelet a început să consume mult procesor:

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

Nimănui nu va plăcea asta, așa că ne-am înarmat perfect și a început să se ocupe de problemă. Rezultatele anchetei au fost următoarele:

  • Kubelet își petrece mai mult de o treime din timpul procesorului extragând date de memorie din toate cgroups:

    6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

  • În lista de corespondență a dezvoltatorilor de kernel puteți găsi discutia problemei. Pe scurt, ideea se reduce la asta: diverse fișiere tmpfs și alte lucruri similare nu sunt complet eliminate din sistem la ștergerea unui cgroup, așa-numitul memcg Zombie. Mai devreme sau mai târziu vor fi șterse din memoria cache a paginii, dar există multă memorie pe server și kernel-ul nu vede rostul să piardă timpul cu ștergerea lor. De aceea se continuă să se adune. De ce se întâmplă chiar asta? Acesta este un server cu joburi cron care creează în mod constant noi locuri de muncă și, cu ele, noi poduri. Astfel, sunt create noi cgroup-uri pentru containerele din ele, care sunt șterse în curând.
  • De ce pierde cAdvisor în kubelet atât de mult timp? Acest lucru este ușor de văzut cu cea mai simplă execuție time cat /sys/fs/cgroup/memory/memory.stat. Dacă pe o mașină sănătoasă operația durează 0,01 secunde, atunci pe cron02 problematic durează 1,2 secunde. Chestia este că cAdvisor, care citește foarte lent datele din sysfs, încearcă să țină cont de memoria folosită în cgroup-urile zombie.
  • Pentru a elimina cu forță zombii, am încercat să ștergem memoria cache, așa cum se recomandă în LKML: sync; echo 3 > /proc/sys/vm/drop_caches, - dar nucleul s-a dovedit a fi mai complicat și a prăbușit mașina.

Ce să fac? Problema este rezolvată (comite, iar pentru o descriere vezi mesaj de eliberare) actualizarea nucleului Linux la versiunea 4.16.

Istoric 3. Systemd și montura sa

Din nou, kubelet-ul consumă prea multe resurse pe unele noduri, dar de data aceasta consumă prea multă memorie:

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

S-a dovedit că există o problemă în systemd folosit în Ubuntu 16.04 și apare la gestionarea monturilor care sunt create pentru conexiune subPath din ConfigMaps sau secrete. După ce podul și-a încheiat activitatea serviciul systemd și suportul său de service rămân în sistem. De-a lungul timpului, un număr mare dintre ele se acumulează. Există chiar și probleme pe această temă:

  1. #5916;
  2. kubernetes #57345.

... dintre care ultimul se referă la PR în systemd: #7811 (problema în systemd - #7798).

Problema nu mai există în Ubuntu 18.04, dar dacă doriți să continuați să utilizați Ubuntu 16.04, este posibil să găsiți utilă soluția noastră pe acest subiect.

Deci, am realizat următorul 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

... și folosește următorul script:

#!/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

... și rulează la fiecare 5 minute folosind supercronicul menționat anterior. Dockerfile-ul său arată astfel:

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

Povestea 4. Competitivitate la programarea podurilor

S-a observat că: dacă avem un pod plasat pe un nod și imaginea sa este pompată pentru o perioadă foarte lungă de timp, atunci un alt pod care „lovinte” același nod va pur și simplu nu începe să tragă imaginea noului pod. În schimb, așteaptă până când imaginea podului precedent este trasă. Drept urmare, un pod care era deja programat și a cărui imagine ar fi putut fi descărcată în doar un minut va ajunge în starea de containerCreating.

Evenimentele vor arăta cam așa:

Normal  Pulling    8m    kubelet, ip-10-241-44-128.ap-northeast-1.compute.internal  pulling image "registry.example.com/infra/openvpn/openvpn:master"

Se pare că o singură imagine dintr-un registru lent poate bloca implementarea pe nod.

Din păcate, nu există multe modalități de ieșire din situație:

  1. Încercați să utilizați Registrul Docker direct în cluster sau direct cu clusterul (de exemplu, Registrul GitLab, Nexus etc.);
  2. Utilizați utilități precum Kraken.

Povestea 5. Nodurile se blochează din cauza lipsei de memorie

În timpul funcționării diferitelor aplicații, am întâlnit și o situație în care un nod încetează complet să fie accesibil: SSH nu răspunde, toți demonii de monitorizare cad și apoi nu există nimic (sau aproape nimic) anormal în jurnale.

Vă voi spune în imagini folosind exemplul unui nod în care a funcționat MongoDB.

Așa arată vârful la accidente:

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

Și așa - după accidente:

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

În monitorizare, există și un salt brusc, la care nodul încetează să mai fie disponibil:

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

Astfel, din capturi de ecran reiese clar că:

  1. RAM-ul de pe mașină este aproape de sfârșit;
  2. Există un salt brusc în consumul de RAM, după care accesul la întreaga mașină este dezactivat brusc;
  3. O sarcină mare ajunge pe Mongo, ceea ce obligă procesul DBMS să utilizeze mai multă memorie și să citească în mod activ de pe disc.

Se pare că dacă Linux rămâne fără memorie liberă (se instalează presiunea memoriei) și nu există nicio schimbare, atunci la Când ucigașul OOM sosește, poate apărea un act de echilibru între aruncarea paginilor în memoria cache a paginii și scrierea lor înapoi pe disc. Acest lucru este realizat de kswapd, care eliberează cu curaj cât mai multe pagini de memorie pentru distribuția ulterioară.

Din păcate, cu o încărcare mare I/O cuplată cu o cantitate mică de memorie liberă, kswapd devine blocajul întregului sistem, pentru că sunt legați de el toate alocările (defecțiuni de pagină) ale paginilor de memorie din sistem. Acest lucru poate dura foarte mult timp dacă procesele nu mai doresc să folosească memoria, dar sunt fixate chiar la marginea abisului ucigaș OOM.

Întrebarea firească este: de ce ucigașul OOM vine atât de târziu? În iterația sa actuală, criminalul OOM este extrem de prost: va ucide procesul numai atunci când încercarea de a aloca o pagină de memorie eșuează, de exemplu. dacă eroarea paginii eșuează. Acest lucru nu se întâmplă pentru o perioadă lungă de timp, deoarece kswapd eliberează cu curaj paginile de memorie, aruncând cache-ul paginii (întregul disc I/O din sistem, de fapt) înapoi pe disc. Mai detaliat, cu o descriere a pașilor necesari pentru a elimina astfel de probleme din nucleu, puteți citi aici.

Acest comportament ar trebui să se îmbunătățească cu nucleul Linux 4.6+.

Povestea 6. Podurile se blochează în starea În așteptare

În unele clustere, în care funcționează într-adevăr multe păstăi, am început să observăm că majoritatea „atârnă” de foarte mult timp în stat. Pending, deși containerele Docker în sine rulează deja pe noduri și pot fi lucrate manual.

Mai mult, în describe nu este nimic gresit:

  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

După câteva săpături, am presupus că kubelet-ul pur și simplu nu are timp să trimită toate informațiile despre starea pod-urilor și testele de viață/preparare către serverul API.

Și după ce am studiat ajutorul, am găsit următorii parametri:

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

Așa cum se vede, valorile implicite sunt destul de mici, iar în 90% acopera toate nevoile... Totuși, în cazul nostru acest lucru nu a fost suficient. Prin urmare, setăm următoarele valori:

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

... și am repornit kubeletele, după care am văzut următoarea imagine în graficele apelurilor către serverul API:

6 erori de sistem distractive în funcționarea Kubernetes [și soluția lor]

... și da, totul a început să zboare!

PS

Pentru ajutorul lor în colectarea erorilor și pregătirea acestui articol, îmi exprim profunda recunoștință față de numeroși ingineri ai companiei noastre și, în special, colegului meu din echipa noastră de cercetare și dezvoltare Andrey Klimentyev (zuzzas).

PPS

Citește și pe blogul nostru:

Sursa: www.habr.com

Adauga un comentariu