Kubernetes生产部署实践指南:从版本选型到滚动更新的完整避坑手册

发布时间:2026/10/22 15:08:01
Kubernetes生产部署实践指南:从版本选型到滚动更新的完整避坑手册
这套部署方案在压测环境稳定运行了三个月期间经历了十几次滚动发布、三次节点故障恢复以及一次完整的集群迁移。如果你正准备把业务迁到Kubernetes上或者已经在用了但总被各种疑难杂症卡住这篇笔记应该能帮你少走不少弯路。1. 摸清家底再动手实践前的整体规划与方案选型1.1 我的版本选型与节点规划先说版本。Kubernetes的版本迭代速度很快但生产环境千万别追新。我选型的准则是当前稳定版再退两个小版本。举个例子如果官方最新稳定版是1.30那我优先考虑1.28或1.29。为什么因为新版本的特性往往需要周边组件CNI、CSI、Ingress Controller等同步适配太新的版本容易踩到兼容性坑。而且社区里针对旧版本的问题讨论沉淀更充足真出了毛病搜解决方案也更容易命中。节点规划上我采用的是经典的三主多节点架构3个Master节点分别部署在三个不同的故障域、3个Worker节点。Master节点的规格不需要太高4核8G就够用但磁盘一定要上SSDetcd的读写延迟直接影响整个集群的稳定性。Worker节点才是真正干活的地方我用的8核16G配置跑业务Pod加监控组件都绰绰有余。这里有个大多数人容易忽视的点操作系统选型建议统一。我踩过的坑是集群里混用了不同版本的发行版导致后面排查节点问题时需要分别适配不同的网络配置路径非常痛苦。最好统一用同一种内核版本的稳定发行版并且关闭swapKubelet默认不支持swap开启的状态。1.2 网络方案与存储方案怎么定网络插件CNI是K8s选型里最让人纠结的一块。我试过Flannel、Calico、Cilium三种最终留下的是Cilium。Flannel最轻量部署简单但只支持基础的Overlay网络NetworkPolicy的支持很弱Calico性能不错NetworkPolicy很强大但配置复杂排障的时候指向性不够清晰Cilium基于eBPF性能最好可观测能力也最强虽然上手门槛高一些但一旦熟悉了它的配置逻辑后续排查网络问题会非常顺畅。如果你只是小规模实验环境选Flannel完全足够如果是生产环境且对网络策略有安全合规要求建议直接上Calico或Cilium。存储方案这块我经历了从本地存储裸奔到分布式存储统一管理的转变。刚开始图省事用的hostPath结果Pod被调度到别的节点后数据就丢了数据库直接变成薛定谔的状态。后来切到了Rook-Ceph虽然部署和维护成本高一些但胜在稳定和完整。这里给个建议如果数据量不大百G级别用NFS或者云厂商的云盘就够用如果规模上来了再考虑Ceph这类分布式存储。一开始就把架构搞得太复杂只会让排障难度成倍增加。1.3 从Compose迁移到K8s的坑前认知如果你是从Docker Compose迁过来的我先给你打个预防针K8s不是Compose的简单升级版而是完全不同的编排哲学。Compose里你定义一个服务指定镜像、端口、环境变量就完事了K8s里你需要考虑工作负载类型Deployment还是StatefulSet、调度策略节点亲和还是污点容忍、配置注入方式ConfigMap还是Secret、服务发现机制Service的ClusterIP还是Headless、滚动更新策略MaxSurge和MaxUnavailable怎么配……这中间的学习曲线相当陡峭。我的迁移经验是分三步走第一步先把无状态服务比如Web前端、API网关迁上去第二步迁有状态服务比如MySQL、Redis但要先解决存储和网络标识的问题第三步再逐步把定时任务、消息消费者这类辅助服务纳入管理。这个顺序能让你的试错成本可控而且每一步的收益都能看得见。2. 核心细节逐个拆解迁移改造中的实操要点2.1 工作负载选型Deployment、StatefulSet还是DaemonSet很多新手搞不清这三个控制器到底怎么选我用实际场景来说明。Deployment适合无状态应用——也就是任意一个Pod都能独立处理请求挂了直接重建一个新的就行。比如Nginx、后端API服务、前端静态文件服务。Deployment的滚动更新机制是K8s里最核心的运维能力之一后面我会详细讲。StatefulSet适合有状态应用——每个Pod需要有稳定的网络标识和独立的存储卷。最典型的就是数据库。我个人用StatefulSet跑过一套高可用的MySQL集群基于半同步复制踩的坑主要集中在Pod的名称变更会导致主从关系失效这个问题在配置Headless Service时要注意。DaemonSet适合需要在每个节点上都跑一个Pod的场景典型的是日志采集Fluentd、监控采集Prometheus Node Exporter、网络插件Cilium的自愈组件。这类应用不需要考虑调度K8s会自动保证每个节点有且仅有一个副本。举个具体的例子。我在某次迁移中遇到过一个治理系统包含两个后端服务和两个数据库组件。一开始图省事全用Deployment托底结果数据库重启后数据丢了直接把我从能跑就行的幻想里拽回现实。最后MySQL和Redis都改成StatefulSet 持久化存储卷才算稳下来。决策的核心标准很简单Pod挂了能不能直接换一个新的如果不能就是StatefulSet。2.2 配置管理ConfigMap与Secret的正确打开方式配置管理是最容易出问题、也最容易被忽视的环节。我把所有非敏感配置连接超时时间、日志级别、特性开关放到ConfigMap里敏感配置数据库密码、API密钥、TLS证书放到Secret里。有人会问Secret只是Base64编码不也算明文吗确实K8s原生的Secret并不加密。所以生产环境我建议接入外部密钥管理服务比如Vault或云厂商的KMS或者至少用开源的Sealed Secrets方案对Secret进行加密在Git里只保存加密后的内容。这里有个实操细节我刚开始总搞错ConfigMap更新后Pod里的环境变量并不会自动更新。需要把配置以Volume挂载的方式注入并且配置Kubelet的--configmap-config-change-detection支持热更新。即便如此很多应用框架不会热加载文件变化所以最稳妥的做法是更新ConfigMap - 滚动重启Deployment通过kubectl rollout restart。我在实践中总结了一个高效操作流# 更新配置 kubectl create configmap app-config --from-fileconfig.yaml -n production --dry-runclient -o yaml | kubectl apply -f - # 触发滚动重启 kubectl rollout restart deployment/app-backend -n production # 观察状态 kubectl rollout status deployment/app-backend -n production2.3 应用暴露Service、Ingress与负载均衡的配合服务暴露是另一个高频踩坑点。K8s里的Service有四层TCP/UDP负载均衡能力有几种类型ClusterIP集群内部访问外部不可达。默认类型适合服务间调用。NodePort在每个节点的IP上开放一个端口范围30000-32767通过节点IP:NodePort访问。适合临时调试或没有负载均衡器的小规模环境。LoadBalancer依赖云厂商的负载均衡器把外部流量导入集群。适合公有云环境。Headless不提供负载均衡直接暴露每个Pod的独立IP和DNS记录。适合数据库主从节点间的通信。真正让外部流量进入集群的入口我推荐用Ingress Controller如Nginx Ingress或Traefik。Ingress只负责七层HTTP/HTTPS路由规则实际干活的是背后的Controller。生产环境我用的Nginx Ingress配了TLS终止和基于注解的流量管理比如nginx.ingress.kubernetes.io/canary做灰度发布。实际部署还有一个坑如果你的Service是TCP协议比如gRPC、WebSocketNginx Ingress默认配置可能处理不了长连接。需要在Ingress注解里启用nginx.ingress.kubernetes.io/proxy-read-timeout和nginx.ingress.kubernetes.io/proxy-send-timeout并把nginx.ingress.kubernetes.io/ssl-redirect设为false避免强制跳转。2.4 资源请求与限制别让Pod裸奔资源管理是K8s里最基础又最关键的一环。很多业务故障的根源不是代码问题而是Pod没有合理的请求requests和限制limits。requests调度器依据这个值决定Pod放在哪个节点。假如你设置requests为2核4G调度器就会在节点剩余资源足够的情况下部署Pod。limits运行时的资源上限。假如Pod实际使用内存超过limits内核会触发OOM Killer把进程杀掉。我见过不少事故比如两台Worker节点上面各跑了一堆无限制的Pod高峰时总内存超限Kubelet直接把整个节点杀掉——结果雪崩式地重建Pod业务全部中断。建议是先压测再设置。在灰度环境里跑一套完整的业务链路观察Prometheus监控里的CPU和内存峰值然后设置requests为峰值的70%、limits为峰值的120%。比如某个服务在压测时CPU峰值是1.8核内存峰值是3.2G那requests就配1.2核、内存2.2Glimits配2.2核、内存3.8G。别上来就抄网上的模板每个业务的资源画像都不一样。3. 完整实操过程记录从集群搭建到滚动发布3.1 集群安装与初始化我这边采用kubeadm方式安装集群。初始化Master节点时有个参数必须改就是--apiserver-advertise-address要指定为Master节点的内网IP。如果不指定kubeadm会默认取第一个网络接口的IP容易出现多网卡环境下的IP选择错误。初始化完成后记录好生成的kubeadm join命令Master和Worker节点的加入全靠它。网络插件要在集群就绪后第一时间装好。以Cilium为例helm repo add cilium https://helm.cilium.io helm install cilium cilium/cilium --namespace kube-system \ --set ipam.modekubernetes \ --set kubeProxyReplacementpartial \ --set ingressController.enabledtrue \ --set ingressController.defaulttrue装完后一定要检查cilium status --wait确认所有组件都是Ready状态。然后装Metrics ServerHPA水平自动扩缩容依赖它。kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml到这里一个能跑业务的集群骨架就出来了。3.2 应用容器化与清单文件编写容器化这一步有一个必须养成的好习惯Dockerfile的构建阶段和运行阶段分离。用多阶段构建让最终镜像只包含编译产物和必要运行库体积能缩小一半以上。# 构建阶段 FROM golang:1.22 AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED0 GOOSlinux go build -o app . # 运行阶段 FROM alpine:3.20 RUN apk --no-cache add ca-certificates tzdata WORKDIR /root/ COPY --frombuilder /app/app . COPY config.yaml . EXPOSE 8080 ENTRYPOINT [./app]写K8s清单文件YAML时我最常犯的错误就是把所有资源写在一个文件里导致更新时互相覆盖、找问题非常困难。后来养成的习惯是每个应用一个目录里面按资源类型分文件。app-backend/ ├── deployment.yaml ├── service.yaml ├── configmap.yaml ├── secret.yaml └── ingress.yamlDeployment的核心片段apiVersion: apps/v1 kind: Deployment metadata: name: app-backend namespace: production spec: replicas: 3 selector: matchLabels: app: backend strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: backend spec: containers: - name: backend image: registry.internal/app-backend:1.4.2 ports: - containerPort: 8080 envFrom: - configMapRef: name: app-config - secretRef: name: app-secret resources: requests: cpu: 1 memory: 2Gi limits: cpu: 2 memory: 3Gi readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 30 periodSeconds: 10这里我要重点强调一下maxUnavailable: 0的配置。它的含义是滚动更新过程中必须保证有Pod存活能够提供服务不允许出现没有可用Pod的情况。代价是更新速度会慢一些因为需要先拉起一个新的Pod等它Ready后才会销毁一个旧的Pod。这种做法在业务低峰期没有任何问题在高峰期也能保证服务不中断。反观默认的25% maxUnavailable在只有3个副本的情况下更新时会先杀掉1个Pod如果新Pod一直不Ready可用的Pod数量就会从3降到2影响容量。3.3 部署过程中的资源验证部署不是kubectl apply看到Pod Running就完事了。我每次部署都会强制自己走一遍完整的验证流程# 查看资源状态 kubectl get all -n production # 查看Pod详细事件 kubectl describe pod pod-name -n production # 验证服务发现 kubectl run test-pod --imagebusybox -it --rm -- /bin/sh / # nslookup app-backend.production.svc.cluster.local # 验证Ingress curl -H Host: api.yourdomain.com https://ingress-address/healthz一次部署我在describe里看到了FailedScheduling事件原因是节点上没那么多可用资源了。这个错误信息直接告诉你调度不成功的具体原因如果没有查看describe而是直接看log很难定位到是资源问题还是代码问题。所以记住Pod起不来先describe再kubectl logs。3.4 滚动更新与回滚演练滚动更新是K8s最常用的运维操作。打个比方一辆公交车正在行进中你要换掉老旧的座椅但不能让乘客站到马路上。操作方式是——先把新座椅搬上车新增Pod再把旧座椅卸下去干掉旧Pod逐个轮替。这个过程中车上始终有座椅可坐。# 更新镜像版本 kubectl set image deployment/app-backend backendregistry.internal/app-backend:1.5.0 -n production # 观察滚动状态 kubectl rollout status deployment/app-backend -n production # 如果发现异常回滚到上一个版本 kubectl rollout undo deployment/app-backend -n production回滚操作在发布后发现严重Bug的场景下是救命稻草。我经历的一次事故某次发布的新版本存在内存泄漏问题上线后Pod持续被OOM Kill滚动更新还没结束整个服务的可用率就在下降。当时我直接执行了rollout undo40秒内服务恢复到上一个稳定版本可用的Pod数量迅速回升。记住不要在新版本上修Bug除非你有100%的把握否则先回滚止血再在测试环境慢慢修。还有一个细节rollout undo默认回滚到上一个版本如果你连续发布了两个坏版本需要先看kubectl rollout history deployment/app-backend然后用--to-revision具体版本号回滚到指定版本。4. 常见问题与排查技巧实录4.1 Pod一直Pending资源请求分配的排查思路Pod持续Pending是最常见的现象之一。遇到这个情况我有一套固定的排查SOPkubectl describe pod pod-name -n production你会在Events里看到类似这样的信息Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 4s default-scheduler 0/3 nodes are available: 1 Insufficient cpu, 2 Insufficient memory.这说明一个核心问题当前集群里的节点资源不足以满足该Pod的requests。接下来就是看集群整体资源情况kubectl top nodes如果显示insufficient resource解决办法是清理一些不用的Pod或者扩容节点。还有一种情况是节点上有污点TaintsPod没有对应的容忍Tolerations调度器直接跳过该节点。排查时可以用kubectl describe node node-name | grep Taints查看污点。4.2 CrashLoopBackOff反复崩溃这个状态的意思是Pod启动了但反复崩溃K8s按指数退避策略10秒、20秒、40秒、80秒……不断重启它。查看日志是第一步kubectl logs pod-name -n production --previous--previous参数非常关键它会读取上一次崩溃实例的日志这往往能让你看到真正的报错信息。我遇到的典型崩溃原因有以下几种启动命令路径错误比如镜像里没有/app/start.sh这个脚本容器一启动就失败。配置文件挂载路径不匹配应用读取的配置路径和Volume挂载的路径不一致启动时加载不到配置直接退出。启动时连接数据库超时数据库服务的访问地址写错了或者数据库还没就绪应用在启动流程里直接报错退出。LivenessProbe配置太激进探针的initialDelaySeconds设得太短应用还没完成启动就被判定为无响应然后K8s杀掉它。典型的探针误杀问题我初学时踩过这个坑——明明应用本身好好的就是起一个服务要8秒我却把探针初始延迟设为3秒结果无限循环重启。4.3 镜像拉取失败的问题镜像拉取失败ImagePullBackOff在离线环境或私有仓库环境下极其常见。主要排查方向kubectl describe pod pod-name -n production | grep -A 10 Events常见错误信息Failed to pull image registry.internal/app-backend:1.5.0: rpc error: code Unknown desc Error response from daemon: manifest unknown如果是manifest unknown镜像版本不存在检查一下你打的tag是不是推到仓库了。如果是unauthorized: authentication required说明镜像仓库需要认证这时需要创建Secretkubectl create secret docker-registry registry-key \ --docker-serverregistry.internal \ --docker-username你的用户名 \ --docker-password你的密码 -n production然后在Deployment里引用这个Secret。如果节点上配置了HTTP私有仓库不走HTTPS你还需要在每个节点的容器运行时配置里加上insecure-registries参数否则容器运行时默认拒绝非HTTPS仓库。4.4 服务间DNS解析时通时不通这个故障现象太典型了服务A有时候能通过Service名访问服务B有时候报connection refused或Name or service not known。让我用一个真实的案例来说明某次压测环境中服务A调用服务B报错率在50%左右。我先查看CoreDNS的Pod状态是否正常kubectl get pods -n kube-system -l k8s-appkube-dns发现CoreDNS一直是Running没异常。继续深入查看了CoreDNS的日志kubectl logs -n kube-system -l k8s-appkube-dns --tail50发现大量SERVFAIL记录问题聚焦在上游DNS服务器/etc/resolv.conf里的forward地址不可用。原来我用的上游DNS不稳定CoreDNS转发解析外部域名时频繁失败。解决办法是修改CoreDNS的ConfigMap把上游DNS改为集群内更稳定的DNS比如8.8.8.8改成10.96.0.10自身或者改成一个内网稳定的DNS服务器。还有一个容易被忽视的问题Service与Pod的标签选择器不匹配。如果Service的selector写得不对Endpoints对象就一直是空的没有可用的后端地址导致DNS能解析到Service IP但请求过去后没有后端处理直接connection refused。# 检查Endpoints kubectl get endpoints service-name -n production如果显示的Endpoints为空马上检查Service的selector是否和Pod的labels对得上。4.5 存储卷权限与挂载问题用StatefulSet挂载持久化存储卷PVC时最常见的问题是权限不足Permission Denied或挂载目录为空。先说权限。很多镜像运行时用非root用户比如UID 1000启动应用而存储卷特别是NFS默认挂载权限是rootUID 0容器里的应用根本没有写权限。解决方法是要么在存储服务端把目录权限改为UID 1000要么在K8s的Pod定义里用securityContext指定fsGroupspec: securityContext: fsGroup: 1000这里的fsGroup会自动把挂载卷的group ownership改成对应UID让容器内的非root进程可以正常读写。挂载目录为空的故障我遇到过两次原因都是PVC没有正确绑定到存储后端。排查步骤是kubectl get pvc -n production kubectl describe pvc pvc-name -n production重点关注Status是否已经是Bound。如果一直是Pending查看Events里的报错比如存储类StorageClass不存在、存储后端不响应等。如果PVC已经Bound但Pod挂载后目录为空查看存储后端上的实际目录是否真的写入了数据。4.6 节点NotReady的处置流程节点状态变为NotReady是所有故障里最紧急的直接影响整个集群的稳定性。我的处置流程分两步第一步查看Kubelet状态systemctl status kubelet journalctl -u kubelet --since 10 minutes ago --no-pager | grep -i error常见原因包括磁盘空间已满、Docker/Containerd服务挂了、网络插件组件异常导致NodeController上报节点健康状况失败。第二步如果日志里反复出现Failed to update node或Error updating node status优先看容器运行时是否正常systemctl status containerd如果是磁盘满清理磁盘是第一步docker system prune或清理日志文件同时检查是否有大量无用的镜像占满磁盘。有一次集群里某个节点宕机就是因为节点的系统盘被Docker日志文件塞满单个容器日志一天写了20G监控没有及时告警。后来我在每个节点上加了一个cron任务定时清理历史日志才根治这个问题。这个教训让我深刻理解到K8s集群的节点运维光靠K8s本身是不够的操作系统层的基本保障必须跟上。最后再分享两个实用技巧第一给所有资源都加上namespace。我见过不少新手把所有资源一股脑丢到default命名空间集群规模一旦上来管理成本直接翻倍。建议按团队或项目划分命名空间配合RBAC做权限隔离谁负责哪块一目了然。第二平时多练习kubectl rollout操作尤其是在测试环境模拟发布失败后立即回滚的场景。真实的生产事故中你的操作速度和准确性就是业务影响范围的关键。多练几遍让回滚变成肌肉记忆。这套实践笔记里的所有坑都是我一个个踩出来的希望你读完能少走几步弯路。如果照着做过程中碰到新的问题欢迎在工作社区里搜索类似案例——大多数的问题都有人比你先踩过。