1. 程式人生 > >記一次 k8s 叢集單點故障引發的血案

記一次 k8s 叢集單點故障引發的血案

寫在前面

公司使用了 k8s 叢集來管理一些比較基礎的有狀態叢集,基於 k8s 進行了簡單的二次開發,使之可以支援有狀態的叢集(並沒有使用自帶的petset,現在改名為statefulset了好像)。運行了了挺長時間,一直比較正常。但由於一些歷史原因及僥倖心理,k8s 叢集中的 apisever 一直都是單點,是的,只有一臺機器。世上很多事情就是這樣,你做了防範,事故卻從不出現,顯得防範毫無用處,當你沒做防範的時候,事故總是如約而至,打你個措手不及。

血案經過

  • 那是一個安靜的晚上,樓主在安靜的擼碼,突然發現一臺機器 down 掉了,就是那臺跑了 apiserver controller-manager scheduler 的機器(一掛全掛。不作就不會死啊真的是)
  • 但是樓主並沒有太在意,在樓主的認知裡,這些掛了,對正在執行的容器應該不會有影響。但為了保險起見,樓主又在別的機器上搭建了新的 k8s 控制組件,並用 ha 作了高可用。共用的都是同一套 etcd 叢集。這個是不可避免的。做完後看了下業務容器,都是正常的。
  • 但是,過了五分鐘,同事向我反映,他的業務的 pod 的狀態全部變成 terminating 了。我趕緊看了一下,果然如此。當時直接嚇尿,趕緊檢視實際的業務容器,發現都是正常的。這是怎麼回事?
  • 想了一下,突然想起重新搭建的 k8s apiserver 對現在叢集的 kubelet 來說,是根本訪問不到的,因為現在 kubelet 啟動連線的 apiserver 地址還是之前的地址。另外,依稀記得 controller-manager 超過 5min 檢測不到來自 node 的心跳,就會認為這個 node 已經掛掉,然後就會把該 node 上的 pod 全部刪除。所以,目前的情況符合 pod eviction 條件,所有 node 上的 pod 已經被標記為刪除,但由於 kubelet 現在與 apiserver 無法通訊,所以容器並沒有被實際幹掉。
  • (事後來看,這時候正確的處理辦法是停掉所有 node 上的 kubelet,保證業務容器的安全,然後再想其它解決辦法,跳過 apiserver 直接修改 etcd 是最有效的辦法;或者之前不啟動新的 apiserver 及其它元件,當時有別的業務需要訪問 apiserver,所以需要搭建一套, 但 controller-manager 是用不到的,如果沒有啟動 controller-manager 則 node-controller 就不會進行 pod eviction, 後面的悲劇就不會出現了)
  • 之前掛掉的 apiserver 機器我們無法直接控制,後面網路突然又恢復了,所有 kubelet 開始同步狀態,殺掉正在執行的容器,血案爆發。

覆盤

這次故障最大的教訓就是不要留有僥倖心理,系統中不要留下單點,做好高可用。但如果處理得當,故障是可以止於機器故障的,不會引發後來的血案,究其原因,還是自己對 k8s 理解和實踐的不夠深入,對一些並不常用但其實很重要的特點沒有深入研究。比如這次故障主要涉及到的 node controller 和 pod eviction,之前只是簡單瞭解過,真正遇到問題,完全無法進行完善的處理。對故障涉及到的 k8s 特性進行了一些瞭解,下面主要記錄一下這些內容。

  • 首先是 node controller 和 pod eviction,node controller 是 k8s 眾多 controller 的一種。主要作用是檢測叢集中 node 的狀態,並進行相應的處理。下面是 controller manager 在執行時與 node controller 相關的一些選項。
    首先是 1.3 版本,處理的比較粗暴簡單,血案用的版本,後升級到 1.5 版本,對比之下可以看到其中的改進。

    --deleting-pods-burst=1: Number of nodes on which pods are bursty deleted in case of node failure. For more details look into RateLimiter
    --deleting-pods-qps=0.1: Number of nodes per second on which pods are deleted in case of node failure
    上面的兩個選項主要是用來作限速的,比如第二個選項,當多個 node failure 時,不用立即將所有 node 上的 pod 刪除,而是每 10s 中刪除一個 node 上的 pod 。這樣處理一方面是減輕了 apiserver 的壓力,同時也防止出現 node 一掛上面的 pod 就被立即刪除的情況,畢竟 node 有可能迅速恢復的。
    --node-monitor-grace-period=40s: Amount of time which we allow running Node to be unresponsive before marking it unhealty. Must be N times more than kubelet's nodeStatusUpdateFrequency, where N means number of retries allowed for kubelet to post node status.
    --node-monitor-period=5s: The period for syncing NodeStatus in NodeController.
    --node-startup-grace-period=1m0s: Amount of time which we allow starting Node to be unresponsive before marking it unhealty.
    --pod-eviction-timeout=5m0s: The grace period for deleting pods on failed nodes.
    node 在 node controller 中主要有兩個時間,一個是 lastprobetime, 一個是 lasttransitiontime。lastprobetime 是 node controller 設定的,在 nc 對 node 狀態進行例行檢查時,如果發現儲存的 node lasttransitiontime 發生了變化,就將 lastprobetime 設定為 now。如果沒有發生變化,則保持之前的內容。
    lasttransitiontime 為 kubelet 上報自己狀態時所帶的時間。
    如果 time.now after lastprobetime + pod-eviction-timeout,則 node 標記為下線,將 node 加入 pod eviction 佇列。

    以上就是 1.3 版本的 node controller,可以看到只要 node 被標記下線了,就會執行 pod eviction。下面是 1.5 版本的相關配置選項。

    --large-cluster-size-threshold int32                                Number of nodes from which NodeController treats the cluster as large for the eviction logic purposes. --secondary-node-eviction-rate is implicitly overridden to 0 for clusters this size or smaller. (default 50)
    --node-eviction-rate float32                                        Number of nodes per second on which pods are deleted in case of node failure when a zone is healthy (see --unhealthy-zone-threshold for definition of healthy/unhealthy). Zone refers to entire cluster in non-multizone clusters. (default 0.1)
      --node-monitor-grace-period duration                                Amount of time which we allow running Node to be unresponsive before marking it unhealthy. Must be N times more than kubelet's nodeStatusUpdateFrequency, where N means number of retries allowed for kubelet to post node status. (default 40s)
      --node-monitor-period duration                                      The period for syncing NodeStatus in NodeController. (default 5s)
      --node-startup-grace-period duration                                Amount of time which we allow starting Node to be unresponsive before marking it unhealthy. (default 1m0s)
      --pod-eviction-timeout duration                                     The grace period for deleting pods on failed nodes. (default 5m0s)
      --secondary-node-eviction-rate float32                              Number of nodes per second on which pods are deleted in case of node failure when a zone is unhealthy (see --unhealthy-zone-threshold for definition of healthy/unhealthy). Zone refers to entire cluster in non-multizone clusters. This value is implicitly overridden to 0 if the cluster size is smaller than --large-cluster-size-threshold. (default 0.01)
      可以看到,pod eviction 的策略細緻了很多。加入了 big cluster 和 zone 的概念。預設情況下是一個 zone kubernetes。分為幾種情況來考慮:
      1、叢集是 healthy 的,則按 pod-eviction-rate 刪除。
      2、叢集是 unhealthy 的,且是小叢集,則不進行刪除。
      3、叢集是 unhealthy 的大叢集,則按 secondary-node-eviction-rate 進行刪除。
  • 第二部分是關於繞過 apiserver 直接修改 etcd 狀態的。理論上這個肯定是可行的,但當時嘗試採用這個方法修改 pod 狀態的時候,一直不奏效。以為是 kubelet 有地方沒搞明白,看了半天 kubelet 程式碼,沒有發現問題。後來進行了一下測試,發現按之前的方法修改 etcd ,watch apiserver pods 的時,獲取不到修改的資訊。查看了 apiserver 相關程式碼,發現瞭如下的一個選項:

    --watch-cache                                             Enable watch caching in the apiserver (default true)

    這個 watch-cache 很明顯是快取 etcd watch 的結果的。但是當修改 etcd 的時候,watch-cache 會判斷之前快取的資源的 resourceversion 和從 etcd watch 到的對應資源的 resourceversion 作比較,如果後者不比前者大,則不進行更新。在修改 etcd 的時候,一定要注意把 resourceversion 修改掉。或者在 apiserver 中把 watch-cache 這個選項關掉也可以。

    結束語

    kubernetes 的發展很快,一定要注意新版本的變化。對一些特性要多加實驗。並深入到原始碼層次。