Kubernetes 官方出品調(diào)試工具上手指南(無需安裝,開箱即用)
調(diào)試容器化工作負(fù)載和 Pod 是每位使用 Kubernetes 的開發(fā)人員和 DevOps 工程師的日常任務(wù)。通常情況下,我們簡單地使用 kubectl logs 或者 kubectl describe pod 便足以找到問題所在,但有時(shí)候,一些問題會(huì)特別難查。這種情況下,大家可能會(huì)嘗試使用 kubectl exec,但有時(shí)候這樣也還不行,因?yàn)?Distroless 等容器甚至不允許通過 SSH 進(jìn)入 shell。那么,如果以上所有方法都失敗了,我們要怎么辦?
更好的方法
其實(shí)我們只需要使用更合適的工具。如果在 Kubernetes 上調(diào)試工作負(fù)載,那么合適的工具就是 kubectl debug。 這是不久前添加的一個(gè)新命令(v1.18),允許調(diào)試正在運(yùn)行的 pod。它會(huì)將名為 EphemeralContainer(臨時(shí)容器)的特殊容器注入到問題 Pod 中,讓我們查看并排除故障。kubectl debug 看起來非常不錯(cuò),但要使用它需要臨時(shí)容器,臨時(shí)容器到底是什么?
臨時(shí)容器其實(shí)是 Pod 中的子資源,類似普通 container。但與普通容器不同的是,臨時(shí)容器不用于構(gòu)建應(yīng)用程序,而是用于檢查。 我們不會(huì)在創(chuàng)建 Pod 時(shí)定義它們,而使用特殊的 API 將其注入到運(yùn)的行 Pod 中,來運(yùn)行命令并檢查 Pod 環(huán)境。除了這些不同,臨時(shí)容器還缺少一些基本容器的字段,例如 ports、resources。
那么我們?yōu)槭裁床恢苯邮褂没救萜??這是因?yàn)槲覀儾荒芟?Pod 添加基本容器,它們應(yīng)該是一次性的(需要隨時(shí)刪除或重新創(chuàng)建),這會(huì)導(dǎo)致難以重現(xiàn)問題 Pod 的錯(cuò)誤,排除故障也會(huì)很麻煩。這就是將臨時(shí)容器添加到 API 的原因——它們允許我們將臨時(shí)容器添加到現(xiàn)有 Pod,從而檢查正在運(yùn)行的 Pod。
雖然臨時(shí)容器是作為 Kubernetes 核心的 Pod 規(guī)范的一部分,但很多人可能還沒有聽說過。這是因?yàn)榕R時(shí)容器處于早期 Alpha 階段,這意味著默認(rèn)情況下不啟用。Alpha 階段的資源和功能可能會(huì)出現(xiàn)重大變化,或者在 Kubernetes 的某個(gè)未來版本中被完全刪除。因此,要使用它們必須在 kubelet 中使用Feature Gate(功能門)顯式啟用。
Configuring Feature Gates
現(xiàn)在如果確定要試用 kubectl debug,那么如何啟用臨時(shí)容器的功能門?這取決于集群設(shè)置。 例如,現(xiàn)在使用kubeadm啟動(dòng)創(chuàng)建集群,那么可以使用以下集群配置來啟用臨時(shí)容器:
- apiVersion: kubeadm.k8s.io/v1beta2
- kind: ClusterConfiguration
- kubernetesVersion: v1.20.2
- apiServer:
- extraArgs:
- feature-gates: EphemeralContainers=true
在以下示例中,為了簡單和測試目的,我們使用 KinD(Docker 中的 Kubernetes)集群,這允許我們指定要啟用的功能門。創(chuàng)建我們的測試集群:
- # 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
隨著集群的運(yùn)行,我們需要驗(yàn)證其有效性。最簡單方法是檢查 Pod API,它現(xiàn)在應(yīng)該包含臨時(shí)容器部分以及通常容器:
- ~ $ kubectl explain pod.spec.ephemeralContainers
- KIND: Pod
- VERSION: v1
- RESOURCE: ephemeralContainers <[]Object>
- DESCRIPTION:
- List of ephemeral containers run in this pod....
- ...
現(xiàn)在都有了,可以開始使用 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>
我們首先啟動(dòng)一個(gè)名為 some-app 的 Pod 來進(jìn)行“調(diào)試”。然后針對這個(gè) Pod 運(yùn)行 kubectl debug,指定 busybox 為臨時(shí)容器的鏡像,并作為原始容器的目標(biāo)。此外,還需要包括 -it 參數(shù),以便我們立即附加到容器獲得 shell 會(huì)話。
在上面的代碼中可以看到,如果我們在 Pod 上運(yùn)行 kubectl debug 后對其進(jìn)行描述,那么它的描述將包括具有之前指定為命令選項(xiàng)值的臨時(shí)容器部分。
Process Namespace Sharing
kubectl debug 是非常強(qiáng)大的工具,但有時(shí)向 Pod 添加一個(gè)容器還不足以獲取 Pod 的另一個(gè)容器中運(yùn)行的應(yīng)用程序相關(guān)信息。當(dāng)故障容器不包括必要的調(diào)試工具甚至 shell 時(shí),可能就是這種情況。在這種情況下,我們可以使用 Process Sharing(進(jìn)程共享)來使用注入的臨時(shí)容器檢查 Pod 的原有容器。
進(jìn)程共享的一個(gè)問題是它不能應(yīng)用于現(xiàn)有的 Pod,因此我們必須創(chuàng)建一個(gè)新 Pod,將其 spec.shareProcessNamespace 設(shè)置為 true,并將一個(gè)臨時(shí)容器注入其中。這樣有點(diǎn)麻煩,尤其是需要調(diào)試多個(gè) Pod 或容器,亦或者需要重復(fù)執(zhí)行該操作時(shí)。幸運(yùn)的是,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;
- ...
上面的代碼表明,通過進(jìn)程共享,我們可以看到 Pod 中另一個(gè)容器內(nèi)的所有內(nèi)容,包括其進(jìn)程和文件,這對于調(diào)試來說非常方便。另外,除了 --share-processes 還包括了 --copy-to=new-pod-name,這是因?yàn)槲覀冃枰獎(jiǎng)?chuàng)建一個(gè)新的 Pod,其名稱由該 flag 指定。如果我們從另一個(gè)終端列出正在運(yùn)行的 Pod,我們將看到以下內(nèi)容:
- # 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
這就是我們在原始應(yīng)用程序 Pod 上的新調(diào)試 Pod。與原始容器相比,它有 2 個(gè)容器,因?yàn)樗€包括臨時(shí)容器。此外,如果想在任何時(shí)候驗(yàn)證 Pod 中是否允許進(jìn)程共享,那么可以運(yùn)行:
- ~ $ kubectl get pod some-app-debug -o json | jq .spec.shareProcessNamespace
- true
好好使用
既然我們已經(jīng)啟用了功能并且知道命令是如何工作的,那就試著使用它并調(diào)試一些應(yīng)用程序。 想象這樣一個(gè)場景——我們有一個(gè)問題應(yīng)用程序,我們需要在它的容器中對網(wǎng)絡(luò)相關(guān)的問題進(jìn)行故障排除。該應(yīng)用程序沒有我們可以使用的必要的網(wǎng)絡(luò) CLI 工具。為了解決這個(gè)問題,我們通過以下方式使用 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
在啟動(dòng)一個(gè) Pod 之后,我們首先嘗試將 shell 會(huì)話放入它的容器中,這看起來有效,但是實(shí)際上我們嘗試運(yùn)行一些基本命令時(shí),將看到那里什么都沒有。所以,我們要使用 praqma/network-multitool 將臨時(shí)容器注入到 Pod 中,該鏡像包含了 curl、ping、telnet 等工具,現(xiàn)在我們可以進(jìn)行所有必要的故障排除。
在上面的例子中,我們進(jìn)入 Pod 的另一個(gè)容器中就足夠了。但有時(shí)可能需要直接查看有問題的容器。這種情況下,我們可以像這樣使用進(jìn)程共享:
- ~ $ 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)
在這里,我們再次運(yùn)行使用 Distroless 鏡像的容器。我們無法在它的 shell 中做任何事情。我們運(yùn)行 kubectl debug 以及 --share-processes --copy-to=...,它創(chuàng)建了一個(gè)新的 Pod,帶有額外的臨時(shí)容器,可以訪問所有進(jìn)程。當(dāng)我們列出正在運(yùn)行的進(jìn)程時(shí),能看到應(yīng)用程序容器的進(jìn)程有 PID 8,可以用它來探索文件和環(huán)境。為此,我們需要通過 /proc/<PID>/... 目錄,這個(gè)例子中是 /proc/8/root/app/...。
另一種常見情況是應(yīng)用程序在容器啟動(dòng)時(shí)不斷崩潰,這讓調(diào)試非常困難,因?yàn)闆]有足夠的時(shí)間將 shell 會(huì)話導(dǎo)入容器并運(yùn)行故障排除命令。在這種情況下,解決方案是創(chuàng)建具有不同入口點(diǎn)、命令的容器,這可以阻止應(yīng)用程序立即崩潰并允許我們調(diào)試:
- ~ $ 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
調(diào)試集群節(jié)點(diǎn)
本文主要關(guān)注 Pod 及其容器的調(diào)試,但任何集群管理員都知道常常需要調(diào)試的是節(jié)點(diǎn)而不是 Pod。幸運(yùn)的是,kubectl debug 允許通過創(chuàng)建 Pod 來調(diào)試節(jié)點(diǎn),該 Pod 將在指定節(jié)點(diǎn)上運(yùn)行,節(jié)點(diǎn)的根文件系統(tǒng)安裝在 /root 目錄中。我們甚至可以用 chroot 訪問主機(jī)二進(jìn)制文件,這本質(zhì)上充當(dāng)了節(jié)點(diǎn)的 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
在上面的代碼中,我們首先確定了想要調(diào)試的節(jié)點(diǎn),然后使用 node/... 作為參數(shù)顯式運(yùn)行 kubectl debug 以訪問我們集群的節(jié)點(diǎn)。在那之后,當(dāng)連接到Pod后,我們使用 chroot /host 突破 chroot,并完全進(jìn)入主機(jī)。最后,為了驗(yàn)證是否真的可以看到主機(jī)上的所有內(nèi)容,我們了查看一部分的 kubeadm.conf,最終看到我們在文章開頭配置的內(nèi)容 feature-gates: EphemeralContainers=true。
小結(jié)
能夠快速有效地調(diào)試應(yīng)用程序和服務(wù)可以節(jié)省大量時(shí)間,但更重要的是,它能極大地幫助解決那些如果不立即解決可能最終會(huì)花費(fèi)大量資金的問題。 這就是為什么 kubectl debug 之類的工具能隨意使用非常重要,即使它們尚未正式發(fā)布或默認(rèn)啟用。如果啟用臨時(shí)容器不是一種選擇,那么嘗試替代調(diào)試方法可能是一個(gè)好主意,例如使用包含故障排除工具的應(yīng)用程序鏡像的調(diào)試版本;或臨時(shí)更改 Pod 的容器命令以阻止其崩潰。