調試 Kubernetes 就是這麼簡單

調試容器化工作負載和 Pod 是每位使用 Kubernetes 的開發人員和 DevOps 工程師的日常任務。通常情況下,我們簡單地使用 kubectl logs 或者 kubectl describe pod 便足以找到問題所在,但有時候,一些問題會特別難查。這種情況下,大家可能會嘗試使用 kubectl exec,但有時候這樣也還不行,因爲 Distroless 等容器甚至不允許通過 SSH 進入 shell。那麼,如果以上所有方法都失敗了,我們要怎麼辦?

更好的方法
其實我們只需要使用更合適的工具。如果在 Kubernetes 上調試工作負載,那麼合適的工具就是 kubectl debug。這是不久前添加的一個新命令(v1.18),允許調試正在運行的 pod。它會將名爲 EphemeralContainer(臨時容器)的特殊容器注入到問題 Pod 中,讓我們查看並排除故障。kubectl debug 看起來非常不錯,但要使用它需要臨時容器,臨時容器到底是什麼?

臨時容器其實是 Pod 中的子資源,類似普通 container。但與普通容器不同的是,臨時容器不用於構建應用程序,而是用於檢查。我們不會在創建 Pod 時定義它們,而使用特殊的 API 將其注入到運的行 Pod 中,來運行命令並檢查 Pod 環境。除了這些不同,臨時容器還缺少一些基本容器的字段,例如 ports、resources。

那麼我們爲什麼不直接使用基本容器?這是因爲我們不能向 Pod 添加基本容器,它們應該是一次性的(需要隨時刪除或重新創建),這會導致難以重現問題 Pod 的錯誤,排除故障也會很麻煩。這就是將臨時容器添加到 API 的原因——它們允許我們將臨時容器添加到現有 Pod,從而檢查正在運行的 Pod。

雖然臨時容器是作爲 Kubernetes 核心的 Pod 規範的一部分,但很多人可能還沒有聽說過。這是因爲臨時容器處於早期 Alpha 階段,這意味着默認情況下不啓用。Alpha 階段的資源和功能可能會出現重大變化,或者在 Kubernetes 的某個未來版本中被完全刪除。因此,要使用它們必須在 kubelet 中使用Feature Gate(功能門)顯式啓用。

Configuring Feature Gates
現在如果確定要試用 kubectl debug,那麼如何啓用臨時容器的功能門?這取決於集羣設置。例如,現在使用kubeadm啓動創建集羣,那麼可以使用以下集羣配置來啓用臨時容器:

apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
kubernetesVersion: v1.20.2
apiServer:
  extraArgs:
    feature-gates: EphemeralContainers=true

在以下示例中,爲了簡單和測試目的,我們使用 KinD(Docker 中的 Kubernetes)集羣,這允許我們指定要啓用的功能門。創建我們的測試集羣:

# File: config.yaml
# Run:  kind create cluster --config ./config.yaml --name kind --image=kindest/node:v1.20.2
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  EphemeralContainers: true
nodes:
- role: control-plane

隨着集羣的運行,我們需要驗證其有效性。最簡單方法是檢查 Pod API,它現在應該包含臨時容器部分以及通常容器:

~ $ kubectl explain pod.spec.ephemeralContainers
KIND:     Pod
VERSION:  v1

RESOURCE: ephemeralContainers <[]Object>

DESCRIPTION:
     List of ephemeral containers run in this pod....
...

現在都有了,可以開始使用 kubectl debug。從簡單的例子開始:

~ $ kubectl run some-app --image=k8s.gcr.io/pause:3.1 --restart=Never
~ $ kubectl debug -it some-app --image=busybox --target=some-app
Defaulting debug container name to debugger-tfqvh.
If you don't see a command prompt, try pressing enter.
/ #
# From other terminal...
~ $ kubectl describe pod some-app
...
Containers:
  some-app:
    Container ID:   containerd://60cc537eee843cb38a1ba295baaa172db8344eea59de4d75311400436d4a5083
    Image:          k8s.gcr.io/pause:3.1
    Image ID:       k8s.gcr.io/pause@sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea
...
Ephemeral Containers:
  debugger-tfqvh:
    Container ID:   containerd://12efbbf2e46bb523ae0546b2369801b51a61e1367dda839ce0e02f0e5c1a49d6
    Image:          busybox
    Image ID:       docker.io/library/busybox@sha256:ce2360d5189a033012fbad1635e037be86f23b65cfd676b436d0931af390a2ac
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Mon, 15 Mar 2021 20:33:51 +0100
    Ready:          False
    Restart Count:  0
    Environment:    <none>
    Mounts:         <none>

我們首先啓動一個名爲 some-app 的 Pod 來進行“調試”。然後針對這個 Pod 運行 kubectl debug,指定 busybox 爲臨時容器的鏡像,並作爲原始容器的目標。此外,還需要包括 -it 參數,以便我們立即附加到容器獲得 shell 會話。

在上面的代碼中可以看到,如果我們在 Pod 上運行 kubectl debug 後對其進行描述,那麼它的描述將包括具有之前指定爲命令選項值的臨時容器部分。

Process Namespace Sharing
kubectl debug 是非常強大的工具,但有時向 Pod 添加一個容器還不足以獲取 Pod 的另一個容器中運行的應用程序相關信息。當故障容器不包括必要的調試工具甚至 shell 時,可能就是這種情況。在這種情況下,我們可以使用 Process Sharing(進程共享)來使用注入的臨時容器檢查 Pod 的原有容器。

進程共享的一個問題是它不能應用於現有的 Pod,因此我們必須創建一個新 Pod,將其 spec.shareProcessNamespace 設置爲 true,並將一個臨時容器注入其中。這樣有點麻煩,尤其是需要調試多個 Pod 或容器,亦或者需要重複執行該操作時。幸運的是,kubectl debug 可以使用 --share-processes 做到:

~ $ kubectl run some-app --image=nginx --restart=Never
~ $ kubectl debug -it some-app --image=busybox --share-processes --copy-to=some-app-debug
Defaulting debug container name to debugger-tkwst.
If you don't see a command prompt, try pressing enter.
/ # ps ax
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    8 root      0:00 nginx: master process nginx -g daemon off;
   38 101       0:00 nginx: worker process
   39 root      0:00 sh
   46 root      0:00 ps ax
~ $ cat /proc/8/root/etc/nginx/conf.d/default.conf 
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;
...

上面的代碼表明,通過進程共享,我們可以看到 Pod 中另一個容器內的所有內容,包括其進程和文件,這對於調試來說非常方便。另外,除了 --share-processes 還包括了 --copy-to=new-pod-name,這是因爲我們需要創建一個新的 Pod,其名稱由該 flag 指定。如果我們從另一個終端列出正在運行的 Pod,我們將看到以下內容:

# From other terminal:
~ $ kubectl get pods
NAME             READY   STATUS    RESTARTS   AGE
some-app         1/1     Running   0          23h
some-app-debug   2/2     Running   0          20s

這就是我們在原始應用程序 Pod 上的新調試 Pod。與原始容器相比,它有 2 個容器,因爲它還包括臨時容器。此外,如果想在任何時候驗證 Pod 中是否允許進程共享,那麼可以運行:

~ $ kubectl get pod some-app-debug -o json  | jq .spec.shareProcessNamespace
true

好好使用
既然我們已經啓用了功能並且知道命令是如何工作的,那就試着使用它並調試一些應用程序。想象這樣一個場景——我們有一個問題應用程序,我們需要在它的容器中對網絡相關的問題進行故障排除。該應用程序沒有我們可以使用的必要的網絡 CLI 工具。爲了解決這個問題,我們通過以下方式使用

kubectl debug:

~ $ kubectl run distroless-python --image=martinheinz/distroless-python --restart=Never
~ $ kubectl exec -it distroless-python -- /bin/sh
# id
/bin/sh: 1: id: not found
# ls
/bin/sh: 2: ls: not found
# env
/bin/sh: 3: env: not found
#
...

kubectl debug -it distroless-python --image=praqma/network-multitool --target=distroless-python -- sh
Defaulting debug container name to debugger-rvtd4.
If you don't see a command prompt, try pressing enter.
/ # ping localhost
PING localhost(localhost (::1)) 56 data bytes
64 bytes from localhost (::1): icmp_seq=1 ttl=64 time=0.025 ms
64 bytes from localhost (::1): icmp_seq=2 ttl=64 time=0.044 ms
64 bytes from localhost (::1): icmp_seq=3 ttl=64 time=0.027 ms

在啓動一個 Pod 之後,我們首先嚐試將 shell 會話放入它的容器中,這看起來有效,但是實際上我們嘗試運行一些基本命令時,將看到那裏什麼都沒有。所以,我們要使用 praqma/network-multitool 將臨時容器注入到 Pod 中,該鏡像包含了 curl、ping、telnet 等工具,現在我們可以進行所有必要的故障排除。

在上面的例子中,我們進入 Pod 的另一個容器中就足夠了。但有時可能需要直接查看有問題的容器。這種情況下,我們可以像這樣使用進程共享:

~ $ kubectl run distroless-python --image=martinheinz/distroless-python --restart=Never
~ $ kubectl debug -it distroless-python --image=busybox --share-processes --copy-to=distroless-python-debug
Defaulting debug container name to debugger-l692h.
If you don't see a command prompt, try pressing enter.
/ # ps ax
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    8 root      0:00 /usr/bin/python3.5 sleep.py  # Original container is just sleeping forever
   14 root      0:00 sh
   20 root      0:00 ps ax
/ # cat /proc/8/root/app/sleep.py 
import time
print("sleeping for 1 hour")
time.sleep(3600)

在這裏,我們再次運行使用 Distroless 鏡像的容器。我們無法在它的 shell 中做任何事情。我們運行 kubectl debug 以及 --share-processes --copy-to=...,它創建了一個新的 Pod,帶有額外的臨時容器,可以訪問所有進程。當我們列出正在運行的進程時,能看到應用程序容器的進程有 PID 8,可以用它來探索文件和環境。爲此,我們需要通過 /proc//... 目錄,這個例子中是 /proc/8/root/app/...

另一種常見情況是應用程序在容器啓動時不斷崩潰,這讓調試非常困難,因爲沒有足夠的時間將 shell 會話導入容器並運行故障排除命令。在這種情況下,解決方案是創建具有不同入口點、命令的容器,這可以阻止應用程序立即崩潰並允許我們調試:

~ $ kubectl get pods
NAME                READY   STATUS             RESTARTS   AGE
crashing-app        0/1     CrashLoopBackOff   1          8s

~ $ kubectl debug crashing-app -it --copy-to=crashing-app-debug --container=crashing-app -- sh
If you don't see a command prompt, try pressing enter.
# id
uid=0(root) gid=0(root) groups=0(root)
#
...
# From another terminal
~ $ kubectl get pods
NAME                READY   STATUS             RESTARTS   AGE
crashing-app        0/1     CrashLoopBackOff   3          2m7s
crashing-app-debug  1/1     Running            0          16s

調試集羣節點
本文主要關注 Pod 及其容器的調試,但任何集羣管理員都知道常常需要調試的是節點而不是 Pod。幸運的是,kubectl debug 允許通過創建 Pod 來調試節點,該 Pod 將在指定節點上運行,節點的根文件系統安裝在 /root 目錄中。我們甚至可以用 chroot 訪問主機二進制文件,這本質上充當了節點的 SSH 連接:

~ $ kubectl get nodes
NAME                 STATUS   ROLES                  AGE   VERSION
kind-control-plane   Ready    control-plane,master   25h   v1.20.2

~ $ kubectl debug node/kind-control-plane -it --image=ubuntu
Creating debugging pod node-debugger-kind-control-plane-hvljt with container debugger on node kind-control-plane.
If you don't see a command prompt, try pressing enter.
root@kind-control-plane:/# chroot /host
# head kind/kubeadm.conf
apiServer:
  certSANs:
  - localhost
  - 127.0.0.1
  extraArgs:
    feature-gates: EphemeralContainers=true
    runtime-config: ""
apiVersion: kubeadm.k8s.io/v1beta2
clusterName: kind
controlPlaneEndpoint: kind-control-plane:6443

在上面的代碼中,我們首先確定了想要調試的節點,然後使用 node/... 作爲參數顯式運行 kubectl debug 以訪問我們集羣的節點。在那之後,當連接到Pod後,我們使用 chroot /host 突破 chroot,並完全進入主機。最後,爲了驗證是否真的可以看到主機上的所有內容,我們了查看一部分的 kubeadm.conf,最終看到我們在文章開頭配置的內容 feature-gates: EphemeralContainers=true。

小結
能夠快速有效地調試應用程序和服務可以節省大量時間,但更重要的是,它能極大地幫助解決那些如果不立即解決可能最終會花費大量資金的問題。這就是爲什麼 kubectl debug 之類的工具能隨意使用非常重要,即使它們尚未正式發佈或默認啓用。如果啓用臨時容器不是一種選擇,那麼嘗試替代調試方法可能是一個好主意,例如使用包含故障排除工具的應用程序鏡像的調試版本;或臨時更改 Pod 的容器命令以阻止其崩潰。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章