Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

在生產中使用 Kubernetes 的這些年裡,我們累積了許多有趣的故事,說明各種系統組件中的錯誤如何導致令人不快和/或難以理解的後果,從而影響容器和 Pod 的運作。 在本文中,我們選擇了一些最常見或最有趣的內容。 即使你從來沒有足夠幸運遇到這樣的情況,閱讀這樣的短篇偵探故事 - 尤其是“第一手” - 總是很有趣,不是嗎?

故事 1. Supercronic 和 Docker 掛起

在其中一個叢集上,我們定期收到凍結的 Docker,這幹擾了叢集的正常運作。 同時,在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

…

我們對該錯誤最感興趣的是以下訊息: pthread_create failed: No space left on device。 快速學習 文件 解釋說 Docker 無法分叉進程,這就是它定期凍結的原因。

在監控中,對應的情況如下圖:

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

在其他節點上也觀察到類似的情況:

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

在相同的節點上我們看到:

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>

事實證明,這種行為是 pod 與 超長期的 (我們用來在 pod 中執行 cron 作業的 Go 實用程式):

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

問題是這樣的:當一個任務在 supercronic 中運行時,它產生的進程 無法正確終止, 轉變為 殭屍.

注意:更準確地說,進程是由 cron 任務產生的,但 supercronic 不是 init 系統,不能「採用」其子進程產生的進程。 當發出 SIGHUP 或 SIGTERM 訊號時,它們不會傳遞給子進程,導致子進程不會終止並保持殭屍。 您可以閱讀有關這一切的更多信息,例如, 這樣的文章.

有幾種方法可以解決問題:

  1. 作為臨時解決方法 - 在單一時間點增加系統中 PID 的數量:
           /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. 或不直接在 supercronic 中啟動任務,而是使用相同的 TINI,它能夠正確終止進程並且不會產生殭屍。

故事 2. 刪除 cgroup 時出現“殭屍”

Kubelet 開始消耗大量 CPU:

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

沒有人會喜歡這樣,所以我們武裝自己 PERF 並開始處理這個問題。 調查結果如下:

  • Kubelet 花了超過三分之一的 CPU 時間從所有 cgroup 中提取記憶體資料:

    Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

  • 在核心開發者的郵件列表中你可以找到 問題的討論。 簡而言之,要點可以歸結為: 各種 tmpfs 檔案和其他類似的東西沒有從系統中完全刪除 當刪除一個cgroup時,所謂的 記憶庫 殭屍。 它們遲早會從頁面快取中刪除,但伺服器上有大量內存,核心認為沒有必要浪費時間刪除它們。 這就是為什麼它們不斷堆積。 為什麼會發生這種情況? 這是一個有 cron 作業的伺服器,它不斷創建新作業以及新的 Pod。 因此,會為其中的容器建立新的 cgroup,但很快就會被刪除。
  • 為什麼 kubelet 中的 cAdvisor 會浪費這麼多時間? 透過最簡單的執行就很容易看出這一點 time cat /sys/fs/cgroup/memory/memory.stat。 如果在健康的機器上該操作需要 0,01 秒,那麼在有問題的 cron02 上則需要 1,2 秒。 問題是 cAdvisor 從 sysfs 讀取資料的速度非常慢,它試圖考慮殭屍 cgroup 中使用的記憶體。
  • 為了強制刪除殭屍,我們嘗試按照 LKML 中的建議清除快取: sync; echo 3 > /proc/sys/vm/drop_caches, - 但結果發現內核更複雜,導致車子崩潰。

怎麼辦? 問題正在修復(犯罪,有關說明,請參閱 發布訊息)將 Linux 核心更新到版本 4.16。

歷史 3. Systemd 及其掛載

同樣,kubelet 在某些節點上消耗了太多資源,但這次它消耗了太多記憶體:

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

事實證明,Ubuntu 16.04 中使用的 systemd 存在問題,並且在管理為連接創建的掛載時出現問題 subPath 來自 ConfigMap 或機密。 Pod 完成其工作後 systemd 服務及其服務掛載仍然存在 在系統中。 隨著時間的推移,它們會累積大量。 甚至還有關於這個主題的問題:

  1. 第5916章;
  2. Kubernetes #57345.

……最後一個指的是 systemd 中的 PR: #7811 (systemd 中的問題 - #7798).

這個問題在 Ubuntu 18.04 中不再存在,但如果您想繼續使用 Ubuntu 16.04,您可能會發現我們關於此主題的解決方法很有用。

所以我們做瞭如下的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

....它使用以下腳本:

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

……並且它使用前面提到的 supercronic 每 5 分鐘運行一次。 它的 Dockerfile 如下所示:

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

故事 4. 調度 Pod 時的競爭力

人們注意到:如果我們將一個 pod 放置在一個節點上,並且它的鏡像被抽出很長時間,那麼「擊中」同一節點的另一個 pod 就會簡單地 不開始拉取新 Pod 的鏡像。 相反,它會等待直到拉取前一個 pod 的映像。 結果,一個已經調度的 pod,其鏡像在一分鐘內就可以下載完畢,最終會處於以下狀態: containerCreating.

事件看起來像這樣:

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

事實證明, 來自慢速註冊表的單一映像可能會阻止部署 每個節點。

不幸的是,擺脫這種情況的方法並不多:

  1. 嘗試直接在叢集中或直接與叢集一起使用您的 DockerRegistry(例如,GitLabRegistry、Nexus 等);
  2. 使用諸如 .

故事 5. 節點因記憶體不足而掛起

在各種應用程式的運行過程中,我們也遇到節點完全無法存取的情況:SSH沒有回應,所有監控守護程式都掉了,然後日誌中沒有任何(或幾乎沒有)異常。

我將以 MongoDB 運行的節點為例,透過圖片告訴您。

這就是上面的樣子 事故:

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

就像這樣—— 事故:

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

在監控中,也有一個急劇的跳躍,此時節點不再可用:

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

因此,從截圖中可以清楚看出:

  1. 機器上的RAM已接近尾聲;
  2. RAM 消耗急劇增加,然後突然禁用對整個電腦的存取;
  3. 一個大任務到達 Mongo,這迫使 DBMS 進程使用更多記憶體並主動從磁碟讀取。

事實證明,如果 Linux 耗盡了可用記憶體(記憶體壓力開始出現)並且沒有交換區,那麼 當 OOM 殺手到來時,將頁面放入頁面快取和將其寫回磁碟之間可能會出現平衡行為。 這是由 kswapd 完成的,它勇敢地釋放盡可能多的記憶體頁以供後續分配。

不幸的是,由於 I/O 負載較大且可用記憶體較少, kswapd成為整個系統的瓶頸,因為他們與它聯繫在一起 所有 系統中記憶體頁面的分配(頁面錯誤)。 如果進程不想再使用內存,而是固定在 OOM 殺手深淵的邊緣,這種情況可能會持續很長時間。

自然的問題是:為什麼 OOM 殺手來得這麼晚? 在當前的迭代中,OOM 殺手非常愚蠢:只有當嘗試分配記憶體頁面失敗時(即,它才會終止進程)。 如果頁面錯誤失敗。 這種情況在很長一段時間內都不會發生,因為 kswapd 勇敢地釋放記憶體頁面,將頁面快取(實際上是系統中的整個磁碟 I/O)轉儲回磁碟。 更詳細地,您可以閱讀消除內核中此類問題所需步驟的描述 這裡.

這種行為 應該改進 Linux 核心 4.6+。

故事 6.Pod 陷入 Pending 狀態

在某些叢集中,確實有很多 pod 正在運行,我們開始注意到大多數 pod 都處於「掛起」狀態很長時間 Pending,儘管 Docker 容器本身已經在節點上運行並且可以手動使用。

同時,在 describe 沒有任何錯誤:

  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

經過一番挖掘,我們假設 kubelet 根本沒有時間將有關 pod 狀態和活動/就緒測試的所有資訊傳送到 API 伺服器。

並且研究了help後,我們發現了以下參數:

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

如所見, 預設值相當小,並且 90% 的情況滿足了所有需求...但是,在我們的案例中這還不夠。 因此,我們設定以下值:

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

……並重新啟動 kubelet,之後我們在 API 伺服器的呼叫圖中看到了下圖:

Kubernetes 運行中的 6 個有趣的系統錯誤 [及其解決方案]

……是的,一切都開始飛翔!

聚苯乙烯

感謝我們公司的眾多工程師,特別是我們研發團隊的同事 Andrey Klimentyev(Andrey Klimentyev)在收集 bug 和準備本文方面提供的協助。祖札斯).

聚苯硫醚

另請閱讀我們的博客:

來源: www.habr.com

添加評論