调试容器化工作负载和Pod是每个使用Kubernetes的开发人员和DevOps工程师的日常任务。通常,简单地使用kubectllogs或kubectldescribepod就可以发现问题,但有时,有些问题会特别难发现。在这种情况下,可以尝试kubectlexec,但有时这不起作用,因为像Distroless这样的容器甚至不允许SSH进入shell。那么如果以上方法都失败了怎么办呢?更好的方法其实我们只需要使用更合适的工具即可。如果您在Kubernetes上调试工作负载,正确的工具是kubectldebug。这是不久前添加的一个新命令(v1.18),它允许调试正在运行的pod。它将一个名为EphemeralContainer(临时容器)的特殊容器注入到问题Pod中,方便我们查看和排查。kubectldebug看起来很不错,但是使用它需要一个临时容器,究竟什么是临时容器?临时容器其实就是Pod中的子资源,类似于普通容器。但与普通容器不同,临时容器不用于构建应用程序,而是用于检查。我们不是在创建Pod时定义它们,而是使用特殊的API将它们注入正在运行的Pod中以运行命令并检查Pod环境。除了这些差异之外,临时容器还缺少基础容器的一些字段,比如端口和资源。那么我们为什么不直接使用基础容器呢?这是因为我们不能将基础容器添加到pod中,它们应该是一次性的(需要随时删除或重新创建),这使得问题pod中的错误很难重现,并且排查起来很麻烦。这就是将临时容器添加到API的原因——它们允许我们将临时容器添加到现有的Pod中,从而检查正在运行的Pod。虽然临时容器是Kubernetes核心的Pod规范的一部分,但你们中的许多人可能没有听说过它。这是因为临时容器处于早期alpha阶段,这意味着它们在默认情况下未启用。处于alpha阶段的资源和功能可能会发生重大变化,或者在Kubernetes的某些未来版本中被完全删除。因此,要使用它们,必须使用FeatureGate在kubelet中显式启用它们。配置FeatureGates现在如果你确定要尝试kubectldebug,如何启用临时容器的featuregate?这取决于集群设置。例如,如果您使用kubeadm创建集群,则可以使用以下集群配置启用临时容器:apiVersion:kubeadm.k8s.io/v1beta2kind:ClusterConfigurationkubernetesVersion:v1.20.2apiServer:extraArgs:feature-gates:EphemeralContainers=true在下面的示例中,为了简单和测试目的,我们使用KinD(Docker中的Kubernetes)集群,它允许我们指定启用哪些功能门。创建我们的测试集群:#File:config.yaml#Run:kindcreatecluster--config./config.yaml--namekind--image=kindest/node:v1.20.2kind:ClusterapiVersion:kind.x-k8s.io/v1alpha4featureGates:EphemeralContainers:truenodes:-role:control-plane在集群运行的情况下,我们需要验证其有效性。最简单的方法是检查PodAPI,它现在应该包含临时容器部分以及常用容器:~$kubectlexplainpod.spec.ephemeralContainersKIND:PodVERSION:v1RESOURCE:ephemeralContainers<[]Object>DESCRIPTION:Listofephemeralcontainersruninthispod。......现在有了现在,你可以开始使用kubectldebug了。从简单的例子开始:~$kubectlrunsome-app--image=k8s.gcr.io/pause:3.1--restart=Never~$kubectldebug-itsome-app--image=busybox--target=some-appDefaultingdebugcontainernametodebugger-tfqvh.如果你没有看到命令提示符,试着按回车键。/##Fromotherterminal...~$kubectldscribepodsome-app...Containers:some-app:ContainerID:containerd://60cc537eee843cb38a1ba295baaa172db8344eea59de4d75311400436d4a5083Image:k8s.guse:g3.io1.io/pause@sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea...EphemeralContainers:debugger-tfqvh:ContainerID:containerd://12efbbf2e46bb523ae0546b2369801b51a61e1367dda839ce0e02f0e5c1a49d6Image:busyboxImageID:docker.io/library/busybox@sha256:ce2360d5189a033012fbad1635e037be86f23b65cfd676b436d0931af390a2acPort:HostPort:State:RunningStarted:Mon,15Mar202120:33:51+0100Ready:FalseRestartCount:0Environment:Mounts:我们首先启动一个名为some-app的Pod以进行“调试”。然后针对此Pod运行kubectldebug,将busybox指定为临时容器映像和原始容器的目标。此外,需要包含-it参数,以便我们立即附加到容器以获取shell会话。正如您在上面的代码中看到的,如果我们在运行kubectldebug之后描述一个Pod,它的描述将包括临时容器部分,其中包含先前指定为命令选项的值。ProcessNamespaceSharingkubectldebug是一个非常强大的工具,但有时将一个容器添加到Pod中并不足以获取有关在该Pod的另一个容器中运行的应用程序的信息。当故障容器不包含必要的调试工具甚至shell时,可能会出现这种情况。在这种情况下,我们可以使用ProcessSharing来检查Pod的原始容器和注入的临时容器。进程共享的一个问题是它不能应用于现有的Pod,因此我们必须创建一个新的Pod,将其spec.shareProcessNamespace设置为true,并向其中注入一个临时容器。这可能有点麻烦,特别是如果您需要调试多个pod或容器,或者如果您需要重复操作。幸运的是,kubectl调试可以使用--share-processes:~$kubectlrunsome-app--image=nginx--restart=Never~$kubectldebug-itsome-app--image=busybox--share-processes--copy-to=some-app-debugDefaultingdebugcontainernametodebugger-tkwst.Ifyoudon'ttseeacommandprompt,trypressingenter./#psaxPIDUSERTIMECOMMAND1root0:00/pause8root0:00nginx:masterprocessnginx-gdaemonoff;381010:00nginx:workerprocess$rootprocess$rootprocess39rootax6cat0~0:0x/etc/等d/default.confserver{listen80;listen[::]:80;server_namelocalhost;...上面的代码表明,通过进程共享,我们可以看到Pod中另一个容器的所有内容,包括它的进程和文件,很方便用于调试。此外,除了--share-processes之外,还包括--copy-to=new-pod-name因为我们需要使用此标志指定的名称创建一个新的Pod。如果我们从另一个终端列出正在运行的pod,我们会看到以下内容:#Fromotherterminal:~$kubectlgetpodsNAMEREADYSTATUSRESTARTSAGEsome-app1/1Running023hsome-app-debug2/2Running020s这是我们在原始应用程序pod上的新调试pod。与原始容器相比,它有2个容器,因为它还包括临时容器。此外,如果在任何时候你想验证你的Pod中是否允许进程共享,你可以运行:~$kubectlgetpodsome-app-debug-ojson|jq.spec.shareProcessNamespacetrue然后尝试使用它并调试一些应用程序。想象一下这个场景——我们有一个有问题的应用程序,我们需要解决其容器中的网络相关问题。该应用程序没有我们可以使用的必要WebCLI工具。为了解决这个问题,我们使用kubectldebug通过:~$kubectlrundistroless-python--image=martinheinz/distroless-python--restart=Never~$kubectlexec-itdistroless-python--/bin/sh#id/bin/sh:1:id:notfound#ls/bin/sh:2:ls:notfound#env/bin/sh:3:env:notfound#...kubectldebug-itdistroless-python--image=praqma/network-multitool--target=distroless-python--shDefaultingdebugcontainernametodebugger-rvtd4.Ifyoudon'ttseeacommandprompt,trypressingenter./#pinglocalhostPINGlocalhost(localhost(::1))56databytes64bytesfromlocalhost(::1):icmp_seq=1ttl=64time=0.025ms64bytesfromlocalhost(::1)ict=lseq=264time=0.044ms64bytesfromlocalhost(::1):icmp_seq=3ttl=64time=0.027ms在启动一个pod之后,我们首先尝试将一个shellsession放入它的容器中,这似乎可行,但实际上我们在尝试运行一些基本命令,你会看到那里什么都没有。因此,我们将使用praqma/network-multitool将临时容器注入Pod,该镜像包含curl、ping、telnet等工具。现在我们可以进行所有必要的故障排除。在上面的示例中,我们进入Pod的另一个容器就足够了。但有时可能需要直接查看有问题的容器。在这种情况下,我们可以像这样使用进程共享:~$kubectlrundistroless-python--image=martinheinz/distroless-python--restart=Never~$kubectldebug-itdistroless-python--image=busybox--share-processes--copy-to=distroless-python-debugDefaultingdebugcontainernametodebugger-l692h.Ifyoudon'ttseeacommandprompt,trypressingenter./#psaxPIDUSERTIMECOMMAND1root0:00/pause8root0:00/usr/bin/python3.5sleep.py#Originalcontainerisjustcatsleepingforever14rootapp/proaxleep0:00s/sh2复制到=distroless-python-debugDefaultingdebugcontainernametodebugger-l692h。.pyimporttimeprint("sleepingfor1hour")time.sleep(3600)这里我们使用Distroless镜像再次运行容器。我们不能在它的外壳中做任何事情。我们使用--share-processes--copy-to=...运行kubectldebug,这会创建一个新的Pod,其中包含可以访问所有进程的额外临时容器。当我们列出正在运行的进程时,我们可以看到应用程序容器的进程具有PID8,可用于探索文件和环境。为此,我们需要遍历/proc//...目录,在本例中为/proc/8/root/app/...。另一种常见情况是应用程序在容器启动时不断崩溃,这使得调试非常困难,因为没有足够的时间将shell会话导入容器并运行故障排除命令。在这种情况下,解决方案是创建具有不同入口点命令的容器,这会立即阻止应用程序崩溃并允许我们进行调试:~$kubectlgetpodsNAMEREADYSTATUSRESTARTSAGEcrashing-app0/1CrashLoopBackOff18s~$kubectldebugcrashing-app-it--copy-to=crashing-app-debug--container=crashing-app--shIfyoudon'tseeacommandprompt,trypressingenter.#iduid=0(root)gid=0(root)groups=0(root)#...#Fromanotherterminal~$kubectlgetpodsNAMEREADYSTATUSRESTARTSAGEcrashing-app0/1CrashLoopBackOff32m7scrashing-app-debug1/1Running016sDebuggingClusterNodes本文着重于调试Pod及其容器,但任何集群管理员都知道需要调试的往往是节点,而不是Pod。幸运的是,kubectldebug允许通过创建将在指定节点上运行的Pod来调试节点,该节点的根文件系统安装在/root目录中。我们甚至可以使用chroot访问主机二进制文件,它实际上充当与节点的SSH连接:~$kubectlgetnodesNAMESTATUSROLESAGEVERSIONkind-control-planeReadycontrol-plane,master25hv1.20.2~$kubectldebugnode/kind-control-plane-it--image=ubuntuCreatingdebuggingpodnode-debugger-kind-control-plane-hvljtwithcontainerdebuggeronnodekind-control-plane.Ifyoudon'ttseeacommandprompt,trypressingenter.root@kind-control-plane:/#chroot/host#headkind/kubeadm.confapiServer:certSANs:-localhost-127.0.0.1extraArgs:feature-gates:EphemeralContainers=trueruntime-config:""apiVersion:kubeadm.k8s.io/v1beta2clusterName:kindcontrolPlaneEndpoint:kind-control-plane:6443在上面的代码中,我们首先确定了我们要调试的节点,然后使用node/...显式运行kubectldebug作为参数来访问我们集群的节点。之后,当连接到pod时,我们使用chroot/host跳出chroot并获得对主机的完全访问权限。最后,为了验证我们真的可以看到主机上的所有内容,我们查看了kubeadm.conf的一部分,终于看到了我们在文章开头配置的feature-gates:EphemeralContainers=true。总结能够快速有效地调试应用程序和服务可以节省大量时间,但更重要的是,它可以极大地帮助解决如果不立即修复可能最终会花费大量资金的问题。这就是为什么像kubectldebug这样的工具随时可用很重要,即使它们没有正式发布或默认启用。如果启用临时容器不是一个选项,那么尝试其他调试方法可能是个好主意,例如使用包含故障排除工具的应用程序映像的调试版本;或者临时更改pod的容器命令以防止其崩溃。