资讯动态

Kubernetes Pod 完全指南:从设计动机到排障实战

发布时间:2026/10/22 8:24:28 来源:尧图企业网站定制
先抛个问题你在生产环境里见过多少个 Pod 因为只是单容器跑一下而被反复重建我自己排查过的线上问题里至少有一半和 Pod 的多容器协作、探针配置、资源配额有关而不是容器镜像本身的问题。Kubernetes 里最容易被低估、却又最值得吃透的概念就是 Pod。它看起来只是一组容器的壳实际上调度、网络、存储、健康检查、优雅终止全都围绕它展开。这篇内容我会从 Pod 的设计动机讲起一路讲到多容器协作、生命周期、QoS 驱逐、拓扑分布和排障思路尽量把我踩过的坑和想明白的地方都写进去适合刚接触 Kubernetes 的开发者也适合已经跑过几个集群、但总觉得某些行为说不清为什么的运维朋友。1. 为什么最小的部署单元不是容器而是 Pod1.1 单容器方案解决不了的两类真实需求先回忆一下 Docker 时代的使用习惯一个容器一个进程docker run 拉起来就算完事。但在真实业务里很多需求没法用一个容器一个进程的模型优雅解决。举两个最常见的例子第一类是日志采集。你的业务容器把日志写到本地文件如果 Kubernetes 直接调度一个裸容器日志文件就散落在各个节点上。最通用的做法是给业务容器配一个伴生的日志采集容器它负责把日志文件转发到统一的存储里。这两个容器必须同时跑在同一台机器上、共享同一个文件目录否则日志就没法收。第二类是流量代理。业务进程只监听回环地址或只暴露内部协议需要在旁边配一个代理容器把流量转出去这两个容器也得共享网络栈才能通过 localhost 直接互通。如果把每个容器都当成独立调度的单元上面两类需求就得靠人为保证它们落在同一台机器来解决——这显然不可靠。Pod 的出现本质上是把必须同生共死、互相协作的一组容器打包成一个调度、网络、存储的原子单元。1.2 Pod 的本质一组容器的共享运行环境我一直喜欢用一个比喻Pod 是公交车容器是乘客。乘客可以随时上车下车但一旦公交车出发这一车乘客就在同一辆车上共享同一段旅程。对应到 Kubernetes 里同一个 Pod 内的容器有几个强制共享的维度网络命名空间所有容器共享同一个 IP 和端口空间容器间通过 localhost 就能通信。存储卷Pod 级别的存储卷可以挂载给同一个 Pod 内的多个容器实现数据交换。调度结果整个 Pod 只会被调度到一个节点上不会出现容器 A 在节点 1、容器 B 在节点 2 的情况。生命周期Pod 的整体状态由所有容器的状态共同决定Pod 删除时内部容器一起回收。这里有个容易忽略的细节所谓共享网络命名空间实际是 Kubernetes 在每个 Pod 里额外启动一个我们看不见的基础设施容器来持有网络命名空间业务容器加入这个命名空间。这个容器最早叫 pause 容器现在通常由运行时自行实现。你在一台节点上用 docker ps 查的时候可能会看到很多奇怪的容器那就是这些基础设施容器。很多新手第一次看到它以为是被注入的恶意容器其实它是 Pod 网络得以成立的基石。1.3 从 Docker 习惯迁移过来的常见误解我刚接触 Kubernetes 时有一个根深蒂固的错误认知kubectl run创建的一个 Pod就等于一个容器。实际上你执行kubectl run时Kubernetes 会通过控制器创建一个 PodPod 里第一个也是唯一一个容器才是你的业务容器基础设施容器是隐性的。还有一个更隐蔽的误解以为 Pod 里的多个容器可以像 Docker 的进程一样随便互相访问对方的进程 PID。虽然同一个 Pod 的容器共享网络和存储但默认不共享 PID 命名空间除非你在 Pod spec 里显式打开shareProcessNamespace。如果你真的需要容器 A 能看到容器 B 里的进程必须主动配置否则两个容器的进程树是隔离的。这个点在做故障排查时特别容易误导人——在容器 A 里 ps 看不到容器 B 的进程不代表容器 B 挂了。2. 从零拉起一个 Pod核心字段与调度逻辑2.1 最小可用 Pod 的 YAML 拆解不借助控制器只靠 YAML 文件也能直接创建一个 Pod。下面这个例子是日常调试时最常用的最小配置apiVersion: v1 kind: Pod metadata: name: simple-pod namespace: default labels: app: demo spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80 name: http这四部分各管一件事apiVersion和kind告诉 API Server 你创建的是什么资源metadata里的 name 是全局标识labels 是后续 Service、Deployment 做关联选择的依据spec.containers是容器的核心描述可以写多个每个容器有独立的 name 和 image。有两个字段新手经常不问为什么就直接抄ports和imagePullPolicy。ports其实只是声明性信息并不代表 Kubernetes 会帮你做端口映射真正让外部访问到 Pod 的是 Service。其次是imagePullPolicy如果省略默认规则是镜像标签为 latest 或没有标签时总是拉取否则仅在本地不存在时拉取。生产环境如果镜像仓库不稳定这个默认值可能引发不必要的拉取失败建议显式写成IfNotPresent或明确的版本号。2.2 调度器如何决定 Pod 落在哪个节点Pod 的调度过程可以简化成三步先把所有不符合条件的节点过滤掉再给剩余节点打分最后按分数选一个。过滤条件来自你写的各种约束最常见的包括资源请求requests、节点标签选择器nodeSelector、亲和性affinity和端口冲突检查。直接指定节点是最粗暴也最不推荐的方式——通过在 spec 里写nodeName可以绕过调度器直接把 Pod 绑到某个节点。这个字段在测试环境偶尔有用但生产上如果节点宕机Pod 不会自动迁移因为它已经被绑定了。更合理的做法是使用nodeSelector或亲和性规则spec: nodeSelector: disktype: ssd这条配置会要求调度器只在带disktypessd标签的节点上放置 Pod。注意 nodeSelector 是硬性条件如果没有任何节点满足Pod 会一直处于 Pending 状态。亲和性比 nodeSelector 更灵活支持软性偏好preferred和硬性要求required还能基于另一个 Pod 的标签来做调度约束比如让两个服务尽量靠近或尽量分离。2.3 静态 Pod不经过 API Server 的特殊存在很多人不知道有一种 Pod 是 kubelet 在节点上自己决定拉起的根本不需要通过 API Server 创建这就是静态 Pod。kubelet 会扫描某个目录默认是 /etc/kubernetes/manifests下的 YAML 文件发现新的文件就直接在当前节点上创建对应 Pod。API Server 上能看到这个 Pod但你删掉它之后kubelet 会立刻基于那个文件重新拉起。静态 Pod 最常见的用途就是承载控制面组件比如某集群的 API Server、调度器、控制器管理器就是用这种方式固定在主节点上的。这也解释了一个现象你在一台主节点上执行kubectl delete pod xxx过一会儿它又回来了因为你删掉的只是 kubelet 上报给 API Server 的影子而真正的所有者是节点上的静态文件。理解这点后排查控制面组件频繁重启的问题会少走很多弯路。3. 多容器协作的三种经典形态3.1 Init 容器启动主流程前的前置准备Init 容器是 Pod 里最先运行的容器按声明顺序逐个执行每个成功退出后才会启动下一个全部成功后主容器才会被拉起。它和主容器的最大区别是不长期驻留跑完就退出。什么时候需要它我总结出三类典型场景。第一类是等待外部依赖就绪比如数据库还没起来时主容器启动会疯狂报错这时可以在 Init 容器里循环探测数据库端口通了再放行主容器。第二类是预置数据比如从对象存储下载模型文件到共享卷再让主容器读取避免把下载逻辑写进业务代码。第三类是权限准备比如修改共享目录的属主或权限在主容器启动前把环境收拾干净。写 Init 容器时有一个需要特别注意的资源问题Kubernetes 在计算 Pod 的资源请求时会把所有 Init 容器的 requests 取最大值与所有普通容器的 requests 之和做累计。也就是说Init 容器里面的资源占用也会影响调度决策别因为它是临时进程就完全忽略 resources 配置。3.2 边车容器与主容器同生共死的最佳拍档边车容器和 Init 容器不同它会和主容器一起长期运行。最经典的例子是日志采集边车主容器写文件边车容器读文件并转发另一个是网格代理边车负责接管主容器的进出流量。边车容器的核心价值在于无侵入地增强主容器主容器完全不知道边车的存在依然只做自己的业务而日志、监控、流量治理这些横切关注点被剥离出去。我在一个实际项目里见过很典型的用法把产物上传组件做成边车业务容器只要把结果文件放到共享目录边车监听目录变化后自动上传到指定平台业务代码一行没改。实现边车通信的基本前提是共享存储卷。最常用的临时存储卷是 emptyDir它在 Pod 创建时初始化Pod 删除后数据一并销毁正好符合生命周期跟随 Pod的预期。两个容器可以把同一个 emptyDir 挂载到不同路径spec: containers: - name: app image: my-app volumeMounts: - name: logs mountPath: /var/log/app - name: log-shipper image: log-shipper volumeMounts: - name: logs mountPath: /var/log/shared volumes: - name: logs emptyDir: {}这里emptyDir不指定 sizeLimit 时默认使用节点的本地磁盘容量不受 Kubernetes 层限制。生产环境建议配置sizeLimit否则某个容器疯狂写文件可能把节点磁盘打满进而触发整个节点的驱逐风暴。另外要提醒一句emptyDir 是节点本地的存储Pod 被调度到新节点后数据不会跟随不能拿它当持久化存储。3.3 多容器共享的通信与存储细节同一个 Pod 内容器之间通过 localhost 访问彼此的服务端口不需要经过 Service。比如主容器监听 8080代理容器通过http://127.0.0.1:8080就能访问。这里有个隐含条件两个容器不能监听相同的端口因为共享网络命名空间意味着端口是全局唯一的。一旦你写出两个容器都监听 8080 的配置第二个容器会直接启动失败报端口冲突。存储共享除了 emptyDir还可以用 configMap 和 secret 作为卷挂载。两者的共性是以卷形式暴露配置修改了 configMap 后挂载到容器的文件会在一段时间后自动更新不是实时存在延迟但通过环境变量注入的配置不会热更新。所以如果你希望配置变更让进程感知要么用卷挂载方式要么在业务里封装一个重新加载逻辑不要指望环境变量自动刷新。4. Pod 生命周期从 Pending 到 Completed 的每个阶段4.1 状态机与阶段解读Pod 的生命周期由phase字段表达最常见的有五种Pending、Running、Succeeded、Failed、Unknown。Pending 表示 Pod 已被 API Server 接受但容器还没全部就绪可能是镜像还在拉取也可能是调度器还没找到合适节点Running 表示至少有一个容器还在运行Succeeded 表示所有容器都正常退出且不会重启Failed 表示至少有一个容器以非零状态退出Unknown 是 kubelet 与 API Server 失联时出现的状态。很多人只看 phase但 phase 太粗了排查问题时更有用的是conditions。conditions 是一组更细的状态开关包括 PodScheduled、Initialized、ContainersReady、Ready。这四个条件能帮你快速定位卡点如果 PodScheduled 是 False说明调度环节出了问题如果 Initialized 是 False说明 Init 容器还没跑完ContainersReady 和 Ready 的区别尤其值得注意——前者只表示容器内进程已启动后者还要额外满足就绪探针的判定。所以一个 Pod 完全可能 ContainersReadyTrue、ReadyFalse那就是就绪探针在拦截流量。4.2 三种探针各有各的用处别混着用探针是 Kubernetes 判断容器健康的手段很多同学只知道 liveness 和 readiness其实还有第三种 startup。三者的分工很明确-livenessProbe决定容器需不需要被杀掉重启。它探测失败kubelet 会根据 restartPolicy 重启容器。readinessProbe决定容器是否接收 Service 流量。探测失败不会重启只是把 Pod 从 Service 的端点列表里摘掉。startupProbe解决启动慢的容器被 liveness 误杀的问题。在 startup 成功之前liveness 不生效。三种探针支持同样的探测方式httpGet、tcpSocket、exec。httpGet 最直观直接请求一个 HTTP 路径tcpSocket 只检查端口能否建立 TCP 连接适合不提供 HTTP 服务的应用exec 则在容器内执行命令以退出码判断。我给一个实际的 readinessProbe 配置readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3initialDelaySeconds是容器启动后等多久才开始探测failureThreshold是连续失败多少次才把 Pod 标记为未就绪。这里我踩过一个大坑在服务启动需要十几秒的场景里把initialDelaySeconds设成了 1导致探针在应用还没监听端口时就开始探测连续失败几次直接进 CrashLoopBackOff。后来改用 startupProbe 给足启动时间liveness 才放心用短周期去盯运行期状态。记住一个原则启动期用 startupProbe 把关稳定后 liveness 负责保命readiness 负责流量摘除三者配合而不是相互替代。4.3 优雅终止SIGTERM 之后的那几秒Pod 被删除时不是简单地把容器 kill 掉而是有一套固定顺序。首先 API Server 标记 Pod 为 Terminating同时从 Service 的端点列表里摘除新流量不再进入随后 kubelet 向 Pod 内的主容器进程发送 SIGTERM 信号再等待terminationGracePeriodSeconds指定的时间默认 30 秒超时后发送 SIGKILL 强制杀掉。你的应用能做的就是在收到 SIGTERM 后尽快完成收尾工作停止接收新连接、处理完正在处理的请求、关闭数据库连接、最后退出。如果应用里完全没有 SIGTERM 的处理逻辑那默认行为就是直接退出很多长连接任务都会因此被硬生生切断。生产上我建议主动设置terminationGracePeriodSeconds比如 60 秒并确保应用在收到 SIGTERM 后执行优雅关闭否则这个字段调再大也没用。还有个细节如果 Pod 里的进程无论如何不肯退出kubelet 在等待期内还可以通过 preStop 钩子执行额外命令。比如 preStop 里 sleep 10 给下游留出缓冲时间或者通知注册中心摘除实例。不过钩子执行时间会计入 grace period钩子和 SIGTERM 是串行关系别把两件事的时间预算叠加错了。5. 资源配额、QoS 等级与驱逐Pod 的生存权利是怎么被决定的5.1 requests 和 limits 的真实含义Pod 里每个容器都可以声明 resources.requests 和 resources.limits。requests 直接影响调度调度器只会把 Pod 放到已分配资源 新请求不超过节点容量的节点上。limits 则影响运行期的资源上限但 CPU 和内存的限制逻辑完全不一样。CPU 是可压缩资源限了之后只是给你降速一般不会杀死进程内存是不可压缩资源一旦超过 limit内核会直接触发 OOM进程可能被杀掉。这就导致一个常见现象你的应用明明 CPU 吃满了但还在跑内存一旦超限就立刻被杀。所以排查 OOMKilled 时第一件事就是看容器的 limits.memory 和实际占用。另外要注意 requests 和 limits 的比例关系。很多资深开发者会把它们设为相等也就是 Guaranteed QoS这种配置下 Pod 的稳定性最高。如果把 limits 设得远大于 requests可以提升节点装箱率但会引入一类风险多个 Pod 同时突发内存时节点可能 OOM而 OOM 时谁被牺牲由 QoS 等级和实际使用量共同决定。这里没有银弹只能根据业务特点权衡。5.2 三种 QoS 等级决定了谁先死Kubernetes 会根据 requests 和 limits 的配置把 Pod 划分成三个 QoS 等级。Guaranteed 要求每个容器 requests 和 limits 都相等Burstable 是 Pod 里至少有一个容器有 requests 或 limits 但不满足 GuaranteedBestEffort 是所有容器都没有 requests 和 limits。等级排序决定了节点资源紧张时的处理顺序节点层面先驱逐 BestEffort再驱逐 Burstable最后才考虑 Guaranteed。OOM 场景也类似BestEffort 的进程优先被杀。下表可以清楚地看到差异QoS 等级配置特征内存超限时的命运节点资源紧张时的命运Guaranteed所有容器 requests 等于 limits一般先杀其它等级最晚被驱逐Burstable有 requests 或 limits但不全等超限可能被杀中等优先驱逐BestEffort无任何资源配置最容易被 OOM最早被驱逐实际生产中我见过不少团队把所有应用都配成 BestEffort想着反正节点内存够大。等到某天节点被监控组件打满时最先挂掉的全是这些没配资源的业务 Pod。配置资源不是应付检查而是给系统一个该牺牲谁的明确指令。5.3 驱逐机制与 PriorityClass 的联动节点的资源压力过大时kubelet 会执行驱逐。最常见的驱逐条件是 memory.available 低于阈值和 disk 使用率过高。被驱逐的 Pod 会被标记为 Failed然后进入重新调度的流程。如果你在集群里看到大量 Pod 处于 Evicted 状态先去看节点状态和 kubelet 日志而不是一头扎进 Pod YAML 里找原因。和驱逐紧密相关的是 PriorityClass。当一个高优 Pod 无法调度时调度器可能抢占preempt低优 Pod把低优 Pod 直接删掉来腾出位置。这里的逻辑是优先级高的 Pod 拥有抢座位的权利。所以如果你的关键任务 Pod 被莫名删除别只查驱逐也看看是否有更高优先级的 Pod 在抢它。合理做法是核心服务设较高优先级离线任务和测试任务设较低优先级并明确接受低优任务可能被抢占这个事实。6. 进阶实战拓扑分布、原地调试与安全上下文6.1 拓扑分布约束别让所有 Pod 挤在同一台机器集群里最常见的一个隐患是虽然节点有几十台但某个应用的副本全挤在两台上。这通常是节点亲和性配置不当或调度器装箱决策导致的。为了让副本分布更均匀Kubernetes 提供了拓扑分布约束。下面这个配置限制同一应用的不同副本尽量分散到不同的拓扑域spec: topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: demomaxSkew: 1表示不同节点上的副本数之差最多为 1topologyKey用主机名表示同一个节点算一个拓扑域whenUnsatisfiable: DoNotSchedule是硬约束找不到满足条件的节点就不调度。还有软约束ScheduleAnyway它会尽量满足但实在满足不了就照常调度。我建议核心服务用硬约束保证跨节点高可用普通服务用软约束兼顾可用性和调度成功率。6.2 原地调试没有 SSH 的临时入口虽然 Pod 里的进程在但生产环境很多容器镜像刻意不带 Shell。想进容器排查问题时可以选择临时使用调试容器。kubectl debug可以在不重启 Pod 的情况下往目标 Pod 里注入一个临时容器这个容器可以和目标容器共享进程命名空间让你直接看到目标容器的进程。配合shareProcessNamespace: true或调试容器自身开启进程共享排查效果堪比在一台虚拟机里操作。调试容器有几个特点不继承原 Pod 的探针生命周期独立默认不会被 Service 选中用完之后自动清理。这套机制比打补丁注入 SSH安全得多也符合不可变基础设施的思路。我处理过一个诡异的连接数异常问题就是用kubectl debug注入一个带 tcpdump 的容器在共享网络命名空间里直接抓包定位到是连接未释放而不是盲目重启。6.3 安全上下文把容器权限关进笼子里Pod 的安全配置很容易被忽略直到安全审计或实际出问题才追悔莫及。最基础的三项runAsNonRoot、runAsUser、readOnlyRootFilesystem。runAsNonRoot: true强制容器内进程以非 root 身份运行镜像里如果没指定 USER启动时就会报错这反而是好事逼着你把镜像改干净。readOnlyRootFilesystem: true让根文件系统只读应用需要写临时文件时通过挂载 emptyDir 解决这能有效防止容器被写入恶意文件。设置 capabilities 则更精细它可以给容器额外增加能力或去除默认能力。比如一个普通 Java 服务通常只需要 CAP_NET_BIND_SERVICE 来绑定低端口那就把默认 capabilities 全部去掉只保留这一项。对大多数业务来说最小化 privileges 带来的收益远比万一要用某个能力再放开的折腾成本值得。7. 排障实录从现象到根因的完整思考链7.1 Pod 一直 Pending先查调度而不是看日志遇到 Pod 长时间 Pending第一反应不是去看应用日志此时容器还没启动大概率没日志可看而是按顺序排查先kubectl get pods看事件再kubectl describe pod看 Events 部分。最常见的几种原因节点资源不足、镜像拉取失败、节点亲和性/污点拦截、PVC 未绑定。我印象最深的一次是集群里突然有几十个 Pod Pending全指向0/3 nodes are available但节点内存明明有剩余。后来发现是某个节点被打了 NoSchedule 污点同时有一批 Pod 没配 nodeSelector按理说不会只卡住这批。继续往深处查才知道调度失败的真实原因是 PVC 动态供给的 StorageClass 被误删PVC 一直 PendingPod 自然跟着卡住。所以描述里写着节点资源不足时别忘了检查 PVC 状态——事件信息有时候会给你一个表面原因真正的链路要靠 event 里的顺序去串。7.2 CrashLoopBackOff 的排查路径CrashLoopBackOff 应该是曝光率最高的状态了。它的含义是容器反复启动后崩溃kubelet 在退避重试。排查时我习惯按这条链走先kubectl logs看最近输出如果日志为空就加--previous看上一次容器的输出再用kubectl describe pod看容器 state 里的 exit code最后结合 exit code 判断方向。这里有一个新手容易忽略的区分点exit code 137 经常被人解读为进程被杀但 137 其实是 1289表示收到了 SIGKILL。它可能是 OOM也可能是手动删除 Pod 或节点驱逐。而 exit code 143 是 12815表示 SIGTERM通常是优雅终止流程触发的主动退出。如果你看到 137 直接往 OOM 方向查方向就偏了。正确的做法是同时看 events 里有没有 Kill/Graceful Shutdown 的标记再决定是查内存还是查删除记录。还有一种很隐蔽的 CrashLoopBackOff 原因容器启动时依赖的持久化卷里存了损坏的元数据应用每次启动都在读同一份坏数据后崩溃。这类问题看日志能定位但修复往往需要把卷里的坏文件清理掉而不是无脑调大 restartPolicy 让它自己恢复。7.3 典型误用把 Pod 当成虚拟机来用最后一个想提醒的坑是把 Pod 当成持久化的虚拟机来用。有人习惯在容器里手动装包、改配置、写临时文件然后期望 Pod 重启后一切都还在。这是和 Kubernetes 设计理念相悖的Pod 是可替代的任意时刻可能因发布、驱逐、节点故障被重建重建后一切写入容器层的改动全部丢失。正确的姿势是镜像构建时把依赖装好运行时状态写入可挂载卷或外部存储配置文件通过 ConfigMap 注入需要持久化的数据用 PersistentVolume 承载。如果团队里有成员经常手动进容器改东西建议把容器文件系统改成只读挂载配合审计日志让不规范的变更直接失败。这样虽然一开始会多一些抱怨但长期看能省下大量为什么发布后配置又变回去了之类的排查时间。我在实际处理生产问题时发现最耗时的往往不是单点故障本身而是对 Pod 的认知不够准确导致的方向性错误。花一个下午把 Pod 的调度、探针、资源、生命周期从原理上过一遍后面替你省下的可能是好几个不眠之夜。如果这篇梳理能帮你少走一段弯路那我的目的就达到了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑