Kubernetes 调试证据链:logs、Events、exec、debug 与 port-forward
Pod 在重启,Service 也访问不了
一次发布后,kubectl get pods 显示 CrashLoopBackOff。同事先执行 exec,却因为容器已经退出而失败;临时把镜像换成带 shell 的版本后,Pod 终于运行,port-forward service/... 又提示没有可用 Pod。此时继续随机敲命令,只会把启动故障、容器工具缺失和 Service 选路故障混在一起。
Kubernetes 调试的关键不是记住更多子命令,而是判断失败发生在哪个状态转换。调度器、kubelet、容器运行时、应用进程、就绪探针、Service 控制器和端口转发各自留下不同证据。先读对象状态,再读 Event,再读进程输出;只有目标容器稳定运行时才 exec,镜像没有工具时才引入调试容器;应用可用后,再检查 Service 与 EndpointSlice。这个顺序能避免“一个命令失败,就误判整层系统”。
建一个故意失败、但不依赖业务代码的现场
实验只需要一个可用 Kubernetes 集群和与 API Server 兼容的 kubectl。首次变更前先固定目标;共享集群还必须确认自己拥有创建实验对象的权限。以下命令中的 context 使用 k3d-tooling-lab,若使用其他本地集群,只替换这个显式值,不依赖终端当前状态。
kubectl --context k3d-tooling-lab version
kubectl --context k3d-tooling-lab auth can-i create deployments -n tooling-lab
kubectl --context k3d-tooling-lab auth can-i get pods/log -n tooling-lab
kubectl --context k3d-tooling-lab auth can-i create pods/exec -n tooling-lab
kubectl --context k3d-tooling-lab auth can-i create pods/portforward -n tooling-lab
kubectl --context k3d-tooling-lab auth can-i update pods/ephemeralcontainers -n tooling-lab第一个调用同时显示客户端与 server,能暴露 context、网络或证书问题。后五个调用只做授权判定,计划使用的能力应输出 yes;no 表示命令会在 API 授权阶段失败,不等于 Pod 或应用坏了。创建 Namespace 本身是集群级动作,共享集群通常应由平台预建;生产环境不应为了调试临时授予 cluster-admin,后文会给出命名空间内角色。
下面这份完整清单制造两个连续故障。Deployment 的 init container 要求 ConfigMap checkout-config 提供 /config/app.conf,但清单故意没有创建它,所以 Pod 会在初始化阶段失败。Service 的 selector 又故意写成 app: checkout-web,与 Pod 的 app: checkout-api 不一致;修好启动后,Service 仍然没有 EndpointSlice 后端。
apiVersion: v1
kind: Namespace
metadata:
name: tooling-lab
labels:
owner: platform-learning
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-api
namespace: tooling-lab
spec:
replicas: 1
selector:
matchLabels:
app: checkout-api
template:
metadata:
labels:
app: checkout-api
spec:
initContainers:
- name: validate-config
image: busybox:1.36
command:
- sh
- -c
- |
echo "validating /config/app.conf"
test -s /config/app.conf
grep -q '^mode=dev$' /config/app.conf
volumeMounts:
- name: config
mountPath: /config
containers:
- name: app
image: nginx:1.27-alpine
ports:
- name: http
containerPort: 80
readinessProbe:
httpGet:
path: /
port: http
periodSeconds: 2
failureThreshold: 3
volumeMounts:
- name: config
mountPath: /etc/tooling
readOnly: true
volumes:
- name: config
configMap:
name: checkout-config
---
apiVersion: v1
kind: Service
metadata:
name: checkout-api
namespace: tooling-lab
spec:
selector:
app: checkout-web
ports:
- name: http
port: 80
targetPort: http保存为 k8s/debug/checkout-failure.yaml。server-side dry-run 不会持久化同一文件前面的 Namespace,因此要先创建实验 namespace,再让 API Server 校验其余对象;随后应用完整声明:
kubectl --context k3d-tooling-lab create namespace tooling-lab \
--dry-run=client -o yaml | \
kubectl --context k3d-tooling-lab apply -f -
kubectl --context k3d-tooling-lab apply --server-side --dry-run=server \
-f k8s/debug/checkout-failure.yaml
kubectl --context k3d-tooling-lab apply -f k8s/debug/checkout-failure.yamldry-run 成功只证明 schema、准入和当前权限接受这些对象,不证明容器能启动。真正应用后,等待几秒再取一份紧凑状态:
kubectl --context k3d-tooling-lab -n tooling-lab get pods \
-l app=checkout-api -o wide预期 READY 是 0/1,STATUS 常见为 Init:CreateContainerConfigError。这已经把故障归到 init container 之前或期间,尚未到主容器应用进程。
Events 先解释“平台为什么没有启动它”
Event 是控制器、调度器和 kubelet 对对象采取动作时记录的短期证据。按对象 UID 过滤比扫全 namespace 更稳定:
POD=$(kubectl --context k3d-tooling-lab -n tooling-lab get pod \
-l app=checkout-api -o jsonpath='{.items[0].metadata.name}')
POD_UID=$(kubectl --context k3d-tooling-lab -n tooling-lab get pod "$POD" \
-o jsonpath='{.metadata.uid}')
kubectl --context k3d-tooling-lab -n tooling-lab events \
--for pod/$POD --types=Warning,Normal旧版 server 或 kubectl 不支持 kubectl events 时,退回:
kubectl --context k3d-tooling-lab -n tooling-lab get events \
--field-selector involvedObject.uid=$POD_UID \
--sort-by=.metadata.creationTimestamp预期 Warning 的核心信息是 configmap "checkout-config" not found。这条证据来自 kubelet在创建容器配置时解析 volume,应用进程还没有运行,因此此时 kubectl logs ... -c app 没有业务日志完全正常。
Event 有保留期,会聚合重复事件,也不是可靠的长期审计日志。事故响应中应尽早采集,同时以对象状态、集中日志和 API 审计补全时间线。不要把 Event 数量直接当成故障次数或 SLO 指标。
describe 把状态、配置和最近事件放在一起
describe 不是另一个日志命令。它读取对象及相关 Event,适合回答“控制面期望什么、运行时现在是什么”。
kubectl --context k3d-tooling-lab -n tooling-lab describe pod "$POD"输出中应同时看到四组关键字段:Init Containers 下的 Waiting Reason、Containers 下主容器尚未开始、Volumes 下引用 checkout-config,以及末尾的 Warning Event。字段之间形成因果链:Pod 已调度到节点,volume 引用无法解析,init container 未创建,主容器自然不会开始。
需要机器处理时不要解析 describe 文本,直接读取结构化状态:
kubectl --context k3d-tooling-lab -n tooling-lab get pod "$POD" -o jsonpath='{range .status.initContainerStatuses[*]}{.name}{" waiting="}{.state.waiting.reason}{" message="}{.state.waiting.message}{"\n"}{end}'预期 waiting=CreateContainerConfigError,message 直接指出缺失的 ConfigMap。脚本应依赖这些 API 字段;describe 面向人读,其格式不承诺稳定接口。
logs 只回答“进程写了什么”
当前现场中,app 从未启动,读取它的日志可能返回 container ... is waiting to start。这不是 logs 失效,而是有价值的反证:故障发生在业务进程之前。
kubectl --context k3d-tooling-lab -n tooling-lab logs "$POD" -c app --tail=100创建缺失 ConfigMap,让 init container 真正运行:
kubectl --context k3d-tooling-lab -n tooling-lab create configmap checkout-config \
--from-literal=app.conf='mode=dev'
kubectl --context k3d-tooling-lab -n tooling-lab rollout status \
deployment/checkout-api --timeout=90s预期 rollout 完成,Pod 变为 1/1 Running。现在可以分别读 init 与 app:
POD=$(kubectl --context k3d-tooling-lab -n tooling-lab get pod \
-l app=checkout-api -o jsonpath='{.items[0].metadata.name}')
kubectl --context k3d-tooling-lab -n tooling-lab logs "$POD" -c validate-config
kubectl --context k3d-tooling-lab -n tooling-lab logs "$POD" -c app --tail=20init 日志应包含 validating /config/app.conf。Nginx 主容器在没有请求时可能只有启动日志;不能把“没有新日志”直接判定为应用无响应。
为了让 --previous 产生确定证据,再创建一个只打印错误并退出的 Pod。restartPolicy: Always 会让 kubelet 重启它;等待 Ready 必然超时不是好断言,因此直接观察重启计数和等待状态:
kubectl --context k3d-tooling-lab -n tooling-lab run crash-probe \
--image=busybox:1.37 --restart=Always -- \
sh -c 'echo "fatal: demo startup failure" >&2; exit 17'
kubectl --context k3d-tooling-lab -n tooling-lab get pod crash-probe -w看到 RESTARTS 至少为 1 后按 Ctrl+C 停止 watch。Pod 会从 Error 进入 CrashLoopBackOff;退避期间当前容器可能尚未产生输出,而 --previous 读取同一 Pod 上一个已终止容器实例:
kubectl --context k3d-tooling-lab -n tooling-lab logs crash-probe \
--previous --tail=100 --timestamps
kubectl --context k3d-tooling-lab -n tooling-lab get pod crash-probe \
-o jsonpath='{.status.containerStatuses[0].lastState.terminated}{"\n"}'第一条应包含 fatal: demo startup failure,第二条应出现 exitCode:17、reason:Error 和终止时间,两份证据指向同一次失败。把 --previous 改到从未重启的 checkout-api app 容器,会得到 previous terminated container ... not found;这只是说明该 Pod 没有上一实例。容器被控制器替换成新 Pod 后,--previous 不会跨 Pod 保存历史,长期诊断必须依赖集中日志。
exec 的成功依赖一个正在运行的目标进程
Pod Ready 后,exec 通过 API Server 的 pods/exec 子资源在现有容器里启动新进程。命令中的 -- 分隔 kubectl 参数和容器内命令;多容器 Pod 必须显式 -c,避免默认容器变化。
kubectl --context k3d-tooling-lab -n tooling-lab exec "$POD" -c app -- \
cat /etc/tooling/app.conf
kubectl --context k3d-tooling-lab -n tooling-lab exec "$POD" -c app -- \
wget -qO- http://127.0.0.1:80/第一条预期输出 mode=dev,证明 ConfigMap 已挂载到主容器;第二条绕过 Service,从容器网络命名空间访问本地监听端口,预期返回 Nginx HTML。若 cat 成功而 HTTP 失败,故障已经缩到应用进程、监听地址或容器端口,而不是 ConfigMap 或 Service selector。
exec -it ... -- sh 方便,但不是所有镜像都有 shell。distroless、scratch 或最小镜像出现 exec: "sh": executable file not found,只能说明镜像里没有这个可执行文件。生产镜像不应为了临时排障长期加入 shell、curl 和包管理器,那会扩大供应链与攻击面。
debug 与 ephemeral container:把工具带到现场
目标容器正在运行但缺少网络工具时,可以向现有 Pod 添加临时容器:
kubectl --context k3d-tooling-lab -n tooling-lab debug -it pod/"$POD" \
--image=busybox:1.36 \
--target=app \
--container=debugger -- sh--target=app 请求加入目标容器的进程命名空间,能否看到目标进程取决于容器运行时支持。临时容器共享 Pod 网络命名空间,所以在 shell 内执行 wget -qO- http://127.0.0.1:80/ 可以验证监听;它不会自动获得 app 的文件系统根目录,也不能假设看见 app 的 volume mount。退出 shell 后,ephemeral container 记录仍留在 Pod spec/status,不能像普通容器一样删除或重启,只能随 Pod 删除。
如果原 Pod 根本无法运行,添加 ephemeral container 未必能解决初始化或挂载问题。可以复制 Pod 并修改命令:
kubectl --context k3d-tooling-lab -n tooling-lab debug pod/"$POD" -it \
--copy-to=checkout-api-debug \
--container=app \
-- sh复制会创建新 Pod,适合调整命令、镜像或探针后观察,但它不是原 Pod,Service selector、身份、Secret、调度位置和网络状态都要重新核对。调试 Node 的 kubectl debug node/... 权限更高,可能进入宿主命名空间并挂载节点文件系统,不应下放给普通开发角色。
官方已将 ephemeral containers 作为稳定能力,仍受 pods/ephemeralcontainers 子资源授权、Pod Security、准入策略和运行时实现约束。--profile=general、sysadmin 等 profile 会改变 capabilities 与安全上下文;不要在共享或生产集群凭经验套用高权限 profile。先在隔离环境验证策略,再由值班授权。
cp 不是文件系统取证平台
kubectl cp 通过容器内的 tar 打包或解包。镜像没有 tar 时会失败;复制大目录会占用 API 通道、容器 CPU、网络和本地磁盘。先制造一份无敏感内容的小诊断文件:
kubectl --context k3d-tooling-lab -n tooling-lab exec "$POD" -c app -- \
sh -c 'printf "pod=%s\n" "$HOSTNAME" > /tmp/diagnostic.txt'
kubectl --context k3d-tooling-lab -n tooling-lab cp \
"$POD":/tmp/diagnostic.txt ./diagnostic.txt -c app本地文件应只有 Pod 名。若报 tar: not found,可以让应用把诊断输出到标准输出、写到受控对象存储,或针对单文件用 exec -- cat 重定向;不要临时在线安装工具改变事故现场。
容器内的 heap dump、配置、token、证书和请求样本可能包含个人信息或凭证。复制前要有数据分类与最小化,复制后记录操作者、对象、目的、保存位置与删除时间。kubectl cp 请求在审计层通常体现为 pods/exec,文件内容不会自动进入 Kubernetes 审计日志,不能把“API 有审计”误认为“取出的数据已受完整审计”。
第二个故障:Pod 好了,Service 没有后端
现在 Pod Ready,但故意写错的 Service selector 仍未修。先看 Service 声明和 EndpointSlice,不要直接怀疑 kube-proxy:
kubectl --context k3d-tooling-lab -n tooling-lab get service checkout-api -o wide
kubectl --context k3d-tooling-lab -n tooling-lab get pods \
-l app=checkout-web --show-labels
kubectl --context k3d-tooling-lab -n tooling-lab get endpointslice \
-l kubernetes.io/service-name=checkout-api -o wide第二条应返回 No resources found,EndpointSlice 的 ENDPOINTS 为空。Service 控制器按 selector 匹配 Pod labels,把合格地址写入 EndpointSlice;Service 对象存在并不意味着有后端。旧的 Endpoints API 已弃用,诊断与自动化应优先读取 EndpointSlice。
先绕过 Service 验证 Pod 本身:
kubectl --context k3d-tooling-lab -n tooling-lab port-forward \
--address 127.0.0.1 pod/"$POD" 18081:80保持会话运行,在另一个终端访问:
curl http://127.0.0.1:18081/如果 Pod port-forward 返回 Nginx 页面,而 Service EndpointSlice 为空,应用和 Pod 端口已经基本成立,问题就在 Service 选路。用 patch 修正 selector:
kubectl --context k3d-tooling-lab -n tooling-lab patch service checkout-api \
--type merge -p '{"spec":{"selector":{"app":"checkout-api"}}}'
kubectl --context k3d-tooling-lab -n tooling-lab get endpointslice \
-l kubernetes.io/service-name=checkout-api -o widePowerShell 调用原生程序时也可把 JSON 放在单引号内;若团队 shell 对引号处理不同,优先把 patch 写入审查过的文件。修复后 EndpointSlice 应出现 Pod IP 和端口 80。正式项目要修改声明式源文件并重新 apply,现场 patch 只是验证假设;否则下一次部署会把错误恢复。
再转发 Service:
kubectl --context k3d-tooling-lab -n tooling-lab port-forward \
--address 127.0.0.1 service/checkout-api 18080:80另一个终端访问 curl http://127.0.0.1:18080/ 应成功。kubectl 对 Service port-forward 会选择一个后端 Pod,选中的 Pod 终止后会话结束,需要重新建立;它不验证负载均衡、会话保持、NetworkPolicy、Ingress 或外部 LoadBalancer。
若 EndpointSlice 有地址但 Service 转发失败,检查 targetPort。命名端口 http 必须在 Pod container port 中解析;数字写错会让流量到达不存在的监听。然后从集群内临时 Pod 直连 Endpoint IP 与 Service DNS,区分应用监听、Service 数据面和 DNS。集群若不使用 kube-proxy,则继续检查实际 Service 实现,不能机械套用 iptables 结论。
port-forward 是高权限调试隧道
port-forward 的数据路径经过 kubectl、API Server 与 kubelet到 Pod,绕过 Ingress、外部认证和部分网络边界。默认 --address localhost 会尝试绑定 IPv4 与 IPv6 回环;显式 127.0.0.1 能让示例暴露面更清晰。把它改为 0.0.0.0 会向所有网卡开放本地端口,可能把数据库、管理接口或未认证服务带到办公网。
端口转发失败时,错误证据通常落在三处:本地 bind: address already in use 表示端口冲突;pods ... not found 或连接关闭表示选中 Pod 退出;Service 没有 Running 后端时会报告无法选择 Pod。换随机本地端口只能修第一类,不能修后端。
长期共享入口应通过受认证的 Ingress、Gateway、VPN 或开发环境代理,并具备 TLS、访问日志、限流和 owner。port-forward 适合单人、短时、可追溯诊断;终端关闭即释放隧道,它不是发布机制。
用 RBAC 把读取证据和改变现场分开
日志、exec、port-forward 和 ephemeral container 是不同 API 子资源,不能只授予 get pods 就期待全部可用。下面把只读证据和会改变或穿透现场的动作拆成两个 Role;普通值班成员先绑定 workload-evidence-reader,只有获得短期升级授权的人才绑定 workload-interactive-debugger。
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: workload-evidence-reader
namespace: tooling-lab
rules:
- apiGroups: [""]
resources: ["pods", "events", "services"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: workload-interactive-debugger
namespace: tooling-lab
rules:
- apiGroups: [""]
resources: ["pods/exec", "pods/portforward"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/ephemeralcontainers"]
verbs: ["update"]两个 RoleBinding 都应绑定身份提供商管理的组,而不是共享用户或长期 ServiceAccount token;交互角色使用更小的临时组并设置外部到期回收。是否允许 pods/exec 和 ephemeral containers 要单独评估,因为它们可能读取进程环境、挂载的 Secret、服务账号 token 和业务数据,也可能在应用身份下发起网络请求。只读日志角色不应默认获得它们。
授权落地后用模拟检查精确验证:
kubectl --context k3d-tooling-lab auth can-i get pods/log \
-n tooling-lab --as-group=oncall-checkout --as=debug-user
kubectl --context k3d-tooling-lab auth can-i create pods/exec \
-n tooling-lab --as-group=oncall-checkout --as=debug-user
kubectl --context k3d-tooling-lab auth can-i update pods/ephemeralcontainers \
-n tooling-lab --as-group=oncall-checkout --as=debug-user--as 本身要求 impersonate 权限,普通开发身份可能得到 Forbidden;这时由平台流水线执行验证。生产调试凭证应短期、绑定个人身份和工单,API 审计至少记录 user、source IP、namespace、resource、subresource、verb、response code 与时间。审计 policy 和后端容量也要预算:记录所有 request/response body 可能泄露 Secret 或产生巨大日志,完全不记则无法追责。
容量、成本与现场完整性
并发 logs -f、大文件 cp、大量 port-forward 和高权限 debug 都会给 API Server、kubelet、节点网络与审计后端增加长连接和流量。开发集群里这常表现为命令变慢;生产事故中,它可能与业务故障争抢资源。团队应限制并发会话、日志时间窗和复制大小,优先从集中日志与指标读取已有证据,把交互式调试作为有明确假设的动作。
调试动作也会改变现场。exec 启动额外进程,debug 添加不可删除的临时容器,复制 Pod 创建新对象,在线安装软件改变容器文件系统,patch 会改变声明状态。每次动作前记录假设,动作后记录对象 UID 与时间;需要保全证据时,先采集状态、Event、日志和清单,再修改。
镜像选择同样属于供应链。调试镜像应固定 digest,来自受信 registry,定期扫描和更新;busybox:latest 或陌生工具箱可能漂移,也可能把流量与文件带出集群。受限网络中的调试镜像应预置并验证拉取权限,避免故障时才临时开通公网。
清理并证明没有遗留入口
先终止仍运行的 port-forward 和交互式 debug 会话,再删除本地诊断文件。确认文件没有需要留存的事故证据后,按组织的数据销毁流程处理。实验资源可以由清单和 namespace 清理:
kubectl --context k3d-tooling-lab -n tooling-lab delete \
deployment/checkout-api service/checkout-api configmap/checkout-config \
pod/checkout-api-debug pod/crash-probe --ignore-not-found
kubectl --context k3d-tooling-lab delete namespace tooling-lab \
--ignore-not-found --wait=true
kubectl --context k3d-tooling-lab get namespace tooling-lab最后一条预期返回 NotFound。若 namespace 长期 Terminating,先查看 .spec.finalizers 和剩余的 namespaced 对象,不要直接清空 finalizer;那可能绕过控制器清理并留下外部资源。复制出的 checkout-api-debug 和 ephemeral container 都随 namespace 删除。
共享环境不应随手删除整个 namespace。更稳妥的是给实验资源统一 debug-session、owner、expires-at 标签,在工单中记录清理责任,并用受控控制器按 TTL 回收。清理完成还要确认本地端口不再监听、临时 RBAC binding 已撤销、短期凭证已过期、诊断文件和 shell history 没有敏感值。
把命令顺序固化成团队诊断法
一次成熟的排障不会从 exec 开始。先确认 context、namespace、身份和变更时间;再用 get 判断 Pod 位于 Pending、初始化、运行未就绪还是反复重启;用 Event 与 describe 找调度、拉取、挂载和探针证据;容器启动过才读当前或 previous logs;运行中的最小镜像缺工具时,才通过受控 debug 引入工具。Pod 直连成立后,再沿 Service selector、EndpointSlice、targetPort、集群内访问和外部入口推进。
这条顺序并不保证每次故障都相同,却保证每一步都改变故障假设。工具升级时应重跑本实验,特别关注 kubectl events、EndpointSlice、ephemeral container、安全 profile 和 RBAC 子资源行为。Kubernetes 官方的 运行中 Pod 调试、Service 调试、kubectl debug 与 RBAC 文档提供版本对应的命令与 API 事实;团队 runbook 则应保留自己的身份、审计、数据处理和升级门禁。
