☰
K8S Service与Deployment深度解析:从线上故障到生产实践
2026/10/22 10:15:32 网站建设 项目流程

1. 从一次线上故障说起:为什么我要重新理解 Service 和 Deployment

去年冬天的一个凌晨,我被值班同事的电话叫醒,说线上订单服务大面积超时。我迷迷糊糊打开电脑,第一反应是去看 Pod 状态,结果发现所有 Pod 都是 Running,容器日志也没有明显报错。折腾了快二十分钟,最后定位到的问题让我哭笑不得:有人改了一个 Deployment 的标签选择器,导致 Service 的 Endpoints 列表空了,流量根本进不来。Pod 活得好好的,但没人能找到它们。

这件事之后,我把 K8S 里 Service 和 Deployment 这两个最基础的对象重新梳理了一遍。它们看起来简单,文档里几页就讲完了,但真正在生产环境里踩过的坑,远比想象中多。这篇文章就是那次复盘之后的产物,我会从概念、设计逻辑、实操配置、排查技巧几个角度,把这两个对象讲透。不管你是刚接触 K8S 的新手,还是已经用了两三年但总觉得理解不够深的老手,应该都能从里面找到一些有用的东西。

Service 解决的是"怎么找到一组 Pod 并稳定访问"的问题,Deployment 解决的是"怎么让一组 Pod 按照期望状态稳定运行和更新"的问题。这两个对象几乎是所有 K8S 工作负载的起点,理解了它们,后面学 Ingress、StatefulSet、HPA 都会顺很多。下面我按照自己理解的逻辑,一层一层拆开讲。

2. 核心概念拆解:Service 和 Deployment 到底在解决什么问题

2.1 Pod 的天然缺陷:为什么不能直接用 Pod 的 IP

刚学 K8S 的时候,很多人会有一个疑问:Pod 不是有 IP 吗,为什么不能直接访问 Pod IP?答案藏在 Pod 的生命周期特性里。

Pod 是"一次性"的。它可能因为节点故障被驱逐,可能因为滚动更新被替换,可能因为资源不足被重新调度。每次重建,Pod 的 IP 都会变。如果你在代码里写死了某个 Pod IP,那这个服务基本上活不过一天。更麻烦的是,一个应用通常有多个副本,你不可能让调用方自己去维护一份 Pod IP 列表,还要处理某个 IP 失效后的重试逻辑。

这就是 Service 存在的根本原因。它在 Pod 前面加了一层稳定的抽象:你访问 Service 的虚拟 IP(ClusterIP),Service 负责把流量转发到后端健康的 Pod 上。Pod 怎么变、IP 怎么换,对调用方都是透明的。

提示:Service 的 ClusterIP 是虚拟的,它不对应任何真实网卡,只在 K8S 内部的转发规则里存在。所以你在节点上用ping是 ping 不通 ClusterIP 的,这是正常现象,不代表 Service 有问题。

2.2 Deployment 的定位:声明式管理无状态工作负载

如果说 Service 解决的是"访问入口"问题,那 Deployment 解决的是"工作负载编排"问题。它管的是:我要几个副本、用什么镜像、怎么更新、更新失败了怎么办。

Deployment 的核心思想是声明式。你不是告诉 K8S"帮我启动 3 个 Pod",而是告诉它"我希望任何时候都有 3 个符合我描述的 Pod 在运行"。K8S 的控制器会不断对比"期望状态"和"实际状态",发现不一致就自动调整。这种设计的好处是自愈能力强:某个 Pod 挂了,控制器会自动补一个新的,不需要人工干预。

Deployment 下面其实还有一层 ReplicaSet,ReplicaSet 才是真正管副本数量的对象。Deployment 通过管理 ReplicaSet 来实现滚动更新和版本回滚。这个层级关系很多人一开始会忽略,但理解它对后面排查问题很关键。

2.3 两者的协作关系:标签选择器是纽带

Service 和 Deployment 之间没有直接的引用关系,它们是通过标签选择器(Label Selector)松耦合地关联起来的。Deployment 创建的 Pod 会带上指定的标签,Service 通过选择器找到这些标签的 Pod,把它们加入自己的 Endpoints 列表。

这种设计非常灵活:一个 Service 可以同时选中多个 Deployment 的 Pod,一个 Deployment 的 Pod 也可以被多个 Service 选中。但灵活也意味着容易出错,我开头提到的那个故障,就是标签选择器写错导致的。后面我会专门讲怎么排查这类问题。

3. Deployment 实操:从配置文件到滚动更新

3.1 一份完整的 Deployment YAML 该怎么写

先看一份我在生产环境常用的 Deployment 配置模板,然后逐段解释每个字段的作用:

apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: production labels: app: order-service tier: backend spec: replicas: 3 revisionHistoryLimit: 5 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: order-service template: metadata: labels: app: order-service tier: backend spec: containers: - name: order-service image: registry.example.com/order-service:v1.2.3 ports: - containerPort: 8080 resources: requests: cpu: 200m memory: 256Mi limits: cpu: 1000m memory: 512Mi readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /health/live port: 8080 initialDelaySeconds: 30 periodSeconds: 10

replicas: 3是期望副本数,这个数字不是拍脑袋定的,要结合压测结果和资源预算来算。revisionHistoryLimit: 5控制保留多少个历史 ReplicaSet,方便回滚,默认是 10,生产环境我一般调到 5,避免 ReplicaSet 堆积太多。

strategy部分是最容易出问题的地方。maxSurge: 1表示更新时最多多出 1 个 Pod,maxUnavailable: 0表示更新过程中不允许有 Pod 不可用。这两个参数组合起来,意味着滚动更新时会先创建一个新 Pod,等它 Ready 之后再删一个旧 Pod,全程保证可用副本数不低于 3。这个配置适合对可用性要求高的服务,代价是更新速度会慢一些。

3.2 滚动更新的参数计算与选择逻辑

maxSurge和maxUnavailable这两个参数怎么选,是很多人纠结的地方。我分享一个自己的计算思路。

假设你有 10 个副本,希望更新过程中至少保持 8 个可用,那maxUnavailable可以设为 2。如果希望更新快一点,可以同时设maxSurge: 2,这样更新时会先扩到 12 个 Pod,然后逐步替换。整个更新过程需要的额外资源是 2 个 Pod 的量,这个要提前确认集群资源够不够。

如果服务对可用性极其敏感,比如支付核心链路,那就用maxSurge: 1, maxUnavailable: 0,牺牲更新速度换稳定性。反过来,如果是内部工具类服务,可以用maxSurge: 25%, maxUnavailable: 25%,更新快,资源占用也均衡。

注意:maxSurge和maxUnavailable不能同时为 0,否则更新会卡死。这个坑我见过有人踩过,配置提交后 Deployment 一直卡在更新中,最后只能手动改配置。

3.3 就绪探针和存活探针:别让流量打到没准备好的 Pod 上

探针配置是 Deployment 里最容易被忽视、但影响最大的部分。我见过太多服务因为探针配错,导致更新时出现短暂 502。

readinessProbe决定 Pod 什么时候可以被加入 Service 的 Endpoints。如果这个探针配得太激进,Pod 还没初始化完就被加入流量,请求就会失败。如果配得太保守,更新速度会变慢。我的经验是,initialDelaySeconds要略大于应用冷启动时间,periodSeconds设 5 秒左右比较合适。

livenessProbe决定 Pod 什么时候需要被重启。这个探针要谨慎,如果阈值设得太敏感,应用只是短暂卡顿就被重启,反而会造成雪崩。我一般把failureThreshold设为 3,periodSeconds设为 10,给应用足够的容错空间。

还有一个细节:readinessProbe和livenessProbe最好用不同的端点。/health/ready检查依赖是否就绪(比如数据库连接),/health/live只检查进程是否存活。这样即使数据库短暂不可用,Pod 也不会被误杀,只是暂时不接收流量。

4. Service 实操:四种类型与流量转发机制

4.1 ClusterIP、NodePort、LoadBalancer、ExternalName 怎么选

Service 有四种类型,用途完全不同,选错了要么访问不了,要么暴露面过大。

类型访问范围典型场景注意事项
ClusterIP集群内部微服务之间调用默认类型,最常用
NodePort集群外部通过节点 IP测试环境、临时暴露端口范围 30000-32767
LoadBalancer集群外部通过云负载均衡生产环境对外服务依赖云厂商支持
ExternalName集群内部访问外部域名对接外部数据库只是 DNS 别名,无代理

ClusterIP 是默认类型,也是我 90% 场景下用的类型。微服务之间通过 Service 名字互相调用,简单可靠。NodePort 我一般只在测试环境用,因为它会在每个节点上开一个端口,安全性和管理都不太方便。LoadBalancer 适合生产环境对外暴露,但要注意云厂商的负载均衡器是要收费的,别不小心开了一堆。

ExternalName 比较特殊,它不创建任何代理,只是在 DNS 层面做一个 CNAME 记录。比如你想让集群内的应用通过db.internal访问外部数据库,就可以用 ExternalName 把db.internal指向真实的外部域名。

4.2 Endpoints 是怎么生成的:一次完整的流量链路

理解 Service 的流量转发,关键要理解 Endpoints 对象。Service 本身只是一个抽象,真正记录"后端有哪些 Pod"的是 Endpoints。

整个链路是这样的:Service 通过标签选择器找到匹配的 Pod,把这些 Pod 的 IP 和端口写入同名的 Endpoints 对象。kube-proxy 监听 Endpoints 的变化,在节点上更新 iptables 或 IPVS 规则。当请求到达 ClusterIP 时,内核根据规则把流量转发到某个后端 Pod。

这个链路里任何一环出问题,都会导致访问失败。我总结了一个排查顺序:先看 Service 的选择器是否匹配到 Pod,再看 Endpoints 是否有记录,然后看 Pod 是否 Ready,最后看 kube-proxy 是否正常。这个顺序能覆盖 90% 的 Service 访问问题。

# 查看 Service 详情,重点看 Selector 和 Endpoints kubectl describe svc order-service -n production # 查看 Endpoints 列表,如果为空说明选择器没匹配到 Pod kubectl get endpoints order-service -n production # 查看 Pod 标签,确认是否和 Service 选择器一致 kubectl get pods -n production --show-labels

4.3 会话保持与负载均衡策略

默认情况下,Service 的负载均衡是随机的,每次请求可能打到不同的 Pod。对于无状态服务这没问题,但如果你的服务有会话状态,就需要配置会话保持。

apiVersion: v1 kind: Service metadata: name: order-service spec: selector: app: order-service ports: - port: 80 targetPort: 8080 sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800

sessionAffinity: ClientIP会让同一个客户端 IP 的请求始终打到同一个 Pod,timeoutSeconds控制会话保持的时间。这个配置要慎用,因为它会破坏负载均衡的均匀性,如果某个客户端请求量特别大,对应的 Pod 压力会很高。

提示:会话保持只是权宜之计,更好的做法是把会话状态外置到 Redis 之类的共享存储,让服务真正无状态。这样扩缩容和故障转移都会简单很多。

5. 常见问题排查:那些年我踩过的坑

5.1 Service 访问不通的排查清单

Service 访问不通是最常见的问题,我整理了一份排查清单,按顺序检查基本能定位到原因。

排查项检查命令常见原因
Service 是否存在kubectl get svc名字写错、命名空间不对
选择器是否匹配kubectl describe svc标签拼写错误、大小写不一致
Endpoints 是否为空kubectl get endpointsPod 未 Ready、选择器不匹配
Pod 是否 Readykubectl get pods探针失败、镜像拉取失败
端口是否正确kubectl describe svctargetPort 和 containerPort 不一致
网络策略是否拦截kubectl get networkpolicyNetworkPolicy 限制了流量

我遇到最多的问题是标签不匹配。有一次一个同事把 Deployment 的标签写成app: order-service,Service 的选择器写成app: orders-service,多了一个 s,结果 Endpoints 一直是空的。这种问题肉眼很难发现,用kubectl describe对比一下就能看出来。

5.2 滚动更新卡住的几种典型情况

Deployment 更新卡住也很常见,表现是kubectl rollout status一直不返回,或者kubectl get pods看到新旧 Pod 混杂。

第一种情况是资源不足。新 Pod 因为节点资源不够一直 Pending,旧 Pod 又不敢删,更新就卡住了。这时候要么扩容节点,要么调整maxSurge减少额外资源需求。

第二种情况是探针配置有问题。新 Pod 一直起不来,readinessProbe 一直失败,Deployment 就不会继续更新。这时候要看 Pod 的事件和日志,确认应用是否真的启动成功了。

第三种情况是镜像拉取失败。镜像地址写错、私有仓库认证失败、镜像不存在,都会导致新 Pod 卡在 ImagePullBackOff。这个看 Pod 的 Events 就能定位。

# 查看更新状态 kubectl rollout status deployment/order-service -n production # 查看更新历史 kubectl rollout history deployment/order-service -n production # 回滚到上一个版本 kubectl rollout undo deployment/order-service -n production # 回滚到指定版本 kubectl rollout undo deployment/order-service -n production --to-revision=3

5.3 回滚操作的正确姿势

回滚看起来简单,但有几个细节要注意。首先,revisionHistoryLimit决定了你能回滚多少个版本,如果设得太小,老版本被清理了就回不去了。其次,回滚本质上是把旧的 ReplicaSet 重新扩容,所以旧版本的镜像必须还在仓库里,如果镜像被删了,回滚也会失败。

我个人的习惯是,每次上线前先记录当前版本号,更新后观察至少 10 分钟再确认。如果发现问题,第一时间回滚,不要试图在线修复,因为线上环境每一秒都在产生损失。

注意:回滚不会自动回滚配置变更(比如 ConfigMap、Secret),如果问题是由配置引起的,回滚 Deployment 也没用。这种情况需要单独回滚配置。

6. 生产环境的一些经验补充

6.1 资源配额与调度策略的配合

Deployment 里的resources.requests不仅影响资源分配,还影响调度。K8S 调度器根据 requests 来判断节点是否有足够资源。如果 requests 设得太小,节点可能被过度分配,导致实际运行时资源争抢。如果设得太大,资源利用率又上不去。

我的做法是,先通过压测确定单副本的峰值资源消耗,然后 requests 设为峰值的 60% 左右,limits 设为峰值的 120%。这样既能保证调度合理,又给突发流量留了余量。同时配合 HPA,在负载高的时候自动扩容。

6.2 多副本下的反亲和性配置

默认情况下,同一个 Deployment 的多个 Pod 可能被调度到同一个节点。如果这个节点挂了,所有副本同时失效,服务就彻底不可用了。生产环境建议配置反亲和性,让副本尽量分散到不同节点。

affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: order-service topologyKey: kubernetes.io/hostname

这里用preferred而不是required,是因为强制反亲和可能导致 Pod 无法调度(比如节点数少于副本数)。用preferred让调度器尽量分散,但不强制,灵活性更好。

6.3 标签和注解的规范管理

标签选择器是 Service 和 Deployment 的纽带,标签管理混乱是很多问题的根源。我建议团队制定一套标签规范,至少包含这几个:app(应用名)、tier(层级)、version(版本)、env(环境)。注解则用来记录一些非标识性的信息,比如负责人、代码仓库地址、变更记录。

这套规范看起来是小事,但在排查问题时能省很多时间。我经历过一次故障,就是因为两个团队的 Service 选择器写得太宽泛,互相选中了对方的 Pod,导致流量串了。如果标签规范清晰,这种问题根本不会发生。

7. 我个人的一些实操体会

Service 和 Deployment 这两个对象,文档看一遍可能半小时就够了,但真正理解它们,需要在生产环境里反复实践和踩坑。我最大的体会是:不要把它们当成孤立的配置,而要理解它们背后的控制器逻辑和流量链路。当你知道了 Endpoints 是怎么生成的、kube-proxy 是怎么工作的、滚动更新的每一步发生了什么,排查问题时就不会盲目。

另外一个小技巧:我习惯在测试环境用kubectl get events --watch实时观察资源变化,更新 Deployment 的时候盯着事件流,能很直观地看到 Pod 的创建、调度、启动、就绪全过程。这个习惯帮我快速定位过很多问题,比事后翻日志高效得多。

最后分享一个我常用的调试命令组合,当你怀疑 Service 有问题时,可以快速确认链路是否通畅:

# 在集群内起一个临时 Pod,测试 Service 连通性 kubectl run debug --rm -it --image=busybox --restart=Never -- sh # 在临时 Pod 内执行 wget -qO- http://order-service.production.svc.cluster.local/health nslookup order-service.production.svc.cluster.local

这个临时 Pod 的方式比在业务 Pod 里排查更干净,不会污染业务环境,用完即删。DNS 解析和 HTTP 请求分开测,能快速区分是 DNS 问题还是网络问题。这套方法我在多个项目里用过,基本上一分钟内就能定位到问题所在。

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

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

立即咨询