Kubernetes Deployment与Service实战:对象关系与链路验证
Kubernetes 这套东西刚上手的时候最劝退的不是概念多而是概念之间的套娃感——你创建一个 Deployment它背着你生成 ReplicaSetReplicaSet 又生成 PodPod 跑在某个 Namespace 里然后你还得再补一个 Service 才能让别人访问到它。这一圈下来很多人对着kubectl get all的输出发愣这么多对象到底谁管谁这篇是系列第四篇前面聊过 Namespace、Pod、Label 这些基础对象这次把 Deployment 和 Service 这两块拼图补上重点讲清楚它们和前面几个对象是怎么咬合在一起的。我默认你已经在本地或者测试集群里跑过kubectl能看懂 YAML 的基本结构但还没真正把一个服务从镜像到可访问这条链路走通过。如果你正好卡在这个阶段这篇的内容应该能帮你把脑子里那堆零散概念串成一条线。下面所有操作我都用最小可复现的例子命令可以直接抄参数我会解释为什么这么填。1. 先把对象关系图在脑子里立起来在动手写 YAML 之前有必要先花几分钟把对象之间的从属和引用关系理清楚。很多人学 K8s 效率低根本原因不是命令记不住而是脑子里没有一张谁引用谁的图导致排查问题时不知道从哪一层往下查。1.1 从一次创建动作看四层对象的生成链当你执行kubectl create deployment或者kubectl apply -f deployment.yaml时实际发生的事情是分层的。Deployment 是一个声明式控制器它本身不直接跑容器它的职责是保证期望的副本数始终成立。它会创建一个 ReplicaSetReplicaSet 再去创建具体数量的 Pod。这个链条是这样的Deployment管理版本和滚动更新持有 ReplicaSet 的历史记录ReplicaSet保证某一时刻 Pod 副本数量精确匹配Pod真正承载容器的最小调度单元ContainerPod 内部实际运行的进程理解这一层的关键在于你改 Deployment 的镜像版本它不会原地改 Pod而是新建一个 ReplicaSet再逐步替换旧 Pod。这就是滚动更新的底层机制。我第一次接触时以为改个镜像就是重启一下结果发现kubectl get rs里多了一行旧的那个还在只是副本数被缩到 0 了——这个认知差是后面理解回滚的基础。1.2 Namespace 是逻辑隔离而不是物理隔离Namespace 常被误解成类似虚拟机的隔离。实际上它只是给对象名字加了一个作用域前缀让不同 Namespace 里可以存在同名对象。网络层面默认情况下跨 Namespace 的 Pod 是能互相访问的除非你上了 NetworkPolicy这一点特别容易踩坑。我见过有同学以为把测试环境放test命名空间、生产放prod就安全了结果一个配错的 Service 直接打到了另一个环境的数据库上。所以记住Namespace 管的是对象命名和 RBAC 权限边界不是网络边界。资源配额ResourceQuota也是按 Namespace 生效的这个倒是实打实的隔离手段。1.3 Label 和 Selector所有对象串联的胶水Label 是键值对挂在任何对象上Selector 是按 Label 筛选对象的查询条件。Deployment 靠 Selector 找到自己该管的 PodService 靠 Selector 找到自己该转发的 Pod。整个 K8s 的对象协作本质就是一堆 Selector 在互相匹配。这里有个必须记住的约束Deployment 的.spec.selector.matchLabels必须和 Pod 模板里的labels对得上而且 Deployment 创建后 selector 是不可变的。我踩过一次坑手贱改了这个字段kubectl apply直接报field is immutable只能删了重建。所以写 YAML 时先把 Label 规划好别事后改。对象靠什么找到下级关键约束Deploymentselector.matchLabels 匹配 Pod labelsselector 创建后不可变ReplicaSet同上由 Deployment 自动生成一般不手动操作Serviceselector 匹配 Pod labels匹配不到 Pod 时 Endpoints 为空Pod被上面两者选中labels 要写全这张表建议存下来排查Service 访问不通时第一件事就是对照它检查 Label 是否匹配。2. 用 Deployment 管住 Pod 的生命周期单独写 Pod 的 YAML 在生产里几乎见不到因为 Pod 挂了没人拉起来。Deployment 才是日常使用的入口。这一节我把一个完整的 Deployment 拆开讲每个字段为什么这么写都说清楚。2.1 一份可以直接用的 Deployment 清单先看完整文件保存为web-deployment.yamlapiVersion: apps/v1 kind: Deployment metadata: name: web-demo namespace: default labels: app: web-demo spec: replicas: 3 selector: matchLabels: app: web-demo strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: web-demo tier: frontend spec: containers: - name: web image: nginx:1.25 ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 5 periodSeconds: 10逐段说几个容易写错的地方。apiVersion对 Deployment 是apps/v1不是v1写错了会报no matches for kind。selector.matchLabels里的app: web-demo必须和template.metadata.labels里的同名键值完全一致少一个字符都不行。replicas: 3是期望副本数但注意它只是期望实际数量受节点资源、调度约束影响。resources.requests决定调度器按多少资源来挑节点limits决定容器最多能用多少。这两个值填得太随意是常见问题——requests 填太小会导致节点超卖填太大会导致 Pod 一直 Pending 调度不上去。2.2 maxSurge 和 maxUnavailable 到底怎么配滚动更新的两个核心参数很多人直接抄默认值其实值得想一下。maxSurge是更新时允许超出期望副本数的上限maxUnavailable是允许不可用的副本数上限。上面我配的是maxSurge: 1, maxUnavailable: 0意思是更新过程中先起一个新 Pod等它 Ready 了再杀一个旧的全程保证可用副本不少于 3 个。这个配置适合对可用性敏感的服务代价是更新期间会短暂多占一个 Pod 的资源。如果你的集群资源紧张可以改成maxSurge: 0, maxUnavailable: 1先杀后起省资源但会短暂少一个副本。两个值不能同时为 0否则更新会卡死——这是 K8s 的校验规则我当初试过apply 直接被拒。提示maxUnavailable: 0配合readinessProbe才有意义。没有就绪探针新 Pod 一启动就被算作可用滚动更新等于没保护。2.3 就绪探针为什么不能省上面 YAML 里的readinessProbe是我强烈建议每个 Deployment 都配的。它的作用是告诉 K8s这个 Pod 现在能不能接流量。没有它Pod 一进入 Running 状态就被塞进 Service 的 Endpoints但容器里的应用可能还在初始化、连数据库、加载配置这时候打进来的请求全都会失败。initialDelaySeconds: 5是容器启动后等 5 秒再开始探测periodSeconds: 10是每 10 秒探一次。这两个值要根据应用启动速度调。启动慢的应用比如要预热缓存的 Java 服务initialDelaySeconds给到 30 甚至 60 都正常。探针路径/返回 200 就算成功如果你的应用有专门的健康检查端点用那个更准。和它配套的还有livenessProbe存活探针探测失败会重启容器。这两个别搞混就绪探针失败只是把 Pod 从流量里摘出去存活探针失败才会重启。我见过把 liveness 配得太激进导致容器反复重启的案例应用只是启动慢结果被判定为死了陷入重启循环。经验是 liveness 的阈值要比 readiness 宽松。3. Service 把 Pod 变成一个稳定入口Pod 的 IP 是会变的重建一次换一个。Service 的价值就是给一组 Pod 提供一个固定的虚拟 IP 和 DNS 名字让调用方不用关心后面到底有几个 Pod、IP 是多少。3.1 ClusterIP、NodePort、LoadBalancer 三种类型的取舍Service 的类型决定了它怎么暴露。写 YAML 前先想清楚访问方是谁类型访问范围典型场景注意事项ClusterIP集群内部服务间调用默认类型最常用NodePort集群外通过节点 IP测试、临时暴露端口范围 30000-32767LoadBalancer集群外云上生产依赖云厂商实现大部分内部服务用 ClusterIP 就够了。NodePort 适合本地测试但要注意它会在每个节点上都开这个端口而且端口号是固定的多个服务抢端口会冲突。LoadBalancer 在自建集群里通常没有实现申请了会一直 Pending别在没装对应组件的情况下用它。一个 ClusterIP 的 Service 长这样保存为web-service.yamlapiVersion: v1 kind: Service metadata: name: web-demo-svc namespace: default spec: type: ClusterIP selector: app: web-demo ports: - name: http protocol: TCP port: 80 targetPort: 80selector里的app: web-demo就是去匹配 Pod 的 Label。port是 Service 自己暴露的端口targetPort是 Pod 容器实际监听的端口。这两个可以不一样比如port: 8080, targetPort: 80做一层端口映射。3.2 Endpoints 为空时该往哪查Service 创建完第一件事是kubectl get endpoints web-demo-svc。如果输出里ENDPOINTS是none说明 selector 没匹配到任何 Pod流量根本转发不出去。这是最高频的Service 不通原因。排查顺序我总结成三步kubectl get pods --show-labels看 Pod 的实际 Label对照 Service 的 selector逐字比对键值检查 Pod 是否 Ready未 Ready 的 Pod 不会进 Endpoints第三步特别容易被忽略。Pod 处于 Running 但 readinessProbe 没过它就不会被加入 Endpoints。所以看到 Pod 明明在跑、Service 却不通先kubectl describe pod看 Conditions 里的 Ready 是不是 True。3.3 用 port-forward 做本地验证在 Service 配好之前想快速验证 Pod 里的应用是否正常kubectl port-forward是最省事的手段kubectl port-forward pod/web-demo-xxxxx 8080:80然后本地浏览器访问localhost:8080。这个命令把本地端口直接隧道到 Pod 端口绕过了 Service 和 Ingress适合排查到底是应用问题还是网络配置问题。如果 port-forward 能通、Service 不通那问题一定在 Service 的 selector 或 Endpoints 上范围一下就缩小了。注意port-forward 只用于调试别拿它当长期访问方案连接一断就没了。4. 把整条链路跑通并验证前面分开讲了 Deployment 和 Service这一节把它们串起来走一遍从创建到访问的完整流程中间穿插验证命令。这套流程我每次搭新服务都会走一遍形成肌肉记忆后排查问题会快很多。4.1 创建顺序与验证节奏先 apply Deployment再 apply Service顺序其实无所谓因为 Service 是靠 selector 动态匹配的Pod 后起来也会被自动纳入 Endpoints。但验证的节奏有讲究kubectl apply -f web-deployment.yaml kubectl rollout status deployment/web-demo kubectl get pods -l appweb-demo -o wide kubectl apply -f web-service.yaml kubectl get endpoints web-demo-svckubectl rollout status会阻塞直到滚动更新完成比手动get pods反复刷靠谱。-o wide能看到 Pod 落在哪个节点、IP 是多少。最后get endpoints确认有三个 IP 被列出来就说明 Service 已经正确关联到 Pod 了。4.2 集群内验证 Service 的 DNS 解析Service 创建后会自动获得一个 DNS 名字格式是service-name.namespace.svc.cluster.local。在同一个 Namespace 里直接curl web-demo-svc就能访问。验证方法是在集群里起一个临时 Podkubectl run tmp-shell --rm -it --imagebusybox --restartNever -- sh进去之后执行wget -qO- web-demo-svc或者nslookup web-demo-svc。如果 DNS 解析不出来检查 CoreDNS 是否正常如果解析出来但连不上回到上一节查 Endpoints。这个临时 Pod 用完--rm会自动删掉不留垃圾。4.3 滚动更新与回滚的实操改一下 Deployment 的镜像版本观察滚动更新的过程kubectl set image deployment/web-demo webnginx:1.26 kubectl rollout status deployment/web-demo kubectl get rs你会看到一个新的 ReplicaSet 被创建旧的副本数逐步缩到 0。kubectl get rs里新旧两个 RS 并存这是 Deployment 保留历史版本用于回滚的机制。回滚命令kubectl rollout undo deployment/web-demo kubectl rollout history deployment/web-demorollout history能看到版本记录配合--revision可以回滚到指定版本。这里有个经验默认保留的 revision 数量是 10通过.spec.revisionHistoryLimit调整。历史版本太多会占用 etcd 空间太少又没法回滚到更早的版本一般保持默认就行。5. 几个真实踩过的坑和对应解法概念讲完最后这部分是我在实际操作中反复遇到的问题都是文档里不会重点写、但一踩就卡半天的类型。提前知道能省不少时间。5.1 镜像拉取失败的两种典型表现kubectl describe pod里看到ImagePullBackOff或ErrImagePull原因通常两类镜像名写错包括 tag 不存在或者私有仓库没配认证。前者好排查docker pull本地试一下就知道后者需要创建imagePullSecrets并在 Pod 模板里引用。还有一种隐蔽情况镜像 tag 用了latest节点上已有旧版本缓存导致新 Pod 跑的还是老代码。生产环境永远不要用 latest tag用明确的版本号这是血泪教训。5.2 资源限制导致的 OOMKilledPod 反复重启kubectl describe pod显示Last State: Terminated, Reason: OOMKilled说明容器内存超了 limits 被内核杀掉。这时候要么调大limits.memory要么去查应用是不是有内存泄漏。注意 requests 和 limits 的差值不要拉太大否则节点上容易出现资源争抢。5.3 Service 端口配反的经典错误port和targetPort写反是新手高频错误。记住口诀port 是 Service 的门牌号targetPort 是容器里应用真正监听的口。调用方访问的是 portService 转发到 targetPort。如果应用监听 8080Service 的 targetPort 就必须是 8080port 可以随便定。5.4 跨 Namespace 访问忘了写全 DNS在default里访问prod命名空间的 Service不能只写服务名要写web-demo-svc.prod或者全称web-demo-svc.prod.svc.cluster.local。只写服务名只会在当前 Namespace 里找找不到就报 DNS 解析失败。这个错误在微服务拆分后特别常见。6. 对象协作的完整心智模型把这一篇的内容收拢一下其实就一张关系网Deployment 通过 selector 管 ReplicaSetReplicaSet 管 PodPod 带着 LabelService 通过 selector 找到带对应 Label 的 Pod把流量转发过去Namespace 给这一整套对象划定命名和权限边界。任何一个环节的 Label 对不上链路就断。我个人的习惯是每写一个新服务的 YAML先在纸上画三个框Deployment、Service、Namespace然后把 Label 和 selector 的键值写在连线上确认三处一致再动手。这个笨办法帮我省掉了大量apply 成功但访问不通的排查时间。K8s 的声明式模型好处是你描述要什么坏处是描述错了它不会报错只会安静地不工作——所以把对象关系想清楚比背命令重要得多。后面如果继续往下走自然会碰到 Ingress七层入口、ConfigMap/Secret配置注入、HPA自动扩缩容这些它们都是在这套基础对象之上叠加的。基础打牢了往上加东西就是查文档拼装的事。