今天想把这个K8s系列里最绕不开的一个对象好好聊透——Pod。前面几篇我们把集群搭起来、基础概念过了一遍到了真正写业务YAML的时候很多人第一个卡住的地方就是Pod。原因也很简单Pod这东西看着不起眼不就是包一个容器嘛但实际用起来会发现它涉及到的字段、状态、生命周期、优雅终止、探针、Init容器、Sidecar模式……一环扣一环任何一个细节没搞清楚线上出了问题都很难定位。这篇我打算换个讲法不按官方文档的章节顺序来而是从“为什么需要Pod”这个根源问题切入然后直接教你写一份完整的Pod YAML再把常用命令和故障排查串起来。目标很明确读完这篇你能独立写出一份生产可用级别的Pod配置并且遇到Pod起不来、一直重启、调度不上去之类的问题时知道从哪里下手找原因。1. Pod 到底是什么K8s世界里的最小调度单元1.1 为什么不是直接跑容器而是先有Pod这个问题几乎每次给团队做分享都会被问到。Docker已经很成熟了为什么K8s不直接以容器为最小单位去调度非要再套一层Pod答案藏在“调度”这个词里。K8s调度的最小粒度不是容器而是Pod。你可以把Pod理解成一组容器的“合租室友”——它们共享同一个网络命名空间、共享同一个IP、共享存储卷而且必须被调度到同一台节点上。那为什么要这样设计因为有些场景下几个容器必须“住在一起”。最典型的例子就是日志采集。你的业务容器把日志写到文件里旁边需要有个Sidecar容器专门负责把日志转发到消息队列或者日志平台。这两个容器如果被调度到不同机器上日志文件就共享不了了这套架构直接崩掉。再比如代理模式。业务容器只监听localhost旁边的代理容器负责接收外部流量再转发给业务容器。如果被拆到两台机器上localhost就成了笑话。所以Pod这个抽象本质上解决的是“一组紧密协作的容器如何被统一调度、统一生命周期管理”的问题。还有个很实际的原因如果直接以容器为单位调度那容器重启的代价是很大的——IP会变、存储要重新挂载、依赖关系要重新梳理。而有了Pod这层封装Pod内部的容器可以随时重启Pod本身不重建IP不变存储不丢。这对运维来说是非常重要的稳定性保障。1.2 Pod 里面到底装了什么一个Pod里面可以有一个容器也可以有多个容器。但不管里面有几个容器Pod作为一个整体有几个固定的组成部分Pod 的 IP 地址整个Pod共享一个IP所有容器通过localhost互相访问。共享存储卷Pod级别的VolumePod内的所有容器都能挂载。容器运行时环境包括网络、主机名、UTS命名空间等都由Pod统一管理。标签Labels与注解Annotations这是Pod的元数据Service、Deployment等上层对象就是靠标签来选中Pod的。你可以把Pod理解成一个沙盒容器是沙盒里的进程。Pod负责提供运行环境容器负责跑业务。另外一个很重要的点Pod是“原子”的。什么意思呢调度器判断的是整个Pod需要多少资源而不是单个容器。比如一个Pod里有2个容器一个请求了1核CPU另一个请求了2核CPU那调度器会按3核来评估节点是否满足条件。这就意味着Pod内的所有容器是同时被创建、同时被销毁的生命周期完全绑定。很多初学者容易踩的坑是以为Pod内的容器可以独立控制启停。实际上kubectl exec只能进入某个容器但kubectl delete删的是整个Pod里面所有容器一起结束。所以设计Pod内容器的时候一定要想清楚“它们是否需要同生共死”。2. 从零手写一个 Pod 的 YAML每个字段都讲透2.1 YAML 基础与 K8s 资源清单的标准骨架先说YAML本身。K8s的所有资源描述文件都是YAML格式原因很简单JSON虽然机器友好但人看着累YAML是JSON的超集用缩进表达层级关系写起来清晰很多。一个标准的K8s资源清单固定有这么几个顶级字段apiVersionAPI版本。不同资源、不同K8s版本这个值不一样。比如Pod早期用v1Deployment早期用apps/v1现在统一用apps/v1。kind资源类型。Pod、Deployment、Service、ConfigMap……注意大小写敏感。metadata元数据。名字、命名空间、标签、注解都写在这里。spec期望状态。这个字段是核心不同资源的spec结构完全不同。还有一个status字段注意这个字段不是你来写的而是K8s控制面写进去的。你定义的是“期望状态”K8s通过各种控制器把实际状态往期望状态去靠拢。这也是K8s声明式API的核心思想——你说你要什么剩下的交给系统。我见过不少新手试图在YAML里手动写status然后报错说字段无法解析。记住spec是声明status是结果一个是输入一个是输出。2.2 核心字段逐一拆解metadata、spec、status咱们拿一个最简单的Pod举例apiVersion: v1 kind: Pod metadata: name: nginx-pod namespace: default labels: app: nginx env: prod spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80这里metadata.name是Pod的名字同一个命名空间内必须唯一。如果你不指定namespace默认就是default。但生产环境强烈建议命名空间隔离比如dev、prod、test各一个空间避免资源互相干扰。labels是给Pod打标签这个标签不是给人看的是给K8s的控制器看的。Service通过selector选择带特定标签的PodDeployment通过标签管理Pod副本。标签的key/value都建议有规范比如app、tier、version是常用约定。再看spec.containers这是核心中的核心。注意containers是个数组所以每个容器前面都有- name这个列表项标记。每个容器对象有几个重要字段image镜像地址。如果是私有仓库的镜像还需要配合imagePullSecrets才能拉取。command覆盖镜像默认的启动命令。跟Docker的Entrypoint类似。args给启动命令传的参数。env环境变量支持直接从ConfigMap或Secret引用。resources资源请求与限制。ports容器监听的端口。volumeMounts挂载哪些存储卷到容器内。livenessProbe/readinessProbe存活探针和就绪探针。还有一个字段容易被忽略restartPolicy。它有三个可选值Always默认、OnFailure、Never。对Pod来说默认永远是Always——不管容器正常退出还是异常退出K8s都会尝试重启。注意如果你通过Job来跑一次性任务restartPolicy必须改成OnFailure或Never否则任务永远结束不了。2.3 实战示例从最小 Pod 到带健康检查的完整案例先看最小Pod。上面那个nginx的例子就够了直接保存为nginx-pod.yaml然后kubectl apply -f nginx-pod.yaml就能创建。但生产环境不能就这么裸奔至少得加资源限制和健康检查。下面这个例子是稍微完整一点的配置apiVersion: v1 kind: Pod metadata: name: web-app-pod namespace: prod labels: app: web-app version: v1 spec: containers: - name: web-app image: registry.example.com/web-app:1.2.3 ports: - containerPort: 8080 protocol: TCP env: - name: SPRING_PROFILES_ACTIVE value: prod - name: DB_URL valueFrom: configMapKeyRef: name: app-config key: db_url resources: requests: cpu: 500m memory: 256Mi limits: cpu: 1 memory: 512Mi livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5重点说几个字段resources.requests是调度器判断节点是否满足条件的依据。500m是0.5个CPU核心256Mi是256兆内存。limits是运行时的硬上限如果容器内存超过512Mi会被OOM killer杀死。CPU超限不会杀容器只是会被限流。这里有个铁律limits必须大于等于requests否则K8s会拒绝创建。livenessProbe和readinessProbe的区别我总结一句话liveness探针决定“这个容器要不要杀掉重启”readiness探针决定“这个容器要不要开始接收流量”。两个探针的路径最好分开比如Spring Boot用/actuator/health/liveness和/actuator/health/readiness别共用一个接口否则服务还在初始化你就把流量打进来了或者服务已经假死了但探针还是通的。initialDelaySeconds是指容器启动后等多少秒才开始第一次探测这个值建议根据应用启动时间来定。很多Java应用启动就要40秒你写5秒的延迟探针必然失败然后容器被不断杀掉重启陷入死循环。这个参数是故障高发区后面会细讲。3. Pod 生命周期与控制器为什么你的 Pod 说没就没3.1 Pod 的相位Phase与容器状态用kubectl get pods的时候STATUS那一列会显示Pod当前处于什么状态。常见的状态有这么几种PendingPod已经被K8s接受但容器还没创建完成。可能是调度器还在找合适的节点也可能是镜像正在拉取。RunningPod已经绑定到某个节点所有容器已经创建成功至少有一个容器还在运行。SucceededPod里所有容器都正常退出了退出码为0。通常用在Job任务里。FailedPod里至少有一个容器以非0状态退出了。UnknownKubelet无法获取Pod状态通常是主节点和Pod所在节点之间通信出了问题。还有几个辅助状态CrashLoopBackOff表示容器反复崩溃K8s已经用退避策略在延迟重启了ImagePullBackOff表示镜像拉取失败ContainerCreating表示容器正在创建。这里有个容易混淆的点状态和相位其实是两个概念。kubectl get pods显示的STATUS是聚合状态而kubectl describe pod里你可以看到更细粒度的容器状态Waiting、Running、Terminated。容器的Terminated状态又包含退出码137是被杀通常OOM143是收到SIGTERM正常终止1通常是应用自己报错。我在排查问题的时候第一眼必看退出码。退出码几乎就告诉了你排查方向。3.2 重启策略与探针让 Pod 学会自愈Pod的restartPolicy决定了容器异常退出后怎么处理。但这个策略是Pod维度的注意不是容器维度的Always容器退出后无论退出码是什么都自动重启。这也是Deployment管理的Pod的默认策略。OnFailure只有容器以非0退出码退出时才重启。Never不管容器怎么退出的都不重启。这里有个顺序问题如果Pod里包含多个容器restartPolicy对每个容器都生效但重启是互相独立的——一个容器崩溃了不代表另一个容器也要重启。探针这块再深入一点。除了HTTP探针还有tcpSocket探针和exec探针。exec探针是在容器内执行一条命令命令返回0就算健康。比如你有个非HTTP服务可以用pgrep或自带的健康检查脚本来探测livenessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 5 periodSeconds: 5这个配置的意思是每5秒在容器里执行一次cat /tmp/healthy如果文件存在就返回0容器健康否则就杀掉重启。探针的参数也值得细看。periodSeconds是探测间隔timeoutSeconds是单次探测超时时间failureThreshold是连续失败多少次才触发动作。这三个参数组合起来需要根据业务实际调优。比如timeoutSeconds设得比响应时间短会出现探针误判——接口本来没事只是响应慢了点就被判定为不健康。我建议HTTP探针的timeoutSeconds至少5秒起步failureThreshold默认3次是比较合理的。3.3 控制器如何管理 Pod写到这里必须提一句你平时直接创建Pod的机会真的不多。大多数情况是创建Deployment、StatefulSet、DaemonSet这些控制器由控制器来管理Pod。那控制器和Pod是什么关系我用一句话概括Pod是被管的对象控制器是管事的。Deployment负责保证指定数量的Pod副本始终运行某个Pod挂了Deployment会重新创建一个新的Pod出来。StatefulSet则是给Pod提供稳定的网络标识和稳定的存储适合数据库这类有状态应用。DaemonSet保证每个节点上都跑一个Pod副本典型应用是日志采集器、监控Agent、CNI插件。理解这层关系很重要因为很多人排错的时候直接把Pod删了结果发现删完之后新Pod又起来了——这不是你删得不彻底而是控制器把它拉起来了。你要做的是改控制器的spec而不是跟Pod较劲。那什么时候需要直接创建Pod三个场景一是调试临时跑一个Pod验证网络或配置二是静态Pod——由kubelet直接管理的Pod不经过API Server用于部署控制面组件三是一些特殊的系统级任务。4. Pod 常用操作命令实战4.1 创建、查看、删除最基础三板斧先列一个最常用的命令清单后面的场景基本都是围绕这些命令展开的。# 创建或更新Pod推荐使用apply幂等 kubectl apply -f pod.yaml # 创建Podcreate只创建不更新已存在会报错 kubectl create -f pod.yaml # 查看所有命名空间的Pod kubectl get pods -A # 查看default命名空间的Pod带IP和节点信息 kubectl get pods -o wide # 实时跟踪Pod创建过程ctrlC退出 kubectl get pods -w # 查看Pod的详细描述包括事件、状态、挂载卷 kubectl describe pod pod-name # 删除Pod如果被控制器管理控制器会自动重建 kubectl delete pod pod-name # 删除Pod并忽略控制器重建不推荐生产使用 kubectl delete pod pod-name --grace-period0 --forceapply和create的区别新手经常搞混。create是“我要创建一个新对象”如果同名对象已经存在直接报错。apply是“我要把这个YAML描述的期望状态同步到集群里”如果对象不存在就创建已存在就更新差异部分。日常使用强烈建议统一用apply。-w参数很好用部署新版本的时候我一般会开三个终端一个kubectl get pods -w实时看状态一个kubectl logs -f看日志一个留着手动操作。还有-o yaml非常实用可以把Pod当前的完整状态导出来包括K8s自动补全的字段。我做配置审计的时候经常用这个命令检查线上Pod到底被塞了什么默认配置# 导出Pod完整YAML注意这个YAML里包含status等自动生成的字段 kubectl get pod pod-name -o yaml pod-current.yaml4.2 进入容器与日志查看日志和调试应该是使用频率最高的操作了。# 查看容器日志单容器Pod直接写Pod名即可 kubectl logs pod-name # 查看多容器Pod中指定容器的日志 kubectl logs pod-name -c container-name # 实时跟踪日志tail 500行 kubectl logs -f pod-name --tail500 # 如果容器之前崩溃过查看上一次的日志排查崩溃原因的关键 kubectl logs pod-name --previous # 进入Pod的某个容器多容器Pod必须指定-c kubectl exec -it pod-name -- /bin/sh kubectl exec -it pod-name -c container-name -- /bin/bash--previous这个参数我重点说一下。容器崩溃重启后当前日志会被新进程覆盖如果不加--previous你只能看到新进程的输出完全找不到崩溃原因。我第一次排查CrashLoopBackOff的时候就是不知道这个参数看日志一直只看得到“重新启动后的输出”绕了很久才定位到问题。exec进入容器后注意有些精简镜像里连ps、ls都没有更别说bash了。比如alpine只有shdistroless镜像甚至没有shell。这时候可以用kubectl debug创建临时调试容器这个后面讲。4.3 标签选择、字段过滤与端口转发Pod多了以后按标签筛选是必备技能# 按标签筛选 kubectl get pods -l appnginx kubectl get pods -l appnginx,env in (prod,staging) # 按字段筛选比如只看崩溃的Pod kubectl get pods --field-selectorstatus.phaseFailed # 把Pod的端口映射到本机调试服务非常好用 kubectl port-forward pod/nginx-pod 8080:80port-forward是调试神器。很多服务只暴露ClusterIP外部访问不到而你又不想临时改Service配置直接一条命令把Pod端口映射到本地浏览器打开localhost:8080就能访问了。标签选择器这块-l后面支持、!、in、notin、exists等操作符。注意逗号是“与”的关系不是“或”。如果真的要“或”要么分开查询要么使用--selector配合稍复杂的表达式或者拆多个标签组合。4.4 资源与节点维度的查看命令排障的时候光看Pod本身还不够节点的资源情况往往才是根因# 查看节点状态和资源 kubectl top nodes # 查看各Pod资源占用 kubectl top pods -A # 查看节点上所有Pod的分布情况 kubectl get pods -A -o wide | sort -k8 # 查看节点详情包括Taints、Conditions、容量 kubectl describe node node-namekubectl top依赖Metrics Server。如果你集群没装Metrics Server这个命令会报错。但装好之后几乎每个排障场景都会用到——Pod一直在Pending十有八九是节点资源不够Pod时不时被杀重启看看是不是内存超标了。5. 多容器 Pod 与 Init 容器进阶场景实战5.1 边车Sidecar模式实战前面提到过Pod支持多个容器共享网络和存储。这个特性的黄金搭档就是Sidecar模式。举一个最常见的例子。假设你的业务容器里跑的是Nginx只负责处理HTTP请求日志写到/var/log/nginx/access.log。你现在想把日志实时同步到对象存储但不想改Nginx镜像——因为改了镜像就要重新发布还要维护多套镜像。做法是加一个Filebeat容器作为Sidecarspec: containers: - name: nginx image: nginx:1.25 volumeMounts: - name: logs mountPath: /var/log/nginx - name: filebeat image: docker.elastic.co/beats/filebeat:8.11.0 volumeMounts: - name: logs mountPath: /var/log/nginx-source - name: filebeat-config mountPath: /usr/share/filebeat command: [filebeat, -e, -c, /usr/share/filebeat/filebeat.yml] volumes: - name: logs emptyDir: {} - name: filebeat-config configMap: name: filebeat-config这里的核心是emptyDir卷。emptyDir是Pod级别的临时卷容器崩溃删了再重建只要Pod还在数据就还在。Nginx容器把日志写到/var/log/nginxFilebeat容器挂载同一个卷到/var/log/nginx-source两边看到的是同一批文件。这个模式的好处是解耦。Nginx镜像保持官方纯净日志采集能力通过Sidecar动态加进去。要换采集方案改Sidecar容器就行业务容器完全不用动。注意多容器Pod创建的时候K8s会同时启动所有容器没有先后顺序。如果你希望日志采集器先启动等业务容器启动后再采集日志需要给业务容器加一个启动阻塞条件或者用Init容器来保证顺序。5.2 Init 容器先决条件的顺序保障Init容器是Pod正式启动前需要完成的一组一次性的初始化容器。它们的执行是串行的前一个成功退出后下一个才开始。全部成功后才开始启动业务容器。典型使用场景等待依赖的数据库或配置中心就绪。给共享卷写入初始数据或配置文件。执行数据库迁移脚本。修改共享卷的目录权限。spec: initContainers: - name: wait-for-db image: busybox:1.36 command: [sh, -c, until nc -z db-service 5432; do echo waiting for db; sleep 2; done] containers: - name: app image: myapp:1.0这个Init容器会不断探测db-service的5432端口直到数据库可以连接才退出然后主容器才开始启动。这比应用自己去做重连要干净得多不用在代码里写一堆等锁逻辑。Init容器有几个坑注意一下第一它不参与Pod的就绪状态判断只影响Pod是否能进入Running第二Init容器如果失败K8s会按restartPolicy的重启策略处理第三Init容器会占用Pod的资源配额如果你Pod的request和limit配得很紧Init容器跑起来很可能导致资源不足。社区里很多Pod一直Pending排查下来是Init容器需要的资源没算进去。5.3 静态 Pod 与控制面组件的运行方式静态Pod是一种特殊的Pod它不经过API Server直接由节点上的kubelet根据指定目录下的YAML文件创建。kubelet会持续监控这个目录文件变了就更新Pod文件删了就删除Pod。静态Pod的应用场景是部署K8s控制面组件。用kubeadm搭建的集群API Server、etcd、Controller Manager、Scheduler都是以静态Pod的形式跑在每个master节点上的。你可以在/etc/kubernetes/manifests/目录下看到它们的YAML文件。静态Pod的名字有个特点节点名后缀。比如kube-apiserver-master01。而且kubectl delete是删不掉静态Pod的——你删了kubelet会立刻从manifest目录重建。要修改静态Pod配置直接改对应目录下的YAML文件然后kubelet会自动感知并滚动更新。这个知识点在日常运维里不太常用但理解静态Pod机制你对“Pod到底是谁创建出来的”会有更清晰的认知。遇到kubectl delete pod删不掉的情况先想想这个Pod是不是静态Pod或被控制器管理的Pod。5.4 Pod 亲和性与反亲和性Pod的调度位置也可以精细控制。比如你想让某两个服务尽量部署在同一台节点上减少网络延迟或者想让数据库的两个副本分散在不同节点上避免单点故障。affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: redis topologyKey: kubernetes.io/hostname这个配置的意思是调度这个Pod的时候要避开那些已经有appredis标签Pod的节点。topologyKey定义了“同一个拓扑域”的粒度kubernetes.io/hostname表示同一台节点算一个拓扑域也就是“不能和已有的redis Pod在同一个节点上”。亲和性这块生产上经常配合StatefulSet使用。有状态应用的多副本如果调度到同一台机器上宕机等于全部挂掉。加上反亲和性能把副本分散到不同节点提高可用性。6. 常见问题和排查记录Pod 故障处理实录6.1 Pod 一直 PendingPending状态说明调度器还没把这个Pod绑定到任何节点上。最常见的原因就那么几个节点资源不足。用kubectl describe pod看Events如果有Insufficient cpu或Insufficient memory那就是节点资源不够。然后用kubectl top nodes确认各节点实际剩余资源。解决办法要么扩容节点要么调低Pod的requests。节点有污点Taint。如果Events里有MatchNodeSelector相关错误说明节点上有污点而Pod没有对应的容忍Toleration。比如某些节点专门给GPU任务用的会打上污点普通Pod调度不过去。PVC没有绑定。如果Pod用了持久化存储而PVC一直Pending整个Pod也会一直Pending。用kubectl get pvc和kubectl describe pvc查看存储类是否能动态供给。排查思路固定三步先kubectl describe pod看EventsEvents太太太重要了——几乎80%的Pending问题都能在Events里直接看到根因。6.2 镜像拉取失败ImagePullBackOffImagePullBackOff这个状态用kubectl describe pod会在Events里看到具体原因。常见的有这么几类镜像地址拼错了。检查registry地址、仓库名、tag大小写。私有仓库认证失败。镜像在私有仓库里Pod没有配置imagePullSecrets。需要在metadata里加上spec: imagePullSecrets: - name: registry-secret镜像不存在。尤其是用了latest标签很容易出现本地构建完忘记推送或者推到了别的仓库。节点无法访问外网。有些离线环境或内网环境节点不能访问镜像仓库。需要配置镜像仓库的私有地址或者用registry mirror。排查步骤先kubectl get pods看状态再kubectl describe pod看Events里的具体报错如果是认证问题可以用kubectl create secret docker-registry创建密钥然后在YAML里引用。6.3 容器崩溃重启CrashLoopBackOffCrashLoopBackOff意味着容器起来又挂K8s用退避周期10秒、20秒、40秒翻倍增长不断重启。排查这个状态最关键的是一句话看日志特别是上一次的日志。kubectl logs pod-name --previous如果日志里直接显示了异常堆栈问题就简单了一般是应用本身的问题配置不正确、依赖服务连不上、启动参数写错等。如果日志是空的那可能是容器根本没启动起来。排查启动命令和入口脚本用kubectl describe pod看有没有报错比如command not found、权限不足、环境变量缺失等。还有一种情况是OOM被杀。kubectl describe pod的Last State如果是OOMKilled退出码通常是137。解决办法是调高内存limits或者优化应用内存占用。K8s中OOM是分两种的节点OOM会杀掉占用最高的容器容器超过limits的OOM则直接重启。6.4 Pod Sandbox 创建失败这类报错通常长这样failed to create pod sandbox: rpc error: code Unknown desc failed to create network sandbox。结合热词里出现的failed to create pod sandbox这基本是容器运行时或CNI网络插件的问题。第一次遇到这个报错我折腾了很久。后来总结出排查的关键在于拆开两个词pod sandbox和network sandbox。Pod Sandbox是容器运行时的概念也就是containerd或CRI-O创建的隔离环境Network Sandbox是CNI插件创建的网络命名空间比如Calico、Flannel、Cilium。常见原因有CNI插件崩溃或配置错误。检查节点上CNI插件的Pod是否正常比如kubectl get pods -n kube-system看看calico-node或flannel是否Running。CNI的配置文件不再生效。节点上/etc/cni/net.d/目录下的配置文件是否被改动或删除。容器运行时和K8s版本不兼容。比如containerd的版本和K8s版本差太多通信协议对不上。节点网络问题。桥接、iptables规则被清掉、IP转发被关闭等。这个问题的排查牵扯到容器运行时和网络插件是运维K8s过程中比较棘手的一类故障。我的建议是先在节点上手动跑一下crictl ps -a看看容器运行时是否正常再检查CNI相关Pod日志逐步缩小范围。6.5 一个完整的排错流程演示假设现在线上报了一个Pod一直不正常我们从零开始走一遍排查流程。第一步找到这个Podkubectl get pods -A | grep web-app第二步看状态。假设状态显示CrashLoopBackOff那进入第三步。第三步看具体描述kubectl describe pod pod-name -n prod重点关注两个地方最上面的Containers段落里的Last State和Exit Code以及底部的Events。如果退出码是137往下查内存如果退出码是1去看日志。第四步看日志kubectl logs pod-name -n prod --previous --tail100假设日志里一直循环报“connection refused”连不上数据库。用kubectl get svc -n prod确认数据库服务名是否存在用kubectl exec进Pod里手动测试连通性kubectl exec -it pod-name -n prod -- sh -c nc -vz db-service 3306测不通就去看数据库所在的Pod状态。整个排错就变成一个链条应用连不上数据库 → 数据库Pod是否正常 → 数据库的Service是否正确选择Pod → 网络策略是否放行。排错到最后很多问题都能归结到一处配置问题。而配置问题最隐蔽的是拼写错误和命名空间不匹配——Service和Pod不在同一个命名空间svc名解析不到或者标签选择器写错了Service选择不到任何Pod。写在最后的几点经验从入门到写出生产可用的Pod配置中间跨越的其实不是技术栈的难度而是对K8s设计思想的理解。我掏心窝子说几个实际经验希望对你有用第一尽量别直接创建裸Pod。除了调试和特殊场景生产环境所有Pod都应该由Deployment、StatefulSet或DaemonSet管理。裸Pod一旦挂了没有人帮你拉起来。第二写YAML的时候resources字段一定要写而且要写准。不写limits的Pod在节点资源紧张的时候最先被OOM杀掉。社区有个经验limits写多少先压测多少别拍脑袋。第三探针一定要区分liveness和readiness。启动慢的应用initialDelaySeconds宁可多写一点也不要少写。探针太激进导致的滚动发布事故我见过太多次了。第四多容器Pod的Sidecar模式确实好用但也要警惕“把所有辅助能力都塞进一个Pod”的倾向。每次往Pod里添加一个容器之前先问自己这个容器真的需要和业务容器同生共死吗如果不是拆成独立Deployment可能更合理。最后想说的是Pod作为K8s的最小调度单元理解它就像理解进程之于操作系统——它本身就是K8s世界观的起点。把Pod吃透了后面学Service、Deployment、网络策略都会顺畅很多。这篇算是一个从入门到实战的完整梳理内容比较多建议收藏起来写YAML或者排错的时候随时回来翻一翻。