6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

Gennem årene med brug af Kubernetes i produktionen har vi samlet mange interessante historier om, hvordan fejl i forskellige systemkomponenter førte til ubehagelige og/eller uforståelige konsekvenser, der påvirker driften af ​​containere og pods. I denne artikel har vi lavet et udvalg af nogle af de mest almindelige eller interessante. Selvom du aldrig er så heldig at støde på sådanne situationer, er det altid interessant at læse om sådanne korte detektivhistorier - især "førstehånds" - ikke?

Historie 1. Supercronic og Docker hængende

På en af ​​klyngerne modtog vi med jævne mellemrum en frossen Docker, som forstyrrede den normale funktion af klyngen. På samme tid blev følgende observeret 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, der interesserer os mest ved denne fejl, er meddelelsen: pthread_create failed: No space left on device. Hurtig undersøgelse dokumentation forklarede, at Docker ikke kunne forgrene en proces, hvorfor den med jævne mellemrum frøs.

Ved overvågning svarer følgende billede til, hvad der sker:

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

En lignende situation observeres på andre noder:

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

Ved de samme knudepunkter 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 sig, at denne adfærd er en konsekvens af, at poden arbejder med superkronisk (et Go-værktøj, som vi bruger til at køre cron-job 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 opgave køres i supercronic, er processen affødt af den kan ikke afsluttes korrekt, bliver til zombie.

Bemærk: For at være mere præcis er processer affødt af cron-opgaver, men supercronic er ikke et init-system og kan ikke "adoptere" processer, som dets børn affødte. Når SIGHUP- eller SIGTERM-signaler hæves, videregives de ikke til underordnede processer, hvilket resulterer i, at underordnede processer ikke afsluttes og forbliver i zombiestatus. Alt dette kan du læse mere om, for eksempel i sådan en artikel.

Der er et par måder at løse problemer på:

  1. Som en midlertidig løsning - øg antallet af 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 opgaver i supercronic ikke direkte, men ved hjælp af det samme kar, som er i stand til at afslutte processer korrekt og ikke afføde zombier.

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

Kubelet begyndte at forbruge en masse CPU:

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

Ingen vil kunne lide dette, så vi bevæbnede os perf og begyndte at håndtere problemet. Resultaterne af undersøgelsen var som følger:

  • Kubelet bruger mere end en tredjedel af sin CPU-tid på at trække hukommelsesdata fra alle cgroups:

    6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

  • I kerneudviklernes mailingliste kan du finde diskussion af problemet. Kort sagt kommer pointen ned til dette: forskellige tmpfs-filer og andre lignende ting fjernes ikke fuldstændigt fra systemet ved sletning af en cgroup, den såkaldte memcg Zombie. Før eller siden vil de blive slettet fra sidecachen, men der er meget hukommelse på serveren, og kernen ser ikke meningen med at spilde tid på at slette dem. Det er derfor, de bliver ved med at hobe sig op. Hvorfor sker det overhovedet? Dette er en server med cron-jobs, der konstant skaber nye jobs, og med dem nye pods. Der oprettes således nye cgroups til containere i dem, som snart slettes.
  • Hvorfor spilder cAdvisor i kubelet så meget tid? Dette er let at se med den enkleste udførelse time cat /sys/fs/cgroup/memory/memory.stat. Hvis operationen på en sund maskine tager 0,01 sekunder, så tager den på den problematiske cron02 1,2 sekunder. Sagen er, at cAdvisor, som læser data fra sysfs meget langsomt, forsøger at tage højde for den hukommelse, der bruges i zombie cgroups.
  • For at fjerne zombier med magt prøvede vi at rydde caches som anbefalet i LKML: sync; echo 3 > /proc/sys/vm/drop_caches, - men kernen viste sig at være mere kompliceret og styrtede bilen.

Hvad skal man gøre? Problemet er ved at blive rettet (begå, og for en beskrivelse se frigive besked) opdatering af Linux-kernen til version 4.16.

Historie 3. Systemd og dets montering

Igen bruger kubelet for mange ressourcer på nogle noder, men denne gang bruger den for meget hukommelse:

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

Det viste sig, at der er et problem i systemd brugt i Ubuntu 16.04, og det opstår ved håndtering af monteringer, der er oprettet til forbindelse subPath fra ConfigMap's eller secret's. Efter at poden har afsluttet sit arbejde systemd-tjenesten og dens servicemontering forbliver i system. Over tid akkumuleres et stort antal af dem. Der er endda problemer om dette emne:

  1. #5916;
  2. kubernetes #57345.

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

Problemet eksisterer ikke længere i Ubuntu 18.04, men hvis du vil fortsætte med at bruge Ubuntu 16.04, kan du finde vores løsning på dette emne nyttig.

Så vi lavede 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 det bruger følgende 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

... og den kører hvert 5. minut ved hjælp af den tidligere nævnte supercronic. Dens Dockerfile ser sådan ud:

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. Konkurrenceevne ved planlægning af pods

Det blev bemærket, at: hvis vi har en pod placeret på en node, og dens billede pumpes ud i meget lang tid, så vil en anden pod, der "hitter" den samme node simpelthen begynder ikke at trække billedet af den nye pod. I stedet venter den, indtil billedet af den forrige pod er trukket. Som et resultat vil en pod, der allerede var planlagt, og hvis billede kunne være blevet downloadet på blot et minut, ende i status som containerCreating.

Begivenhederne kommer til at se sådan ud:

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 sig, at et enkelt billede fra en langsom registreringsdatabasen kan blokere implementeringen pr. node.

Desværre er der ikke mange veje ud af situationen:

  1. Prøv at bruge din Docker Registry direkte i klyngen eller direkte med klyngen (for eksempel GitLab Registry, Nexus osv.);
  2. Brug hjælpeprogrammer som f.eks Kraken.

Historie 5. Noder hænger på grund af manglende hukommelse

Under driften af ​​forskellige applikationer stødte vi også på en situation, hvor en node helt ophører med at være tilgængelig: SSH reagerer ikke, alle overvågningsdæmoner falder af, og så er der intet (eller næsten intet) unormalt i logfilerne.

Jeg fortæller dig i billeder ved at bruge eksemplet på en knude, hvor MongoDB fungerede.

Sådan ser det ud på toppen til ulykker:

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

Og sådan her - efter ulykker:

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

I overvågning er der også et skarpt spring, hvor noden ophører med at være tilgængelig:

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

Fra skærmbillederne er det således klart, at:

  1. RAM på maskinen er tæt på enden;
  2. Der er et kraftigt spring i RAM-forbruget, hvorefter adgangen til hele maskinen brat deaktiveres;
  3. En stor opgave ankommer til Mongo, som tvinger DBMS-processen til at bruge mere hukommelse og aktivt læse fra disk.

Det viser sig, at hvis Linux løber tør for ledig hukommelse (hukommelsestrykket sætter ind), og der ikke er nogen swap, så til Når OOM-morderen ankommer, kan der opstå en balancegang mellem at smide sider ind i sidecachen og skrive dem tilbage til disken. Dette gøres af kswapd, som modigt frigør så mange hukommelsessider som muligt til efterfølgende distribution.

Desværre, med en stor I/O-belastning kombineret med en lille mængde ledig hukommelse, kswapd bliver flaskehalsen i hele systemet, fordi de er bundet til det alle allokeringer (sidefejl) af hukommelsessider i systemet. Dette kan fortsætte i meget lang tid, hvis processerne ikke ønsker at bruge hukommelsen længere, men er fastgjort helt på kanten af ​​OOM-dræberens afgrund.

Det naturlige spørgsmål er: hvorfor kommer OOM-morderen så sent? I sin nuværende iteration er OOM-morderen ekstremt dum: den dræber kun processen, når forsøget på at tildele en hukommelsesside mislykkes, dvs. hvis sidefejlen fejler. Dette sker ikke ret længe, ​​fordi kswapd modigt frigør hukommelsessider og dumper sidecachen (faktisk hele disk I/O i systemet) tilbage til disken. Mere detaljeret, med en beskrivelse af de nødvendige trin for at eliminere sådanne problemer i kernen, kan du læse her.

Denne adfærd bør forbedres med Linux-kerne 4.6+.

Historie 6. Pods sidder fast i afventende tilstand

I nogle klynger, hvor der er rigtig mange bælg, der opererer, begyndte vi at bemærke, at de fleste af dem "hænger" i meget lang tid i staten Pending, selvom selve Docker-beholderne allerede kører på noderne og kan arbejdes med manuelt.

På samme tid, i describe der er intet 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

Efter lidt gravning antog vi, at kubelet simpelthen ikke har tid til at sende alle oplysninger om pods tilstand og liveness/readiness-test til API-serveren.

Og efter at have studeret hjælp fandt vi følgende 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)

Som set, standardværdierne er ret små, og i 90% dækker de alle behov... Men i vores tilfælde var dette ikke nok. Derfor sætter vi følgende værdier:

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

... og genstartede kubelets, hvorefter vi så følgende billede i graferne af kald til API-serveren:

6 underholdende systemfejl i driften af ​​Kubernetes [og deres løsning]

... og ja, alt begyndte at flyve!

PS

For deres hjælp til at indsamle fejl og forberede denne artikel, udtrykker jeg min dybe taknemmelighed til de mange ingeniører i vores virksomhed, og især til min kollega fra vores R&D-team Andrey Klimentyev (zuzzas).

PPS

Læs også på vores blog:

Kilde: www.habr.com

Tilføj en kommentar