6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

Gjennom årene med bruk av Kubernetes i produksjon, har vi samlet mange interessante historier om hvordan feil i ulike systemkomponenter førte til ubehagelige og/eller uforståelige konsekvenser som påvirket driften av containere og pods. I denne artikkelen har vi laget et utvalg av noen av de mest vanlige eller interessante. Selv om du aldri er heldig nok til å møte slike situasjoner, er det alltid interessant å lese om slike korte detektivhistorier - spesielt "førstehånds" - ikke sant?

Historie 1. Supercronic og Docker hengende

På en av klyngene mottok vi med jevne mellomrom en frossen Docker, som forstyrret den normale funksjonen til klyngen. Samtidig ble følgende observert i Docker-loggene:

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

…

Det som interesserer oss mest med denne feilen er meldingen: pthread_create failed: No space left on device. Rask studie dokumentasjon forklarte at Docker ikke kunne splitte en prosess, og derfor frøs den med jevne mellomrom.

Ved overvåking tilsvarer følgende bilde det som skjer:

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

En lignende situasjon observeres på andre noder:

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

På de samme nodene ser vi:

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>

Det viste seg at denne oppførselen er en konsekvens av at poden jobber med superkronisk (et Go-verktøy som vi bruker til å kjøre cron-jobber i pods):

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

Problemet er dette: når en oppgave kjøres i supercronic, ble prosessen skapt av den kan ikke avsluttes riktig, blir til zombie.

Note: For å være mer presis, prosesser skapt av cron-oppgaver, men supercronic er ikke et init-system og kan ikke "adoptere" prosesser som barna har skapt. Når SIGHUP- eller SIGTERM-signaler heves, sendes de ikke videre til barneprosessene, noe som resulterer i at barneprosessene ikke avsluttes og forblir i zombiestatus. Alt dette kan du lese mer om, for eksempel i en slik artikkel.

Det er et par måter å løse problemer på:

  1. Som en midlertidig løsning – øk antallet PID-er i systemet på et enkelt tidspunkt:
           /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. Eller start oppgaver i supercronic ikke direkte, men ved å bruke det samme tini, som er i stand til å avslutte prosesser på riktig måte og ikke skape zombier.

Historie 2. "Zombies" når du sletter en cgroup

Kubelet begynte å bruke mye CPU:

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

Ingen vil like dette, så vi bevæpnet oss perf og begynte å håndtere problemet. Resultatene av undersøkelsen var som følger:

  • Kubelet bruker mer enn en tredjedel av CPU-tiden sin på å trekke minnedata fra alle cgroups:

    6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

  • I kjerneutviklernes e-postliste kan du finne diskusjon av problemet. Kort fortalt kommer poenget ned til dette: ulike tmpfs-filer og andre lignende ting fjernes ikke fullstendig fra systemet ved sletting av en cgruppe, den såkalte memcg zombie. Før eller siden vil de bli slettet fra sidebufferen, men det er mye minne på serveren og kjernen ser ikke poenget med å kaste bort tid på å slette dem. Det er derfor de stadig hoper seg opp. Hvorfor skjer dette i det hele tatt? Dette er en server med cron-jobber som stadig skaper nye jobber, og med dem nye poder. Dermed opprettes det nye cgroups for containere i dem, som snart blir slettet.
  • Hvorfor kaster cAdvisor i kubelet bort så mye tid? Dette er lett å se med den enkleste utførelse time cat /sys/fs/cgroup/memory/memory.stat. Hvis operasjonen på en frisk maskin tar 0,01 sekunder, tar den på den problematiske cron02 1,2 sekunder. Saken er at cAdvisor, som leser data fra sysfs veldig sakte, prøver å ta hensyn til minnet som brukes i zombie cgroups.
  • For å fjerne zombier med makt, prøvde vi å tømme cacher som anbefalt i LKML: sync; echo 3 > /proc/sys/vm/drop_caches,- men kjernen viste seg å være mer komplisert og krasjet bilen.

Hva å gjøre? Problemet blir fikset (begå, og for en beskrivelse se slipp melding) oppdaterer Linux-kjernen til versjon 4.16.

Historie 3. Systemd og dets montering

Igjen, kubelet bruker for mange ressurser på noen noder, men denne gangen bruker den for mye minne:

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

Det viste seg at det er et problem i systemd brukt i Ubuntu 16.04, og det oppstår når du administrerer monteringer som er opprettet for tilkobling subPath fra ConfigMap's eller secret's. Etter at poden har fullført arbeidet systemd-tjenesten og dens servicemontering gjenstår i systemet. Over tid akkumuleres et stort antall av dem. Det er til og med problemer om dette emnet:

  1. #5916;
  2. kubernetes #57345.

...den siste refererer til PR i systemd: #7811 (problem i systemd - #7798).

Problemet eksisterer ikke lenger i Ubuntu 18.04, men hvis du vil fortsette å bruke Ubuntu 16.04, kan du finne vår løsning på dette emnet nyttig.

Så vi laget følgende 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

... og den bruker følgende 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

... og den kjører hvert 5. minutt ved å bruke den tidligere nevnte supercronic. Dockerfilen ser slik ut:

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

Historie 4. Konkurranseevne når du planlegger pods

Det ble lagt merke til at: hvis vi har en pod plassert på en node og bildet pumpes ut i veldig lang tid, vil en annen pod som "treffer" den samme noden ganske enkelt begynner ikke å trekke bildet av den nye poden. I stedet venter den til bildet av den forrige poden er trukket. Som et resultat vil en pod som allerede var planlagt og hvis bilde kunne ha blitt lastet ned på bare et minutt havne i statusen til containerCreating.

Arrangementene vil se omtrent slik ut:

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

Det viser seg at et enkelt bilde fra et tregt register kan blokkere distribusjon per node.

Dessverre er det ikke mange veier ut av situasjonen:

  1. Prøv å bruke Docker Registry direkte i klyngen eller direkte med klyngen (for eksempel GitLab Registry, Nexus, etc.);
  2. Bruk verktøy som f.eks Kraken.

Historie 5. Noder henger på grunn av mangel på hukommelse

Under driften av forskjellige applikasjoner møtte vi også en situasjon der en node slutter å være tilgjengelig fullstendig: SSH reagerer ikke, alle overvåkingsdemoner faller av, og så er det ingenting (eller nesten ingenting) unormalt i loggene.

Jeg skal fortelle deg i bilder ved å bruke eksemplet på en node der MongoDB fungerte.

Slik ser det ut på toppen til ulykker:

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

Og sånn - etter ulykker:

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

I overvåking er det også et skarpt hopp, der noden slutter å være tilgjengelig:

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

Så fra skjermbildene er det klart at:

  1. RAM-en på maskinen er nær slutten;
  2. Det er et kraftig hopp i RAM-forbruket, hvoretter tilgangen til hele maskinen brått deaktiveres;
  3. En stor oppgave kommer til Mongo, som tvinger DBMS-prosessen til å bruke mer minne og aktivt lese fra disk.

Det viser seg at hvis Linux går tom for ledig minne (minnetrykket setter inn) og det ikke er noe bytte, så til Når OOM-morderen ankommer, kan det oppstå en balansegang mellom å kaste sider inn i sidebufferen og skrive dem tilbake til disken. Dette gjøres av kswapd, som modig frigjør så mange minnesider som mulig for senere distribusjon.

Dessverre, med en stor I/O-belastning kombinert med en liten mengde ledig minne, kswapd blir flaskehalsen i hele systemet, fordi de er knyttet til det alle allokeringer (sidefeil) av minnesider i systemet. Dette kan pågå i svært lang tid hvis prosessene ikke ønsker å bruke minne lenger, men er fikset helt på kanten av OOM-killer-avgrunnen.

Det naturlige spørsmålet er: hvorfor kommer OOM-morderen så sent? I sin nåværende iterasjon er OOM-morderen ekstremt dum: den vil drepe prosessen bare når forsøket på å tildele en minneside mislykkes, dvs. hvis sidefeilen mislykkes. Dette skjer ikke på ganske lenge, fordi kswapd modig frigjør minnesider, og dumper sidebufferen (faktisk hele disk I/O i systemet) tilbake til disken. Mer detaljert, med en beskrivelse av trinnene som kreves for å eliminere slike problemer i kjernen, kan du lese her.

Denne oppførselen bør forbedres med Linux-kjerne 4.6+.

Historie 6. Pods blir sittende fast i ventende tilstand

I noen klynger, der det virkelig er mange pods som opererer, begynte vi å legge merke til at de fleste av dem "henger" veldig lenge i staten Pending, selv om selve Docker-beholderne allerede kjører på nodene og kan arbeides med manuelt.

Dessuten, i describe Det er ikke noe galt:

  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

Etter litt graving antok vi at kubelet rett og slett ikke har tid til å sende all informasjon om tilstanden til podene og liveness/beredskapstester til API-serveren.

Og etter å ha studert hjelp, fant vi følgende parametere:

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

Som sett, standardverdiene er ganske små, og i 90 % dekker de alle behov... Men i vårt tilfelle var ikke dette nok. Derfor setter vi følgende verdier:

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

... og startet kubelets på nytt, hvoretter vi så følgende bilde i grafene av kall til API-serveren:

6 underholdende systemfeil i driften av Kubernetes [og deres løsning]

... og ja, alt begynte å fly!

PS

For deres hjelp med å samle feil og forberede denne artikkelen, uttrykker jeg min dype takknemlighet til de mange ingeniørene i selskapet vårt, og spesielt til min kollega fra vårt FoU-team Andrey Klimentyev (zuzzas).

PPS

Les også på bloggen vår:

Kilde: www.habr.com

Legg til en kommentar