Kubernetes核心三件套:Pod、Deployment、Service的关系与排障实践
2026/9/9 12:52:18 网站建设 项目流程

第一次正经接触Kubernetes,是在公司做容器化迁移那阵子。当时我看完一堆教程,照着例子把Pod、Deployment、Service的yaml文件写好,apply下去,服务也能跑,心里还挺得意。可等到线上出了问题——Pod一直在重启、Service访问超时、滚动发布卡死——我才发现自己对这三个对象的理解只是"会照着写",完全没有到"理解它们为什么这么设计"的程度。

后来维护集群的时间长了,加上把官方文档和源码相关的剖析材料反复读了几遍,我才渐渐理清一条主线:Pod、Deployment、Service这三个对象,本质上是在解决三个完全不一样的问题。Pod回答的是"用什么形态去运行一个应用实例",Deployment回答的是"怎么维持我想要的那个运行数量",Service回答的是"怎么给这些随时会变的实例提供一个固定的访问入口"。把这条线抓住了,Kubernetes其他概念(比如ReplicaSet、探针、Ingress、HPA)基本上都能顺着推出来。

这篇东西面向的读者,是已经在用Docker、对Kubernetes有基础了解,但还没有把几个核心对象的关系彻底理顺的人。我会把这三个对象的定位讲清楚,再给出一份可以直接照做的部署示例,最后分享几个我实际踩过、查了很久才解决的坑。不需要你提前会写复杂的编排,跟着过一遍就能对Kubernetes的核心模型有个系统化的认识。

1. 先搞清一件事:为什么调度的最小单位是Pod,不是容器

1.1 容器已经是"最小可运行单元",但还缺"最小协作单元"

用Docker的时候,我们习惯把容器当成最小的运行单元:一个镜像跑起来就是一个容器,进程隔离、文件系统隔离都做好了。但Kubernetes的设计者没有直接把容器作为调度对象,而是引入了Pod这个概念,Pod里面可以放一个或者多个容器。

这看起来像多此一举,实际是必须的。因为真实业务里有很多场景是"几个进程必须绑在一起,不能分开调度"。举两个最常见的例子:

  • 日志采集场景:应用容器负责业务逻辑,旁边要挂一个sidecar容器专门采集日志并发往日志平台。这两个容器要共享同一个日志目录,最好还共享同一个网络身份,否则应用侧写日志、采集侧读日志,对不上路径和来源,排查问题会非常痛苦。
  • 本地代理场景:主容器里跑了一个HTTP服务,但前面需要一个小代理容器做本地转发、TLS终止或者请求打点。这个代理和主服务必须通过localhost通信才足够快、足够安全。

如果调度的最小单位是容器,这种"成对出现、不可拆分"的关系就没法表达。Kubernetes的方案是:把Pod当作一个原子调度单位,Pod里的容器共享同一个网络命名空间,能通过localhost互相访问,也可以声明共享存储卷。用大白话说,Pod就是把一组"必须绑在一条船上"的容器打包在一起。

1.2 Pod的IP是临时身份,不是稳定标识

每个Pod创建后都会获得一个独立的集群内IP地址。但我要提醒你,这个IP在Kubernetes世界里是"临时身份",不是"固定门牌"。原因有三个:

  • Pod被删除后,由Deployment重新创建的Pod是一个全新的对象,IP完全不一样。
  • 节点宕机或重启,Pod被调度到别的节点,IP同样会变。
  • 滚动升级时,新Pod替换旧Pod,IP会持续更替。

你可以做个简单实验:创建Deployment后执行kubectl get pods -o wide,记下Pod IP,然后kubectl delete pod <pod名>,等新Pod起来后再看IP,肯定变了。这种不稳定性直接决定了我们后面需要Service这种抽象来做"稳定入口"。

1.3 多容器Pod是特例,不要为用而用

虽然Pod支持多个容器,但我在日常工作中接触到的绝大多数Pod都是单容器。多容器Pod主要用于强依赖的sidecar模式,也就是上面提到的日志采集、本地代理这类场景。

有些新手看了网上的最佳实践,喜欢把业务容器、日志容器、监控agent一股脑塞进同一个Pod,理由是"这样最省资源"。实际用起来会发现排障非常难受:你没办法单独对某个容器做网络抓包,Pod里的容器是并行启动的,虽然有postStart钩子但并不可靠,而且任何一个容器异常退出,整个Pod都会被重启,影响范围反而更大。

我的习惯是:除非有明确的数据共享、网络共享需求,否则默认一个Pod一个容器。这样每个容器的生命周期、日志、监控都更加独立,出问题时定位也更快。

2. Deployment不是"部署工具",而是一个"状态维护循环"

2.1 手动创建的Pod,消失了就真的消失了

如果你直接执行:

kubectl run myapp --image=nginx

这个Pod确实会跑起来。但一旦它因为节点故障、OOM、人为误删等原因消失,没有人会帮你重新拉起一个。生产环境不可能接受这种"一次性用品"。

Deployment最大的价值,是引入了"期望状态"这个核心概念。你在Deployment里声明"我要3个副本,镜像版本是nginx:1.25",Deployment的控制器就会持续比对"当前实际运行的Pod数量"与"期望值",有偏差就自动调整。Pod少了就补,Pod多了就删,节点挂了就在别的节点重新调度。

这种"声明期望状态、控制器负责收敛"的思想贯穿整个Kubernetes。理解了Deployment,后面学StatefulSet、DaemonSet、Job都会轻松很多,因为它们本质上都是控制器模式的具体实现。

2.2 Deployment真正管理的是ReplicaSet,不是直接管Pod

很多人一开始学Deployment,会忽略它下面还隔着一层ReplicaSet。实际对象层级是这样的:

  • Deployment:负责声明期望状态,并编排版本升级和回滚。
  • ReplicaSet:负责维持某一版本Pod模板的副本数量。
  • Pod:真正运行应用实例的最小单元。

为什么中间要隔一层ReplicaSet?因为滚动发布必须有"旧版本ReplicaSet"和"新版本ReplicaSet"并存的过程。Deployment做滚动升级时,会创建一个新的ReplicaSet,然后把新ReplicaSet的副本数从0加到期望值,同时把旧ReplicaSet的副本数从N减到0,直到全部切换完成。

如果你执行kubectl get rs,会看到类似这样的输出:

NAME DESIRED CURRENT READY AGE myapp-5dcf7d9d6f 3 3 3 10m myapp-6b8d4f2a7c 0 0 0 10m

那个DESIRED为0、READY为0的ReplicaSet,就是滚动升级留下的历史版本。保留它的意义在于:回滚速度极快。只需要把Deployment的revision回滚到对应版本,控制器就会反过来做一次扩缩容,新版本缩到0、旧版本扩到期望值。

2.3 maxUnavailable和maxSurge是怎么协同控制发布节奏的

Deployment滚动更新时,有两个关键参数决定发布节奏:maxUnavailablemaxSurge,默认值都是25%。

  • maxUnavailable:更新过程中最多允许多少比例的Pod不可用。默认25%意味着,一个100副本的服务,更新时最多允许25个副本同时不可用,保证至少75个副本在线。
  • maxSurge:更新过程中最多允许超出期望值多少个Pod。默认25%意味着,100副本的服务在更新时最多可以临时扩展到125个副本,先启动新版本Pod,再下线旧版本Pod。

这个组合策略保证了发布期间服务不中断。但这里有个容易被忽略的坑:如果你的服务只有1个副本(replicas: 1),默认的滚动更新设置依然会造成短暂不可用。因为Kubernetes要先启动一个新的replica以达到surge要求,但旧Pod的termination和新Pod的ready之间可能会出现时间差。对单副本服务,我一般会设置:

strategy: rollingUpdate: maxUnavailable: 0 maxSurge: 1

这样至少保证新Pod就绪后旧Pod才被终止,抖动会小很多。但要做到真正的零停机,光靠这两个参数不够,必须配合readinessProbe和适当的多副本。

2.4 就绪探针才是决定"Pod算不算可用"的裁判

Deployment判断"新Pod是否可用"的依据,不是容器进程起来了,而是就绪探针是否通过。没有配置readinessProbe时,只要容器启动成功(进程存在),Pod就会被标记为Ready,流量就会打进去。

如果你的应用初始化需要10秒,而容器1秒就启动完毕,那在就绪探针配置缺失的情况下,用户在这9秒内访问服务,会直接看到连接被拒绝或502。配了readinessProbe之后,Pod只有在探针连续返回成功后才被纳入Service的负载均衡池,滚动更新才算真正有意义。

所以我的建议是:所有Deployment都要配readinessProbe,最好再配一个livenessProbe。readiness管的是"能不能接流量",liveness管的是"进程卡死了要不要重启",两者职责不同,不要混用。

3. Service的入口作用:让随时会死的Pod拥有一个固定门牌

3.1 为什么说Pod IP是靠不住的

回到第1章的问题。Pod IP会随着重建、节点迁移、滚动升级不断变化,而业务方不可能每次Pod重建都去改配置。Service就是用来解决这个问题的。

Service会分配一个固定的集群内虚拟IP(ClusterIP),通过标签选择器(selector)动态匹配一组Pod,并自动维护对应的Endpoints列表。不管Pod怎么重建、IP怎么换,只要标签不变,Service都能找到新的Pod并把流量转发过去。

这就像公司里人员流动很频繁,工位经常换,但每个人都有一个固定的分机号。你不需要记他现在坐哪,直接拨分机号就行。Service就是这个"总机"。

3.2 ClusterIP、NodePort、LoadBalancer怎么选

创建Service时可以指定type,日常最常见的是三种:ClusterIP、NodePort、LoadBalancer。我把它们的区别和适用场景整理成了一张表:

类型访问方式适用场景
ClusterIP集群内部通过Service名或ClusterIP访问微服务内部调用、中间件互相访问
NodePort通过任意节点的IP加固定端口访问临时对外暴露、测试环境、没有负载均衡器可用
LoadBalancer通过云厂商负载均衡器IP访问生产环境对外提供服务

ClusterIP是默认类型,只在集群内部可达;NodePort是在ClusterIP基础上,在每个节点上开一个端口,默认范围是30000-32767,外部可以用任意节点IP:NodePort访问;LoadBalancer则是由云厂商创建负载均衡器,把外部流量导入到所有节点的NodePort上,再由Service转发到Pod。

NodePort适合应急和测试,端口范围有限,且需要自己管理节点IP列表;生产环境如果有云厂商的LB能力,优先用LoadBalancer。

3.3 Service转发背后的iptables和ipvs

Service不是某个具体进程,它本质上是节点上的一组网络规则。创建Service后,kube-proxy组件会监听API Server上的变化,并把规则写入节点。

在iptables模式下,访问ClusterIP的流量会在节点上被DNAT转发到某个Pod的IP。Pod的IP是动态的,但Service的选择器会持续同步Endpoints,所以从外部看,Service就像一个稳定的负载均衡器。

在ipvs模式下,kube-proxy会创建IPVS虚拟服务器,底层支持轮询、最少连接等调度算法,规则更多、性能更好。现在很多集群默认就是ipvs模式。

这一块有个容易混淆的点:Service本身不做应用层健康检查。它只向Ready状态的Pod Endpoints转发流量,至于Pod是不是真的准备好,由readinessProbe说了算。所以你在排查Service访问异常时,第一反应应该是看Endpoints列表,而不是盯着Service的yaml反复看。

3.4 集群内部的DNS解析与跨命名空间访问

集群内通常会部署CoreDNS,Pod之间可以直接用Service名访问。例如,在default命名空间下有一个叫webapp的Service,其他Pod可以直接访问:

http://webapp:8080

如果目标Service在别的命名空间,就要写成"Service名.命名空间名":

http://webapp.prod:8080

这个机制是微服务之间相互调用的基础。只要约定好Service名和端口,就不需要硬编码任何Pod IP。理解这一点,你就能明白为什么Kubernetes生态里服务发现这么依赖Service。

4. 三个对象一起上:一份最小可运行的部署拆解

4.1 从Deployment的yaml开始

我们部署一个基于nginx的静态站点,副本数3,通过Service暴露。先写Deployment:

apiVersion: apps/v1 kind: Deployment metadata: name: webapp labels: app: webapp spec: replicas: 3 selector: matchLabels: app: webapp template: metadata: labels: app: webapp spec: containers: - name: webapp image: nginx:1.25 ports: - containerPort: 80 readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 3 periodSeconds: 5

这里有一个新手最容易踩的坑:spec.selector.matchLabels必须和spec.template.metadata.labels完全对应。如果你在selector里写了app: webapp,但在template里写的标签是name: webapp,Deployment不会报错,但它将无法管理任何Pod。你会看到replicas一直不满足,Pod却不在rs的管辖范围内。

4.2 Service只认标签,不认Deployment

然后是Service:

apiVersion: v1 kind: Service metadata: name: webapp spec: selector: app: webapp ports: - protocol: TCP port: 80 targetPort: 80

注意,Service里没有写"我要关联哪个Deployment",它只写selector和端口。它并不关心后端Pod是不是由Deployment创建的,只要带有app: webapp这个标签的Pod,都会被纳入Endpoints。这就是Service和Deployment解耦的关键。

我习惯把Service和Deployment写在两个文件里,而不是硬塞进一个yaml。因为Service可以先创建,此时即使没有匹配的Pod,它也会先存在并处于可用状态,等Pod起来后自动补上Endpoints。这样单独更新Service、单独排查问题都更方便。

4.3 部署顺序、验证命令与常见误判

实际执行顺序如下:

kubectl apply -f deployment.yaml kubectl apply -f service.yaml

推荐用apply而不是create,因为apply更符合声明式管理习惯,后续改动直接改文件再apply即可。

验证步骤:

# 查看Pod状态 kubectl get pods -o wide # 查看Service与Endpoints kubectl get svc webapp kubectl get endpoints webapp # 进入一个临时Pod,用Service名访问 kubectl run curl-test --image=curlimages/curl --rm -it -- sh curl http://webapp

有一个常见误判是:看到Pod全部Running,就认为服务一定通了。但实际上,只要某个Pod的readinessProbe没过,它就不会出现在Endpoints列表里。所以真正的健康检查命令应该是kubectl get endpoints。Endpoints里面有几个IP,决定了Service实际能把流量转发给几个Pod。

4.4 滚动升级时观察三个对象如何协作

假设我们要把镜像从nginx:1.25升级到nginx:1.26,直接修改Deployment的image字段后重新apply,然后观察过程:

kubectl rollout status deployment/webapp kubectl get pods

你会看到新Pod一批批起来、旧Pod一批批消失。如果新版本readinessProbe失败,发布会卡住,但不会把全部Pod都拖死。这种保护机制来自ReplicaSet之间的扩缩容协调:新ReplicaSet的副本扩不上去,旧ReplicaSet的副本就不会继续往下缩。这也是Deployment在生产环境里最让人放心的一点——最坏情况下只是发布暂停,服务还是好的。

5. 我实际踩过的三个坎:排查链路全记录

5.1 Pod明明Running,Service却不通

有一次我在测试环境部署一个应用,kubectl get pods看到3个Pod全部Running,但curl Service的ClusterIP就是超时。

第一步,查看Endpoints:

kubectl get endpoints myservice

发现Endpoints是空的。这说明Service的selector没有匹配到任何Pod,或者Pod的状态不满足Ready条件。

第二步,查看Pod的标签:

kubectl get pods --show-labels

结果发现Pod上的标签是app.kubernetes.io/name: myapp,而Service的selector写的是app: myapp。标签对不上,Service当然找不到人。Kubernetes的标签匹配是精确匹配,不存在"近似相等"。这是Service设计上最基础也最容易踩的坑。

5.2 Endpoints算出来了,访问还是会间歇性失败

另一个场景是:Endpoints有IP,但访问Service时偶尔504。

这种问题往往是readinessProbe给了"假阳性"。探针通过只代表你配置的检查路径返回了2xx/3xx,但如果你的应用连接外部数据库还没完成,或者内部缓存还没加载,就绪探针可能返回200,而实际业务请求仍会被拒绝。

解决办法是给就绪探针配置一个专门用于就绪检查的路径,比如/actuator/health/readiness,并且在该路径中做真实依赖检查。如果依赖没有就绪,就返回503或429,让Kubernetes不把这个Pod标记为Ready。依赖检查不要做得太重,否则探针本身会成为性能瓶颈,但也不要只是简单返回200。

5.3 发布卡住时,别急着看Pod,先看ReplicaSet

发布失败的时候,很多人的第一反应是kubectl describe pod去看某个Pod为什么崩溃,其实更高效的排查顺序是先看ReplicaSet:

kubectl rollout history deployment/webapp kubectl get rs -o wide

通过对比新旧ReplicaSet的镜像版本,能快速判断发布是否卡住、卡在哪个阶段。如果新ReplicaSet的DESIRED一直是0或者一直小于期望值,大概率是镜像拉取失败、探针不通过或者资源不足。然后再针对具体Pod执行describe,看Events里的错误类型,比如ImagePullBackOff、CrashLoopBackOff、Unschedulable。这样一层层缩窄范围,比随机翻Pod事件高效得多。

6. 挑着说几个容易忽略的细节性经验

6.1 除非临时调试,永远不要直接创建裸Pod

不管出于什么原因,生产环境都不要kubectl create pod或者直接apply一个没有控制器的Pod yaml。裸Pod没有控制器保证生命周期,节点故障后不会自动恢复,也不会有滚动更新能力。如果你只是想快速跑一个临时调试容器,用kubectl run--restart=Never再配合--rm,用完自动删,这样不会有残留。

6.2 port和targetPort千万别当成一回事

Service里的port是Service对外暴露的端口,targetPort是转发到Pod内容器的端口。两者可以不一样。比如你容器里监听的是8080,但Service希望对外用80,那配置就是:

ports: - port: 80 targetPort: 8080

很多线上问题的根源就是这里写反了,尤其是容器镜像里改了默认监听端口,而Service没有同步。排查的时候先看Pod日志是否正常,再用kubectl exec进Pod直接curl localhost,如果能通,问题基本就出在Service的targetPort或selector上。

6.3 NodePort默认是随机分配的

创建NodePort类型的Service时,如果没有显式指定nodePort,Kubernetes会在30000-32767之间随机分配。如果要固定端口,可以显式写上:

type: NodePort ports: - port: 80 targetPort: 80 nodePort: 30080

但要注意,同一个集群里不能有两个Service占用同一个NodePort,否则创建会报冲突。

6.4 ClusterIP只能在集群内访问

ClusterIP不是给你的宿主机用的,它只存在于集群虚拟网络中。不要在宿主机上直接wget一个ClusterIP,十有八九不通。想排障,要么进入集群里的临时Pod,要么用NodePort或端口转发。kubectl port-forward也可以,但只适合个别调试,不适合批量访问。

6.5 Pod不是虚拟机,多容器要克制

很多人习惯把Pod当成一台小型虚拟机,见什么都往里面塞。但实际上,Pod里的容器共享网络和存储,生命周期强耦合,一个容器异常可能拖垮整个Pod。最典型的问题是:业务容器和日志采集容器放在一起,日志采集容器版本升级时挂了一下,整个Pod被重启,业务反而受影响。我的经验是:能拆就拆,多容器Pod只留给真正需要共享网络或数据卷的sidecar场景,其他情况宁可多维护几个Deployment,也不要图省事堆在同一个Pod里。

回到开头那句话,Pod、Deployment、Service这三个对象是Kubernetes的骨架。只要把这三者各自的职责边界和协作关系理清了,后面再看Ingress、HPA、StatefulSet,你会发现它们都是在解决某一类更具体的问题而已。真正多写几个yaml、多排几次障,这些概念会慢慢变成一种直觉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询