6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

Selama bertahun-tahun menggunakan Kubernetes dalam produksi, kami telah mengumpulkan banyak cerita menarik tentang bagaimana bug di berbagai komponen sistem menyebabkan konsekuensi yang tidak menyenangkan dan/atau tidak dapat dipahami yang memengaruhi pengoperasian container dan pod. Pada artikel ini kami telah memilih beberapa yang paling umum atau menarik. Sekalipun Anda tidak pernah cukup beruntung untuk menghadapi situasi seperti itu, membaca cerita detektif pendek seperti itu - terutama yang β€œlangsung” - selalu menarik, bukan?..

Cerita 1. Supercronic dan Docker gantung

Di salah satu cluster, kami secara berkala menerima Docker yang dibekukan, yang mengganggu fungsi normal cluster. Pada saat yang sama, hal berikut diamati di log Docker:

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

…

Yang paling menarik bagi kami tentang kesalahan ini adalah pesannya: pthread_create failed: No space left on device. Belajar Cepat dokumentasi menjelaskan bahwa Docker tidak dapat melakukan fork suatu proses, itulah sebabnya proses tersebut terhenti secara berkala.

Dalam pemantauan, gambar berikut sesuai dengan apa yang terjadi:

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

Situasi serupa diamati pada node lain:

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

Pada node yang sama kita melihat:

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>

Ternyata perilaku ini merupakan konsekuensi dari kerja pod tersebut superkronik (utilitas Go yang kami gunakan untuk menjalankan tugas cron di pod):

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

Masalahnya adalah ini: ketika suatu tugas dijalankan dalam mode superkronik, proses yang dihasilkan oleh tugas tersebut tidak dapat diakhiri dengan benar, berubah menjadi zombie.

Catatan: Lebih tepatnya, proses dihasilkan oleh tugas cron, tetapi supercronic bukanlah sistem init dan tidak dapat β€œmengadopsi” proses yang dihasilkan oleh turunannya. Ketika sinyal SIGHUP atau SIGTERM dimunculkan, sinyal tersebut tidak diteruskan ke proses anak, sehingga proses anak tidak berhenti dan tetap dalam status zombie. Anda dapat membaca lebih lanjut tentang semua ini, misalnya, di artikel seperti itu.

Ada beberapa cara untuk menyelesaikan masalah:

  1. Sebagai solusi sementara - tingkatkan jumlah PID dalam sistem pada satu waktu:
           /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. Atau meluncurkan tugas di supercronic tidak secara langsung, tetapi menggunakan hal yang sama kecil, yang mampu menghentikan proses dengan benar dan tidak memunculkan zombie.

Cerita 2. β€œZombie” saat menghapus cgroup

Kubelet mulai memakan banyak CPU:

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

Tidak ada yang akan menyukai ini, jadi kami mempersenjatai diri Perf dan mulai mengatasi masalahnya. Hasil penyelidikannya adalah sebagai berikut:

  • Kubelet menghabiskan lebih dari sepertiga waktu CPU-nya untuk menarik data memori dari semua cgroup:

    6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

  • Di milis pengembang kernel, Anda dapat menemukannya pembahasan masalah tersebut. Singkatnya, intinya begini: berbagai file tmpfs dan hal serupa lainnya tidak sepenuhnya dihapus dari sistem saat menghapus cgroup, yang disebut memcg zombie. Cepat atau lambat mereka akan dihapus dari cache halaman, tetapi ada banyak memori di server dan kernel tidak melihat gunanya membuang waktu untuk menghapusnya. Itu sebabnya mereka terus menumpuk. Mengapa ini bisa terjadi? Ini adalah server dengan pekerjaan cron yang terus-menerus menciptakan pekerjaan baru, dan dengan itu pod baru. Dengan demikian, cgroup baru dibuat untuk wadah di dalamnya, yang segera dihapus.
  • Mengapa cAdvisor di kubelet membuang banyak waktu? Ini mudah dilihat dengan eksekusi paling sederhana time cat /sys/fs/cgroup/memory/memory.stat. Jika pada mesin yang sehat pengoperasiannya memerlukan waktu 0,01 detik, maka pada mesin cron02 yang bermasalah memerlukan waktu 1,2 detik. Masalahnya adalah cAdvisor, yang membaca data dari sysfs dengan sangat lambat, mencoba memperhitungkan memori yang digunakan dalam grup zombie.
  • Untuk menghapus zombie secara paksa, kami mencoba membersihkan cache seperti yang direkomendasikan di LKML: sync; echo 3 > /proc/sys/vm/drop_caches, - tapi kernelnya ternyata lebih rumit dan membuat mobil crash.

Apa yang harus dilakukan? Masalahnya sedang diperbaiki (melakukan, dan untuk deskripsi lihat rilis pesan) memperbarui kernel Linux ke versi 4.16.

Sejarah 3. Systemd dan mountnya

Sekali lagi, kubelet memakan terlalu banyak sumber daya pada beberapa node, namun kali ini kubelet memakan terlalu banyak memori:

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

Ternyata ada masalah pada systemd yang digunakan di Ubuntu 16.04, dan itu terjadi saat mengelola mount yang dibuat untuk koneksi subPath dari ConfigMaps atau rahasia. Setelah pod menyelesaikan tugasnya layanan systemd dan mount layanannya tetap ada dalam sistem. Seiring waktu, sejumlah besar dari mereka terakumulasi. Bahkan ada isu mengenai topik ini:

  1. #5916;
  2. kubernet #57345.

...yang terakhir mengacu pada PR di systemd: #7811 (masalah di systemd - #7798).

Masalahnya tidak lagi ada di Ubuntu 18.04, tetapi jika Anda ingin terus menggunakan Ubuntu 16.04, solusi kami tentang topik ini mungkin berguna bagi Anda.

Jadi kami membuat DaemonSet berikut:

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

... dan menggunakan skrip berikut:

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

... dan itu berjalan setiap 5 menit menggunakan superkronik yang disebutkan sebelumnya. Dockerfile-nya terlihat seperti ini:

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

Cerita 4. Daya saing saat menjadwalkan pod

Telah diketahui bahwa: jika kita mempunyai sebuah pod yang ditempatkan pada sebuah node dan gambarnya dipompa keluar dalam waktu yang sangat lama, maka pod lain yang β€œmenabrak” node yang sama akan langsung terpompa keluar. tidak mulai menarik gambar pod baru. Sebaliknya, ia menunggu hingga gambar dari pod sebelumnya ditarik. Akibatnya, pod yang sudah dijadwalkan dan gambarnya dapat diunduh hanya dalam satu menit akan berstatus containerCreating.

Acaranya akan terlihat seperti ini:

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

Ternyata bahwa satu gambar dari registri yang lambat dapat memblokir penerapan per simpul.

Sayangnya, tidak banyak jalan keluar dari situasi ini:

  1. Coba gunakan Docker Registry Anda secara langsung di cluster atau langsung dengan cluster (misalnya, GitLab Registry, Nexus, dll.);
  2. Gunakan utilitas seperti Kraken.

Cerita 5. Node hang karena kekurangan memori

Selama pengoperasian berbagai aplikasi, kami juga menghadapi situasi di mana sebuah node benar-benar tidak lagi dapat diakses: SSH tidak merespons, semua daemon pemantauan hilang, dan kemudian tidak ada (atau hampir tidak ada) apa pun yang aneh di log.

Saya akan memberi tahu Anda melalui gambar menggunakan contoh satu node tempat MongoDB berfungsi.

Seperti inilah penampakan di atas untuk kecelakaan:

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

Dan seperti ini - setelah kecelakaan:

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

Dalam pemantauan, ada juga lompatan tajam, di mana node tidak lagi tersedia:

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

Jadi, dari tangkapan layar terlihat jelas bahwa:

  1. RAM pada mesin hampir habis;
  2. Ada lonjakan tajam dalam konsumsi RAM, setelah itu akses ke seluruh mesin dinonaktifkan secara tiba-tiba;
  3. Tugas besar tiba di Mongo, yang memaksa proses DBMS menggunakan lebih banyak memori dan secara aktif membaca dari disk.

Ternyata jika Linux kehabisan memori bebas (tekanan memori terjadi) dan tidak ada swap, maka untuk Ketika pembunuh OOM tiba, tindakan penyeimbangan mungkin muncul antara memasukkan halaman ke dalam cache halaman dan menulisnya kembali ke disk. Hal ini dilakukan oleh kswapd, yang dengan berani mengosongkan halaman memori sebanyak mungkin untuk distribusi selanjutnya.

Sayangnya, dengan beban I/O yang besar ditambah dengan jumlah memori bebas yang sedikit, kswapd menjadi penghambat keseluruhan sistem, karena mereka terikat padanya semua alokasi (kesalahan halaman) halaman memori dalam sistem. Ini bisa berlangsung sangat lama jika prosesnya tidak ingin menggunakan memori lagi, tetapi tetap berada di ujung jurang pembunuh OOM.

Pertanyaan wajarnya adalah: mengapa pembunuh OOM datang terlambat? Dalam iterasi saat ini, pembunuh OOM sangat bodoh: ia akan mematikan proses hanya ketika upaya untuk mengalokasikan halaman memori gagal, yaitu. jika kesalahan halaman gagal. Hal ini tidak terjadi dalam waktu yang cukup lama, karena kswapd dengan berani membebaskan halaman memori, membuang cache halaman (sebenarnya seluruh disk I/O dalam sistem) kembali ke disk. Anda dapat membaca lebih detail dengan deskripsi langkah-langkah yang diperlukan untuk menghilangkan masalah seperti itu di kernel di sini.

Perilaku ini harus ditingkatkan dengan kernel Linux 4.6+.

Cerita 6. Pod terjebak dalam status Tertunda

Di beberapa cluster, di mana terdapat banyak sekali pod yang beroperasi, kami mulai memperhatikan bahwa sebagian besar dari mereka β€œmenggantung” untuk waktu yang sangat lama di negara bagian tersebut. Pending, meskipun container Docker sendiri sudah berjalan di node dan dapat digunakan secara manual.

Selain itu, di describe tidak ada yang salah:

  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

Setelah melakukan beberapa penggalian, kami berasumsi bahwa kubelet tidak mempunyai waktu untuk mengirimkan semua informasi tentang status pod dan uji keaktifan/kesiapan ke server API.

Dan setelah mempelajari bantuan, kami menemukan parameter berikut:

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

Seperti yang terlihat, nilai defaultnya cukup kecil, dan 90% mencakup semua kebutuhan... Namun, dalam kasus kami ini tidak cukup. Oleh karena itu, kami menetapkan nilai berikut:

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

... dan memulai ulang kubelet, setelah itu kita melihat gambar berikut di grafik panggilan ke server API:

6 bug sistem menghibur dalam pengoperasian Kubernetes [dan solusinya]

... dan ya, semuanya mulai terbang!

PS

Atas bantuan mereka dalam mengumpulkan bug dan mempersiapkan artikel ini, saya mengucapkan terima kasih yang sebesar-besarnya kepada banyak insinyur di perusahaan kami, dan terutama kepada rekan saya dari tim R&D kami Andrey Klimentyev (zuzza).

PPS

Baca juga di blog kami:

Sumber: www.habr.com

Tambah komentar