1. Pod是什么:K8S里最小的调度单元,到底小到什么程度
先回答一个很多刚接触Kubernetes的人都会问的问题:为什么K8S不直接管容器,非要中间加一层Pod?
我刚开始学K8S的时候也觉得Pod是个多余的抽象,容器跑得好好的,多包一层反而增加理解成本。但真正在生产环境里踩过几次坑之后就明白了,这一层抽象是K8S最核心的设计决策之一。
Pod是Kubernetes里最小的调度和部署单元,它不是一个进程,而是一个"容器运行环境"的集合。你可以把Pod理解成一个"逻辑主机"——这台上可以跑一个容器,也可以跑多个容器,这些容器共享这个主机的网络栈、存储卷和生命周期。
为什么需要这样一个中间层?最典型的场景是"边车模式"(Sidecar)。假设你有一个Web应用容器,你想给它加一个日志收集的Agent,如果让Agent单独跑在另一个Pod里,它就没法直接访问应用的本地日志文件;如果硬塞进同一个容器,又会造成进程管理混乱。放在同一个Pod里,两个容器共享文件系统和网络,Agent可以直接读应用的日志文件,通过localhost互相通信。这种设计在传统虚拟机时代是不需要额外抽象的,因为VM里天然可以跑多个进程,但容器是单进程模型,所以要由Pod这个上层概念来承担"进程组"的职责。
Pod的核心特性可以用三条来概括:共享网络命名空间(每个Pod有一个独立的IP,Pod内所有容器共用这个IP和端口空间)、共享存储卷(Pod内的容器可以挂载同一个Volume)、共享生命周期(Pod是一个整体,K8S只会整体调度、整体重启、整体删除)。
1.1 为什么K8S不直接调度容器
市面上的容器编排工具其实分两派:一派像Docker Compose,直接操作容器,简单但表达能力有限;另一派像K8S,通过Pod这一层抽象来间接管理容器,复杂但灵活。
关键原因在于,容器这个粒度在编排层面"太小了"。在生产环境里,很多应用不是单容器能搞定的,而是多个进程需要紧密协作。比如一个应用需要从本地Unix Socket读数据,或者需要共享内存做IPC通信,强制拆到不同Pod里就得引入Service Mesh、外部存储这些额外设施,代价太高。
另一个原因是容器本身不具备"原子调度"的能力。如果一个应用由A、B两个容器组成,它们必须调度到同一台机器上才能工作,那编排系统就必须把这两个容器当成一个整体来处理。Pod天然支持这种原子性——Pod内的所有容器一定调度到同一个Node上,不存在一个Pod里的两个容器被分配到不同机器的情况。
从资源管理的角度看也一样,Pod是资源配额(ResourceQuota)和资源限制(LimitRange)的最小单位。你在创建Pod时声明的CPU、内存请求,是加总这个Pod里所有容器的声明值。如果去掉Pod这一层,资源管理就只能到容器粒度,调度器的决策空间就会被严重压缩。
1.2 一个Pod里到底能装什么
很多新手对"一个Pod里放几个容器"这个问题特别困惑,我来给一个明确的标准。
最常规的情况是一个Pod一个容器,这也是绝大多数应用的标准姿势。这个"单容器Pod"其实是K8S中最常见的Pod形态,Deployment默认创建的就是这种。
什么时候需要多容器共享一个Pod?我总结了三类典型场景:
- Sidecar模式:主容器负责核心业务,辅助容器负责日志收集、流量代理、配置同步等辅助功能。最经典的例子是Istio的Envoy代理,它会自动注入到每个业务Pod里,和业务容器共享网络。
- Ambassador模式:辅助容器充当主容器的代理,把外部不同的后端服务统一暴露成localhost接口给主容器访问。
- Adapter模式:辅助容器把主容器产生的数据进行格式转换,比如把应用日志转换成统一标准格式再交给下游系统。
但我要提醒一点:多容器Pod不要滥用。两个容器必须满足"要么一起生、要么一起死"的耦合关系,才有必要放进同一个Pod。如果只是互相依赖的服务,用Service去解耦是更合理的方案。我见过有人把数据库和应用放在同一个Pod里的操作,这种设计在Pod重启时数据库也会跟着重启,简直是自己给自己挖坑。
1.3 静态Pod:一种容易被忽略的Pod形态
除了通过API Server创建的普通Pod,K8S还有一种特殊形态叫静态Pod(Static Pod)。静态Pod由kubelet直接管理,不经过API Server和调度器,YAML文件放在节点的特定目录下(默认是/etc/kubernetes/manifests)。
静态Pod最大的特点是"自愈依赖本机"。kubelet会持续监控这个目录,文件变化时创建或删除Pod,Pod崩溃时kubelet直接在当前节点重新拉起。因为不走调度器,它永远只会在本机运行。
你可能好奇这东西有什么用。最著名的用途是部署K8S控制平面组件——apiserver、etcd、controller-manager、scheduler在kubeadm搭建的集群里全都是静态Pod。我自己排查过几次控制平面异常,基本都是直接去/etc/kubernetes/manifests下改配置然后等kubelet自动重建。
明白了Pod是什么,下一步要搞清楚的就是它的生命周期。这个问题不搞透,后面排查故障的时候会非常痛苦。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Pod生命周期与状态流转:从Pending到Running再到Failed
Pod的生命周期管理是K8S里最核心的机制之一,也是排查问题时的第一道关卡。我在实际工作中几乎每天都要通过看Pod状态来快速判断问题方向。
2.1 六种状态与阶段流转
首先要把Pod状态(Status)和容器状态区分开。Pod层面有五个阶段(Phase),每个阶段代表Pod的生命周期位置:
- Pending:Pod已被API Server接受,但还没有完成调度或者镜像还没拉取完
- Running:Pod至少有一个容器在运行中,或者正在启动/重启
- Succeeded:所有容器都正常退出,退出码为0,且不会重启
- Failed:至少有一个容器异常退出,退出码非0
- Unknown:API Server无法获取Pod状态,通常意味着节点失联或kubelet挂了
一个正常Pod的流转路径是Pending → Running → Succeeded(对于一次性任务)或者长期保持Running。异常路径则是卡在Pending(调度失败)或者在Running之后反复重启(CrashLoopBackOff)。
这里有个容易误解的点:Running状态不等于Pod健康。Running只代表容器进程在跑,进程内部是否正常工作、是否能够响应请求,K8S根本不关心。要验证应用真的可用,必须靠后面的探针(Probe)机制。
2.2 容器状态:Waiting、Running、Terminated
Pod状态是宏观视角,容器状态才是微观视角。我们用kubectl get pod看到的STATUS那一列,其实综合了Pod阶段和容器状态。深入看容器本身,只有三种状态:
- Waiting:容器还没开始运行,可能是在拉镜像、在等配置、或者在等卷挂载
- Running:容器正在执行
- Terminated:容器曾执行过,但现在已经退出
排查问题时的关键技巧是看Reason字段。Waiting状态最常见的Reason是ImagePullBackOff(拉镜像失败)、ContainerCreating(容器创建中)、CreateContainerConfigError(配置错误)。Terminated状态要重点看Exit Code,0代表正常退出,137代表被kill(通常是内存超限被OOM Killer干掉),1或2通常是应用自身报错。
2.3 为什么Pod一直Pending?排查思路
Pod卡在Pending是新手最常遇到的问题,但原因其实就那么几类。我按可能性从高到低列一下排查顺序:
第一,资源不足。调度器判断某个节点没有足够的CPU、内存、GPU等资源来满足Pod的requests声明,Pod就会一直Pending。用kubectl describe pod就能看到类似"0/3 nodes are available: insufficient cpu"这样的提示。解决办法要么扩充节点,要么降低Pod的资源声明。
第二,镜像拉取失败。注意,镜像拉取实际上是在调度完成后、kubelet创建容器时才发生的,所以它通常会让Pod卡在ContainerCreating而不是Pending。但如果你配置了nodeSelector指定的节点有问题,或者亲和性规则不满足,也会表现为Pending。
第三,污点和容忍度不匹配。节点上有NoSchedule或NoExecute的污点,而Pod没有对应的容忍度,调度器会直接跳过这个节点。排查时用kubectl describe nodes看Taints字段。
第四,持久卷无法挂载。如果Pod声明了PVC但PV没有绑定成功,Pod会卡在ContainerCreating阶段,而不是Pending。这个问题在创建StatefulSet时特别常见,排查时看Events里的FailedMount提示。
Pod状态只是表面现象,要真正把Pod用起来,核心在于怎么写好Pod的配置。我下面把常用配置逐段拆开讲。
3. 从零手写一个Pod:常用字段与资源配置详解
很多教程上来就给一个完整的Deployment YAML,其实对想理解K8S的人来说,先把裸Pod的配置吃透更重要。Pod的配置是所有控制器(Deployment、StatefulSet等)的基础,你看懂了Pod,再看控制器就是"换了一层壳"的事。
3.1 一个最小可用的Pod YAML拆解
先看一个最常见的Pod配置:
yaml复制apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
namespace: default
labels:
app: nginx
env: dev
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
这里面每个字段都有讲究。apiVersion对于裸Pod必须是v1,如果你写成apps/v1那API Server会直接报错,因为apps/v1是给Deployment、StatefulSet这类控制器用的。metadata.labels是Pod的"标签",K8S里的Service、ReplicaSet都是通过标签选择器来关联Pod的,标签命名规范建议用"应用名+环境+版本"的结构,方便后面做筛选和灰度发布。
spec.containers是必填字段,至少要声明一个容器。注意containers是一个数组,这就是前面说的一个Pod里可以放多个容器的原因。ports字段其实只是一个"声明",不写K8S也能正常工作——容器进程监听哪个端口是容器本身的事,K8S不会因为你声明了80就帮你做端口映射,这个声明主要作用是让人和工具能快速了解Pod暴露了什么端口。
3.2 资源requests与limits的语义与陷阱
用kubectl describe pod看到的资源字段,背后是K8S的资源模型在起作用。生产环境里最核心的两个字段是requests和limits。
yaml复制spec:
containers:
- name: nginx
image: nginx:1.25
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
requests和limits的语义完全不同,新手最容易在这里翻车。
requests是"调度保证",声明的是容器运行所需的最低资源。调度器在选择节点时,会检查节点的可分配资源能否满足所有Pod的requests总和。如果你的Pod声明requests.cpu为100m,节点上剩余CPU不够100m,这个Pod就不会被调度到这个节点。这里有个细节:CPU的100m是"100毫核"的意思,1核CPU等于1000m;内存的Mi是二进制单位,1Mi等于1024Ki。
limits是"运行限制",声明的是容器最多能使用的资源上限。CPU的limits是CPU时间片限制,超过会被限流,容器不会死;但内存的limits一旦超过,容器会被OOM Killer直接杀掉。这就是为什么很多容器莫名其妙被kill,退出码是137的原因——它吃内存超过了limits。
实际操作里我给一个配置建议:requests不要拍脑袋填,要基于应用的真实使用情况来定。很多人在测试环境随便填requests导致生产环境调度上不去,或者requests填得太小而实际负载高导致节点过载。最好的做法是先不加limits观察几天,用kubectl top pod看真实占用,再反推合理的requests和limits。
还有一个关于CPU limits的隐藏坑:CPU的limits设了之后,容器的CPU使用率会被严格限制在limits以内,这会导致Java这类运行时在启动时检测到的CPU核数远小于机器真实核数,进而影响线程池大小和JVM堆的默认配置。我遇到过不止一次:服务在测试机上是8核,部署到K8S里变成2核(limits设了2000m),线程池配置全部偏小。
3.3 三种探针:startupProbe、livenessProbe、readinessProbe怎么选
探针(Probe)是K8S对容器进行健康检查的机制,也是最容易被忽视又最重要的配置。它的重要性体现在一个场景里:你的服务没有崩溃,K8S怎么知道它"不健康"?答案就是探针。
K8S有三种探针,很多人搞混它们的用途,我用一句话概括:
- livenessProbe(存活探针):告诉K8S"这个容器还活着吗"。如果探针失败,K8S会杀掉容器并按照restartPolicy重启。它的作用是处理"进程死锁、僵死但没退出"这种情况,比如应用卡死无法响应请求,但进程还在。
- readinessProbe(就绪探针):告诉K8S"这个容器可以接收流量吗"。如果探针失败,K8S会把这个Pod从Service的Endpoints里摘掉,但不会重启容器。它的作用是处理"服务在启动过程中还没有完全准备好,或者出现短暂过载"的情况。
- startupProbe(启动探针):告诉K8S"容器启动完成了吗"。如果探针失败,K8S会重启容器。它专门解决慢启动应用的问题——有些Java应用启动要1到2分钟,livenessProbe的初始延时设短了会在启动过程中被杀掉。
实际配置一个HTTP接口的探针:
yaml复制spec:
containers:
- name: spring-app
image: spring-app:v1.0
ports:
- containerPort: 8080
startupProbe:
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 30
periodSeconds: 5
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
注意这个配置的精妙之处:startupProbe给了最多30×5=150秒的启动时间窗口,在它成功之前livenessProbe不会生效,这就避免慢启动应用被误杀。startupProbe成功后,livenessProbe才开始接管。
探针的配置有几个容易踩的坑:
- livenessProbe的periodSeconds不要设太短,否则探针请求本身会消耗大量资源,尤其在高并发场景下压垮应用。
- 探针路径不要依赖外部依赖服务,比如健康检查接口里查数据库导致数据库抖动时自杀式重启所有实例,这就是典型的"探针连锁故障"。
- 严重的问题:timeoutSeconds和failureThreshold必须合理配合,timeoutSeconds默认只有1秒,如果你的健康检查接口处理超过1秒,探针会误判失败。
3.4 环境变量与ConfigMap/Secret的注入方式
容器里跑的程序离不开配置,K8S把配置分成了两大类:ConfigMap存非敏感配置,Secret存敏感配置。
ConfigMap的推荐用法有四种,我用一个例子说明:
yaml复制apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_ENV: production
LOG_LEVEL: info
database.properties: |
url=jdbc:mysql://db.internal:3306/app
username=admin
---
apiVersion: v1
kind: Pod
metadata:
name: config-pod
spec:
containers:
- name: app
image: app:v1.0
env:
- name: APP_ENV
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV
envFrom:
- configMapRef:
name: app-config
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-config
第一种是直接用env.valueFrom.configMapKeyRef,把ConfigMap中某个key的值注入成环境变量;第二种是envFrom直接导入ConfigMap所有键值对为环境变量;第三种是把ConfigMap挂载成文件;第四种是通过sidecar容器读取ConfigMap并实现热更新。
Secret的用法和ConfigMap几乎一样,只是数据存储时会做Base64编码(注意,这只是编码不是加密),并且可以配置KMS加密。实际使用中建议把数据库密码、API密钥、证书这类数据放Secret,比如:
yaml复制apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data:
password: cGFzc3dvcmQxMjM= # 这是password123的Base64编码
---
spec:
containers:
- name: app
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
这里有个实践中的要点:Secret会被挂载为tmpfs(内存文件系统),不会写入节点的磁盘,安全性比ConfigMap高。但如果你用envFrom把整个Secret全部灌入环境变量,那么所有key都会暴露在容器环境里,即使应用用不到也会被看到。建议只注入业务实际需要的key。
3.5 用kubectl命令快速生成YAML骨架
手写YAML容易漏字段,我推荐一个效率工具:kubectl create命令加--dry-run参数可以直接生成YAML骨架,不用去官方文档查格式。
bash复制kubectl create deployment nginx --image=nginx:1.25 --replicas=3 --dry-run=client -o yaml > nginx-deployment.yaml
这条命令会在本地生成一个Deployment的YAML,不实际创建资源(--dry-run=client)。拿到之后你只需要在这个基础上改配置,比从空白文件写起高效得多。
同理可以生成Service:
bash复制kubectl create service clusterip nginx --tcp=80:80 --dry-run=client -o yaml
对于手写Pod,还可以直接用kubectl run:
bash复制kubectl run nginx-pod --image=nginx:1.25 --restart=Never --dry-run=client -o yaml
生成的YAML里会自动包含labels、restartPolicy这些必要字段。注意kubectl run默认生成的Pod带restartPolicy=Always,加--restart=Never才会生成裸Pod。
配置写好了,接下来就是实际部署。这一节我用一个完整的服务发布流程来演示Pod从创建到对外提供服务的全过程,顺便解决那个很常见的疑问:Pod到底怎么暴露给外部访问。
4. 实战:从YAML到服务发布,一次完整的Pod部署流程
我听到很多新手问一个问题:我自己写了一个服务,怎么把它发布到K8S里让别人访问?很多人以为创建了Pod就能直接通过IP访问,实际上Pod的IP是集群内部的,外部网络根本访问不到,还需要Service来做服务发现和负载均衡。
下面我以一个Nginx服务为例,走一遍完整的部署流程。
4.1 部署一个Nginx测试Pod并验证网络
先把YAML文件准备好,我建议直接在命令行里操作,方便验证每一步的效果。
bash复制# 创建名为web-pod的Pod
kubectl run web-pod --image=nginx:1.25 --port=80 --restart=Never
# 查看Pod创建状态
kubectl get pods -o wide
输出里有一个Pod的IP地址,比如10.244.0.5。这个IP是集群内部的IP,你如果想在集群外访问它,必须先进入集群内部网络。验证Pod能不能正常工作,我通常用两个方法:
方法一,进入Pod内部访问本地服务:
bash复制kubectl exec -it web-pod -- curl http://localhost
方法二,在集群内另一个Pod里访问:
bash复制kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -- curl http://10.244.0.5:80
第二种方法里的--rm参数会在命令执行后自动删除这个临时Pod,不会留下垃圾资源。这个技巧在排查跨Pod通信问题时特别有用。
4.2 用kubectl port-forward临时调试
业务开发阶段,你不想暴露一个正式的Service,但又想从本地浏览器访问集群内的Pod来调试页面或接口,port-forward是最快的方案。
bash复制kubectl port-forward pod/web-pod 8080:80
这条命令会把本机的8080端口转发到web-pod的80端口,然后你在浏览器里打开http://localhost:8080就能看到Nginx的默认页面。
port-forward本质上是在kubectl客户端和目标Pod之间建立一条隧道,需要kubectl进程一直保持运行,所以它只能用于开发和调试,不能用于生产环境。
4.3 在Dashboard里发布新Pod作为新服务
很多中文用户习惯用kubernetes dashboard做可视化操作。从Dashboard发布一个新Pod作为新服务,操作路径大概是这样的:
首先登录Dashboard,点击右上角的"+"号或左侧菜单的"创建资源",会看到一个文本输入框,支持直接粘贴YAML或者选择文件导入。你把上面写好的Pod或Deployment的YAML粘贴进去,点"上传",资源就会被创建。
创建成功后,在"工作负载"菜单能看到Pod的运行状态。如果要让外部能访问这个Pod,需要再创建一个Service:同样用YAML方式创建,kind为Service,type根据需求选择ClusterIP(集群内访问)、NodePort(节点端口访问)或LoadBalancer(云负载均衡)。
我之前遇到一个常见的Dashboard使用误区:有人直接在Dashboard里点"创建Deployment",然后发现创建的Deployment只能在集群内部访问,外部访问不到。这是因为Dashboard创建Deployment时不会自动创建Service,你还需要单独创建一个Service并做好关联。关联方式就是通过选择器匹配Pod的标签。
4.4 从Pod到Deployment:为什么不建议直接管理Pod
前面演示了裸Pod的创建和访问,但我要明确告诉各位:生产环境千万不要直接创建裸Pod,这是一个所有K8S工程师的血泪教训。
裸Pod最大的问题是"无人管理"。你手动创建的Pod,如果所在节点宕机了,K8S不会自动帮你在其他节点重建。我们部署应用,要的是"无论发生什么,服务始终可用",而裸Pod做不到这一点。
正确的做法是使用Deployment。Deployment是管理Pod的控制器,它保证指定数量的Pod副本始终运行。看一个标准配置:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
注意两个关键点:
第一,spec.selector.matchLabels必须和spec.template.metadata.labels完全匹配。上面的例子都是app: nginx。这个标签选择器是Deployment管理Pod的唯一依据,一旦不匹配,Deployment创建的Pod就会失控或无法创建。
第二,升级应用版本只需要改image字段,Deployment会自动做滚动更新(默认策略),并且可以一键回滚:
bash复制# 升级镜像版本
kubectl set image deployment/nginx-deployment nginx=nginx:1.26
# 查看滚动更新状态
kubectl rollout status deployment/nginx-deployment
# 回滚到上一个版本
kubectl rollout undo deployment/nginx-deployment
部署Deployment之后,还需要一个Service来暴露服务。一个最简单的ClusterIP Service配置是这样:
yaml复制apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
Service的spec.selector必须和Deployment里Pod模板的labels一致。上面配置里Service的selector是app: nginx,它会找到所有带这个标签的Pod,自动把流量分发到这些Pod上。
如果你要对外提供服务,把Service的type改成NodePort,然后K8S会在每个节点上分配一个30000-32767之间的端口,外部通过"任意节点IP+NodePort"就能访问。比如:
bash复制kubectl expose deployment nginx-deployment --type=NodePort --port=80 --target-port=80
kubectl get service nginx-deployment
输出里显示的PORT(S)列如果显示80:30876/TCP,就说明你可以在集群外通过http://任意节点IP:30876访问这个服务。
整个流程走完,Pod的部署和发布链路就通了。但实际操作中你会遇到各种各样的报错,我挑几个高频问题拆解一下排查思路。
5. 常见问题与故障排查实录
K8S的排障靠的是"看Event、看Logs、看配置",这三板斧能解决90%的问题。我自己整理了一套高频问题的速查思路,每个都是实操中真实踩过的坑。
5.1 ImagePullBackOff / ErrImagePull
现象:Pod状态是ImagePullBackOff或ErrImagePull,Events里显示Failed to pull image。
排查思路按优先级排列:
第一,仔细看镜像名是否拼写正确。镜像名写错是最常见的原因,比如nginx写成ngin、镜像tag写错(nginx:1.25写成了nginx:1.250)。用kubectl describe pod查看Events里的具体错误信息,如果是"manifest unknown",基本就是tag不存在。
第二,检查私有仓库认证。如果镜像是从私有仓库拉取的,需要先在命名空间里创建imagePullSecret:
bash复制kubectl create secret docker-registry regcred \
--docker-server=<你的仓库地址> \
--docker-username=<用户名> \
--docker-password=<密码>
# 在Pod里指定使用凭据
spec:
containers:
- name: app
image: private.registry.com/app:v1
imagePullSecrets:
- name: regcred
第三,检查节点上有没有配置代理。如果你用了代理拉镜像,但代理配置不对,会报"dial tcp: lookup xxx on ... no such host"这类网络错误。
第四,磁盘空间不足也会导致拉取失败,报"no space left on device"。
5.2 CrashLoopBackOff:启动即崩溃
现象:Pod反复重启,状态显示CrashLoopBackOff。这个状态意味着容器启动后立刻退出,或启动过程中报错退出,K8S按restartPolicy不断重启,但始终失败。
排查方向:
最关键的是看日志:
bash复制kubectl logs <pod-name> --previous
加--previous参数可以看容器上一次退出时的日志,因为当前容器可能已经崩溃重启了好几次,默认拿到的日志不一定是报错的那一段。
然后看退出码,退出码对应的问题方向:
- 137:内存超限被OOM Killer杀掉,优先检查limits是否设置过低
- 143:SIGTERM,通常是手动删除或停机信号
- 1:应用自身报错,去看应用日志
- 126:权限问题或命令执行失败
- 127:命令找不到,检查镜像里的EntryPoint是否配置正确
我遇到过一个很隐蔽的CrashLoopBackOff:容器启动时连接不到数据库,应用直接panic退出,但数据库本身没问题,是Pod的启动时序问题——数据库是另一个命名空间的服务,网络策略把Pod到数据库的流量挡了。排查方法就是进到Pod里手动测试连通性:
bash复制kubectl exec -it <pod-name> -- bash
# 在容器里测试
curl -v telnet://db-service:3306
5.3 CreateContainerConfigError / CreateContainerError
现象:Pod处于ContainerCreating状态,Events里显示CreateContainerConfigError。
这个报错通常是配置找不到导致的。最常见的原因是Pod引用了不存在的ConfigMap或Secret,或者引用的key不存在。排查时查看Pod详情:
bash复制kubectl describe pod <pod-name>
Events里会明确提示类似"configmap "app-config" not found"或者"secret "db-secret" not found"。解决办法很直接:创建对应的ConfigMap/Secret,或者修改Pod里的引用名称。
CreateContainerError则常见于挂载卷失败,比如PVC未绑定、Mount被拒绝。检查PVC状态:
bash复制kubectl get pvc
kubectl describe pvc <pvc-name>
如果是Pending,再看PV是否存在、存储类(StorageClass)是否配置正确。
5.4 Failed to create pod sandbox:运行时层面的问题
这是kubelet在创建Pod的沙箱(sandbox)环境时遇到的错误。沙箱是容器运行时的隔离环境,containerd或CRI-O在创建Pod网络、命名空间时如果失败,就会报这个错误。
网上搜索"failed to create pod sandbox"会有大量结果,原因多种多样,但最常见的是这几类:
第一,CNI网络插件故障。Pod沙箱创建时需要CNI插件配置网络,如果CNI的配置文件损坏、插件二进制缺失、或者网络接口冲突,就会报错。排查方法:
bash复制# 查看容器运行时日志
journalctl -u kubelet -f | grep -i "sandbox"
# 检查CNI配置文件
ls /etc/cni/net.d/
第二,Pod的端口或网络命名空间冲突。我之前遇到过Pod一直报sandbox创建失败,排查半天发现是宿主机上残留了大量未清理的虚拟网络接口,导致新Pod的网络空间创建失败。重启kubelet或者重启节点有时能解决,但根源还是要清理残留网络接口。
第三,容器运行时本身异常。比如containerd服务没启动,或者Docker和containerd版本不兼容。遇到这类问题优先检查运行时状态:
bash复制systemctl status containerd
ctr version
5.5 排障顺序总结:一套通用的排查方法论
我把自己排查K8S问题的顺序总结成一个标准的操作流程,每次遇到Pod异常都按这个顺序执行,基本能在5分钟内定位问题:
第一步,看Pod状态全貌:
bash复制kubectl get pods -o wide
kubectl describe pod <pod-name>
describe是排障第一利器,里面包含了事件(Events)、调度结果、镜像信息、挂载信息,大多数问题在这一步就能找到原因。
第二步,看容器日志。用kubectl logs查看应用日志,必要时加--previous看上一次退出日志。
第三步,看节点状态。如果怀疑是资源或网络问题:
bash复制kubectl describe node <node-name>
kubectl top node
第四步,看集群组件状态。selinux、防火墙、磁盘空间这类底层因素也不能忽略。
这个排查链条里我特别想强调一点:很多人习惯把报错信息直接复制到搜索引擎,我理解这种做法,但建议先自己看一遍Events和日志,因为错误信息往往会指向表面的症状,而不是根本原因。比如failed to create pod sandbox,如果直接去搜,可能搜出十几种说法,但结合你的具体环境(kubelet日志、CNI配置、运行时日志)才能准确定位。
写在最后:Pod是K8S的地基,值得多花时间把它搞透
讲了这么多,我最后想说的是,Pod虽然是K8S最基础的概念,但它恰恰是理解整个K8S的钥匙。我见过太多人一上来就急着部署Deployment、配置Service,倒也能跑起来,但一旦出问题就完全无从下手,就是因为对Pod这一层的运行机制没有建立直觉。
根据我自己的经验,把Pod彻底搞明白的最佳路径是:先手写几个裸Pod练手,故意制造一些故障(比如镜像名写错、探针配置错误、资源设置过大),然后用kubectl describe和kubectl logs去排查,这个过程能帮你快速建立"Pod状态变化→底层原因"的对应关系。等你对Pod的脾气摸透了,再上手Deployment、StatefulSet、DaemonSet这些控制器,就会有融会贯通的感觉。
如果你正在学习K8S,遇到某个概念想不通,不妨先把教程放下,开一个测试集群,亲手把Pod的资源限制改大改小看会发生什么,把探针的路径改成不存在的接口看会发生什么。这种实打实的操作给你的理解深度,是看任何文档都替代不了的。
后续有时间我再整理一下Service的完整工作原理,以及Deployment滚动更新过程中的那些隐藏坑。希望这篇Pod实战文章能帮你少走一些弯路。
