
用户无感发布实战指南从应用到运维的完整闭环以 Spring Boot 3.2.5 / Java 17 / 模块化 DDD为例讲清楚要做到「用户在发布期间零感知」应用端和运维端各自需要做什么以及大版本发布和小版本发布在策略上的本质差异。一、什么是「无感发布」无感发布的标准只有一条发布过程中任何一个真实用户的请求都不应失败、不应被中断、不应感受到明显延迟。它要求三件事同时成立请求不丢发布期间没有 5xx、连接拒绝、超时会话不断进行中的业务不被截断如未完成的支付、长查询数据不乱新旧版本同时运行不冲突数据库结构兼容这三条目标决定了无感发布必然是应用 运维 数据库三方协作的结果缺一不可。二、为什么发布会让用户「有感」只有先理解失败模式才能针对性解决。常见的几类「有感」来源失败模式根因连接被拒绝Connection refused旧实例被直接kill -9端口立即关闭502 Bad GatewayLB 还在路由到已经停掉的实例504 Gateway Timeout新实例还没启动完成就接收流量处理超时业务异常 / 数据回滚在途事务被强制中断字段不存在错误新代码部署到一半旧实例读到了新代码写入的字段首请求慢JIT 未编译、连接池未建立、缓存未预热无感发布的所有手段本质都是为了消除上述失败模式。三、整体架构流量调度与生命周期无感发布的核心思想是流量与实例生命周期解耦┌─────────────┐ │ 用户流量 │ └──────┬──────┘ ▼ ┌─────────────┐ │ 负载均衡 │ ← 唯一的流量决策者 └──────┬──────┘ ┌──────┴──────┐ ▼ ▼ ┌────────┐ ┌────────┐ │ 旧 Pod │ │ 新 Pod │ └────────┘ └────────┘ readiness readiness DOWN UP (摘除中) (服务中)谁能接流量由readiness 探针说了算进程何时退出由graceful shutdown terminationGracePeriod说了算。两者协同新旧版本平滑切换。四、应用端要做什么应用是无感发布的基础。如果应用本身不支持优雅关闭、不暴露健康端点、状态依赖本地再好的运维策略也救不了。4.1 引入 Actuator 暴露健康端点dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-actuator/artifactId/dependency4.2 配置健康检查区分 liveness 和 readiness# 仅暴露必要端点 management.endpoints.web.exposure.includehealth,info management.endpoint.health.show-detailswhen-authorized # 启用 K8s 风格的子探针 management.endpoint.health.probes.enabledtrue # readiness 含 DB 检查DB 不可用 → LB 摘除流量 management.endpoint.health.group.readiness.includereadinessState,db # liveness 只判断进程存活避免外部组件故障导致 Pod 被误杀 management.endpoint.health.group.liveness.includelivenessState关键区别探针检查对象失败后果liveness应用进程是否还活着K8s 重启 Pod重readiness是否可以接收流量K8s 摘除流量轻新手最常见的错误是把 DB 检查放在 liveness 里结果 DB 抖动 → Pod 全部重启 → 雪崩。liveness 必须只判断进程自身。4.3 启用优雅关闭# 收到 SIGTERM 后停止接收新请求等待在途请求完成 server.shutdowngraceful # 最长等待时间超时强制关闭 spring.lifecycle.timeout-per-shutdown-phase30sSpring Boot 3.x 内置实现当收到SIGTERMTomcat 不再accept()新连接但会继续处理已建立的请求直到所有请求完成或超时。4.4 应用必须无状态无感发布的前提是任何一个实例都能处理任何一个请求。这要求❌ 不要用HttpSession存业务状态❌ 不要用本地Map做缓存❌ 不要在本地磁盘存运行时数据✅ 鉴权用 JWT✅ 缓存用 Redis✅ 文件用 OSS / S3当前架构是无状态的这是它能做无感发布的天然优势。4.5 启动预热可选但推荐应用刚启动时 JIT 未编译第一次请求会慢。两种处理方式方式一延迟就绪# 配合 K8s readiness 的 initialDelaySeconds给 JVM 一段热身时间方式二主动预热实现SmartLifecycle或ApplicationRunner在标记 ready 之前主动发起几次内部调用触发关键路径的 JIT 编译和缓存加载。4.6 长任务的优雅处理server.shutdowngraceful只能等待 HTTP 在途请求对以下场景无效后台定时任务Scheduled消息队列消费者WebSocket 长连接文件上传500MB 的上传可能超过 30s需要自己实现DisposableBean或监听ContextClosedEventComponentpublicclassJobGracefulShutdownimplementsDisposableBean{Overridepublicvoiddestroy(){// 1. 停止接收新任务// 2. 等待当前任务完成带超时// 3. 持久化未完成任务状态}}对于文件上传建议在网关/Nginx 层做超时控制或评估是否真需要 500MB 限制。4.7 应用端清单项目状态备注spring-boot-starter-actuator✅ 已加server.shutdowngraceful✅ 已加spring.lifecycle.timeout-per-shutdown-phase✅ 已加30sliveness / readiness 分组✅ 已加无状态架构✅ JWT 鉴权启动预热⚠️ 待补可选长任务优雅退出⚠️ 待评估视业务而定五、运维端要做什么应用端只是「具备能力」能不能用起来靠运维侧的编排。5.1 K8s 滚动策略maxUnavailable0spec:replicas:2strategy:type:RollingUpdaterollingUpdate:maxSurge:1maxUnavailable:0# 关键发布期间容量不下降maxUnavailable0的含义是新 Pod 没起来之前旧 Pod 一个都不能停。这是无感发布最重要的一个开关。5.2 探针正确指向readinessProbe:httpGet:path:/actuator/health/readinessport:36029initialDelaySeconds:20periodSeconds:5failureThreshold:3livenessProbe:httpGet:path:/actuator/health/livenessport:36029initialDelaySeconds:60periodSeconds:15failureThreshold:5# 给大些避免误杀startupProbe:# 慢启动应用必备httpGet:path:/actuator/health/livenessport:36029periodSeconds:5failureThreshold:30# 最长允许 150s 启动5.3 优雅终止时间链这是最容易配错的地方。三个时间必须满足严格的不等关系terminationGracePeriodSeconds preStop sleep 应用 graceful timeout 45s 10s 30s任何一个时间错位都会导致旧 Pod 被强杀。spec:terminationGracePeriodSeconds:45containers:-lifecycle:preStop:exec:command:[/bin/sh,-c,sleep 10]为什么需要preStop sleepK8s 触发 Pod 停止时“通知 LB 摘除” 和 “发送 SIGTERM” 是同时发生的但 LBkube-proxy/Ingress同步 endpoint 有几秒延迟。这几秒内 LB 还会把流量发到正在退出的 Pod造成 502。preStop sleep 10强行让 SIGTERM 等 10 秒给 LB 同步时间。这是 K8s 滚动发布的必备技巧。5.4 不要把/actuator暴露到公网在 Nginx / Ingress 层屏蔽location /actuator/ { deny all; return 403; }/actuator/health即使设为show-detailswhen-authorized仍然会暴露{status:DOWN}这类信息对攻击者是有用的指纹。5.5 运维端清单项目关键配置副本数 ≥ 2replicas: 2滚动策略maxUnavailable0,maxSurge1探针路径正确/actuator/health/{liveness,readiness}优雅终止时间terminationGracePeriodSeconds: 45preStop 缓冲sleep 10Actuator 不暴露公网Nginx/Ingress 拦截监控告警5xx 率、Pod 重启、JVM 内存六、数据库经常被忽视的第三方应用端和运维端做到位但 DBA 一个ALTER TABLE DROP COLUMN照样让用户感知到错误。6.1 双版本兼容原则发布期间新旧代码同时运行数据库结构必须对两个版本都兼容✅ 允许新增表、新增可空列、新增索引、新增枚举值❌ 禁止删列、改列名、改类型、加非空约束、删枚举值6.2 字段重命名的正确姿势把user.name改成user.full_name不能一步到位必须分四次发布发布应用行为DB 操作v1同时写 name full_name读 name新增 full_name 列--后台脚本回填历史数据v2同时写两列读 full_name-v3只读写 full_name-v4-删除 name 列每次发布都保证「上个版本和当前版本能共存」否则无感发布无从谈起。七、大版本发布 vs 小版本发布很多团队把 “发布” 视为同质化的事件这是无感发布最大的误区。7.1 定义类型典型变更小版本Bug 修复、性能优化、文案调整、新增接口、灰度小特性大版本不兼容的 API 变更、数据库结构重构、依赖大升级如 Spring Boot 3→4、协议变更7.2 核心差异维度小版本大版本回滚策略直接kubectl rollout undo不能直接回滚DB 已变DB 兼容通常无需变更 / 仅加列多阶段、严格双向兼容流量切换一次性滚动即可灰度 → A/B → 全量持续时间分钟级天/周级风控监控 5xx、延迟业务指标全链路对比协调成本单团队跨团队前端、移动端、DBA降级方案不需要必须有功能开关测试范围单元 接口测试全链路 兼容性 性能压测7.3 小版本发布的标准动作适用于绝大多数日常发布整个流程几分钟内完成1. 镜像构建 → 推送 2. kubectl set image deployment/xx xx...:v1.2.4 3. 观察 rollout status 4. 验证关键接口自动化冒烟测试 5. 5 分钟监控期5xx 率、P99 延迟、JVM 6. 异常时 kubectl rollout undo特征单次决策、快速反馈、可瞬时回滚。7.4 大版本发布的进阶策略大版本的核心是可控、可观测、可回退。常见三种部署模式模式一蓝绿部署┌──────────┐ ┌──────────┐ │ Blue │ │ Green │ │ v1.x │ │ v2.0 │ │ (生产) │ │ (待切换) │ └────┬─────┘ └────┬─────┘ ▲ │ │ │ ┌────┴──────────────┴────┐ │ Load Balancer │ │ (一次性切流量) │ └────────────────────────┘优点切换瞬间完成回退也只是切回去缺点需要双倍资源DB 仍是单一份本质问题没解决适合可独立运行的服务如静态站点、无 DB 的微服务模式二金丝雀灰度发布┌─────────────────────────┐ │ Load Balancer │ │ 95% 流量 5% 流量 │ └────┬────────────┬───────┘ ▼ ▼ ┌────────┐ ┌────────┐ │ v1.x │ │ v2.0 │ │ 19个 │ │ 1个 │ │ Pod │ │ Pod │ └────────┘ └────────┘优点风险可控逐步放大缺点监控复杂需要按用户/地域/特征分流适合用户量大、业务关键的服务灰度比例梯度建议1% → 5% → 20% → 50% → 100%每一阶段观察 30 分钟以上。模式三功能开关Feature Toggleif(featureToggle.isEnabled(new-pricing-logic,userId)){returnnewPricingService.calculate(req);}else{returnoldPricingService.calculate(req);}优点部署和发布解耦可针对单个用户精确控制缺点代码复杂度上升需要管理开关生命周期用完必须清理适合业务逻辑变更、A/B 测试最佳实践是三者组合使用金丝雀部署做基础设施层灰度功能开关做业务层灰度。7.5 大版本发布的完整流程以「订单系统重构」这类大版本为例阶段内容时长1. 准备双版本兼容评估、DB 迁移脚本、回退方案、功能开关代码2 周2. 数据库迁移新增表 / 列回填数据建索引不影响线上1 天3. 部署新代码功能开关默认关闭新代码上线但不生效1 小时4. 内部灰度仅对内部员工开启开关1 天5. 用户灰度1% → 5% → 20% → 50%每档观察1 周6. 全量开关 100% 打开当天7. 清理删除旧代码、旧字段、关闭开关1 周后整个周期可能持续两到三周但每一步都可在数分钟内回退。7.6 大小版本对比速查小版本代码差异小 → 滚动发布 → 出问题就回滚 ↓ 1 个流程动作 大版本代码差异大 → 解耦部署与发布 → 灰度放量 → 数据迁移分阶段 ↓ N 个流程动作每一步独立可控判断一次发布属于哪一类的简单原则如果不能kubectl rollout undo一键回退它就是大版本。八、监控验证发布是否真的「无感」光靠人盯日志不够必须有量化指标支撑。8.1 必须监控的指标指标阈值工具HTTP 5xx 率 0.01%Prometheus GrafanaHTTP 4xx 率突增不超过基线 2 倍PrometheusP99 延迟不超过基线 1.5 倍APMPod 重启次数 0K8s 事件JVM 老年代 GC频率不增Micrometer业务指标与同期对比 ±5%业务监控8.2 发布期间持续压测# 整个滚动过程中持续打流量wrk-t8-c200-d600s--latencyhttp://api/health-business观察整个发布期间错误数是否为 0。这是最直接的无感验证。8.3 错误预算Error Budget成熟团队会用 SRE 的错误预算思路SLO99.95% 可用性 → 每月允许 21.6 分钟不可用每次发布消耗的错误预算可量化预算用完 → 暂停发布优先稳定性九、实施清单落到本项目上的具体行动项9.1 应用侧已完成spring-boot-starter-actuator依赖server.shutdowngracefulspring.lifecycle.timeout-per-shutdown-phase30sliveness / readiness 探针配置无状态架构JWT9.2 应用侧建议补充启动预热针对 marketplace 等热点接口后台任务的优雅退出如果存在Scheduled评估 500MB 上传接口在 30s graceful 内能否完成9.3 运维侧待执行K8s Deployment 使用maxUnavailable0, maxSurge1探针指向/actuator/health/{liveness,readiness}terminationGracePeriodSeconds: 45preStop: sleep 10Nginx/Ingress 屏蔽/actuator/**公网访问接入 Prometheus 监控9.4 DBA 侧流程规范制定数据库变更评审流程禁止运行时执行不兼容 DDL字段重命名走多阶段流程十、总结无感发布不是一个开关而是架构 流程 工具的综合能力架构上应用必须无状态、支持优雅关闭、健康检查准确流程上小版本能秒级滚动大版本必须解耦部署与发布工具上K8s 编排 探针 监控告警形成闭环记住三个关键不等式1. 优雅终止时间 preStop graceful timeout 2. readiness 失败阈值 liveness 失败阈值 3. 新旧版本兼容期 ≥ 滚动发布完成时间只要这三条不破发布期间用户就不会有任何感知。