
前几天在一个运维群里看到有人问pod 明明 Running日志里却什么都看不出来怎么进去看一眼底下齐刷刷回复kubectl exec -it xxx -- /bin/bash然后提问的人甩回来一句executable file not found in $PATH。再往上翻还有人被container not found、unable to upgrade connection: Forbidden轮番教育过。这就是 k8s 使用里最典型的一个现象进入 k8s 部署的 pod命令看着只有一行但真要顺利进去、并且进去之后能查出东西中间隔着一堆前提条件。这篇小记不讲编排原理就聊进 pod这一件事——它为什么有时进不去、多容器 pod 到底该进哪个、镜像里连 shell 都没有时怎么办、报错怎么快速分类以及进去之后哪些动作我劝你别做。内容适合刚接手 k8s 环境、正在被排障折磨的同学也适合已经能敲命令但说不清链路的老手对照一遍。1. 进 pod 这件事坑比命令多1.1 一条 kubectl exec 背后到底走了什么链路很多人把kubectl exec当成登录一台机器这是个危险的直觉。它既不是 SSH也不是 docker exec 的平替它是一条经过四五个组件转发的流式请求。完整链路是这样的你的 kubectl 把请求发给 apiserverapiserver 做认证、鉴权通过之后把请求转给目标节点上的 kubeletkubelet 再调用 CRI 接口底层通常是 containerd 或 CRI-O最终由容器运行时在目标容器里 fork 出一个进程把 stdin/stdout 通过一条升级后的长连接SPDY 或 WebSocket双向搬回你的终端。这条链上任何一环出问题报错形态都不一样apiserver 拒绝是Forbiddenkubelet 连不上是error dialing backend运行时找不到容器是container not found容器里没有对应可执行文件是executable file not found。先看报错措辞判断断在哪一环比盲目重启 pod 有用得多。还有一个经常被忽略的点pod 不是运行单位容器才是。pod 只是一个调度和网络的包装壳真正跑进程的是里面的容器。所以进入 pod这个说法其实不严谨准确讲是进入 pod 里的某个容器。单容器 pod 时这个区别无所谓一旦 pod 里塞了 sidecar这个区别就是你能不能查到问题的分水岭。1.2 三条进入路径各自的适用边界我把平时用的进入方式归成三类它们的权限要求、副作用和适用场景差别很大选错了要么白忙要么给自己惹麻烦。方式典型命令前置条件副作用适用场景进业务容器kubectl exec -it pod -c ctr -- sh镜像里有 shellRBAC 有pods/exec权限在业务容器里多一个进程占用其进程空间日常排查、看文件、验证配置临时容器kubectl debug -it pod --imagebusybox --targetctrk8s 1.25集群开启临时容器特性新增一个临时容器pod 不会因此重启镜像无 shelldistroless、需要工具集节点侧进入crictl exec -it id sh/nsenter能登上节点有 root直接操作宿主机风险最高运行时异常、exec 接口本身不通选型的逻辑很简单能用 exec 解决就别用 debug能用 debug 解决就别上节点。越往下走权限越大、审计越难、误伤面越广。我曾经为了图快直接上节点crictl exec结果手滑在宿主机上敲了个 rm好在路径不对从那以后我给自己定了规矩上节点前先把pwd敲三遍。顺带提一句kubectl port-forward和kubectl cp不在上表里但它们经常和进 pod配合使用——一个解决网络不通一个解决把文件取出来分析后面第 4 节会细讲。2. 动手前的两分钟排查找到该进的容器2.1 定位 pod、容器名和所在节点排障时最常见的浪费是进错了 pod。尤其是 Deployment 滚动更新期间同一个应用有两三个 ReplicaSet 的 pod 同时在跑名字前缀差不多你随手复制的那个可能是旧版本的。我的习惯是三步定位。第一步看状态和重启次数kubectl get pods -n ns -o wide | grep app-name-o wide能直接带出所在节点这一步信息量很大——同一节点的多个 pod 同时出问题八成是节点侧的公共依赖挂了。第二步拿容器名kubectl get pod pod -n ns -o jsonpath{.spec.containers[*].name}第三步看这个 pod 到底为什么处于当前状态kubectl describe pod pod -n nsdescribe里的 Events 区是金矿Started、Killing、Unhealthy这些事件会告诉你容器有没有被探针杀过、有没有被 OOMKilled。如果容器正在频繁重启你 exec 进去大概率几秒钟后会被踢出来这时候应该先解决重启问题而不是急着进去翻文件。注意如果 pod 处于CrashLoopBackOffexec 的连接会随着容器重启反复断开你会看到会话莫名其妙退出。这种情况优先用kubectl logs --previous看上一个实例的日志。2.2 权限与镜像能力的自检定位完 pod动手前先做两个自检能省掉一大半的失败尝试。权限自检kubectl auth can-i create pods/exec -n ns返回yes才有资格继续。这里有个细节exec 属于 pod 的子资源RBAC 里要单独授权pods/exec的create动作只有pods的 get/list 权限是不够的。很多同学拿到的是一套只读账号看得到 pod 但进不去报Forbidden不是集群坏了是权限没给。镜像自检kubectl get pod pod -n ns -o jsonpath{.spec.containers[*].image}看到distroless、scratch、alpine这类字眼就要有心理准备distroless 和 scratch 里根本没有 shellalpine 里只有/bin/sh没有 bash。这时候直接用第 4 节的临时容器方案别在 exec 上浪费五次尝试。2.3 判断容器里到底有什么可用如果拿不准镜像里有哪些可执行文件有两个不改动环境的小办法。一是看镜像的历史层docker history image或者用工具看层内容能在本地就摸清布局二是如果你的环境里有 init 容器或者入口脚本翻一下command和args字段kubectl get pod pod -n ns -o jsonpath{.spec.containers[*].command}入口脚本用什么解释器基本就决定了容器里有哪些二进制可用。这个技巧我用得很多比进去之后一条条ls试要快得多。补充一点同一个 pod 里不同容器的镜像可以完全不同业务容器是 distroless、sidecar 是 alpine 是常态所以这个 pod 里有没有 shell这个问题必须按容器回答不能按 pod 回答。3. kubectl exec 实战参数逐条拆解3.1 基础语法和那几个容易搞混的参数标准写法kubectl exec -it pod-name -n namespace -- /bin/sh-i是把标准输入接到容器-t是分配一个 TTY。两个一起用才是交互式会话。只写-t会导致输入没法正常传进去敲键盘没反应只写-i则没有回显、没有提示符命令能跑但体验极差。我见过有人只写-t然后抱怨进去了但打不了字其实就是少了-i。那个--不是装饰品。它告诉 kubectl 后面的内容全部是容器内要执行的命令不要再当成 kubectl 自己的参数解析。少了它kubectl exec pod -it sh有时候能跑通有时候会报奇怪参数错误取决于命令里有没有和 kubectl 标志同名的东西。养成永远写--的习惯能避开一整类玄学问题。还有一个高频错误是路径写错。sh能跑/bin/bash在 alpine 里就报executable file not found。Ubuntu、Debian 系的镜像 bash 在/bin/bashAlpine、BusyBox 只有/bin/sh。另外很多官方镜像为了瘦身把 shell 里的常见命令都删了你进去了却发现ps、netstat、curl全都没有这不是环境坏了是镜像本来就没装。提示如果只是临时需要某个诊断命令可以用kubectl exec pod -- cat /etc/os-release先确认发行版再决定后续用哪套工具。3.2 多容器 pod 到底该进哪一个pod 里只有一个容器时-c可以省略。一旦有多个容器不带-c时 kubectl 默认进第一个容器也就是spec.containers数组里的第一个。这个默认行为坑过不少人想进业务容器结果进了 istio-proxy 或者日志采集 sidecar看到一堆代理配置一脸懵。显式指定kubectl exec -it pod -c container-name -- sh怎么确认该进哪个看容器的职责。业务容器负责处理请求sidecar 负责网络代理、日志转发、指标暴露。判断方法很直接kubectl get pod pod -o jsonpath{.spec.containers[*].name}出来的名字一般能一眼看出来带proxy、sidecar、agent、exporter的多半是辅助容器。有个例外要单独说pod 里其实还有一个隐含的 sandbox 容器pause它负责持有网络命名空间。你kubectl get pod -o jsonpath是看不到它的尝试进它也会失败。这不是容器丢了是它本来就不在containers列表里。排查网络问题时如果想共享它的 netns用的是临时容器的--target机制不是直接 exec。多容器还有一个坑辅助容器出问题会拖垮整个 pod。比如日志 sidecar 因为磁盘写满挂掉pod 的状态可能还是 Running但业务容器已经拿不到日志了。所以进 pod 之前先describe把所有容器的状态扫一遍别只盯着业务容器。3.3 把 exec 用进脚本和自动化流程交互式 exec 是给人用的脚本里千万别带-it。非交互用法是kubectl exec pod -n ns -- env | grep -i timeout这种写法会把容器内命令的退出码原样返回可以直接在 shell 里判断if kubectl exec pod -- test -f /app/config/db.yaml; then echo 配置文件存在 fi退出码有个细节值得记一下容器内命令不存在时通常返回 126 或 127命令本身失败返回它自己的退出码而链路层面的错误连不上 kubelet 之类返回 1。脚本里想区分命令跑失败和根本没连上可以拿这个做粗略判断。批量操作时我会加超时兜底避免一个卡死的 pod 把整个脚本拖住timeout 10 kubectl exec pod -- cat /proc/1/status超时返回 124。这个timeout是外层 shell 的命令不是 kubectl 的所以它对整条链路都有效。另外提一下kubelet 侧对这类长连接有空闲超时保护默认在小时级别长时间挂着不动的会话会被回收。所以别把 exec 会话当持久终端用放在后台挂着忘了收回头再敲命令没响应不是集群出问题是连接被回收了。我的习惯是每次用完立刻exit脚本里的 exec 一律加timeout。4. 镜像里没有 shell临时容器与替代路径4.1 kubectl debug 临时容器的正确姿势distroless 这类无 shell镜像现在很流行因为它体积小、攻击面小。代价就是常规 exec 完全失效。官方的解法是临时容器ephemeral containerkubectl debug -it pod -n ns \ --imagebusybox:1.36 \ --targetcontainer-name \ -- sh这行命令做了三件事一是往现有 pod 里注入一个新的临时容器二是通过--target让它共享目标容器的进程命名空间三是把这个临时容器作为你的工作台。注入不是重启原 pod 的容器不会有任何感知业务不中断——这一点比给镜像加个 shell 重新发布要优雅得多也比上节点 nsenter安全得多。--target是这个命令的灵魂。没有它临时容器只是一个独立的空壳你啥也看不到加上它之后在临时容器里ps aux就能看到目标容器的进程列表能读/proc/pid/root/下的目标容器文件系统还能直接给目标进程发信号。排查无 shell 容器的正确姿势是借一个壳看别人的进程空间而不是想办法往业务镜像里塞工具。用临时容器有几个实际约束要记住。第一版本要求临时容器特性在 k8s 1.25 才正式稳定老集群可能不支持会直接报不认识这个参数。第二临时容器一旦注入就不能删除只能等 pod 整体被删掉所以排查完记得记一笔别让一堆带 debug 容器的 pod 长期挂着。第三临时容器不参与探针、不影响就绪状态但它会占资源如果你给 pod 设了严格的内存 limit注入之后可能触发驱逐生产环境要留意。4.2 借进程命名空间之后具体怎么查进入临时容器后有几组操作是高频的我按使用顺序列一下。看进程ps aux共享进程命名空间后这里会同时显示目标容器的进程和你自己的进程。PID 是相对于这个共享命名空间的所以你能直接对目标进程做操作。找到业务主进程的 PID 后可以看它的启动参数、环境变量、打开的文件cat /proc/pid/environ | tr \0 \n cat /proc/pid/cmdline | tr \0 ls -l /proc/pid/fd这三条基本能回答这个进程到底加载了哪个配置、连的哪个地址、卡在哪个文件描述符上。特别是fd目录如果某个 socket 或者文件一直挂着不释放一眼就能看出来。想看目标容器的文件系统ls /proc/pid/root/app//proc/pid/root是目标进程视角的根目录读它不受你临时容器镜像的限制。这个路径在排查配置文件是不是被覆盖了证书是不是过期了时非常好用。想临时抓包或者测连通性就在临时容器里装工具。因为它是独立容器你可以随便折腾装curl、tcpdump、netcat都不会污染业务容器。这就是它相比直接 exec 进业务容器的最大价值调试工具和业务运行时彻底隔离。业务容器保持精简调试诉求由临时容器兜底这个分工模式我强烈建议推广到团队的排障规范里。4.3 没有 shell 时的其他几条路临时容器不是万能的权限不够或者集群版本太老时会用不了这时候还有几条备选路。第一条是kubectl cp。它要求镜像里有tar但不需要交互式 shellkubectl cp ns/pod:/var/log/app.log ./app.log -c container把文件捞到本地再分析很多场景下比在里面硬查更高效。注意它走的是 exec 通道如果没有 tar 照样会失败会提示exec: tar: executable file not found。这时候可以试kubectl cp的反向用法把必要的静态二进制比如静态编译的 busybox推进去但生产环境推二进制要慎重涉及审计和一致性。第二条是kubectl port-forward。如果问题只是进不去但想连服务端口看看根本不需要 shellkubectl port-forward -n ns pod 18080:8080本地curl localhost:18080就能直接打业务端口验证服务是否正常响应。我处理接口偶尔 502这类问题时第一步基本都是 port-forward 加本地压几发比进容器翻半天日志快得多。第三条是从节点侧看。如果 CRI 层面还正常可以用crictl ps找到容器 ID再用crictl inspect看它的运行配置、挂载、网络命名空间路径。这一步已经属于宿主机操作需要严格授权和审计只在前两条路都走不通时用。上节点之前先确认自己有变更窗口避免在业务高峰期动宿主机。5. 高频报错速查与真实踩坑记录5.1 报错对照表下面这张表是我自己攒的覆盖了这些年遇到过的绝大部分进不去场景。排查时按报错原文对号入座基本能直接定位到环节。报错片段断在哪一环常见根因处理方向executable file not found in $PATH容器内镜像没有该 shell 或命令换/bin/sh或用临时容器container not found运行时容器名写错、pod 正在重启用 jsonpath 重新确认容器名等稳定后再进Forbidden/cannot create resource pods/execapiserver 鉴权RBAC 缺pods/exec的 create 权限找管理员授权或换有权限的上下文error dialing backend/EOFapiserver 到 kubelet节点 kubelet 异常、网络策略拦截查节点状态、看 kubelet 是否可用container is not running运行时容器已退出或卡在启动阶段看 events 和 previous 日志先解决启动问题Unable to use a TTY - input is not a terminal客户端在非终端环境用了-t去掉-t脚本里只用-i或不带-i会话几秒后自动退出pod 生命周期容器被探针判定失败后重启先调探针参数或修复应用启动这张表的价值在于把报错和环节绑定。很多人一看到失败就重启 pod其实Forbidden重启一百次也没用那是权限问题executable file not found重启更没用那是镜像问题。5.2 几条不会写在官方文档里的经验第一条先看 events再敲命令。kubectl describe pod的输出里藏着 80% 的答案。我见过太多人跳过这步直接 exec然后在容器里瞎翻。探针失败、镜像拉取慢、挂载卷失败这些都会在 events 里明说。第二条注意 pod 的启动顺序对你操作的影响。如果 pod 里有 init 容器还在跑主容器其实还没启动这时候 exec 主容器会失败或者进去发现服务没起来。这不是异常是正常流程。看kubectl get pod的STATUS是Init:x/y就别急着进。第三条临时容器注入后 pod 的资源账要重算。有些人给 pod 设了很紧的 memory limit注入一个 busybox 排查工具后加起来超了 limit可能触发驱逐。我的做法是排查期间临时确认一下 limit 余量别在关键业务上翻车。第四条会话超时和exit的习惯。用完一定要exit因为每个挂在后台的 exec 会话都会在 kubelet 侧保留一个流式连接占资源也干扰审计。团队里如果有安全审计要求这些连接都会被记录挂着不关只会给自己添麻烦。第五条容器里不要用交互式命令去改文件。这条不是技术问题是流程问题下一节展开。5.3 一个真实的排查过程复盘说个具体例子感受一下上面的东西怎么串起来。某次一个 Go 服务在 k8s 里偶发超时日志里没有任何异常。我先kubectl get pods -o wide确认只有一个副本节点正常。然后kubectl describe podevents 干净没有重启记录。到这里可以排除生命周期问题。接着我试kubectl exec -it pod -- sh直接报了executable file not found——镜像用的是 distroless。于是我改用临时容器kubectl debug -it pod --imagebusybox:1.36 --targetapp -- sh进去之后ps aux找到主进程 PIDcat /proc/pid/environ看环境变量发现连接池大小被环境变量覆盖成了一个很小的值和配置文件里的不一致。再回看 Deployment 的 env 配置果然是有人后来加了一条覆盖。整个排查从报错到定位大概十分钟关键在于每一步都基于上一层的结论做选择而不是东敲一下西敲一下。这个案例还说明一件事偶发超时不一定是网络问题配置漂移是很常见的原因。而配置漂移只有在你能看到进程真实的运行时状态时才能发现这就是进 pod这项技能的价值所在。6. 进去之后把临时操作变成可复现的结论6.1 不要在容器里直接改配置这是我踩过最贵的一个坑。早年线上一个服务出问题我 exec 进去顺手改了配置文件重启了容器内进程服务恢复了我很得意。结果第二天 Pod 因为节点维护被重新调度新 pod 从镜像启动我改的东西全没了问题原样复现而且没人知道前一天到底改了什么。容器的文件系统是可抛弃的任何在容器里的手改都不具备持久性也不会被版本控制记录。正确的做法是容器里只做读操作和临时验证比如改一个环境变量跑一次、看日志、抓个包。一旦确认了修复方案回到 Deployment/StatefulSet 的 YAML 上去改走正常的发布流程。这样变更可追溯、可回滚、可复制到其他副本。如果确实需要临时验证某个文件的效果可以用kubectl cp把文件推进去试一次验证完立刻恢复并且在群里或者工单里留个记录。我现在的习惯是凡是往容器里写东西的操作先在笔记里写一行我改了 X在哪里为什么什么时候恢复。6.2 把一次排查沉淀成能复用的动作同样的故障不要查两遍。我的做法是每次排查完把怎么发现的、用了哪条命令、结果是什么、根因是什么整理成一段简短的记录。时间长了这些记录会自动聚类成几种固定模式生命周期类重启、探针、配置类环境变量、挂载、依赖类下游服务、网络、资源类内存、磁盘。针对这四类我准备了四个固定的动作模板。生命周期类直接看 describe 加 previous 日志配置类用临时容器看 environ 和挂载点依赖类先 port-forward 验证本地连通再看服务暴露和网络策略资源类看 cgroup 的限制值和实际用量。有了模板新人上来也能按图索骥不用每次从零摸索。最后分享一个我自己一直在用的小习惯给常用的排查命令做一层薄封装比如一个kdebug脚本传入 pod 名自动判断容器数量、自动挂上临时容器、自动带 busybox 镜像。省下来的敲键盘时间不多但它把正确的排查路径固化成了一个命令比口头强调十遍都管用。我把它放在团队共享的脚本仓库里现在基本没有人再问这个镜像没 shell 怎么进了。