我最早接触K8s的时候最困惑的就是这个“Pod”概念。明明容器已经能跑应用了为什么还要在外面再套一层后来在生产环境里排了几次障、被Pod的各种状态折腾过几轮才慢慢摸清楚这个东西的设计逻辑。这篇笔记不打算按官方文档的顺序来就从一个使用者的角度把我理解中的Pod、实操中反复用到的配置、以及踩过的坑都拆开讲一遍。适合刚上手K8s的同学也适合那些YAML能写、但遇到Pod异常就懵的人。1. 理解 PodKubernetes 世界的最小调度单元1.1 为什么不是直接运行容器很多人第一个反应是既然Docker容器已经很方便了K8s为什么还要搞出Pod这个概念直接拿容器作为最小调度单位不行吗答案在K8s的设计哲学里Pod才是调度的原子单位容器只是Pod内部的组成元素。一个Pod可以包含一个或多个容器这些容器共享同一个网络命名空间、共享存储卷并且总是被调度到同一个节点上运行。为什么这样设计考虑一个真实场景你的应用需要边跑业务边采集日志。主容器负责处理请求日志采集容器在旁边把日志转发到远端。这两个容器如果被调度到不同节点日志采集就断了。如果把它们放在同一个Pod里它们就永远在一起被调度、被启停共享同一个生命周期。这就是Pod存在的意义——把多个需要紧密协作的进程绑定成一个整体。我试过直接把日志采集作为独立容器部署结果它和业务容器之间只能通过外部网络通信不仅延迟高日志同步还经常因为网络抖动断掉。后来改成一个Pod挂两个容器共享同一个日志目录问题直接消失。这个设计在K8s里叫“Sidecar模式”本质就是把辅助能力塞进Pod内部让主业务容器保持纯粹。1.2 Pod 内部网络与存储的共享逻辑Pod内的所有容器共享一个网络命名空间这个特性带来几个非常实际的影响。第一同一个Pod里的容器可以通过localhost互相访问。它们共享IP地址、共享端口空间这意味着你不能在同一个Pod里跑两个都监听80端口的容器。端口冲突会直接导致第二个容器启动失败。我见过有人尝试在同一个Pod里同时放两个Nginx结果第二个容器反复CrashLoopBackOff这就是端口空间冲突的典型表现。第二Pod内的容器通过共享Volume实现数据交换。最常见的是emptyDir它在Pod创建时生成一个空目录Pod内所有容器都可以挂载它。主容器往里面写日志Sidecar容器读日志转发整个过程走的是本地磁盘IO开销比网络传输小得多。第三Pod是一个相对脆弱的实体。它没有自愈能力节点宕机或Pod被删除后不会自动重建。如果你直接创建一个Pod而不是通过Deployment等控制器管理那么这个Pod挂了就真的挂了不会有人替你拉起一个新的。1.3 多容器 Pod 的三种经典协作模式多容器Pod在实践中有三种常见模式搞懂它们能帮你少写不少冤枉配置。Sidecar模式是让一个辅助容器伴随主容器运行。典型场景是日志采集、流量代理。前面提到的日志采集就是Sidecar的典型用法。Adapter模式则是对外输出做统一适配。主容器提供了某个格式的接口Sidecar容器负责把它转换成下游系统需要的格式起到翻译层的作用。我见过一个项目业务服务输出的是自己的私有格式而监控系统需要Prometheus格式就在Pod里放了一个Adapter容器做格式转换避免了直接改业务代码。Ambassador模式把Pod内的Sidecar当成外部服务的代理。比如主容器需要连一个数据库但数据库地址会变这时可以放一个Ambassador容器主容器只需要访问localhost:3306实际连接由Ambassador转发。这样主容器不感知环境变化配置更稳定。这三种模式理解后你会意识到Pod的边界是“进程组”的边界而不是应用的边界。什么样的情况适合放一个Pod、什么样的情况必须拆多个Pod判断标准就是这个紧密协作关系是否存在。没有直接依赖的独立服务无论如何都应该拆开部署。2. Pod 生命周期与关键配置解析2.1 Pod 的各种状态到底意味着什么K8s里Pod状态是排查问题时的第一入口。一句kubectl get pod输出的一列状态背后对应着完全不同的原因必须理解清楚。Pending表示Pod已被集群接受但还没有完成调度或者调度后镜像还没拉完。这是最常见的一条状态意味着Pod在等资源、等调度或等镜像下载并不一定代表出了问题。Running说明Pod至少有一个容器处于运行状态。但如果只有部分容器在跑也要继续排查。Succeeded说明Pod里所有容器都成功退出且不会重启这通常是Job任务跑完的正常结果。Failed表示有容器以非零状态退出。Unknown说明K8s无法获取Pod状态通常是节点通信出了问题。有个细节很多人没注意CrashLoopBackOff并不是Pod状态而是容器状态。当容器启动后立刻崩溃且恶意或者因错误不断重启时就会显示这个状态。它的核心信息是容器在反复地启动-崩溃-重启而不是只崩溃一次就放弃了。查看Pod详细状态请记住这条命令kubectl describe pod pod-name这是把Pod从创建到当前的事件全部打印出来调度失败、镜像拉取失败、探针失败的原因都会以Event的形式出现在最后面的部分。状态列只能告诉你怎么了Event才会告诉你为什么。2.2 静态 Pod 与普通 Pod 的区别还有一种容易被忽略的Pod类型叫静态PodStatic Pod。它不通过API Server创建而是由节点上的kubelet直接读取本地目录下的YAML文件生成的。静态Pod有几个典型特征它不受Deployment等控制器管理没有自愈能力你通过kubectl delete删不掉它因为kubelet会检测到文件还在立刻重新创建它的名字会在末尾加上节点名后缀。静态Pod最典型的应用场景是K8s自身的核心组件部署。像kube-apiserver、kube-controller-manager这类控制面组件在自建集群中往往就是通过静态Pod方式来部署的。这样只要kubelet活着这些核心组件就会一直存在。对普通使用者来说需要知道静态Pod的存在但大多数情况下我们接触的是通过Deployment、DaemonSet这些工作负载控制器创建的Pod。了解区别主要是为了排障时不至于被“删不掉的Pod”吓到。2.3 关键配置字段解析restartPolicy 与 imagePullPolicy写Pod的YAML时有两个字段虽然看似简单但配置错了会带来完全不同的行为。restartPolicy决定了容器退出后如何处理。Always是默认值不管容器以什么状态退出都会重新启动。OnFailure只在容器异常退出时重启正常退出则不重启。Never则完全不重启。这对一次性任务的场景非常重要。我有个朋友把一次性批处理任务写成了PodrestartPolicy用了默认的Always结果任务跑完容器正常退出但由于策略是AlwaysPod又被拉起重新执行了一遍形成了一个循环。改成OnFailure后再没发生过。imagePullPolicy控制镜像拉取策略。Always表示每次创建容器都拉取最新镜像IfNotPresent表示本地已有镜像就不拉取没有才拉取Never表示只用本机镜像否则报错。还有一个隐藏规则如果镜像tag是latest或者tag被省略那么imagePullPolicy默认为Always如果是具体版本号如nginx:1.25则默认为IfNotPresent。这个默认规则在生产中容易踩坑。有人为了每次部署都用最新的代码刻意不写tag让镜像默认latest这种做法有它的便利但代价是每次Pod启动都可能拉到一个不一致的版本导致环境差异。我的建议是开发环境可以适度用latest生产环境必须指定具体版本号并且显式声明imagePullPolicy不要依赖默认值。2.4 资源请求与限制调度和运行的隐形规则resources字段是Pod能否顺利调度、稳定运行的关键。requests表示调度时预留的资源它决定Pod被放到哪个节点。K8s调度器在选择节点时会检查该节点剩余资源是否满足Pod的requests。如果节点资源不足Pod就会一直停在Pending状态。limits则是运行时资源上限超过限制会触发不同的处理机制。CPU使用超过limits会被限流表现为容器性能下降内存使用超过limits则可能直接触发OOM容器被杀重启。CPU的单位是毫核1核1000m。100m表示0.1个CPU核心。内存的单位常见是Mi、Gi注意和MB、GB的区别Mi表示1024的进制而MB表示1000进制混用会导致配置偏差。给一个我常用的配置参考resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi这里有个常见误区有些人会把requests和limits设成一样。这样做的好处是Pod的QoS级别会变成Guaranteed调度更稳定不容易被驱逐坏处是资源浪费严重因为requests也是一种预留预留着但不用其他Pod就用不了。对于大多数业务预留和上限之间留出缓冲空间是更经济的做法。3. 从零到一创建一个 Pod 的完整实操3.1 编写第一个 Pod 的 YAML下面给出一个最小可用的Pod定义我加上了注释方便你理解每个字段的含义。apiVersion: v1 # Pod属于v1核心API组 kind: Pod # 资源类型是Pod metadata: name: nginx-demo # Pod名称集群内唯一 namespace: default # 命名空间不写默认是default labels: app: nginx # 标签后续选择器全靠它 spec: containers: - name: nginx # 容器名这个Pod内的标识 image: nginx:1.25 # 具体版本号别省略tag ports: - containerPort: 80 # 声明容器监听的端口仅供参考 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi这个YAML创建出的Pod只有一个Nginx容器。有人会问ports这个字段有什么用说实话它只是文档性质的声明真正决定是否能访问到服务的是容器内进程实际监听的端口。K8s不需要你声明端口就能把流量送入容器声明了反而方便别人理解和排查。3.2 使用 kubectl 部署与验证保存文件为nginx-demo.yaml然后执行kubectl apply -f nginx-demo.yaml这条命令会把YAML提交给API Server最终由kubelet在某个节点上启动容器。apply和create的区别在于apply是声明式的支持增量更新推荐用apply而不是create。部署后查看状态kubectl get pod nginx-demo kubectl get pod nginx-demo -o wide-o wide会多显示Pod所在的节点和IP地址这个信息在排查问题时很有用。如果状态是Running基本上就成功了。检查容器日志kubectl logs nginx-demo如果你想进入容器内部kubectl exec -it nginx-demo -- /bin/bash注意--后面是容器内的命令前面是kubectl的参数。这个细节有些人会漏掉写成kubectl exec -it nginx-demo /bin/bash在某些版本下也能用但规范写法必须是--分隔。3.3 多容器 Pod 的部署示例为了展示Pod内多个容器协作我用一个Nginx加一个BusyBox日志查看Sidecar来演示。apiVersion: v1 kind: Pod metadata: name: web-with-sidecar labels: app: web spec: containers: - name: web image: nginx:1.25 ports: - containerPort: 80 volumeMounts: - name: shared-logs mountPath: /var/log/nginx - name: sidecar image: busybox command: [sh, -c, tail -f /var/log/nginx/access.log] volumeMounts: - name: shared-logs mountPath: /var/log/nginx volumes: - name: shared-logs emptyDir: {}这里用的是emptyDir卷它随Pod创建而创建随Pod删除而删除。Nginx把访问日志写到/var/log/nginx/access.log这个路径被挂载成共享卷Sidecar容器用tail -f持续读取同一个文件。你执行kubectl logs web-with-sidecar -c sidecar就能实时看到Nginx的访问日志而无需进入容器敲命令。注意kubectl logs后面要加-c sidecar指定容器名否则在多容器Pod里会提示你指定容器。emptyDir有一点要提醒它默认写在节点本地磁盘上如果Pod被调度到别的节点数据就丢了。它适合存临时数据、缓存和日志中转绝不适合存不能丢失的业务数据。要持久化数据需要换成PersistentVolumeClaim。3.4 端口转发与临时调试技巧有时候Pod运行在集群内部你本地直连不上。这时候可以用端口转发kubectl port-forward pod/nginx-demo 8080:80执行后在本地访问http://localhost:8080流量会被转发到Pod内的80端口。这是最快的调试手段尤其在没有Ingress、没有Service的环境下。我实际开发中经常用这个命令快速验证Pod里的服务是否正常。它比创建Service更快因为不需要额外的资源定义一条命令就能打通本地到Pod的通道。生产环境的流量入口不应该依赖它但它作为调试工具是无可替代的。如果你要在容器里装一些临时排查工具比如curl、ping直接进去装就行。但要注意容器是基于镜像创建的临时环境你安装的工具在容器重建后会消失不要指望在里面做环境固化。4. 常见问题与排障实录4.1 镜像拉取失败ImagePullBackOff 的三大原因Pod停留在ImagePullBackOff状态时直接执行kubectl describe pod name看Events部分会给出明确原因。我排过很多这种问题总结下来无非三类。镜像地址写错了或者镜像tag不存在。对应的Event是Failed to pull image... manifest unknown。这种情况去镜像仓库确认地址和tag是否存在就行。在私有仓库场景下还有一种情况是tag存在但你没有pull权限Event会显示pull access denied。网络问题导致无法连通镜像仓库。Event会显示EOF、dial tcp i/o timeout这类信息。这种情况需要检查节点到镜像仓库的网络如果节点在中国大陆而镜像源在海外就涉及镜像加速器或者内网镜像仓库的配置。我这里不方便展开细节但核心思路是让节点能访问到镜像所在位置。认证失败。私有镜像仓库通常需要凭据。在你手动docker login过的节点上可能没问题但K8s的kubelet不会自动使用你本地的docker凭证。需要通过kubectl create secret docker-registry创建Secret并在Pod里配置imagePullSecrets。spec: imagePullSecrets: - name: regcred4.2 Pod 一直 Pending 不调度的排查方法Pending是一个很容易误导人的状态。Pod卡在Pending不进入Running说明它根本还没被放到节点上。原因是调度器找不到满足条件的节点。打开kubectl describe pod nameEvents里如果出现0/3 nodes are available后面会跟着具体原因。常见的是资源不足也就是节点剩余CPU或内存不满足Pod的requests。这种问题要看节点实际使用量kubectl top nodes如果节点资源明明有剩余但Pod还是Pending那大概率是nodeSelector或污点容忍的问题。比如Pod指定了nodeSelector要求节点带某个标签但集群里没有节点打这个标签或者节点设了污点而Pod没有对应的tolerations调度器会自动跳过这些节点。污点的排查看起来吓人原理并不复杂。节点上的污点好比在说“这个节点有特殊偏好不接受普通Pod”Pod必须显式声明能忍受这个污点才可能被调度上去。没有声明就永远Pending这是K8s一个比较常见但不好排查的现象。4.3 容器启动后立即退出的三类情况容器启动后马上退出反映在Pod上就是CrashLoopBackOff或反复重启。我见过的场景基本可以归为三类。第一镜像里的启动命令和你的预期不符。比如镜像默认前台启动某个服务但你的Pod配置里覆盖了command结果命令执行完就退出容器生命周期结束。解决办法是不覆盖command或者改用常驻进程。第二应用启动时连接外部依赖失败比如连不上数据库、连不上配置中心服务直接panic退出。查看日志会发现具体报错信息。这种情况要注意容器本身可能没有问题待依赖就绪后就能正常启动但是如果进程退出了K8s只能用restartPolicy把它重新拉起来反复失败就CrashLoop。第三配置错误导致应用启动失败。比如环境变量名写错、配置文件路径不对。日志里会直接打出错误原因即便看不出来把日志截取下来和镜像作者核对也是常规做法。排这类问题第一步永远是kubectl logs pod-name --tail100如果容器重启过快日志被清空导致看不到之前的内容可以加上-p参数查看上一次容器实例的日志。kubectl logs pod-name -p这个-p参数排查CrashLoopBackOff时极其好用因为容器重启后默认看到的是新容器的日志真正报错的往往是上一个容器退出前留下的日志。4.4 健康检查失败的排查思路配置了livenessProbe或readinessProbe之后Pod可能一直处于Running状态但不是Ready这会导致Service不把流量转发给它。很多人会困惑容器明明活着为什么不提供服务这就是探针的意义。readinessProbe失败说明容器虽然启动了但还不具备接收流量的能力。原因可能是探针请求的路径不对、探针端口写错、或者应用启动慢但探针的initialDelaySeconds和timeoutSeconds设置太短。我踩过一个典型的坑一个Java应用启动需要40秒但initialDelaySeconds设了10秒结果容器在准备阶段就被readinessProbe判定失败流量永远不会打进来。后来把initialDelaySeconds调大到50秒一切都正常了。另一个常见的是livenessProbe失败导致容器被不断重启。liveness探针的失败处理直接是重启容器所以设置时要格外小心。一个建议是livenessProbe的timeout和failureThreshold要设得宽松一些不要因为一次偶发超时就杀掉一个健康容器。而readinessProbe可以激进一点因为失败顶多是不转发流量不会重启容器。5. 个人实操中的几点感受这条内容我先记一笔等后续继续补充。Pod的学习曲线并不是陡而是知识点零碎。最开始接触到Deployment之后我就几乎不直接使用Pod了。但掌握Pod层面的知识仍然很重要工作负载的配置、排障的方法、集群的调度逻辑归根到底都落在Pod上。不理解Pod就等于学了K8s但是缺了最底下那块地基。我个人建议的学习路径是先花时间把Pod的YAML写熟搞清楚restartPolicy、resources、探针这几个核心字段的行为然后手动部署几个Pod故意制造一些异常来体会各种状态的含义最后再开始研究Deployment、StatefulSet这些控制器。顺序反过来的话遇到问题还是会一头雾水。如果这篇笔记能帮你少踩几个坑那就值了。下一步我准备写Service的网络模型到时候把Pod之间的通信、集群内DNS和Service的联系一起梳理清楚。