ARTICLE DETAIL

资讯详情

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

K8s Pod核心概念与YAML实战:从设计原理到生产避坑指南

K8s Pod核心概念与YAML实战:从设计原理到生产避坑指南 搞K8s的人早晚会遇到一个绕不开的概念Pod。我见过不少新手能背出Pod是Kubernetes最小的调度单元这句话但真到写yaml时一脸茫然——不知道apiVersion填什么不确定limits到底该不该写更搞不清readinessProbe和livenessProbe的区别。这篇是K8s系列的第三篇我会把Pod从设计原理到yaml字段、从常用命令到完整实战一次性讲清楚。内容是我使用K8s这些年积累下来的实操经验不是语法手册的复读适合刚入门K8s、正在准备部署第一个应用的读者。1. 为什么K8s的调度单位是Pod而不是容器先搞懂设计意图很多初学者会问一个问题Docker容器不是已经能跑应用了吗为什么K8s还要在外面套一层Pod这个问题的答案直接决定了你后面能不能写出合理的yaml。1.1 容器模型和真实应用之间的落差Docker容器在设计上偏向单进程模型意思是一个容器最好只跑一个主进程这样才能做到故障隔离、日志归集、资源统计都清晰。但真实业务往往不是单一进程能搞定的一个Web服务需要把访问日志同步给采集器一个应用在启动之前需要先执行数据库迁移一个Java进程需要单独的sidecar做链路追踪。如果硬塞进同一个容器就会碰上PID 1管理混乱、日志文件被多个进程争抢、重启策略难以界定这些麻烦事。K8s设计Pod的初衷就是解决这个问题把一组关系紧密、必须同机部署、共享命运的容器打包成一个整体来调度。Pod里的容器共享同一个网络命名空间、共享存储卷但进程之间仍然通过正常的进程隔离来保证安全。1.2 Pod共享的两样东西网络与存储同一个Pod里的容器网络视角完全一致。它们共享同一个IP、同一个端口空间互相之间用localhost就能访问。这个机制背后靠的是一个隐藏的pause容器也叫infra容器它先启动并持有网络命名空间其他业务容器再加入进来。所以你在节点上用crictl ps会看到每个Pod都有一个pause容器在垫底这是正常现象不是事故。存储方面Pod内的容器可以挂载同一个Volume实现数据共享。典型例子是主容器写日志文件sidecar容器读取同一个文件上报给日志系统。这个模型特别像合租几个室友共享Wi-Fi和冰箱但各用各的笔记本电脑互不干扰代码。1.3 什么该放一个Pod什么不该放判断标准只有一个这些容器是否需要同生共死、是否必须调度到同一个节点、是否要共享网络和存储。常见组合Web容器 日志采集容器日志采集必须跟着业务进程走Pod重建时间点要一致。应用容器 本地缓存容器比如Redis作为应用的内存缓存缓存进程和应用不在同一节点就没意义。主容器 配置热更新sidecar负责监听配置仓库变化把最新配置写到共享卷主容器自动加载。反过来两个没有直接依赖关系的独立服务比如Nginx和MySQL就不该放同一个Pod。它们需要独立扩缩容、独立发布、独立故障恢复强行打包在一起反而让运维动弹不得。这是我的经验之谈刚开始用K8s的人总喜欢把所有东西塞进一个Pod图省事后面扩容和发布时会非常痛苦。2. 手写第一份Pod yaml核心字段逐个讲透yaml是K8s的通用语言任何对象都可以用yaml描述。写Pod的yaml并不难难的是理解每个字段背后的含义。我习惯从骨架到血肉一层层看。2.1 骨架三件套apiVersion、kind、metadataapiVersion: v1 kind: Pod metadata: name: nginx-hello namespace: demo labels: app: nginx-helloapiVersion决定了你用的是哪个API组的哪个版本。Pod属于核心API组所以是v1如果写Deployment就要用apps/v1。kind表示对象类型这里是Pod。metadata.name是Pod的名字同一命名空间内必须唯一命名规则要符合DNS-1123标准小写字母、数字、中划线。namespace是命名空间不写就默认落到default。labels尤其重要Service、Deployment都是靠标签选择器关联Pod的我习惯从一开始就给Pod打好app、env、tier这组标签后面排查问题时能靠标签快速过滤。2.2 spec.containers承载业务的核心配置块spec.containers是列表类型因为Pod可以包含多个容器。每个容器的必填字段是name和image。spec: containers: - name: nginx image: nginx:1.25 imagePullPolicy: IfNotPresent command: [nginx] args: [-g, daemon off;]imagePullPolicy有三个值Always每次都拉镜像IfNotPresent本地没有才拉Never只用本地镜像。如果不写K8s的规则是标签为latest或没写标签时默认Always其他标签默认IfNotPresent。生产环境我推荐显式写IfNotPresent配合固定版本号避免节点每次启动都去仓库校验一遍。command和args对应Dockerfile里的ENTRYPOINT和CMD。如果只写command它会覆盖ENTRYPOINT只写args则只会覆盖CMDENTRYPOINT保留。这个覆盖关系是新手最容易混淆的地方。最简单的记忆方式command是启动进程本身args是传给进程的参数。2.3 resources给Pod申请资源的正确姿势resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mirequests是调度依据K8s调度器会看每个节点剩余可分配资源够不够满足所有Pod的requestslimits是运行时约束CPU超了会被限流内存超了会触发OOM Kill。CPU单位100m代表0.1核内存单位Mi是二进制兆字节1024*1024字节M是十进制兆字节。写配置时别把这两个单位搞混否则资源预估会偏差几十倍。这里必须多说一句requests和limits的组合决定了Pod的QoS等级。两个都设置且相等是Guaranteed最不容易被杀只配limits不配requestsK8s会把requests默认成和limits一样虽然还是Guaranteed但会造成资源浪费都设置且requests limits是Burstable全不设是BestEffort节点内存不足时最先被驱逐。生产环境我建议至少给核心业务设置requests和limits宁可保守一点也别让Pod变成BestEffort被系统优先牺牲。2.4 探针让K8s知道你的Pod到底活没活探针是Pod给K8s的健康自报分为三种livenessProbe存活探针决定容器是否需要重启readinessProbe就绪探针决定流量是否要发给这个PodstartupProbe启动探针用于保护启动很慢的应用避免它还没起来就被存活探针杀掉。livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3探测方式有三种httpGet发起HTTP请求tcpSocket检查端口能否连通exec在容器内执行命令并检查退出码。httpGet最常用但要注意探测路径必须有实际健康含义不要拿首页当健康检查否则一个业务报错就会引发无限重启。initialDelaySeconds是容器启动后等多久再探测这段缓冲期给应用做初始化periodSeconds是探测间隔failureThreshold是连续失败多少次才判定不健康。我的经验是探针参数宁松勿紧。很多线上事故就是livenessProbe太敏感容器启动慢了一点就被反复重启陷入CrashLoopBackOff。2.5 环境变量、ConfigMap与Secret环境变量用env字段配置可以直接写值也可以从ConfigMap或Secret引用。env: - name: APP_ENV value: production - name: DB_URL valueFrom: configMapKeyRef: name: app-config key: db_url - name: DB_PASSWORD valueFrom: secretKeyRef: name: app-secret key: passwordenvFrom可以一次性把整个ConfigMap或Secret的所有键值导入成环境变量适合配置项很多的情况。不过我建议别把Secret大量灌进环境变量K8s的Secret本质只是base64编码不是加密真正敏感的数据应该接入外部密钥管理方案。关于安全的话题这里不展开你只要记住环境变量可能在describe时被看到就够了。2.6 调度相关nodeSelector、nodeName和更高级的调度nodeSelector是最简单的调度方式指定Pod只能调度到包含某标签的节点上nodeSelector: disktype: ssdnodeName则直接把Pod绑定到指定节点它会绕过调度器。千万别在正常流程里用nodeName节点宕机时Pod会一直卡在Pending状态没人接管。更精细的调度要靠nodeAffinity、podAffinity和taint/toleration这些内容适合单独开一篇调度专题这里你只需要知道Pod的落脚点是怎么控制的即可。3. kubectl命令链从创建、进入容器到故障排查会写yaml只是第一步真正日常打交道最多的是kubectl命令。我把Pod生命周期里的常用命令串成一条完整链路你在排查问题时就按这个顺序走。3.1 创建与查看从apply到describekubectl apply -f pod.yaml kubectl get pod -n demo -o wide kubectl describe pod nginx-hello -n demo kubectl get pod -n demo -wapply是声明式创建推荐优先使用create是命令式创建更适合快速起一个测试Pod。get加-o wide能看到Pod所在的节点和IP是排查网络问题时的第一步。describe会输出完整的Pod事件包括镜像拉取、容器启动、探针检查结果这是我排查Pod卡在Pending、CrashLoopBackOff时的第一工具。加-w可以实时追踪Pod状态变化比如观察探针生效的过程。按标签过滤是日常高频操作kubectl get pod -n demo -l appnginx-hello3.2 进容器与看日志排障三板斧kubectl logs pod/nginx-hello -n demo kubectl logs pod/nginx-hello -n demo -c nginx kubectl logs pod/nginx-hello -n demo -f --tail200 kubectl exec -it pod/nginx-hello -n demo -- /bin/shlogs加-c指定容器多容器Pod时必须用加-f实时跟踪加--tailn只看最近n行避免刷屏。exec进入容器内部--后面是你要在容器里执行的命令。容器里不一定有bash有些精简镜像只有sh甚至sh都没有所以exec失败时先试试/bin/sh再不行就要接受这个镜像没有shell的现实。3.3 端口转发与临时调试本地访问集群内Pod的端口用端口转发kubectl port-forward pod/nginx-hello -n demo 8080:80这条命令会把本地8080端口映射到Pod的80端口非常适合调试Service还没配置好的阶段。想从Pod拷文件出来或者把本地文件塞进Pod用kubectl cpkubectl cp ./test.txt demo/nginx-hello:/tmp/test.txt3.4 删除与更新kubectl delete pod nginx-hello -n demo kubectl edit pod nginx-hello -n demo裸Pod被删除就是真的删了不会自动重建。edit是直接打开资源的在线编辑保存后生效。但注意裸Pod大部分spec字段修改不会真正生效比如镜像改动需要删除后重建。这也是为什么生产环境不用裸Pod的原因之一。3.5 我的Pod命令速查表场景命令说明查看指定命名空间的Podkubectl get pod -n demo加-o wide看节点和IP查看Pod详情与事件kubectl describe pod xxx -n demo排查Pending、ImagePullBackOff的利器实时跟踪状态kubectl get pod -n demo -w观察创建和销毁过程查看日志kubectl logs pod/xxx -n demo多容器加-c 容器名进入容器kubectl exec -it pod/xxx -n demo -- /bin/sh容器内调试端口转发kubectl port-forward pod/xxx 8080:80本地访问Pod端口按标签过滤kubectl get pod -n demo -l appxxx快速筛选多Pod4. 完整实战用Pod yaml跑通一个带健康检查的Web服务前面讲的都是知识点这一段我把它们串起来用一个实际例子走一遍完整流程。目标创建一个Nginx Pod配置资源限制、存活探针、就绪探针验证探针在应用假死时如何自动拉起容器。4.1 准备完整的Pod yamlapiVersion: v1 kind: Pod metadata: name: nginx-hello namespace: demo labels: app: nginx-hello spec: restartPolicy: Always terminationGracePeriodSeconds: 30 containers: - name: nginx image: nginx:1.25 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80 env: - name: HELLO_MSG value: hello from pod resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3 livenessProbe: httpGet: path: / port: 80 initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3这个yaml里我故意把livenessProbe的initialDelaySeconds设成15秒给Nginx留足启动时间。readinessProbe设成5秒一次因为就绪探针的失败不会杀掉容器只会把Pod从Service端点里摘掉频率高一点没太大风险。4.2 创建并验证探针行为kubectl create namespace demo kubectl apply -f pod.yaml kubectl get pod -n demo -w创建后你会看到Pod经历Pending→ContainerCreating→Running三个阶段。等READY列变成1/1说明探针已经通过。验证探针的实战操作进入容器杀掉Nginx主进程观察K8s如何反应。kubectl exec -it pod/nginx-hello -n demo -- /bin/sh -c kill 1kill 1杀掉PID 1Nginx主进程容器退出Pod会自动重启。这时kubectl get pod -n demo -w会看到RESTARTS从0变成1。这说明livenessProbe和容器的重启策略共同作用让故障自愈发生了。查看探针详细事件的方法kubectl describe pod nginx-hello -n demo输出的Events区域会显示Readiness probe failed或Liveness probe failed的记录这些是定位为什么重启的关键证据。4.3 从Pod到Service为什么单用Pod不够实战到这里你可能会发现一个问题Pod虽然跑起来了但外部怎么访问它直接用kubectl port-forward只是调试手段生产流量不能靠手工转发。这时就要引入Service对象通过selector匹配Pod的labels把Pod的端口暴露成稳定的服务入口。这块内容属于K8s系列里Service篇的主题你只需要记住一点Pod本身是一次性的IP和名字随时可能变化Service才是对外提供服务的稳定抽象。5. 多容器Pod与initContainer进阶玩法入门阶段单容器Pod就够用了但真实生产环境里多容器Pod是绕不开的高级形态。这一节我重点讲两种模式sidecar和initContainer。5.1 sidecar模式主从容器各司其职sidecar就是边车给主容器提供辅助能力的附属容器。最经典的组合是Web服务 日志采集containers: - name: app image: my-web-app:1.0 volumeMounts: - name: logs mountPath: /var/log/app - name: log-shipper image: fluent-bit:2.1 volumeMounts: - name: logs mountPath: /var/log/app主容器把日志写到共享卷sidecar容器读同一个目录并上传到日志平台。这样日志采集的生命周期完全跟随主容器不需要在节点上额外部署Agent也不会因为采集器版本更新影响主业务。我实际维护的系统里大量应用都采用了这种模式它最大的好处是耦合度低主容器的镜像里不需要装采集器、不需要配置日志路径只负责写文件即可。5.2 initContainer主容器启动前的准备动作initContainer是Pod里特殊的初始化容器它按顺序串行执行全部成功之后才会启动主容器。任意一个失败Pod会重新调度或重启。initContainers: - name: wait-for-db image: busybox:1.36 command: [sh, -c, until nslookup mysql-service; do echo waiting; sleep 2; done]这段initContainer会一直探测mysql-service这个DNS名字直到数据库服务可解析后才退出。主容器启动时数据库已经就绪不用自己处理重试逻辑。类似场景还有初始化数据库表结构、下载启动依赖的配置文件、设置目录权限等。使用initContainer有几个注意点一是K8s 1.26之后强制要求initContainer必须配置resources否则Pod起不来二是initContainer意外退出会重启整个Pod所以里面的命令要幂等重复执行结果一致三是initContainer会延迟主容器启动尽量把耗时的准备工作放进去纯等待类的操作要设置合理超时避免永远卡住。5.3 emptyDir与共享存储实战多容器之间共享数据靠Volume。emptyDir是最简单的卷类型Pod创建时建立空目录Pod删除时整目录销毁。上面日志采集的例子用的就是emptyDir。注意emptyDir的生命周期跟随Pod而不是容器容器重启后数据还在但Pod被删除就彻底没了。如果多个Pod之间需要共享数据就要用pvc一类的持久化存储这是后面存储篇的内容。我的建议是日志、临时缓存这种容忍丢失的数据用emptyDir数据库文件这种绝对不能丢的必须上PV/PVC。6. 生产环境里最常见的Pod坑镜像、资源与生命周期本章内容是我在线上环境踩过、也帮别人排查过的坑每一条都对应过真实的故障。提前知道它们能帮你省掉很多半夜oncall的时间。6.1 镜像拉取策略导致的越跑越老和拉取失败使用固定tag的镜像时如果imagePullPolicy是IfNotPresent节点上只要有这个tag的镜像就拉新的。问题来了如果你重新推送了具有相同tag的镜像但节点缓存里已经是旧版本Pod重建时不会去拉新镜像跑的还是老代码。这种情况通常出现在CI/CD流水线每次构建都打latest或同一个版本号的场景。生产环境的正确做法每次发版用不同的tag日期构建号或者用镜像摘要引用。如果一定要固定tag就显式把imagePullPolicy设为Always用一点网络开销换代码一致性。另一个常见坑是私有镜像仓库没配置凭据导致拉取时报ImagePullBackOff。解决方式是在命名空间里创建imagePullSecret并在Pod的spec里通过imagePullSecrets引用。6.2 QoS等级与节点驱逐顺序前面我提过QoS等级这里展开解释为什么重要。节点内存紧张时Kubelet会按优先级驱逐Pod顺序是BusyTerminating → BestEffort → Burstable根据实际使用超过requests的比例 → Guaranteed。也就是说不写requests和limits的Pod是第一批被杀的。我遇到过一起线上事故某团队部署的监控Agent没配资源节点内存压力一上来Agent先被杀光监控出现大面积盲区随后业务Pod也陆续被驱逐整个故障的定位线索全断了。从那以后凡是进生产集群的Pod我都强制要求至少配置requests。不设limits在某些情况下是可以接受的但完全不设requests等于把Pod放进了优先牺牲名单。6.3 裸Pod没有自愈能力生产环境请用Deployment裸Pod被删除、节点宕机、节点磁盘满都不会自动恢复。生产环境创建服务必须用Deployment或StatefulSet这类控制器由控制器负责维持期望的副本数。控制器会在Pod异常退出、节点故障时重新创建Pod才能真正享受K8s的声明式自愈能力。那裸Pod是不是完全没用也不是。我平时调试单次任务、临时排查网络问题、跑一次性脚本都会直接起一个裸Pod用完即删干净利落。但凡是需要长期运行的业务这个定义范围内的容器都别用裸Pod。6.4 优雅终止为什么代码还在跑就被杀掉了K8s删除Pod时Pod会进入Terminating状态。默认情况下Kubelet等待30秒后强制kill进程如果应用自己处理不了SIGTERM信号就只会得到30秒的最后通牒。很多Java应用启动慢、停止也慢默认的30秒压根不够就会出现日志还没刷完、连接没关闭、请求还在处理中就被SIGKILL强杀的情况。解决方案有三个维度调大terminationGracePeriodSeconds给应用更多收尾时间。加preStophook在收到SIGTERM前先执行一段清理脚本或睡眠等待。应用侧捕获SIGTERM主动完成优雅下线。spec: terminationGracePeriodSeconds: 60 containers: - name: app lifecycle: preStop: exec: command: [sh, -c, sleep 10]这里preStop里sleep 10不是耍流氓它的本意是给Service的端点摘除留出时间窗口避免正在处理中的请求被切断。配合就绪探针在终止前置为失败能达到先摘流量、再停服务的效果。这些坑我都逐个踩过。到现在我养成了一个习惯任何Pod进生产之前先跑一遍kubectl describe看QoS等级再检查探针参数和优雅终止配置。K8s确实方便但方便不等于自动正确——理解Pod的每一层语义你才能真正把它用好。
返回列表