《Spring Boot 实战》18.3 上线、观测与回滚

给出生产级图书借阅服务的上线检查清单、金丝雀蓝绿滚动的适用条件、上线后必看的 RED 指标与业务指标、基于 SLO 的错误预算与燃烧率告警定法,以及代码、配置、特性开关、数据四类回滚的取舍,附向前兼容的数据库迁移写法、发布演练要点与事故复盘模板,最后收束全书。

本节目标:把「能跑的服务」变成「敢上线的服务」——掌握上线检查清单、灰度与分批发布策略、上线后必看的指标、基于 SLO 的告警阈值,以及代码 / 配置 / 特性开关 / 数据四类回滚的取舍与复盘方法。
适用版本:Spring Boot 4.1.x(Java 21)

18.3 上线、观测与回滚

18.2 结束时服务能在本地跑通、集成测试全绿。但「本地能跑」和「敢上线」是两件事。上线真正难的不是发布动作本身,而是发布之后你能不能立刻知道它好不好,出问题时能不能退回去。

本节按「发之前 → 发的时候 → 发之后 → 出问题」四段来讲,最后收束全书。

18.3.1 上线检查清单

发布前逐项核对,任何一项没确认就不发。这份清单是把 15.2 Kubernetes 部署 与 15.3 健康探针与优雅停机 的结论固化成动作:

类别检查项依据
配置环境 profile 与外部配置源正确3.1 多环境配置
密钥数据库口令、JWT 签名密钥来自密钥管理,不在镜像里3.2 密钥管理
迁移Flyway 脚本已在预发执行且可回滚评估12.1 事务边界
探针liveness / readiness 已配置且语义正确15.3 探针
资源CPU / 内存 limit 与 request 合理17.3 JVM 调优
日志生产日志级别为 INFO,敏感字段已脱敏16.3 日志聚合
镜像分层镜像、基础镜像已扫描15.1 镜像分层

探针语义必须分清:readiness 失败会摘流量,liveness 失败会重启容器。数据库连不上时应该只让 readiness 失败(摘流量、等恢复),而不是让 liveness 失败(无限重启)。4.0 起 liveness/readiness 探针默认启用,如需关闭用 management.endpoint.health.probes.enabled。

优雅停机同样要在清单里确认。应用要能在收到终止信号后先停止接收新请求、再等待在途请求完成:

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

少了这段,滚动发布时正在处理的借还请求会被直接切断,用户看到 502。

探针要让依赖健康与自身存活分开。业务接口的 readiness 应包含数据库与关键依赖;而 liveness 只反映进程是否卡死,不该包含外部依赖。4.0 起这两组探针默认启用,需要显式控制时用已确认的属性:

management:
  endpoint:
    health:
      probes:
        enabled: true

判据很简单:重启这个实例能不能修好? 能(进程卡死、死锁)就归 liveness;不能(数据库宕机、MQ 不可达)就归 readiness。把外部故障归到 liveness,重启风暴只会让故障扩散。

18.3.2 灰度与分批发布

三种策略没有绝对优劣,看你能承受多长的发布窗口、有没有可靠的回退路径:

策略做法适用条件代价
滚动逐批替换实例无状态、变更小新旧版本短暂共存
蓝绿起全量新版本,切流量需要秒级回退双倍资源
金丝雀先放少量流量到新版本变更风险高、有指标可判需要流量切分与观测

借还服务属于有状态(数据库共享)的核心链路,推荐金丝雀:先放一小部分流量到新版本,观察一段时间(借出成功率、错误率、P99)无异常再逐步放大。

金丝雀能成立的前提是新旧版本能共存——即数据库与接口都是向后兼容的。如果新版本改了表结构让旧版本读不了,金丝雀就退化成「一起崩」。这就是下一节要讲的向前兼容。

18.3.3 上线后看什么

发布完成不等于结束,接下来要盯指标。指标清单见 16.1 Actuator 与指标 。分四类:

类别指标为什么看
RED请求速率、错误率、P99 延迟最直接的健康信号
资源HikariCP 活跃连接、JVM 堆、GC 暂停定位瓶颈
依赖数据库、MQ 的调用延迟与错误外部故障的第一现场
业务借出成功率、还书成功率、逾期任务完成数技术指标正常但业务出错

业务指标最容易被忽略,却最关键。 一个接口可能 200 正常、P99 正常,但「借出成功率」掉了——因为业务规则判断把大量正常请求拒了。技术指标和业务指标必须一起看。

看 GC 与连接池时注意区分现象与原因。下面是一段示例输出(非本机实测),说明该看哪些字段:

# HikariCP 活跃连接(示例输出)
hikaricp_connections_active{pool="HikariPool-1"} 47
hikaricp_connections_pending{pool="HikariPool-1"} 3

# JVM GC 暂停(示例输出)
jvm_gc_pause_seconds_max{action="end of major GC"} 0.21

活跃连接长期贴住上限、pending 大于 0,说明连接不够用;此时先看慢查询,而不是盲目调大池——调大只会把压力转嫁给数据库,见 11.1 HikariCP 调优 。

18.3.4 告警阈值基于 SLO,不是拍数字

「错误率超过 1% 就告警」这种拍出来的阈值,要么天天误报(团队麻木),要么出事不报。正确做法是先定 SLO,再由 SLO 推导阈值。

概念含义本例
SLI可测量的服务水平指标借还接口成功请求占比
SLOSLI 的目标值月度 99.9%
Error Budget允许的失败额度月度 0.1% 的时间/请求
告警预算消耗过快时触发快速燃烧率触发

告警不直接盯着「错误率 > X%」,而是盯错误预算的燃烧速率:短时间内烧得太快才告警。这样慢性的轻微劣化不会半夜吵醒人,而急性故障会立刻触发。

燃烧率的算法:把 SLO 的失败额度平摊到窗口上,算当前消耗是它的多少倍。例如月度 SLO 99.9%,额度是 0.1%;若某 1 小时窗口内失败率 2%,则消耗速率约为 2% / 0.1% = 20 倍。常见的双窗口配置:

窗口燃烧率阈值含义处理
1 小时14 倍约 2 天烧完一个月额度立即告警
6 小时6 倍约 5 天烧完工单跟进
3 天1 倍正常消耗不告警

阈值不是拍出来的,是从「多久烧完额度」反推出来的。这样调参有依据,团队也信任告警。

业务 SLO 同样要定。例如「借出成功率」,它不等于 HTTP 成功率——被业务规则正确拒绝的请求是成功的(用户看到明确提示),但因 bug 误拒的是失败的。这两者要靠错误码区分,见 18.2 的错误码设计。

18.3.5 出问题时的回滚决策

回滚有四类,代价与速度差别很大,先选代价最小的:

类型手段速度适用
配置回滚改回配置项、重启或热加载快阈值、开关、限流参数调错
特性开关关掉新功能开关快新功能有问题、老功能正常
代码回滚回退到上一个镜像中代码缺陷
数据回滚恢复数据或反向迁移慢、危险数据被写坏

特性开关应该是新功能默认带的。把高风险新逻辑(比如新的逾期罚金算法)包在开关后面,出问题直接关,比回滚代码快得多,也不用等镜像重新部署。用配置属性承载开关,配合 3.1 多环境配置 的配置源:

@Component
class FineCalculator {

    private final boolean newAlgorithm;
    private final FineRule newRule;
    private final FineRule legacyRule;

    FineCalculator(@Value("${app.fine.new-algorithm:false}") boolean newAlgorithm,
                   FineRule newRule, FineRule legacyRule) {
        this.newAlgorithm = newAlgorithm;
        this.newRule = newRule;
        this.legacyRule = legacyRule;
    }

    BigDecimal calculate(Loan loan, LocalDate today) {
        return (newAlgorithm ? newRule : legacyRule).fine(loan, today);
    }
}

开关默认 false,新算法先在小流量租户上打开验证,稳定后再全局放开。开关要成对清理——功能稳定后把旧分支删掉,否则开关会越积越多,代码里全是 if。

代码回滚要能「一键回退到上一个版本」,前提是镜像打了不可变标签(用 commit 或版本号,不用 latest)。用 latest 的团队,出事时根本不知道上一个版本是哪个。

数据回滚最难,因为数据库迁移往往不可逆。 DROP COLUMN 之后就回不来了。所以迁移必须写成向前兼容的,让新旧版本能同时跑:

-- 第 1 步:加列,允许为空(旧版本不写这列,也不受影响)
ALTER TABLE loan ADD COLUMN fine_amount NUMERIC(10,2);

-- 第 2 步:回填历史数据(分批,避免长事务)
UPDATE loan SET fine_amount = 0 WHERE fine_amount IS NULL;

-- 第 3 步:新版本开始写这列;观察稳定后,才加 NOT NULL 约束
ALTER TABLE loan ALTER COLUMN fine_amount SET NOT NULL;

-- 第 4 步:确认旧版本彻底下线后,才考虑删旧列(通常放到下一个版本)

删列、改名、改类型这类破坏性操作永远分两次发布:先兼容,再清理。这样任何一步出问题,旧版本都还能正常读写,回滚不会遇到「表结构已经变了」的死局。

18.3.6 一次事故的复盘模板

事故复盘的目标是改流程,不是追责。用固定模板,保证每次都不漏项:

栏目内容
标题与等级例如「借还接口 P99 超 2s,P2」
时间线发现、定位、缓解、恢复各时间点
影响面受影响租户、请求量、持续时间、资损
根因直接原因 + 深层原因(为什么没被更早发现)
改进项每条有负责人与截止日期
复盘结论是流程问题、技术问题,还是两者

时间线必须用监控数据说话,不靠回忆。这也是 18.1 里把埋点列为「第一天决策」的原因——没有历史数据,复盘只能靠猜。改进项要能验证,例如「增加借出成功率告警」比「加强监控意识」有用得多。

18.3.7 发布演练与预发验证

上线前最后一次「预演」,能在预发环境暴露的问题,绝不带到生产。演练清单:

  • 迁移预演:在预发用生产规模的近似数据量跑一次 Flyway 脚本,确认加索引 / 加列的耗时与锁表风险,见 17.2 压测 的思路。
  • 回滚预演:真的回退到上一个镜像,确认服务能正常启动、能读新表结构(向前兼容的验证)。
  • 探针预演:手动断开数据库,确认 readiness 变红、实例被摘流量,而 liveness 保持绿、不触发重启。
  • 容量预演:用压测工具打到预期峰值的若干倍,观察 HikariCP 与 GC 是否撑得住,必要时用 17.1 Profiling 定位热点。

预发环境最大的价值是能安全地演练回滚——生产上没人愿意为了测试回滚能力而真的回退一次。把回滚演练固化到每次发布的流程里,出事时才不会手忙脚乱。

18.3.8 常见坑

  • 检查清单靠脑子记。 每次上线都漏不同的项,把清单写成脚本或 PR 模板强制过。
  • 探针语义写反。 数据库故障让 liveness 失败,容器无限重启,雪上加霜。
  • 用 latest 标签。 出事时找不到上一个可回滚的版本。
  • 迁移写成破坏性操作。 加非空列、删列、改类型一次性做,回滚无路。
  • 告警阈值拍数字。 误报让团队麻木,真故障反而被淹没。
  • 只看技术指标。 HTTP 全 200,但业务成功率掉了,一样是事故。
  • 从不演练回滚。 回滚路径没测过,出事时才发现回不去,比不回滚更糟。
  • 上线后立刻走人。 发布完就散场,没留观察窗口,故障往往在无人看护时爆发。

小结

到这里,全书走完了一个完整闭环:从模块拆分、配置与测试,到数据访问、安全、异步、可观测,最后在 18.3 收束成一次可交付、可观测、可回滚的生产上线。

  • 上线前用检查清单核对配置、密钥、迁移、探针、资源与日志,不靠记忆。
  • 探针语义要分清:readiness 摘流量、liveness 重启;优雅停机保证在途请求不被切断。
  • 灰度策略看变更风险与回退能力:滚动最简、蓝绿回退最快、金丝雀最稳。
  • 上线后四类指标一起看:RED、资源、依赖、业务,业务指标最容易被漏。
  • 告警阈值由 SLO 推导,盯错误预算燃烧率,而不是拍一个百分比。
  • 回滚优先选代价小的:配置与开关最快,代码次之,数据最难;迁移永远向前兼容、分两次发布。

真正的生产级不是「用了多少组件」,而是每一次变更都有据可查、每一种故障都有退路。检查清单让变更可控,指标与 SLO 让故障可见,回滚与向前兼容让退路存在。把这三节当成一套模板,换一个业务领域,流程依然成立。

发布窗口也值得挑:避开业务高峰与节假日前夕,给观察与回滚留出足够的人手和时间。一次在深夜无人值守时上线的变更,即使问题本身很小,也可能因为没人及时处置而放大成事故。

至此,《Spring Boot 实战》从需求到上线走完了完整的一遍。愿你交付的每一个服务,都既有速度,也有退路。

阅读导航:上一节:18.2 实现与联调 · 下一节:目录 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计