ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Kubernetes核心三件套:Pod、Deployment、Service的关系与排障实践

Kubernetes核心三件套:Pod、Deployment、Service的关系与排障实践 第一次正经接触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替换旧PodIP会持续更替。你可以做个简单实验创建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 --imagenginx这个Pod确实会跑起来。但一旦它因为节点故障、OOM、人为误删等原因消失没有人会帮你重新拉起一个。生产环境不可能接受这种一次性用品。Deployment最大的价值是引入了期望状态这个核心概念。你在Deployment里声明我要3个副本镜像版本是nginx:1.25Deployment的控制器就会持续比对当前实际运行的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滚动更新时有两个关键参数决定发布节奏maxUnavailable和maxSurge默认值都是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会分配一个固定的集群内虚拟IPClusterIP通过标签选择器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和ipvsService不是某个具体进程它本质上是节点上的一组网络规则。创建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解析与跨命名空间访问集群内通常会部署CoreDNSPod之间可以直接用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暴露。先写DeploymentapiVersion: 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: webappDeployment不会报错但它将无法管理任何Pod。你会看到replicas一直不满足Pod却不在rs的管辖范围内。4.2 Service只认标签不认Deployment然后是ServiceapiVersion: 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 --imagecurlimages/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明明RunningService却不通有一次我在测试环境部署一个应用kubectl get pods看到3个Pod全部Running但curl Service的ClusterIP就是超时。第一步查看Endpointskubectl 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为什么崩溃其实更高效的排查顺序是先看ReplicaSetkubectl 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加--restartNever再配合--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时如果没有显式指定nodePortKubernetes会在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、多排几次障这些概念会慢慢变成一种直觉。
返回列表