排障先锁定集群和 namespace,再沿 Pod → EndpointSlice → Service → Ingress 的关系缩小范围。状态字段、探针、连通性和业务正确性分别提供不同证据。
固定 context 与 namespace
kubectl config get-contexts
星号标识当前选择,kubectl config current-context 只显示该名字。下文将 your-context 替换为已核对的 context,default 替换为目标 namespace;每条命令显式指定,复制某一节也不会依赖前文变量。
kubectl -- context your-context - n default get deploy,svc,ingress,pvc
这些资源回答不同层的配置是否存在;Pod 实际位置用下面的查询。NotFound 先查名称与 namespace,Forbidden 则查授权。
kubectl -- context your-context - n default get pods - o wide
-o wide 增加 IP 与节点等列。下文 your-pod 从此列表取得;app 是容器名,通过 describe 的 Containers 部分核对。
kubectl -- context your-context get nodes - o wide
Ready 不证明所有工作负载可调度;还要结合资源请求、污点和亲和性。
kubectl -- context your-context - n default auth can-i get pods
返回 yes / no;只能回答该 verb/resource 的授权,不证明 API 可用或其他操作获准。
将示例 context、namespace 与资源名替换为已核对的目标。查询 Secret 列表不必导出内容;排查 RBAC 时 impersonation 也需要权限,--as 不是绕过授权。
Pod 状态只告诉你停在哪一段
先读事件与各容器状态
kubectl -- context your-context - n default describe pod your-pod
区分当前 State 与 Last State,记录退出码、原因、事件时间和重复次数。事件会过期,不能把没有事件当成从未出错。
kubectl -- context your-context - n default logs your-pod - c app -- tail= 100
-c 选择容器而非 Pod;多容器 Pod 应明确名称。持续观察加 -f,按 Ctrl-C 结束日志跟随。
kubectl -- context your-context - n default logs your-pod - c app -- previous -- tail= 100
重启循环时优先查这一份;没有上次实例或日志已丢失会报错,不是让你再重启一次。
kubectl -- context your-context - n default get events -- sort-by= .metadata.creationTimestamp
排序不是每次重复发生的精确时间线;结合 describe 的 Last Seen 与计数定位。下表按症状选择下一层。
现象 检查
Pending / FailedScheduling Events 中的 requests、配额、亲和性、污点、PVC 绑定;不只看 CPU 空闲
ImagePullBackOff 镜像名/tag、registry 访问、认证和限流;节点能拉取才有意义
CrashLoopBackOff 退出码、lastState、上次实例日志、启动/liveness 探针;这是重试退避状态
Running / 0/1 Ready readiness、readiness gates、各容器状态;Running 不等于能接流量
让配置字段与流量路径对应起来
正在呈现知识画面 重新加载
Service 选到了谁,又把流量送到哪里 普通 selector Service 需要标签匹配、Ready 后端和正确 targetPort。Pod Running 或 port-forward 成功不能单独验证 ClusterIP 路径。
{"id":"ops-k8s-service","title":"Service 选到了谁,又把流量送到哪里","summary":"普通 selector Service 需要标签匹配、Ready 后端和正确 targetPort。Pod Running 或 port-forward 成功不能单独验证 ClusterIP 路径。","height":600,"html":"<main><p class=\"quiet\" data-i18n=\"scope\"></p><div class=\"checks\"><label><input id=\"match\" type=\"checkbox\" checked><span data-i18n=\"match\"></span></label><label><input id=\"ready\" type=\"checkbox\" checked><span data-i18n=\"ready\"></span></label><label><input id=\"port\" type=\"checkbox\" checked><span data-i18n=\"port\"></span></label></div><div class=\"flow\"><div class=\"row\" data-i18n=\"service\"></div><div class=\"arrow\" id=\"edge1\">↓</div><div class=\"row code\" id=\"endpoint\"></div><div class=\"arrow\" id=\"edge2\">↓</div><div class=\"row\"><span data-i18n=\"pod\"></span> :8080</div></div><div class=\"explain\"><div class=\"stable \"><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"yes\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"labels\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"notready\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"wrongport\"></p><p id=\"message\" aria-live=\"polite\"></p></div></div></main>\n","css":"main {\n display: grid;\n gap: 18px;\n min-width: 0;\n}\np {\n overflow-wrap: anywhere;\n}\n.controls {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n}\n.presets {\n display: flex;\n flex-wrap: wrap;\n}\n.presets button {\n flex: 1 1 85px;\n font-size: 15px;\n}\n.cards {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 10px;\n}\n.card {\n border: 1px solid var(--rule);\n border-radius: 10px;\n padding: 14px;\n min-width: 0;\n background: var(--surface);\n}\n.card h3 {\n font-size: 15px;\n color: var(--muted);\n margin-bottom: 8px;\n}\n.card strong {\n font-size: 23px;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.code {\n font-family: var(--viz-font-mono);\n font-size: 15px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.stable {\n display: grid;\n min-width: 0;\n}\n.stable > * {\n grid-area: 1/1;\n margin: 0;\n}\n.stable > .reserve {\n visibility: hidden;\n pointer-events: none;\n}\n.explain {\n border-left: 3px solid var(--accent);\n padding: 12px 14px;\n background: var(--accent-soft);\n}\n.arrow {\n text-align: center;\n color: var(--accent);\n font-size: 25px;\n}\n.row {\n padding: 12px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n min-width: 0;\n}\n.row.on {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n.row.off {\n color: var(--muted);\n}\n.bar {\n height: 18px;\n background: var(--surface);\n border: 1px solid var(--rule);\n border-radius: 5px;\n overflow: hidden;\n margin-top: 8px;\n}\n.bar > i {\n display: block;\n height: 100%;\n background: var(--accent);\n width: 0;\n transition: width 0.22s;\n}\n.checks {\n display: grid;\n gap: 8px;\n}\n.checks label {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 9px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n font-size: 16px;\n}\n.checks input {\n width: 18px;\n height: 18px;\n accent-color: var(--accent);\n flex-shrink: 0;\n}\nlabel.range {\n display: block;\n font-size: 16px;\n}\nlabel.range output {\n float: right;\n font-variant-numeric: tabular-nums;\n}\n.pair {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n}\n.flow {\n display: grid;\n gap: 6px;\n}\n.quiet {\n font-size: 14px;\n color: var(--muted);\n}\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n}\nbutton {\n overflow-wrap: anywhere;\n}\n.large {\n font-size: 32px;\n color: var(--accent);\n font-weight: 500;\n}\n.layers {\n display: grid;\n gap: 12px;\n}\n.layer {\n border-left: 3px solid var(--rule);\n padding-left: 12px;\n}\n.layer.on {\n border-color: var(--accent);\n}\n@media (max-width: 520px) {\n .cards,\n .pair {\n grid-template-columns: 1fr;\n }\n .card {\n padding: 12px;\n }\n .card strong {\n font-size: 22px;\n }\n}\n","js":"const $ = (s) => document.querySelector(s);\nconst $$ = (s) => [...document.querySelectorAll(s)];\nconst t = (k) => viz.t(k);\nconst put = (id, v) => ($(\"#\" + id).textContent = v);\nconst say = (k) => put(\"message\", t(k));\nconst state = (v) => ($(\"main\").dataset.state = JSON.stringify(v));\nconst press = (key, v) =>\n $$(\"[data-\" + key + \"]\").forEach((b) =>\n b.setAttribute(\"aria-pressed\", String(b.dataset[key] === v)),\n );\nfunction draw() {\n const match = $(\"#match\").checked,\n ready = $(\"#ready\").checked,\n port = $(\"#port\").checked ? 8080 : 80;\n put(\n \"endpoint\",\n match ? \"EndpointSlice\\n10.0.0.8:\" + port + \"\\nready: \" + ready : \"EndpointSlice\\n∅\\n—\",\n );\n put(\"edge1\", match ? \"↓\" : \"×\");\n put(\"edge2\", match && ready && port === 8080 ? \"↓\" : \"×\");\n $(\"#endpoint\").classList.toggle(\"on\", match && ready);\n say(!match ? \"labels\" : !ready ? \"notready\" : port !== 8080 ? \"wrongport\" : \"yes\");\n state({ match, ready, port, reachable: match && ready && port === 8080 });\n}\n$$(\"input\").forEach((e) => (e.onchange = draw));\ndraw();\n","audio":false,"strings":{"labels":"没有标签匹配的后端。先查 selector;改 targetPort 不能补出候选 Pod。","match":"标签匹配 app=web","notready":"Pod 被选中但 endpoint 不 Ready,普通 Service 不把它作为正常可用后端。","pod":"Pod 应用监听","port":"targetPort=8080(否则 80)","ready":"Pod Ready","scope":"一个 Pod 始终 Running,应用监听 8080;publishNotReadyAddresses=false,忽略其他网络故障。逐项改变三个条件。","service":"客户端 → Service :80","wrongport":"后端可用,但流量被送到 80;应用实际监听 8080,连接失败。","yes":"候选被选中、Ready、端口正确:本模型流量到达应用。"}} apiVersion : v1
kind : Service
metadata :
name : web
spec :
selector :
app : web
ports :
- name : http
port : 80
targetPort : 8080
这个 Service 选择同 namespace 内标签 app: web 的 Pod,再连接其 8080。应用必须实际监听 Pod 可达地址的 8080;声明 containerPort 不会打开端口,整数 targetPort 也不要求声明 containerPort。命名 targetPort 则需要对应命名端口。
选择器、端点和端口逐层核对
上面的 YAML 是完整 Service 对象示例,保存为 service.yaml 时仍需按发布流程 dry-run / apply;它本身不会创建 Pod。
kubectl -- context your-context - n default get pods - l app=web -- show-labels
无结果先确认标签与 namespace;不要立即改 Service 端口。
kubectl -- context your-context - n default get endpointslice - l kubernetes.io/service-name=web - o yaml
核对 endpoints.addresses、conditions.ready、ports.port;EndpointSlice 可能有多个。端点存在仍需检查网络策略、CNI 与应用监听。
kubectl -- context your-context - n default describe service web
请求 Service 使用 port;最终应用应监听 targetPort。命名端口需能解析到 Pod 中同名容器端口。
端口转发只验证它经过的路径
kubectl -- context your-context - n default port-forward service/web 8080:80
保持此终端运行,在客户端另一终端 curl -v http://127.0.0.1:8080/;Ctrl-C 结束转发。选中的 Pod 结束后可能需要重新建立连接。
kubectl -- context your-context - n default exec client-pod - c app -- curl -v http://web.default.svc.cluster.local/
client-pod/app 换成被允许访问且带 curl 的客户端;default 与 cluster.local 换成实际 namespace 和集群域。该检查覆盖客户端身份下的 DNS 与 Service 路径,再从外部单独测 Ingress。
EndpointSlice 无目标先查 selector 和命名空间;有地址但不就绪查 readiness。端口转发通过 API 通道直达选中的 Pod,不会验证 ClusterIP 的转发、集群 DNS 或 Ingress 。验证 Service 要从合适的集群客户端访问 Service DNS/ClusterIP,再单独检查 Ingress 的 host/path/backend、控制器与外部入口。
发布与调试都要验证作用范围
审阅清单、验证 API,再实际应用
kubectl -- context your-context - n default diff - f manifest.yaml
manifest.yaml 是本次完整变更文件;检查其中 namespace 与集群级对象的实际范围。返回 0 表示无差异,1 表示有差异,其他错误先处理。
kubectl -- context your-context - n default apply -- dry-run= server - f manifest.yaml
会访问 API 与 admission,错误需修复;成功也不证明镜像能拉取或业务可用。
kubectl -- context your-context - n default apply - f manifest.yaml
根据输出确认 created/configured/unchanged 的对象与预期一致,再观察 rollout。
kubectl -- context your-context - n default rollout status deployment/web -- timeout= 120s
超时先查新 ReplicaSet、Pod Events 与日志;成功后仍需实际请求。不要把等待超时误当成自动回滚。
先确认目标 revision,再回滚
kubectl -- context your-context - n default rollout history deployment/web
以 rollout history deployment/web --revision=2 查看候选;2 必须换成已核对的 revision。
kubectl -- context your-context - n default rollout undo deployment/web -- to-revision= 2
接着重新观察 rollout status、Pod 与请求。数据库、外部系统和独立 ConfigMap 的内容不随之恢复。
diff 返回 1 通常表示存在差异,不是命令执行故障;server dry-run 会访问 API/admission。rollout 成功后仍需检查实际请求。rollout undo 只恢复工作负载模板历史,不回滚数据库;rollout restart 触发新 Pod,不修改镜像标签内容。
容器有工具时直接 exec
kubectl -- context your-context - n default exec - it your-pod - c app -- sh
-- 后是容器内命令,-i 保持输入,-t 分配终端。这里只是运行新进程,不更换主进程;退出 shell 不删除 Pod。
精简镜像缺工具时选择临时调试容器
kubectl -- context your-context - n default debug - it your-pod -- image= busybox:1.37 -- target= app
这是 exec 的替代诊断路径,需权限及策略允许;镜像替换为项目批准的版本。退出后记录仍属于该 Pod,不能单独删除临时容器字段。
复制文件时明确方向与容器
kubectl -- context your-context - n default cp your-pod:/tmp/report.txt ./report.txt - c app
先确认目标文件不会被覆盖,并验证文件内容/大小。反向复制会写入容器;kubectl cp 通常要求容器内有 tar,失败先读报错。
kubectl debug 在已有 Pod 中添加临时调试容器,需要集群策略允许;运行时决定 target 进程可见性。kubectl run debug ... 是另建 Pod,网络身份与策略可能不同。kubectl cp 通常依赖容器内 tar;镜像精简时不要误判成网络故障。删除受 Deployment 管理的 Pod 会被重建,排障先保存日志和事件。
Debug Services · Debug Pods · EndpointSlices