调试容器化的Pod是每个使用Kubernetes开发人员和DevOps工程师的日常任务。通常,简单的kubectl logs、kubectl describe pod
就足以找到某些问题的罪魁祸首,但有些问题很难找到。在这些情况下,您可能会尝试使用kubectl exec
,但这可能还不够,因为一些容器(如Distroless)甚至不包含您可以SSH进入的shell。如果以上都失败了,我们还能怎么办呢?
这或许是更好的方法
有时你需要拿一个更大的锤子或使用更合适的工具来完成手头的任务。在Kubernetes上调试工作问题时,合适的工具应该是kubectl debug
,它是不久前添加的一个新命令(v1.18),允许您调试正在运行的pod。它将名为EphemeralContainer的特殊类型的容器注入到有问题的Pod中,允许您四处查看并进行故障排除。这对于介绍中描述的情况或其他交互式调试场景下更可取或更有效。因此,kubectl debug
看起来是可行的方法。但要使用它,我们需要临时性的容器ephemeral containers
,那么这些到底是什么呢?
临时容器是Pod中的一个子资源,类似于普通容器。不过,与常规容器不同的是,临时容器并非用于构建应用程序,而是用于检查应用程序。我们不会在Pod创建时定义它们,而是使用特殊的API将它们注入到运行Pod中,以运行故障排除命令和检查Pod的环境。除了这些不同之外,临时容器还缺少一些基本容器的字段,如port或resource。
但我们为什么需要它们呢?我们不能只用基本的容器吗?好吧,你不能把容器添加到Pod,因为它们应该是一次性的(或者换句话说,在任何时候删除和重新创建),这可能会使故障排除困难,很难重现需要检查的bug。这就是为什么将临时容器添加到API中——它们允许您将容器添加到现有的容器中,从而更容易检查正在运行的容器。
考虑到临时容器是Pod规范的一部分,而Pod规范是Kubernetes的核心,为什么你(可能)还没有听说过它呢?这些特性大多是未知的原因是因为临时容器处于Alpha早期阶段,这意味着它们在默认情况下没有启用。这一阶段的资源和特性可能会在Kubernetes的未来版本中发生重大变化或被完全删除。因此,要使用它们,你必须明确地使用kubelet中的Feature Gates来启用它们。
配置启用特性功能
我们已经确定要尝试kubectl debug
,那么如何启用临时容器特性门呢?这取决于您的集群设置。例如,如果你正在使用kubeadm来创建集群,那么你可以使用以下的ClusterConfiguration来启用临时容器:
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
kubernetesVersion: v1.20.2
apiServer:
extraArgs:
feature-gates: EphemeralContainers=true
在下面的例子中,我们将使用KinD (Docker中的Kubernetes)集群作为简单集群用于测试目的,这也允许我们指定我们想要启用的Feature Gates。因此,要创建我们需要的集群:
# 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,它现在应该在通常的容器旁边包含ephemeralContainers部分:
~ $ 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,这样我们就可以debug一些东西。然后我们对这个Pod运行kubectl调试,指定busybox作为临时容器镜像,并指定一个目标作为原始容器。此外,我们还包括-it参数,以便我们立即附加到容器并获得一个shell会话。
在上面的代码片段中,您还可以看到,如果我们在运行kubectl debug
后描述Pod,那么它的描述将包括Ephemeral Containers部分,其中的值是我们在前面指定的命令选项。
共享进程空间
kubectl debug
是一个非常强大的工具,但有时向Pod添加另一个容器可能不足以获取在Pod的其他容器中运行的应用程序的相关信息。例如当进行故障排除的容器不包括必要的调试工具甚至shell时。在这种情况下,我们可以使用进程共享注入临时容器来检查Pod的原始容器。
但进程共享的一个问题是,它不能应用于现有的Pods,因此我们必须创建一个新的,将spec.shareProcessNamespace设置为true,并向其注入一个临时容器。这样做会非常麻烦,特别是当我们必须调试多个Pod/Container或只是重复执行此操作时。幸运的是,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中其他容器中的所有内容,包括它的进程和文件,这对于调试无疑是非常方便的。
您可能已经注意到,除了——shared-processes之外,我们还包含了——copy-to=new-pod-name,因为我们需要创建一个新的pod,它的名称由这个标志指定。如果我们从另一个终端列出运行的pods,我们将看到以下内容:
# 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。与原来的容器相比,它有两个容器,因为它还包括临时容器。
此外,如果你想验证进程共享是否被允许,那么你可以运行:
~ $ 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 image
将临时容器注入到pod中,其中包含curl、ping、telnet
等工具,现在我们可以执行所有必要的故障排除。
在上面的例子中,我们在Pod中添加另一个容器就足够了。但有时,您可能需要直接查看有问题的容器,而没有办法进入其shell。在这种情况下,我们可以像这样利用进程共享:
~ $ 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 with——share-processes——copy-to=…
,它会创建一个新的Pod,其中包含可以访问所有进程的额外临时容器。然后列出正在运行的进程,可以看到应用程序容器的进程有PID 8,我们可以用它来研究它的文件和环境。要做到这一点,我们必须通过/proc/>PID</
…目录—在本例中是- /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
附加功能-调试集群节点
本文主要关注调试Pods和它们的容器——但是任何集群管理员都知道——通常需要调试的是节点而不是Pods。幸运的是,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的一部分,在这可以看到特性:EphemeralContainers=true
,这是我们在本文开始时配置的。
总结
能够快速有效地调试应用程序和服务可以为您节省大量时间,但更重要的是,它可以极大地帮助您解决无法立即解决或者可能会导致您花费大量金钱的问题。这就是为什么让kubectl这样的工具在您的使用和启用时很重要,即使它们不是GA或默认启用。
无论出于什么原因-启用临时容器可能不是一个最优选项,那么尝试其他调试方法可能是一个好主意,例如使用调试版本的应用程序镜像,其中将包括故障排除工具;或者临时改变Pod的容器的命令指令,以防止它崩溃。
kubectl调试和临时容器只是Kubernetes许多有用的功能之一——但很少有人知道——所以请密切关注后续文章,这些文章将深入研究Kubernetes的其他一些隐藏功能。