云原生不是上云:镜像编排与控制循环的落地实践

发布时间:2026/10/25 19:23:57
云原生不是上云:镜像编排与控制循环的落地实践

简介:这是一份面向云原生入门者、容器与编排技术学习者以及希望系统梳理云原生脉络的开发者的PDF资料,围绕云原生概念、容器技术演进与集群编排架构展开,可用于建立整体认知、准备技术分享和查漏补缺。压缩包内共1个PDF文件,大小约3.98MB,内容以云原生计算基金会的定义、容器发展历程、容器编排技术架构为主线,并涉及服务网格、微服务、不可变基础设施与声明式接口等代表技术。资料还提及监控告警、服务发现、网络接口规范以及无服务器架构、边缘计算等生态方向,帮助读者理解从传统云计算到云原生阶段的转变,把握容器、编排与服务治理之间的关系。已有205人学习,适合入门通读、知识框架搭建和后续专题学习前的背景铺垫。

1. 云原生不是“上云”:把运维对象从机器换成一份声明式期望状态

云原生这三个字,被用得最滥的场景是机房搬迁:把虚拟机上的应用原样塞进容器,改一份 YAML 就宣布改造完成。半年后账单涨了、定位一次故障的时间翻倍,团队才反应过来,自己只是换了一台更贵的机器。

真正的分界线不在容器,而在运维对象的改变。过去你运维的是一台台有 IP、有磁盘、会漂移的机器,出问题就 SSH 上去看;现在你运维的是一份声明式的期望状态,由控制循环不断把现实往这个状态上拉。镜像能不能复现、副本能不能自愈、发布能不能回滚、扩容要不要人工值班,全都由这一句决定。

下面按“前世、今生、落地”三段走:先说这条路线是被哪些具体问题一步步逼出来的,再用一套能跑起来的最小栈把镜像、编排、探针、弹性、灰度串成一条线,最后给迁移评估表和五个真踩过的坑。

2. 四个技术拐点:镜像、编排、控制循环、可观测性是怎么被逼出来的

把云原生当成一堆工具清单去背,很快就会迷失在组件名里。它更像是四次被迫的妥协:每一次都是上一代做法在规模、在变更频率上撑不住了,才长出新的一层。理解这四层各自解决什么问题,后面配参数的时候你才知道每个数字在防什么。

2.1 拐点一:镜像把“环境”变成可版本化的产物

早期部署的经典事故是依赖漂移。同一份代码,测试环境跑的是某个中间版本的解释器,生产环境是另一个;运维用配置管理工具把机器状态写成了脚本,脚本本身能保证“这次装得一样”,却保证不了“三个月后还能装出一样的东西”,因为上游仓库早就换版本了。配置管理解决的是机器之间的一致性,解决不了时间上的一致性。

镜像真正带来的不是打包,而是不可变 + 可寻址。一份镜像构建出来之后,用 digest 锁定,它永远不会变;要改就只能构建新的一份。这一步把“环境”从一台会被人登录、会被手工改配置的机器,变成了一个有版本号、能回滚、能 diff 的产物。分层机制则让这件事在经济上成立:依赖层几乎不变,每次构建只重传最上面那层业务代码。

配套的约束也得跟上。容器里应该是单进程模型,日志直接打 stdout 由平台收集,而不是往容器内写文件再想办法捞出来;状态要么外置到数据库和对象存储,要么明确挂载持久卷。这些约束不是洁癖,是因为容器的生命周期被设计成可以随时被替换——你今天写进容器本地磁盘的东西,明天重建就没了。

维度传统虚拟机部署镜像 + 容器部署
交付物安装包 + 一堆部署脚本一份带 digest 的镜像
环境差异靠文档和人工核对靠基础镜像统一,差异被压到配置层
回滚方式重新跑上一版脚本,常常回不去换成上一版镜像 digest,秒级
扩容粒度整台机器,分钟级单个副本,秒级
故障排查入口登录机器看日志文件平台侧日志与指标,机器不可登录

2.2 拐点二:编排把“调度”从人手里拿走

有了镜像,下一个问题立刻出现:这份镜像该跑在哪台机器上、跑几份、某台机器挂了怎么办。早期是用脚本 + 标签选择来做的,运维在发布系统里写“这台机器跑 A,那台跑 B”。机器一多,这张人肉维护的映射表就成了全公司最容易出错的地方:机器下线忘了摘、新机器加进去忘了分配、某台机器上堆了三个吃内存的服务。

编排器接管的是两件事:一是把“我要几份、每份要多少资源”这件事存下来;二是每次调度时按当前集群的真实水位做决策。调度不是随机的,它有一套打分逻辑:节点剩余资源够不够、有没有打上合适的标签、亲和与反亲和规则满不满足、同一个服务的多个副本有没有被强行摊到不同机架上。这些规则的价值在故障域上体现得最明显——你不写拓扑分布,调度器很可能把三个副本塞到同一台机器,这台机器一挂,服务直接全灭。

# 让同一服务的副本尽量摊开在不同节点上,避免单机故障导致全挂 topologySpreadConstraints: - maxSkew: 1 # 允许的最大不均衡度,1 表示基本平均 topologyKey: kubernetes.io/hostname # 按节点维度打散 whenUnsatisfiable: ScheduleAnyway # 资源不够时降级为"尽量",不阻塞发布 labelSelector: matchLabels: app: order-api

maxSkew设得太小(比如 0 配合 DoNotSchedule)在节点数不是副本数整数倍时会直接把 Pod 卡在 Pending,这是新手最常撞的墙;whenUnsatisfiable用ScheduleAnyway相当于给调度器一个“优先满足、实在不行也别停摆”的台阶。生产上我一般先把 skew 放到 1,观察一周再收紧。

2.3 拐点三:控制循环把运维动作写成代码

命令式脚本的根本毛病是它只描述动作,不描述结果。“如果副本数少于 3 就加一个”这种脚本,跑两次就出错,中途被打断就留下半截状态。而声明式控制器描述的是终态:“我要 3 个副本”。控制器不停地比对期望与实际,不一致就动手,动作本身是幂等的,跑一百次和跑一次结果一样。

这个模式一般叫 level-triggered(电平触发),对应的旧做法是 edge-triggered(边沿触发,只在事件发生时响应一次)。差别在故障场景下非常致命:边沿触发漏掉一个事件就永久错了,电平触发下一轮循环会自动纠正回来。

沿着这个思路,所有运维动作都能被“代码化”:证书快到期自动续、数据库从库延迟超标自动摘流量、某个自定义资源被创建后自动拉起一整套依赖。做法是把领域知识写进自定义资源定义和对应的控制器里,让平台侧只认终态,不认流程。

# 判断集群现状和你提交的声明之间差了什么,比"改完看有没有报错"可靠得多 kubectl diff -f deploy/order-api.yaml # 看某个控制器是不是一直在"纠正",重启次数异常增长通常意味着控制循环在打架 kubectl -n prod rollout status deploy/order-api --timeout=120s kubectl -n prod get pods -l app=order-api -o wide

kubectl diff返回非零退出码就代表有差异,可以接进流水线做“计划阶段”的展示,让人在合并前就看到这次变更到底动了哪几个字段。rollout status卡住不动,八成不是镜像拉取慢,而是新副本一直没通过就绪探针——这时候别急着重启,先去看事件。

2.4 拐点四:可观测性把黑匣子打开

容器化之后,“上机器看看”这条路被彻底堵死了:实例随时被替换、IP 随时在变、容器里连 shell 都不一定有。可观测性不是“多装几个监控大盘”,而是把三类信号打通:指标回答“有没有问题、问题多大”,日志回答“具体哪一行出错”,链路回答“慢在哪一跳”。

方法上我习惯两套并行:面向服务的 RED(请求速率、错误率、耗时分布),面向资源的 USE(使用率、饱和度、错误)。容器场景里最容易被忽略的是饱和度——CPU 使用率 60% 看起来很健康,但如果进程一直在被限流(throttling),实际响应时间已经烂了,这个信号只有专门的指标才有。

指标基数是最容易翻车的地方。把用户 ID、请求路径里的动态 ID 直接当标签打进去,时间序列数量会指数爆炸,最后把监控后端写垮。规矩很简单:标签只放枚举值可控的维度(服务名、接口名、状态码、实例),任何可能无限增长的字符串一律不许进标签。

3. 本地跑通一套最小云原生栈:从 Dockerfile 到 HPA 的五个步骤

原理讲完就该动手了。这一章用一套最小的 Java 服务走完整条链路:构建镜像、推仓库、发 Deployment、接流量、配探针和弹性。每一步我都给出可直接抄的片段,再说明每个参数在防什么事故。本地用 kind 或者单节点集群都行,差别只在 Ingress 那一层。

3.1 最小服务与 Dockerfile:把构建和运行彻底分开

先写一个只暴露一个健康接口和一个业务接口的服务,端口 8080。关键是 Dockerfile 要分两阶段:构建阶段带全套工具链,运行阶段只留 JRE 和产物。不是为了省空间好看,是因为构建工具留在运行镜像里,等于把一整套攻击面和几百兆无关文件塞进了生产环境。

# 阶段一:构建。依赖下载单独成层,pom 不变时这层直接命中缓存 FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /src COPY pom.xml . RUN mvn -B -q dependency:go-offline COPY src ./src RUN mvn -B -q clean package -DskipTests # 阶段二:运行。只带 JRE,不带源码和构建缓存 FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=build /src/target/app.jar /app/app.jar # 让 JVM 按容器可见内存算堆,而不是按宿主机内存 ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError" EXPOSE 8080 ENTRYPOINT ["java","-jar","/app/app.jar"]

MaxRAMPercentage=75.0是这一整篇里最值钱的一行。JVM 默认按宿主机物理内存推算堆大小,容器被限制在 1 GiB 时它会以为自己有几十 GiB,于是堆一路涨到被内核 OOM Kill,而监控面板上显示的内存使用率却一直很“正常”。ExitOnOutOfMemoryError则是让进程在内存耗尽时直接退出,交给编排器重启,而不是半死不活地卡着继续接流量。多个COPY的顺序也有讲究:变化频率低的放前面,pom.xml在源码之前,就是为了缓存不失效。

3.2 构建与推送:三个必须固定的参数

# 用提交号做标签,杜绝 latest 在生产出现 GIT_SHA=$(git rev-parse --short HEAD) REGISTRY=registry.internal.example/order # 跨架构构建时显式指定平台,避免在 ARM 笔记本上构建出跑不了 x86 的镜像 docker build \ --platform linux/amd64 \ --build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -t "${REGISTRY}:${GIT_SHA}" . docker push "${REGISTRY}:${GIT_SHA}" # 推送后立刻把 digest 记下来,部署时用 digest 而不是 tag docker inspect --format='{{index .RepoDigests 0}}' "${REGISTRY}:${GIT_SHA}"

三个参数值得单独说。--platform不写的话,在 ARM 机器上构建出来的镜像到了 x86 节点会直接exec format error,报错信息还很不直观。标签用提交号,是为了让“线上跑的是哪份代码”这个问题有一个确定答案,latest加默认拉取策略的组合会在回滚时把你坑得很难受,第 5 章会细说。部署时用 digest 而不是 tag,是因为 tag 可以被覆盖,digest 不能——这一步做了,才真正兑现了镜像的不可变性。

3.3 Deployment:requests/limits、副本、策略这四组参数

apiVersion: apps/v1 kind: Deployment metadata: name: order-api namespace: prod spec: replicas: 3 revisionHistoryLimit: 5 # 保留可回滚的历史版本数 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # 先多起 1 个新副本 maxUnavailable: 0 # 老副本一个都不许先停,保证容量不塌 selector: matchLabels: { app: order-api } template: metadata: labels: { app: order-api } spec: terminationGracePeriodSeconds: 30 # 留给优雅停机的时间 containers: - name: app image: registry.internal.example/order:${GIT_SHA} ports: [{ containerPort: 8080 }] resources: requests: { cpu: "500m", memory: "1Gi" } # 调度依据 limits: { cpu: "2", memory: "1536Mi" } # 硬上限 env: - name: JAVA_TOOL_OPTIONS value: "-XX:MaxRAMPercentage=70.0" readinessProbe: httpGet: { path: /health/ready, port: 8080 } initialDelaySeconds: 20 periodSeconds: 5 failureThreshold: 3 livenessProbe: httpGet: { path: /health/live, port: 8080 } initialDelaySeconds: 60 periodSeconds: 10 failureThreshold: 3

requests和limits不是一回事。requests是调度器排座位时看的数,写了 500m 就保证这个节点上有 500m 的余量留给你;limits是运行时的硬上限,超了 CPU 被限流、超了内存被杀。最常见的错误是 requests 写得很小、limits 写得很大,结果节点被严重超卖,平时没事,一到晚高峰所有 Pod 一起抢 CPU,谁都跑不动。

maxUnavailable: 0配合maxSurge: 1是最稳的一档:先起新副本,等它就绪了再停老的。代价是发布期间会多占一份资源,节点水位紧张的集群上可能起不来新 Pod,这时要么加节点要么把 surge 调大、unavailable 调成 1,做取舍。

terminationGracePeriodSeconds和优雅停机要配合着看。编排器发删除信号后,会先给进程一个 TERM,等宽限期过再 KILL。如果应用本身没有处理 TERM,正在处理的请求会被直接掐断,用户侧看到的就是发布期间零星的 502。

3.4 Service 与 Ingress:把流量接进来

apiVersion: v1 kind: Service metadata: name: order-api namespace: prod spec: selector: { app: order-api } ports: - name: http port: 80 targetPort: 8080 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: order-api namespace: prod annotations: nginx.ingress.kubernetes.io/proxy-read-timeout: "30" nginx.ingress.kubernetes.io/proxy-body-size: "8m" spec: ingressClassName: nginx rules: - host: order.internal.example http: paths: - path: /api/order pathType: Prefix backend: service: name: order-api port: { number: 80 }

Service 只做一件事:给一组会不断变化的 Pod 一个稳定的虚拟地址。它的 selector 必须和 Pod 的 label 完全对上,对不上时 Service 的 Endpoints 是空的,表现是 Ingress 返回 503,但 Pod 自己跑得好好的——这个现象和“服务挂了”很像,实际只是标签写错了。

Ingress 上的两个注解是踩过坑才加的。proxy-read-timeout默认值偏短,导出类接口很容易超时;proxy-body-size默认 1m,上传稍大一点的文件直接 413。这类参数不属于业务代码,但线上故障里占比不低。

3.5 探针与 HPA:让自愈和弹性真正生效

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: order-api namespace: prod spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: order-api minReplicas: 3 maxReplicas: 12 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 65 # 按 requests 的百分比算 behavior: scaleUp: stabilizationWindowSeconds: 0 policies: [{ type: Percent, value: 100, periodSeconds: 60 }] scaleDown: stabilizationWindowSeconds: 300 # 缩容前观察 5 分钟,防止抖动 policies: [{ type: Percent, value: 50, periodSeconds: 60 }]

这里有个必须讲清楚的点:HPA 的 CPU 利用率是相对 requests算的。requests 写 500m,实际用了 400m,利用率就是 80%,哪怕这台机器有 64 核。所以 requests 调小会让扩容变得异常敏感,一有流量就疯狂扩;调大则永远扩不起来。我的做法是拿稳定期 P95 的实际用量往上浮 30% 作为 requests,再把 HPA 阈值定在 65% 左右,留出突刺的缓冲。

scaleDown的稳定窗口是关键。没有这 5 分钟,流量一抖动副本数就上上下下,冷启动跟不上,用户看到的就是间歇性超时。scaleUp的窗口设 0 是为了抢时间,扩容慢比扩容多更伤。

探针的分工也要说清:readiness决定这个 Pod 要不要接流量,liveness决定要不要杀掉重启。把依赖检查(数据库连不上、下游超时)写进 liveness 是最常见的自伤式配置——下游一抖,所有副本被判定为“死了”集体重启,服务雪崩。依赖检查放 readiness,liveness 只检查进程自身是否还能响应,这是条硬规矩。

4. 传统应用迁移落地:一份 12 要素体检表和四步改造顺序

真到迁移这一步,最忌讳的是“先上容器再说”。我见过太多团队把一坨有状态、启动要三分钟、日志写本地文件的应用直接塞进容器,结果副本数不敢调、发布不敢做、一出问题还得登机器。改造顺序错了,后面全是补丁。

4.1 先做 12 要素体检,别急着改代码

云原生的十二条原则本质是一张“这个应用能不能被随时替换”的体检表。我一般拿它做一轮快速评估,每一项打“符合 / 部分符合 / 不符合”,不符合的项就是改造工作量的来源。

检查项判断标准不符合时的典型代价
代码库一个仓库一个服务,可独立构建改一行要全量发布,回滚粒度失控
依赖依赖声明在清单里,不依赖机器预装换台机器就跑不起来
配置配置来自环境变量或配置中心一份镜像只能跑一个环境
后端服务数据库、缓存、消息通过地址访问无法横向扩容
构建发布构建与运行严格分离镜像里混进构建工具和源码
进程无状态、单进程、可随时启停副本数不能大于 1
端口绑定自己监听端口,不依赖外部容器无法被服务发现接管
并发通过多副本水平扩展只能垂直扩,成本陡增
易处理启动快、能优雅停机发布期间必然掉请求
环境等价开发/预发/生产尽量一致“测试没问题”变成口头禅
日志当作事件流,写 stdout日志留在容器里,重建即丢
管理进程管理任务一次性执行,不常驻定时任务和主服务抢资源

这张表填完,改造的工作量基本就能估出来了。我的经验是:配置外置和日志改造通常占 60% 的工作量,但收益也最直接;无状态化最难,往往涉及会话存储和本地文件,需要单独排期。

4.2 改造顺序:无状态化 → 配置外置 → 健康检查 → 弹性

顺序不能反。先把会话从本地内存挪到共享存储,再谈多副本;先把配置从打包文件里挪到环境变量或配置中心,再谈一份镜像多环境跑;先有能区分“活着”和“能干活”的健康接口,再配探针;先有探针和资源声明,HPA 才有意义。跳过前面几步直接配 HPA,结果就是扩容出来的新副本因为缺配置起不来,越扩越糟。

无状态化这一步有个折中方案值得说:把会话放外部缓存,把上传文件放对象存储,把定时任务拆成独立进程单独部署。这三件事做完,绝大多数业务应用就能安全地跑 3 个以上副本了。

配置外置要注意一件事:不是所有配置都适合热更新。连接池大小、线程池大小这类参数改了必须重启才能生效,硬做成热更新只会让人误以为生效了。我一般把配置分两类:能热更新的(开关、阈值、限流值)走配置中心推送,不能的(池大小、JVM 参数)走环境变量,改了就滚动发布。

4.3 渐进式切流:双跑、灰度、回滚三件套

迁移不是一次性切换,而是让新旧两套同时跑一段,把流量一点点搬过去。我习惯分三段:第一段旧系统接 100%,新系统接内部测试流量,只验证功能和日志;第二段拿出 5% 真实流量进新系统,观察错误率和耗时分布,这一段至少跑满一个业务高峰;第三段按 25%、50%、100% 逐步放量,每一档都要看指标,不看就往下走等于赌博。

回滚方案必须在放量之前就写好,而不是出事再想。要回滚的东西有三层:镜像版本(换回上一个 digest)、配置版本(配置中心的历史版本)、数据(这一层最麻烦,涉及表结构变更时基本回不去)。所以数据库变更一律拆成“先加字段兼容双写、再切读、最后删旧字段”三步,任何一步都能停在中间状态不被发现。

灰度还有一个容易被忽略的细节:按什么维度切。按用户 ID 取模最稳,同一个用户始终走同一侧,体验一致,问题也容易复现;按随机比例切,同一个用户可能一会儿新一会儿旧,登录态和缓存都会出问题。

5. 避坑与排查:云原生落地最容易翻车的五个点

下面五条都是我在真实排障里反复遇到的,症状和根因往往隔得很远。每一条按“现象 → 原因 → 解决”写,能对号入座就直接抄结论。

5.1 Pod 被 OOMKilled,但监控里内存只用了 60%

现象:容器反复重启,事件里写着退出码 137,但内存监控面板的曲线一直在 limit 的六成左右徘徊。

原因:统计口径不一致。监控采集的是容器工作组内存里的一部分,而内核判定 OOM 看的是包括页缓存、堆外内存在内的总量。JVM 应用尤其典型:MaxRAMPercentage按宿主机内存算堆,堆外还有元空间、直接内存、线程栈,加起来轻松越过 limit。Go 和 Node 也有类似问题,GC 目标值和实际驻留集不是一回事。

解决:先把 limit 提到实测峰值的 1.5 倍观察一周,同时把MaxRAMPercentage降到 65%~70%,给堆外留出空间;再打开ExitOnOutOfMemoryError,让进程主动退出而不是被内核杀。排查阶段用容器内的实际内存指标而不是节点级指标,两者差得很远。

5.2 滚动更新把自己滚没了:readiness 探针配错的三种写法

现象:一次普通发布,新副本一直起不来,老副本又被按maxUnavailable停掉了,服务出现整段不可用。

原因:initialDelaySeconds太短,应用还没启动完就被判定为未就绪;failureThreshold太小,一次网络抖动就摘流量;更隐蔽的是把 readiness 指向了一个会检查下游依赖的接口,下游一慢,全集群副本同时被摘流量——这不是发布事故,是设计事故。

解决:initialDelaySeconds按冷启动 P99 再乘 1.5 设置,periodSeconds用 5 秒、failureThreshold给 3 次,别为了“快速发现故障”把参数压到极限。readiness 接口只检查自身是否完成初始化,下游依赖的健康状态放到熔断和降级逻辑里处理。发布前先在预发用同样参数跑一遍,能提前暴露大半问题。

5.3 回滚回到了“更旧的代码”:latest 标签加默认拉取策略

现象:线上出问题要紧急回滚,改回上一个标签,Pod 重建了,但跑的还是出错的那版代码。

原因:用了latest或者可变的标签,加上节点上已有同名镜像,imagePullPolicy: IfNotPresent直接复用了本地缓存。更麻烦的是多个环境的同一个 tag 指向了不同内容,谁也说不清线上到底是什么。

解决:标签一律用提交号或构建号,部署时写 digest;imagePullPolicy用Always或者干脆不写让 digest 生效。回滚用kubectl rollout undo或者改回上一版 digest,回滚后必须验证一次健康检查和关键接口,别只看 Pod 是不是 Running。这条规矩定下来之后,我们的发布事故定位时间从小时级降到了分钟级。

5.4 CPU 没跑满却响应变慢:limit 导致的限流

现象:节点 CPU 使用率 40%,服务 P99 耗时却从 80ms 涨到 900ms,压测和线上都是同一个现象。

原因:容器被 CPU limit 限流了。limit 是硬上限,超过就 throttling,进程被强制挂起等下一个周期,这段时间请求就卡着。使用率 40% 是节点维度,和单个容器的限流完全是两个指标。

解决:先看容器的限流指标,确认是不是这个问题;确认之后,要么把 limits 提到 requests 的 2~3 倍,要么对延迟敏感的服务干脆不设 CPU limit、只设 requests 保住调度。我的默认做法是 CPU 只设 requests 不设 limits,内存两个都设——内存超了是 OOM,性质完全不同。

5.5 改了配置没生效:挂载与环境变量的差别

现象:在配置中心改了参数、也更新了对应的配置对象,重启了应用,行为还是旧的。

原因:用环境变量注入的配置,值在容器启动时就固化了,更新配置对象不会影响已运行的容器,而且有些应用框架在启动时把配置读进了内存,改文件也没用。用卷挂载的配置会在一段时间后同步到容器里,但应用不重新读文件照样无效。

解决:先确认注入方式。环境变量类的配置,更新后必须滚动重启才生效,可以在配置对象上加一个校验和注解触发滚动;挂载类的配置,应用要监听文件变更或者定期重载,否则一样没效果。我倾向于把“改了必须重启”这类配置直接写在配置项的注释里,减少沟通成本。

6. 验收口径:用四个指标判断这次云原生改造到底值不值

改造做完,最容易陷入的错觉是“Pod 都是 Running,成了”。Running 只说明进程活着,说明不了这次改造有没有价值。我一般用四个可量化的口径来验收,每个都能从现有监控里直接取到。

口径具体指标参考目标
交付速度从合并代码到生产可用的中位耗时从小时级降到 15 分钟以内
变更风险发布引发的故障占比、平均回滚耗时回滚 2 分钟内完成
资源效率单位请求的成本、节点平均利用率利用率从 15% 提到 40% 以上
恢复能力单实例故障的业务影响时长单副本挂掉无用户可感知影响

四个口径里,我更看重第二和第四,因为它们直接对应“敢不敢频繁发布”这件事。前面那些技术选型再漂亮,如果团队还是一个月发一次版、出问题还是手工登机器重启,那这次改造就只完成了搬迁。

验收脚本我常备一个,跑一遍就能把关键状态拉齐,比翻好几个页面快:

#!/usr/bin/env bash set -euo pipefail NS=${1:-prod} APP=${2:-order-api} echo "== 副本是否达到期望 ==" kubectl -n "$NS" get deploy "$APP" -o jsonpath='{.spec.replicas}{" 期望 / "}{.status.readyReplicas}{" 就绪\n"}' echo "== 最近 10 分钟内重启过的容器 ==" kubectl -n "$NS" get pods -l app="$APP" \ -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[0].restartCount}{"\n"}{end}' echo "== 是否有 Pod 处于非 Running ==" kubectl -n "$NS" get pods -l app="$APP" --field-selector=status.phase!=Running echo "== 最近 20 条事件(按时间倒序,重点看 FailedMount/OOMKilled/Unhealthy)==" kubectl -n "$NS" get events --sort-by=.lastTimestamp | tail -n 20 echo "== HPA 当前副本与目标 ==" kubectl -n "$NS" get hpa "$APP" -o jsonpath='{.status.currentReplicas}{" 当前 / "}{.status.desiredReplicas}{" 期望\n"}'

脚本里第五段最容易被忽略但最有信息量:HPA 的当前副本数和期望副本数长期不一致,说明扩容跟不上或者资源不够调度,光看 Pod 是 Running 发现不了。第二段的重启次数则是最灵敏的“隐性故障”信号,重启数缓慢上涨往往比一次明显的故障更值得追。

最后说个我自己的习惯:每次做完一轮迁移,我会把当次的探针参数、requests/limits、HPA 阈值和当时的流量曲线一起存档,标注清楚为什么是这几个数。半年后再看,这几个数字是拍脑袋定的还是有依据的,一目了然。云原生这套东西参数多、默认值坑多,靠记忆是靠不住的,能留下来的只有当时写下的记录。希望帮到你。

本文还有配套的精品资源,点击获取