1. 刚上手k8s的那些日子:四个对象为什么会把人绕晕
先说一个我自己的真实经历。几年前第一次正经用Kubernetes部署一个生产服务,我按着文档把Deployment和Service两个YAML贴上去,kubectl get pods看着Pod起来了,浏览器也通了,那一刻觉得自己已经会了。结果旁边老工程师随口问了一句:"这几个Pod是谁拉起来的?Service又是靠什么找到它们的?"我当场有点懵,心里只剩下一个模糊的印象:反正Deployment里写了副本数,Service指向某个端口就通了。后来被布置了一个任务:只更新镜像Tag,不允许动Service配置,发布完还要回滚。当我对着kubectl rollout undo想搞清楚它回滚的到底是哪一层时,才发现自己根本没有建立起Kubernetes里那套"对象层级"的心智模型。
这也是我想写这篇东西的初衷。大部分初学者或者从Docker转过来的老手,第一次接触k8s都会遇到同一个坎:deployments、pods、replica sets、services这几个名词每一个单独查文档都能看懂大意,但它们放在一个集群里到底谁管谁、谁依赖谁、请求从进门到进容器经过哪几道关卡,串不起来。这篇文章不打算教你背API文档,而是想用一套比较接地气的方式,把这四个对象拆开揉碎,再把它们拼回去。适合刚学完kubectl run、准备真正上手管理应用的人,也适合那些已经被各种概念绕晕、想重新把体系理顺的朋友。
我先说一个总的原则,后面所有解释都是围绕它展开的:Pods是干活的,ReplicaSets是保证人数的,Deployments是负责发版和回滚的,Services是提供稳定入口的。 这四层不是并列关系,而是层层向上封装的关系。搞明白这一点,剩下的都是细节。
为什么一定要拆成这么多层,而不是像Docker Compose那样直接定义一个容器集群?因为k8s要解决的是大规模、持续变更场景下的应用生命周期管理。人数要稳定,版本要升级,入口要固定,这三个诉求本身就是矛盾的:如果你直接操作Pod(比如kubectl delete pod),Pod重建后IP就变了,上游根本不知道往哪发请求;如果你直接操作Service,一旦Pod重建,Service指向的目标就全错了。所以k8s把不同职责拆成了不同对象,每一层只解决一个具体问题,上层通过标签选择器(Label Selector)去管理下层。这种设计很符合工程上"单一职责"的思路,但也确实让初学者得多花一点时间去适应。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Pod:真正干活的"最小单元",但不是你直接管的
2.1 一个Pod里可以装好几个容器,但共享一套网络和存储
很多人刚学k8s时会有一个疑问:为什么最小调度单位不是容器?我在Docker里明明一个容器就能跑一个进程,到了k8s这里却非要多套一层Pod?
原因在于,真实业务里经常存在"多个进程需要紧密协作、共享资源"的场景。最典型的就是主业务容器加一个Sidecar容器,比如一个Nginx容器负责对外接收请求,旁边挂一个日志采集容器,它俩共享同一个网络命名空间和存储卷,日志容器能直接读Nginx的日志文件。如果直接把这两个容器当成两个独立调度单位,它们之间的文件共享、网络通信就得自己想办法打通,非常麻烦。Pod相当于把一组"必须部署在同一台机器上、共享网络和存储、同生共死"的容器打包在一起。
从网络角度看,Pod是k8s集群里最小的IP分配单元。同一个Pod里的所有容器共享同一个Pod IP,通过localhost就能互相访问。从存储角度看,Pod可以挂载Volume,Pod内所有容器都能读写这个卷。从生命周期角度看,Pod是整体创建、整体删除的,里面某个容器挂了并不会导致整个Pod重建,而是由容器运行时负责重启它,但如果Pod本身挂了,就需要上层的ReplicaSet来处理。
2.2 Pod的IP是"一次性"的:这是所有问题的根源
Pod还有另一个特别重要的特性:它的IP不固定。只要Pod被删除、重建,它大概率会落到另一台节点上,拿到的IP也变了。这种"临时性"是k8s面向规模场景做出来的设计决定,但它给上层访问带来了一个巨大的麻烦:如果客户端直接访问Pod IP,一旦Pod重建,地址就失效了。
很多从传统运维转过来的人会觉得这个设计不可理喻:既然IP会变,为什么不干脆固定IP,让大家省点心?但你要反过来想,在成百上千个副本、频繁扩缩容的集群里,固定IP是没法做的,因为它要求你在调度之前就把整个集群的资源规划死,弹性也就不存在了。k8s选择的是另一条路:把"寻址"这件事从Pod上剥离出去,交给上层对象解决,Pod只负责跑业务。所以你会看到,Pod本身几乎从不被人手动创建和管理,大家接触到的更多是Deployment、StatefulSet、DaemonSet这些通过控制器来管理Pod的对象。
2.3 kubectl命令里最常见的Pod操作往往不是最终的解决方案
刚开始入门的时候,大家可能都敲过kubectl run nginx --image=nginx,看起来好像是在创建Pod。实际上在比较新的k8s版本里,这个命令默认创建的是一个Deployment,而不是裸Pod。k8s官方其实一直在引导用户往"控制器管理Pod"的方向走,因为裸Pod没有任何保障:节点宕机了不会被自动迁移,副本数变少不会自动补,连重启策略都受限。
我之前遇到过一位同学,为了临时测试环境的方便,直接用了kubectl create pod,结果测试期间Pod所在节点被回收,服务直接停了半个多小时没人发现。这就是典型的"没有控制器兜底"带来的问题。所以这里我得先立一个结论:你把Pod当成集装箱里的货物就行,它确实是你业务真正运行的地方,但你不应该手动去操作它,你操作的是能自动维持Pod数量的控制器。
3. ReplicaSet:保证"数量不缩水",但也别指望它懂发布
3.1 ReplicaSet的核心逻辑:用标签选择器盯住一组Pod
ReplicaSet这个名字直译过来是"副本集",它的职责用一句话就能说清楚:保证任意时刻集群里都有指定数量的Pod在运行。你设置replicas: 3,它就去维护3个Pod;如果一个Pod被误删了,它会立刻重新创建一个新的出来补位。这种能力让集群具备了最基础的"自我修复"功能。
它的实现思路靠的是标签选择器(Label Selector)。ReplicaSet自己在YAML里声明了一组标签条件,比如app: web,它会不停扫描集群里带这个标签的Pod,统计数量,然后对照期望值,少了就创建,多了就删掉。所以ReplicaSet和Pod之间不是"父子"那样在创建时固定绑定的关系,而是通过标签动态关联的。这也是为什么k8s文档反复强调"标签是k8s的对象关联方式"。
这里就会引出很多初学者第一次踩坑的点:因为标签是松耦合的,你自己手动建了一个带相同标签的Pod,ReplicaSet可能不会拦你,甚至会认为这就是它该管的Pod,导致实际数量超出预期。反过来,如果你把某个Pod的标签改掉,ReplicaSet会认为这个Pod"消失了",然后马上再造一个新的补上。所以在集群里操作标签时一定要谨慎。
3.2 ReplicaSet管数量,但管不了版本
如果ReplicaSet已经能保证Pod数量稳定了,为什么我们还要Deployment?答案是:ReplicaSet只管"数量对不对",它不负责"版本怎么变"。
想象一下,你要把一个应用从Nginx 1.20升级到1.21。如果用ReplicaSet来做这件事,你需要手动修改Pod模板里的镜像版本,然后把旧Pod一批批删掉,让ReplicaSet按新模板创建出来。这个过程叫滚动更新,但ReplicaSet本身没有任何发布策略、没有暂停和恢复机制、没有回滚能力,它不会自动帮你控制节奏,也不会保留历史版本审计记录。你发给它的只有最终目标:我要3个Pod,至于是什么镜像,它不太关心,它只认标签和数量。
所以ReplicaSet在真实生产环境里很少被直接创建和使用,它更像是Deployment内部的一个执行层。Deployment规划好发布节奏,ReplicaSet负责具体凑人。从YAML的角度看,Deployment里自带一个replicas字段,Deployment控制器拿到这个值后,会在背后创建或更新一个ReplicaSet,再由这个ReplicaSet去维护Pod数量。层级关系就是这样的:Deployment管ReplicaSet,ReplicaSet管Pod。
3.3 一个容易忽略的细节:ownerReferences
如果大家有时间去kubectl get pods -o yaml查看一下Pod详情,会在metadata里看到ownerReferences字段,里面往往写着一个ReplicaSet的名字。这个字段记录的就是Pod与上层控制器之间的归属关系。k8s靠这个字段让上层控制器知道"这个Pod是我管的",而不是随便看到一个标签就上去抢。
这个字段引出前面说的教训:如果两个ReplicaSet同时指定了相同的标签选择器,就可能出现它们两个都对同一批Pod"宣称所有权"的情况,这在k8s里是不允许的,因为你创建第二个ReplicaSet时,API Server会校验冲突,有时会报错,有时会出现行为异常。规范的做法是不同对象的标签选择器不能重叠,这也意味着你不应该在创建Deployment的同时又手动创建一个与之选配相同标签的裸Pod。好的实践是保持标签规划的唯一性。
4. Deployment:你真正要打交道的"发布控制器"
4.1 Deployment到底解决了ReplicaSet的什么问题
现在我们来到Deployment。如果要说这是k8s里最常用的应用负载对象,应该没有人反对。Deployment在ReplicaSet的基础上,补足了两个关键能力:可控制、可回滚的版本发布和声明式的更新过程。
你不需要在整个升级过程中时刻盯着,也不需要自己手动一串命令去逐个删除Pod。你只需要把Deployment的Pod模板里的镜像版本改掉,然后kubectl apply -f,Deployment控制器会自动创建一个新版本的ReplicaSet,按照你指定的策略(默认是滚动更新),逐步把新Pod拉起来、把旧Pod缩下去,让服务始终保持可用。这个过程你通过kubectl rollout status能看到实时进度。
这背后的设计其实很有意思。Deployment不是直接管理Pod的,它通过ReplicaSet来管理。每当你更新Deployment里的Pod模板,Deployment会生成一个新的ReplicaSet,然后让这个新的ReplicaSet从0开始往上加Pod,同时让旧ReplicaSet往下缩。整个更新结束之后,旧ReplicaSet通常会保留一个副本数设为0的记录躺在那里。这个"躺着的旧ReplicaSet"可不是垃圾,它是回滚操作的依据。执行kubectl rollout undo时,Deployment会找到上一个版本的ReplicaSet,重新把它从0拉起来。
4.2 滚动更新与回滚:为什么要保留旧ReplicaSet
很多人第一次看kubectl rollout history deployment/web的输出时,会发现有多个REVISION。它是按Deployment的Pod模板是否变化来递增记录的。每次模板变化,都会产生一个新版本号,这个版本号跟镜像Tag没有必然关系,只跟模板内容是否变化有关。所以,如果只是扩缩容,而模板没变,REVISION不会增加。
回滚的时候,rollout undo允许你指定--to-revision,比如回到REVISION=2。这个过程本质上就是把目标ReplicaSet重新启用、把当前ReplicaSet缩容。理解了这一层,你就不会再把回滚理解为"把镜像Tag改回去"或者"删除整个Deployment重建"——回滚是Deployment在几个"旧ReplicaSet历史快照"之间做切换。
这里我建议大家在生产环境里设置一个合理的revisionHistoryLimit,默认值通常是10,也就是最多保留10个历史版本。如果你发布非常频繁,保留太多历史会让ReplicaSet堆积,虽然每个副本数是0,但它们依然占用资源、增加列表的噪音。反之,设得太小,比如1或2,回滚选项就很少。一般生产环境保留3到5个比较合适。
4.3 Deployment的部署策略:滚动更新和Recreate
Deployment支持两种更新策略:RollingUpdate(滚动更新)和Recreate(重建)。默认是滚动更新,它会通过maxUnavailable和maxSurge两个参数控制更新速度。maxUnavailable表示更新过程中允许最多有多少比例/数量的Pod处于不可用状态,maxSurge表示允许最多超出期望副本数多少个Pod。举个例子:replicas: 5,maxSurge: 25%、maxUnavailable: 25%,那么在更新时,k8s会先额外创建一个新Pod(因为25%的5向上取整等于2,具体算法是按四舍五入/向上取整规则,不同版本细节略有差异),确保总Pod数不超过7,同时保证至少有3个旧Pod可用。调整这两个参数,你能控制发布是"稳一点慢慢滚"还是"快一点敢于短暂波动"。
Recreate策略就简单粗暴了:先把所有旧Pod删掉,再把新Pod创建出来。这个策略只适合那种不能新旧共存的场景,比如某些用到共享数据卷、且新老版本同时挂载会冲突的状态应用。如果追求高可用,一般不用Recreate。
4.4 生产实践中的两个建议
第一,不要直接修改线上运行的Deployment Pod模板来排查问题。除非你想顺带触发一次发布,否则改模板就意味着滚动更新,流量会开始切换。排查问题时更稳妥的方式是kubectl debug或临时起一个独立的Pod去测试环境里复现。
第二,Deployment发布失败不代表集群坏了。如果新版本镜像启动就崩溃,滚动更新会卡在新Pod一直起不来、旧Pod一直不缩的状态。你运行kubectl rollout status会看到一直等待;kubectl get pods会看到几个新Pod处于CrashLoopBackOff状态。这时候不要慌,直接kubectl rollout undo deployment/web回滚,再把出问题的镜像Tag找出来核对。我见过很多新手在这里反复删Pod,其实Pod删一千遍也没有用,因为Deployment会不断按坏模板重建它们。问题的根源在模板,不在Pod本身。
5. Service:把一组Pod变成稳定入口的"负载均衡器"
5.1 Service解决的核心痛点:Pod IP不固定,访问入口必须固定
现在已经有了Deployment来管理Pod,理论上Pod数量稳定了、版本能发布了。但还有一个问题没解决:用户和上游系统该通过什么地址访问这些Pod?直接记Pod IP不现实,因为它会随Pod重建而变;而且当你有3个副本时,总不能让客户端分别记住3个IP吧。
Service就是为这个场景设计的。它在一组Pod前面提供了一层稳定的抽象:Service有自己的虚拟IP(ClusterIP)和固定的DNS名字,不管背后Pod怎么变,客户端始终访问Service。Service通过标签选择器找到它要代理的后端Pod,并自动维护一个Endpoints列表(在新版本里也叫EndpointSlice),里面记录着当前所有匹配到的Pod IP和端口。Pod发生变化时,Endpoints会被自动刷新。
我用一个生活里的例子解释:Pod是在后台干活的具体员工,Deployment是他们的排班经理,Service则是公司的前台总机号码。外界不需要知道每个员工的分机号(Pod IP),只需要拨打总机号码(Service IP/DNS),前台会把电话转接到当前在岗的任意一位能接活的员工身上。
5.2 Service的三种常见类型
Service通过type字段暴露不同的访问范围,最常见的有三种:
- ClusterIP:默认类型。分配一个只在集群内部可达的虚拟IP。适用于集群内服务间调用,比如后端API给前端应用提供接口。
- NodePort:在ClusterIP基础上,再在每个节点上开一个静态端口(默认范围30000-32767),集群外部可以通过
节点IP:NodePort访问。 - LoadBalancer:通常在公有云环境使用。它调用云厂商的负载均衡器,把外部流量引导到集群内NodePort或直接到Pod。你在云上创建这类Service后,会得到一个外部IP或域名。
如果是本地测试或裸金属环境,没有云负载均衡能力时,很多人也会用Ingress来统一入口。Ingress不是Service的替代品,它是在Service之上的路由层,把HTTP请求按域名/路径转发给Service。数据链路会变成:客户端 → Ingress → Service → Pod。这一层关系搞懂之后,整个入口链路就完整了。
5.3 Service的selector与kube-proxy:工作负载如何被转发
创建Service时最关键的字段就是selector。它和ReplicaSet的选择器作用方式很像,都是通过标签匹配Pod。下面是一个常见的Service示例:
yaml复制apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
selector:
app: web
ports:
- protocol: TCP
port: 80
targetPort: 8080
这个Service会把访问web-service:80的流量转发到所有app=web的Pod的8080端口上。这里我特别强调一个容易踩坑的地方:Service的selector匹配的是Pod的标签,不是Deployment的名字。很多新手会下意识在selector里写deployment: web,结果Service根本匹配不到任何Pod,kubectl get endpoints始终是空列表。正确的做法是看Deployment里Pod模板中定义的labels,Service的selector必须和它一致。
Service之所以能转发流量,靠的是每个节点上的kube-proxy组件。它实时监控Service和Endpoints的变化,在节点上生成iptables规则(或者使用IPVS模式)。当请求到达Service IP时,iptables规则会把包DNAT到某个后端Pod IP上。你执行iptables -t nat -L看到很多KUBE开头的规则,就是这个机制在工作。IPVS模式性能更好、负载均衡算法更丰富,但它的底层接管方式需要ipvsadm等工具配合,这部分属于调优范畴,初学阶段不必过度纠结。
5.4 Service没有Pod可转发时表现出的现象
如果Service的selector没有匹配到任何Pod,kubectl get svc看不出什么异常,Service还是存在,但当你访问这个Service IP时,连接会被拒绝或超时。这时候排查思路是:先看kubectl get endpoints有没有地址,没有就说明selector配错了,或者Pod处于Pending状态还没起来;有地址但访问不通,再去查kubectl describe svc的端口映射和kube-proxy状态。绝大多数Service不通的问题,最后都指向selector或端口映射配置错误。
6. 四者联动:一次完整请求的路径拆解 + YAML实战示例
6.1 从外部请求到Pod处理,中间究竟发生了什么
把前面四个对象串起来,我们现在来看一次完整请求的路径。假设集群里有一个Nginx应用,通过Deployment管理3个副本,通过Service暴露为NodePort类型,节点IP是192.168.50.10,Service端口是30080。
当你在浏览器访问http://192.168.50.10:30080时,数据链路大致分为四步:
- 请求先到达节点的
30080端口,这个端口是kube-proxy在节点上创建的NodePort; - kube-proxy根据iptables/IPVS规则,把请求转发到某个Pod IP的
targetPort(比如8080)上; - 每个Pod内部的容器真正监听在8080端口,Nginx处理请求并返回响应;
- 响应原路返回给客户端。
这个过程中Deployment和ReplicaSet都没有出现在实时数据链路上,它们管的是"当前集群里有没有3个Pod在跑、是不是想要的那个版本"。Service管的是"如果Pod有3个,请求发给谁都行,反正都能处理"。所以你可以从两个维度理解这四个对象的关系:一个是控制面维度(Deployment → ReplicaSet → Pod),另一个是数据面维度(Service → Endpoints → Pod)。
6.2 一个同时使用Deployment和Service的完整YAML
为了让大家能直接上手,我这里给一份基础但完整的示例。这个示例同时创建了一个Deployment和一个Service,Deployment管理3个副本,Service把Pod的80端口暴露出来:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-demo
labels:
app: nginx-demo
spec:
replicas: 3
revisionHistoryLimit: 5
selector:
matchLabels:
app: nginx-demo
template:
metadata:
labels:
app: nginx-demo
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-demo-svc
spec:
type: NodePort
selector:
app: nginx-demo
ports:
- port: 80
targetPort: 80
nodePort: 30080
我把字段中比较容易出错的地方标注一下。spec.selector.matchLabels必须匹配spec.template.metadata.labels,否则Deployment创建不了新的ReplicaSet,它找不到自己该管的Pod,这和你是否手动添加了额外的label无关。Service的spec.selector必须写Pod的标签,也就是nginx-demo这个值对应到Pod模板里,而不是对应到Deployment的名字。如果你把Service的selector写成deployment: nginx-demo,那这个Service什么都转发不了。
ports里有两个关键字段:port是Service对外暴露的端口,也就是其他服务访问它时要用的端口;targetPort是容器真正监听的端口,也就是容器里进程绑定时的端口。如果这两者没搞清楚,就会出现Service端口能通但页面报错的情况,实际是连到了错误的进程端口上。
6.3 用kubectl实际看一遍这些对象之间的关系
把上面的YAML保存为nginx-demo.yaml,执行kubectl apply -f nginx-demo.yaml后,建议按下面的顺序观察一下对象状态:
bash复制kubectl get deployment nginx-demo
kubectl get replicaset -l app=nginx-demo
kubectl get pods -l app=nginx-demo -o wide
kubectl get svc nginx-demo-svc
kubectl get endpoints nginx-demo-svc
你会看到类似这样的现象:Deployment名字是nginx-demo,ReplicaSet名字是nginx-demo-xxxxx,Pod名字是nginx-demo-xxxxx-yyyyy。名字前缀一层套一层,这不是巧合,它反映出对象的层级关系:Deployment创建了ReplicaSet,ReplicaSet创建了Pod。前缀的随机字符串是每次Pod模板变更时重新生成的,这也是为什么更新后Pod名字会变,但Service不用改——它通过标签找到新Pod。
6.4 滚动更新时这几个对象怎么配合
现在模拟一次发布:把镜像从nginx:1.25改成nginx:1.26,再次kubectl apply。此时你如果快速执行kubectl get rs -l app=nginx-demo,会看到两个ReplicaSet,一个旧的(副本数逐渐降到0),一个新的(副本数逐渐升到3)。这个过程中kubectl get pods里新老Pod会同时存在一段时间,老的Pod逐渐被终止,新的Pod逐渐Ready,直到全部替换完成。如果你此时访问Service,流量会同时打到新旧两种Pod上,持续时间取决于你的滚动更新参数。在maxUnavailable: 0的配置下,发布过程甚至可以做到一个旧Pod被摘除之前,先保证一个新Pod已经Ready并加入Endpoints,流量不中断。
这条链路是整个k8s应用发布最核心的脉络,你只要连续做几次发布和回滚,就能把Deployment、ReplicaSet、Pod、Service这四者的关系彻底刻进脑子里,比看十篇文档都管用。
7. 我踩过的坑:为什么总会有人分不清它们,以及一些排错经验
7.1 Service的selector写成了Deployment名字
这是我在社区答疑时遇到最多的问题。现象是Service创建成功,kubectl get svc显示正常,但kubectl get endpoints是空。很多人第一反应是查Pod状态,发现Pod明明Running且Ready,于是更懵了。问题就出在Service的selector上。它匹配的是Pod的标签,不是Deployment的metadata.name。你现在可以把selector理解成一个数据库的WHERE条件,它扫的是Pod清单里的labels列,不是Deployment清单里的name列。以后遇到Service不通,第一件事永远先看endpoints列表,然后在kubectl describe svc和kubectl get pods --show-labels之间来回对比。
7.2 手动删Pod发现根本删不掉
另一个经典场景:有人为了"重启服务",手动删了一个Pod,结果几秒钟之后,新的Pod又起来了,而且名字和前一个不一样。他以为k8s出Bug了。实际上这正是ReplicaSet在工作:它检测到期望副本数和实际副本数不一致,立刻补齐。想通过删Pod来重启服务,在Deployment管理的应用上是无效的。正确的重启方式要么是调整副本数再调回来,要么是kubectl rollout restart deployment/web。rollout restart会在不修改模板内容的前提下触发一次滚动重启,这时候所有Pod会按滚动更新的节奏被重建,很适合用来做配置热加载或解决某个Pod中的临时状态异常。
7.3 只改了Pod的label导致ReplicaSet多管闲事
我用一个例子来说明标签选择器的风险。假设ReplicaSet选择app=web,期望副本数是3,当前3个Pod都匹配这个标签。此时你手动改了其中一个Pod的标签为app=temp,ReplicaSet会发现匹配的Pod变成了2个,于是立刻创建一个新Pod补位。从结果看,集群里还是3个匹配标签的Pod,但那1个被改标签的Pod并没有消失,只是变成了"没人管的孤儿Pod"。这种Pod一旦所在节点故障,不会有人把它迁移到别处。所以如果你在做调试时改了Pod标签,记得用完改回去,否则会造成资源浪费和不可预期的行为。
7.4 更新镜像时忘了imagePullPolicy导致看起来"没更新"
还有一个和Deployment发布直接相关的坑。如果你在本地测试,常把镜像Tag设成latest,却没有显式声明imagePullPolicy: Always,k8s有时候不会重新拉取镜像,于是你改了代码打成latest推到仓库,apply之后发现Pod还是旧版本。原因就是节点上已经有这个镜像Tag了,kubelet可能直接复用了本地镜像。正规做法是:要么生产环境永远使用明确版本号Tag,不要用latest;要么显式指定imagePullPolicy: Always。回滚时也一样,如果你回滚到的那个镜像在节点上不存在,kubelet会去拉取,拉不到就回滚不成功。这属于部署细节,但它确实会影响你对上面几个对象关系的判断——如果你发现rollout显示回滚成功但Pod内容没变,先检查镜像能不能正常拉到。
7.5 排错时我建议的排查顺序
最后分享一套我自己用的排查顺序,遇到应用访问不通或发布异常,按这个顺序走,绝大多数问题能在十分钟内定位:
- 先看
kubectl get pods有没有Ready。如果Pod没有Ready,重点看kubectl describe pod最后面的Events; - 再看
kubectl get endpoints有没有地址。如果没有,检查Service的selector和Pod标签是否对得上; - 能连到Pod但报错,检查Service的
port和targetPort是否正确,以及容器里进程是否真的监听了containerPort; - 想要回滚或重新发布,确认
kubectl rollout history deployment/web里可用的REVISION数量; - 最后才考虑网络层问题,比如节点防火墙、CNI插件的路由是否正常。大部分时候轮不到走到这一步,前面几个对象配置层面的问题就足以解释了。
我自己在实际带人的时候经常说一句话:k8s里的对象关系,核心就是"控制面看ownerReferences,数据面看selector"这两条线。Deployment控制ReplicaSet、ReplicaSet控制Pod、Service选择Pod,每条线上都有明确的实现机制,也都有对应的排错入口。你把这两条线理顺了,后面再学StatefulSet、DaemonSet、HPA甚至Operator,都会事半功倍,因为它们的核心套路依然是"控制器通过标签和owner关系管理下层对象"。
如果这篇文章能让你在看到一个Deployment YAML时,下意识反应过来"它背后会先生成一个ReplicaSet,再由ReplicaSet去维持Pod数量,而Service只关心Pod标签",那我觉得这次分享就没有白写。k8s上手说难也难,说简单也简单,很多时候只是差一个能把概念串起来的解说。你可以拿一个测试环境,先创建一个Deployment,再多执行几次kubectl rollout restart和kubectl rollout undo,配合kubectl get rs看ReplicaSet的变化,相信你很快就能把这一套玩明白。
