在Kubernetes上部署應用時我們常忽略的幾件事

空殼先生發表於2020-09-27

根據我的經驗,大多數人(使用Helm或手動yaml)將應用程式部署到Kubernetes上,然後認為他們就可以一直穩定執行。
然而並非如此,實際使用過程還是遇到了一些“陷阱”,我希望在此處列出這些“陷阱”,以幫助您瞭解在Kubernetes上啟動應用程式之前需要注意的一些問題。

Kubernetes排程簡介

排程器通過 kubernetes 的 watch 機制來發現叢集中新建立且尚未被排程到 Node 上的 Pod。排程器會將發現的每一個未排程的 Pod 排程到一個合適的 Node 上來執行。kube-scheduler作為叢集的預設排程器,對每一個新建立的 Pod 或者是未被排程的 Pod,kube-scheduler會選擇一個最優的 Node 去執行這個 Pod。然而,Pod 內的每一個容器對資源都有不同的需求,而且 Pod 本身也有不同的資源需求。因此,Pod 在被排程到 Node 上之前,根據這些特定的資源排程需求,需要對叢集中的 Node 進行一次過濾。

在一個叢集中,滿足一個 Pod 排程請求的所有 Node 稱之為 可排程節點。如果沒有任何一個 Node 能滿足 Pod 的資源請求,那麼這個 Pod 將一直停留在未排程狀態直到排程器能夠找到合適的 Node。

在做排程決定時需要考慮的因素包括:單獨和整體的資源請求、硬體/軟體/策略限制、親和以及反親和要求、資料局域性、負載間的干擾等等。關於排程更多資訊請官網自行查閱

Pod Requests and Limits

來看個簡單例子,這裡只擷取yaml部分資訊

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx-demo
    image: nginx
    resources:
      limits:
        memory: "100Mi"
        cpu: 100m
      requests:
        memory: "1000Mi"
        cpu: 100m

預設情況下,我們們建立服務部署檔案,如果不寫resources欄位,Kubernetes叢集會使用預設策略,不對Pod做任何資源限制,這就意味著Pod可以隨意使用Node節點的記憶體和CPU資源。但是這樣就會引發一個問題:資源爭搶。
例如:一個Node節點有8G記憶體,有兩個Pod在其上執行。
剛開始執行,兩個Pod都只需要2G記憶體就足夠執行,這時候都沒有問題,但是如果其中一個Pod因為記憶體洩漏或者流程突然增加到導致記憶體用到了7G,這時候Node的8G記憶體顯然就不夠用了。這就會導致服務服務極慢或者不可用。
所以,一般情況下,我們再部署服務時,需要對pod的資源進行限制,以避免發生類似的問題。

如示例檔案所示,需要加上resources;

requests: 表示執行服務所需要的最少資源,本例為需要記憶體100Mi,CPU 100m
limits: 表示服務能使用的最大資源,本例最大資源限制在記憶體1000Mi,CPU 100m

什麼意思呢?一圖勝千言吧。
PS:@@@畫圖我真滴盡力了@@@

Liveness and Readiness Probes

Kubernetes社群中經常討論的另一個熱點話題。 掌握Liveness和Readiness探針非常重要,因為它們提供了一種執行容錯軟體並最大程度減少停機時間的機制。 但是,如果配置不正確,它們可能會對您的應用程式造成嚴重的效能影響。 以下是這兩個探測的概要以及如何推理它們:

Liveness Probe:探測容器是否正在執行。 如果活動性探針失敗,則kubelet將殺死Container,並且Container將接受其重新啟動策略。 如果“容器”不提供活動性探針,則預設狀態為“成功”。

因為Liveness探針執行頻率比較高,設定儘可能簡單,比如:將其設定為每秒執行一次,那麼每秒將增加1個請求的額外流量,因此需要考慮該請求所需的額外資源。通常,我們會為Liveness提供一個健康檢查介面,該介面返回響應程式碼200表明您的程式已啟動並且可以處理請求。

Readiness Probe:探測容器是否準備好處理請求。 如果準備就緒探針失敗,則Endpoint將從與Pod匹配的所有服務的端點中刪除Pod的IP地址。

Readiness探針的檢查要求比較高,因為它表明整個應用程式正在執行並準備好接收請求。對於某些應用程式,只有從資料庫返回記錄後,才會接受請求。 通過使用經過深思熟慮的準備情況探針,我們能夠實現更高水平的可用性以及零停機部署。

Liveness and Readiness Probes檢測方法一致,有三種

  1. 定義存活命令:
    如果命令執行成功,返回值為零,Kubernetes 則認為本次探測成功;如果命令返回值非零,本次 Liveness 探測失敗。
  2. 定義一個存活態 HTTP 請求介面;
    傳送一個HTTP請求,返回任何大於或等於 200 並且小於 400 的返回程式碼表示成功,其它返回程式碼都標示失敗。
  3. 定義 TCP 的存活探測
    向執行埠傳送一個tcpSocket請求,如果能夠連線表示成功,否則失敗。

來看個例子,這裡以常用的 TCP存活探測為例

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx-demo
    image: nginx
    livenessProbe:
      tcpSocket:
        port: 80
      initialDelaySeconds: 10
      periodSeconds: 10
    readinessProbe:
      tcpSocket:
        port: 80
      initialDelaySeconds: 10
      periodSeconds: 10
livenessProbe 部分定義如何執行 Liveness 探測:
1. 探測的方法是:通過tcpSocket連線nginx的80埠。如果執行成功,返回值為零,Kubernetes 則認為本次 Liveness 探測成功;如果命令返回值非零,本次 Liveness 探測失敗。

2. initialDelaySeconds: 10 指定容器啟動 10 之後開始執行 Liveness 探測,一般會根據應用啟動的準備時間來設定。比如應用正常啟動要花 30 秒,那麼 initialDelaySeconds 的值就應該大於 30。

3. periodSeconds: 10 指定每 10 秒執行一次 Liveness 探測。Kubernetes 如果連續執行 3 次 Liveness 探測均失敗,則會殺掉並重啟容器。

readinessProbe 探測一樣,但是 readiness 的 READY 狀態會經歷瞭如下變化:
1. 剛被建立時,READY 狀態為不可用。
2. 20 秒後(initialDelaySeconds + periodSeconds),第一次進行 Readiness 探測併成功返回,設定 READY 為可用。
3. 如果Kubernetes連續 3 次 Readiness 探測均失敗後,READY 被設定為不可用。

為Pod設定預設的網路策略

Kubernetes使用一種“扁平”的網路拓撲,預設情況下,所有Pod都可以直接相互通訊。 但是在某些情況下我們不希望這樣,甚至是不必要的。 會存在一些潛在的安全隱患,例如一個易受攻擊的應用程式被利用,則可以為攻擊者提供完全訪問許可權,以將流量傳送到網路上的所有pod。 像在許多安全領域中一樣,最小訪問策略也適用於此,理想情況下,將建立網路策略以明確指定允許哪些容器到容器的連線。

舉例,以下是一個簡單的策略,該策略將拒絕特定名稱空間的所有入口流量

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-ingress-flow
spec:
  podSelector: {}
  policyTypes:
    - Ingress

此配置的示意圖

通過Hooks和init容器的自定義行為

我們使用Kubernetes系統的主要目標之一就是嘗試為現成的開發人員提供儘可能零停機的部署。 由於應用程式關閉自身和清理已利用資源的方式多種多樣,因此這很困難。 我們遇到特別困難的一個應用是Nginx。 我們注意到,當我們啟動這些Pod的滾動部署時,活動連線在成功終止之前被丟棄。 經過廣泛的線上研究,事實證明,Kubernetes並沒有等待Nginx在終止Pod之前耗盡其連線。 使用停止前掛鉤,我們能夠注入此功能,並通過此更改實現了零停機時間。

通常情況下,比如我們要對Nginx進行滾動升級,但是Kubernetes在停止Pod之前並不會等待Nginx終止連線。這就會導致被停掉的nginx並沒有正確關閉所有連線,這樣是不合理的。所以我們需要在停止錢使用鉤子,以解決這樣的問題。

我們可以在部署檔案新增lifecycle

lifecycle:
  preStop:
    exec:
      command: ["/usr/local/bin/nginx-killer.sh"]

nginx-killer.sh

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
    echo "Waiting while shutting down nginx..."
    sleep 10
done

這樣,Kubernetes在關閉Pod之前,會執行nginx-killer.sh指令碼,以我們定義的方式關閉nginx

另外一種情況就是使用init容器
Init Container就是用來做初始化工作的容器,可以是一個或者多個,如果有多個的話,這些容器會按定義的順序依次執行,只有所有的Init Container執行完後,主容器才會被啟動
例如:

 initContainers:
        - name: init
          image: busybox
          command: ["chmod","777","-R","/var/www/html"]
          imagePullPolicy: Always
          volumeMounts:
          - name: volume
            mountPath: /var/www/html
      containers:
      - name: nginx-demo
        image: nginx
        ports:
        - containerPort: 80
          name: port
        volumeMounts:
        - name: volume
          mountPath: /var/www/html

我們給nginx的/var/www/html掛載了一塊資料盤,在主容器執行前,我們把/var/www/html許可權改成777,以便主容器使用時不會存在許可權問題。
當然這裡只是一個小栗子,Init Container更多強大的功能,比如初始化配置等。。。

Kernel Tuning(核心引數優化)

最後,將更先進的技術留給最後,哈哈
Kubernetes是一個非常靈活的平臺,旨在讓你以自己認為合適的方式執行服務。通常如果我們有高效能的服務,對資源要求比較嚴苛,比如常見的redis,啟動以後會有如下提示

WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.

這就需要我們修改系統的核心引數了。好在Kubernetes允許我們執行一個特權容器,該容器可以修改僅適用於特定執行Pod的核心引數。 以下是我們用來修改/proc/sys/net/core/somaxconn引數的示例。

initContainers:
   - name: sysctl
      image: alpine:3.10
      securityContext:
          privileged: true
       command: ['sh', '-c', "echo 511 > /proc/sys/net/core/somaxconn"]

總結

儘管Kubernetes提供了一種開箱即用的解決方案,但是也需要你採取一些關鍵的步驟來確保程式的穩定執行。在程式上線前,務必進行多次測試,觀察關鍵指標,並實時進行調整。
在我們將服務部署到Kubernetes叢集前,我們可以問自己幾個問題:

  • 我們的程式需要多少資源,例如記憶體,CPU等?
  • 服務的平均流量是多少,高峰流量是多少?
  • 我們希望服務多長時間進行擴張,需要多長時間新的Pod可以接受流量?
  • 我們的Pod是正常的停止了嗎?怎麼做不影響線上服務?
  • 怎麼保證我們的服務出問題不會影響其他服務,不會造成大規模的服務當機?
  • 我們的許可權是否過大?安全嗎?

終於寫完了,嗚嗚嗚~真滴好難呀~

相關文章