qinghuan
← 工程实践 / 全部笔记

单元 101 / 工程实践

P1 · 重点提升L2 · 进阶

CI、发布与监控:怎样确认这次上线可用

从固定构建结果开始,按就绪、真实请求和回退条件检查发布,并分清应用回滚与数据库恢复。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能设计测试到发布的流水线,并为失败发布写出有证据的停止与回退条件。

学习理由、准备要求与知识点

为什么学它

构建成功只证明制品能生成,安全发布还需要就绪检查、流量验证与可操作的回退路径。

先读这些内容

知识点清单

  • 流水线 job 与 step
  • 构建一次
  • 不可变制品
  • 迁移兼容
  • 就绪与存活
  • 优雅停机
  • 指标与告警
  • 应用回滚边界

运行前先确认Spring Boot源码固定3.5.0,运维参考为3.5系列;完整算法Java17标准库。事件片段需应用上下文与平台接线,算法不会执行部署或连接监控系统。

01先弄懂它是什么

交付软件要连续确认三件事:这份代码构建和测试是否通过,验证过的文件是否部署到环境中,真实请求是否正常。CI 提供构建测试记录,发布把制品送入运行环境,监控观察上线后的行为。生成 JAR、容器启动、健康检查成功和业务成功各证明不同的事情;用制品摘要把这些记录对应到同一份内容。

放大流量前先约定:什么情况继续,什么情况暂停,什么情况回退。一起看错误率、耗时、关键业务结果和资源压力,不能只看进程还在。请求太少时,零错误可能只是样本不足,需要继续观察;关键数据错误或安全问题还可以设独立的立即停止条件。

回退应用不等于回退数据。数据库迁移采用先兼容增加、双版本共存、迁移数据、最后移除旧结构的分阶段方式;破坏性变更后旧镜像可能无法读取新数据。恢复方案应包括制品、配置、流量、任务和数据边界,并明确哪些动作可逆。

文中这几个词,先认一认

制品推广
把同一份验证过的构建结果依次用于后续环境,避免每个环境重新构建出不同内容。
就绪与存活
就绪表示现在可以接请求;存活表示程序仍能继续运行,两项检查回答不同问题。
观察窗口
用明确的一段时间和请求样本来判断发布效果,避免只看某个瞬间。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

本题对时间非递减的测量批次维护滚动窗口,每批有总请求数、错误数和是否完整。窗口包含时间在当前时刻减宽度之后、直到当前时刻的批次;左边界不包含。每次加入新批次并移除过期批次,得到窗口总数与错误数。

决策先检查是否存在未知批次或样本不足,有则HOLD;否则错误数除总数超过预设阈值时ROLLBACK,否则PASS。PASS只表示此项错误率门槛通过,不授权实际部署,也不意味着延迟、数据正确性等其他门槛同时通过。

比较错误率时,可以先交叉相乘,避免在临界值附近受浮点近似影响。例如错误数除以请求数,是否超过按百万分之一记录的阈值,可以改成比较两边的整数乘积。乘积可能超出 long,所以这里用 BigInteger;窗口内累加计数时,也会检查溢出并报错。本题把一批请求都记在同一个时间点。如果真实批次横跨窗口边界,就需要更细的时间记录,或明确说明这是怎样的近似。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart TD
  C["确定提交与测试"] --> A["不可变制品摘要"]
  A --> D["小流量部署与兼容检查"]
  D --> M["就绪 关键请求 指标"]
  M --> H["证据不足暂停"]
  M --> P["门槛通过继续"]
  M --> R["失败停止与既定恢复"]

按顺序看,每一步做了什么

  1. 固定提交、依赖、测试环境与制品摘要。
  2. 检查兼容迁移、配置和恢复预案。
  3. 部署小流量并验证就绪与关键业务请求。
  4. 按预先定义窗口判断质量、延迟和错误率。
  5. 达到条件再扩大流量,失败则按既定范围停止或回退。

03对照实现代码

Spring Boot 3.5.0 AvailabilityChangeEvent 源码 (新标签页)↗

Spring Boot 3.5.0固定源码,3.5系列运维文档;算法Java17 · AvailabilityChangeEvent.publish / getState

原创Spring Boot应用上下文片段,说明就绪状态事件。不是原样源码,也不等于执行负载均衡摘流或完整优雅停机;需要实际应用与基础设施接线。

核心机制代码 · java
AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC);
// 发布应用就绪状态,外部流量入口仍需观察并停止路由。
waitForIngressToObserveState(); // 应用自己的协调逻辑,要有明确截止时间。
drainInFlightOperations(); // 应用自己的收尾逻辑,不能假定事件会取消所有后台工作。
// 正常框架关闭流程和容器宽限期需要另外配置并验证。

AvailabilityChangeEvent.publish把可用性状态包装为应用事件交给发布器,getState返回事件载荷。这个类传递状态,不负责直接控制网关或操作系统进程。

就绪表示当前是否适合接收流量,存活表示应用是否仍能继续运行。外部数据库短暂故障不应不加分析地映射为存活失败,否则可能造成大量实例同步重启。

发布 REFUSING_TRAFFIC 后,平台探针、负载均衡和已有连接都可能还没立刻更新。应用要留出信息传递和已有请求完成的时间,不能发完事件下一行就强制退出。

片段中的等待与收尾函数是应用设计占位,不是Spring Boot API。完整算法只给发布监控窗口做门槛判断,不启动应用或执行部署。

代码先调用谁,再调用谁

  1. 发布就绪状态变化
  2. 健康组反映状态
  3. 流量入口观察并摘流
  4. 在途操作按期限收尾
  5. 框架完成正常关闭

04动手写一个例子

含缺失样本的发布滚动窗口门槛

输入有序测量批次、窗口宽度、最小请求数与百万分比错误阈值,输出每批加入后的HOLD、PASS或ROLLBACK。纯决策模型,不执行部署。Java17自测。

输入 1
完整100请求1错误,阈值1%且样本足够
输出 1
PASS
输入 2
窗口含未知批次
输出 2
HOLD

双端队列按批次时间保存窗口内容,滚动总数在入队加、过期出队减。每批最多进入和退出一次,因此不必每次重新扫描窗口。

未知批次数独立维护,缺少采集不能伪装成零错误。只有完整且达到最低样本数时才比较错误比例,这一顺序使证据不足明确呈现。

用errors乘一百万与total乘阈值比较,阈值相等时允许通过。BigInteger只用于安全乘法,long聚合仍有显式上限和溢出异常。

为什么这样写是对的队列只包含时间落在当前半开左边界窗口的批次,total/errors等于其中计数之和,unknown等于不完整批次数。

时间 O(n)次队列与固定宽度计数处理额外空间 O(w+n)含结果,O(w)窗口辅助空间

w为窗口内最多批次数,每批进出一次。BigInteger乘积最多约八十四位,因为输入为long与百万分比,因此本题位宽有固定界。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
import java.util.*;
import java.math.BigInteger;
public class Main {
    record Batch(long time,long total,long errors,boolean complete){}
    static List<String> gates(List<Batch> batches,long width,long minimum,int ppm){
        if(width<=0||minimum<=0||ppm<0||ppm>1000000)throw new IllegalArgumentException("policy");
        Deque<Batch> q=new ArrayDeque<>();List<String> out=new ArrayList<>();long total=0,errors=0,last=-1;int unknown=0;
        for(Batch b:batches){
            if(b.time()<0||b.time()<last||b.total()<0||b.errors()<0||b.errors()>b.total())throw new IllegalArgumentException("batch");last=b.time();
            while(!q.isEmpty()&&q.peekFirst().time()<=b.time()-width){Batch old=q.removeFirst();total-=old.total();errors-=old.errors();if(!old.complete())unknown--;} // 左边界批次已过期。
            q.addLast(b);total=Math.addExact(total,b.total());errors=Math.addExact(errors,b.errors());if(!b.complete())unknown++; // 缺失独立计数,不填零。
            if(unknown>0||total<minimum){out.add("HOLD");continue;}
            BigInteger left=BigInteger.valueOf(errors).multiply(BigInteger.valueOf(1000000));
            BigInteger right=BigInteger.valueOf(total).multiply(BigInteger.valueOf(ppm));
            out.add(left.compareTo(right)>0?"ROLLBACK":"PASS"); // 等于阈值按本题约定通过。
        }return out;
    }
    static void eq(List<String> e,List<String> a){if(!e.equals(a))throw new AssertionError(a);}
    public static void main(String[] args){
        eq(List.of("PASS"),gates(List.of(new Batch(1,100,1,true)),10,100,10000));
        eq(List.of("ROLLBACK"),gates(List.of(new Batch(1,100,2,true)),10,100,10000));
        eq(List.of("HOLD"),gates(List.of(new Batch(1,10,0,true)),10,100,10000));
        eq(List.of("HOLD","PASS"),gates(List.of(new Batch(0,0,0,false),new Batch(10,100,0,true)),10,100,0));
        eq(List.of(),gates(List.of(),1,1,0));
        eq(List.of("PASS"),gates(List.of(new Batch(1,Long.MAX_VALUE,Long.MAX_VALUE,true)),10,1,1000000));
        try{gates(List.of(new Batch(2,1,0,true),new Batch(1,1,0,true)),10,1,0);throw new AssertionError();}catch(IllegalArgumentException expected){}
        System.out.println("PASS 7 tests");
    }
}

这些特殊情况也要试一试

  • PASS仅覆盖本题错误率,不等于完整发布成功。
  • 批次时间代表计数归属,不能混用不同时间桶政策。
  • 数据未知或样本不足时保持观察。
进阶选读 · 后续再读:多窗口燃烧率和渐进发布

场景短窗口敏感但容易抖动,长窗口稳定却反应慢。

做法组合短长窗口与业务严重性,配置保持时间和恢复阈值;对候选和基线分流做可比较观测,避免流量分布变化混淆。

复杂度多个固定窗口可各自线性增量维护;统计置信与因果判断仍依赖实验设计,不能仅靠更多阈值。

好处与代价复杂门槛减少误报也可能延迟停止,阈值应由业务风险和恢复能力决定。

05项目里怎么用

流水线使用明确依赖权限,构建任务不应默认拥有生产凭据。机密从受控机制注入,日志和制品避免包含令牌,第三方工作流组件固定可审查版本。记录每个阶段产物与摘要,使回退可以定位真正部署的内容。

Actuator 提供健康状态等运维接口,要分别设置“哪些接口对外提供”和“谁可以访问”。就绪检查回答当前能不能接新请求;存活检查帮助判断应用是否需要重启。下游短暂不可用时,若把它直接当作应用死亡,可能让大量实例同时反复重启。正常停机应先停止分配新请求,再给正在处理的请求和后台任务留出结束时间;应用的等待时间还要与部署平台强制结束进程的期限配合。

改数据库时先保证新旧应用都能使用,再分步迁移数据和删除旧结构。发布失败后先找出问题在镜像、配置、依赖还是数据;回退应用只能恢复它能控制的部分。恢复演练还要验证关键请求、数据正确性和用时,不能只确认旧容器又启动了。

容易踩的坑

  • 构建成功就宣布线上请求正常。
  • 低流量零错误被当作充分通过证据。
  • 把外部依赖故障无条件设为存活失败。
  • 回滚镜像后假设已提交数据自动还原。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
小流量逐步推广限制初始影响范围需要路由与对比指标有回退能力的服务
完整观测后继续决策证据更清楚低流量可能等待较久高风险关键发布
兼容式数据库演进允许新旧应用共存过程更长且需临时兼容代码滚动升级与可回退系统

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01制品摘要有什么作用?

展开参考答案与推导

将测试、部署和回退关联到确定内容,避免同名标签或重复构建导致证据错位。

02就绪事件会自动摘流吗?

展开参考答案与推导

事件更新应用状态,外部探针和流量入口需要正确接线并有传播时间。

03为什么未知窗口HOLD?

展开参考答案与推导

缺少观测不能推导零错误,应修复采集或等待证据,而不是自动通过。

换个情况,还会处理吗

01健康接口正常但下单失败,说明什么?

展开参考答案与推导

探针只覆盖其定义的条件,应检查关键业务请求、数据库和配置,健康不等于所有功能正确。

02旧镜像读不了新表结构,怎么办?

展开参考答案与推导

回退方案缺少兼容迁移,应根据数据状态选择向前修复或已设计恢复,不能盲目反复切镜像。

03数据库波动导致全部Pod重启,查什么?

展开参考答案与推导

检查存活探针是否错误依赖数据库,将是否接流量与是否需要重启区分。

写代码试一试

01错误率正好等于门槛怎样判?

输入 / 条件
100请求1错,门槛1%
输出 / 预期
PASS

提示 本题用严格大于。

展开参考答案与推导

交叉相乘相等,按事先声明政策通过;真实规则也应先定义边界。

02左边界批次是否保留?

输入 / 条件
当前10,宽度10,批次0
输出 / 预期
移除

提示 窗口为(0,10]。

展开参考答案与推导

条件time小于等于当前减宽度时出队,避免前后窗口口径不一致。

03极大计数如何防乘法溢出?

输入 / 条件
Long.MAX_VALUE乘百万
输出 / 预期
BigInteger比较

提示 计数和仍需检查。

展开参考答案与推导

交叉乘积用大整数,窗口聚合用Math.addExact,不能在乘法溢出后再转大整数。

商户项目 · 本篇加练

商户接口的健康检查与降级

核查日期:2026-10-11。状态:健康检查场景未运行,以下为待实现的教学检查表。

目标与前置知识

区分“进程需不需要重启”“现在适不适合接流量”和“部分业务要怎样降级”。先会 HTTP 状态码、商户查询与缓存回源;本模块补到现有“CI、发布与监控”文章。

信号解决的问题缓存故障时的设计重点
liveness(存活)应用是否陷入需要重启才能恢复的内部故障不能仅因外部 Redis 不可用就失败,否则所有实例可能一起重启
readiness(就绪)当前实例是否应继续接流量根据必要业务能力与故障范围决定,不能机械地把所有共享依赖并进去
业务降级本次商户查询还能返回什么有预算地回源、允许时标记旧数据,或明确返回暂不可用;不伪造最新数据

步骤

  1. 先列出正常查询必须依赖什么,再区分可选缓存与必需数据库。给只读商户接口写清数据库回源并发和等待上限。
  2. 设计四个独立场景:一切正常、Redis 不可用但数据库可用、数据库不可用、业务端口不能接请求但管理端口仍可访问。每个场景先写预期,再运行。
  3. 同时观察存活端点、就绪端点、真实商户请求、重启次数和回源数量。管理端口健康不证明业务端口可用。
  4. 若共享缓存故障导致所有实例就绪失败,记录是否因此没有实例接流量;这是否符合业务决定,要结合降级能力判断,不能靠健康标签自动决定。
  5. 恢复依赖后核对流量与正常路径是否恢复,保留过渡期间的错误,不能只留下最后一次成功。

验收与交付物

交付一张“场景→三个信号→真实响应→重启/流量变化”的表。Redis 单独故障不应触发仅由它导致的 liveness 连锁重启;具体 readiness 和降级期望必须先写在测试契约中。真实结果、耗时和重启次数目前留空。

原始来源与版本

Spring Boot Actuator Endpoints:核查时为 Spring Boot 4.1.1。重点读 Kubernetes Probes、Checking External State With Kubernetes Probes。这里借用健康语义,不要求升级已有 Boot 版本;配置名与端点可用性仍需对应实际依赖版本复核。管理端点的访问授权和对外暴露也要分别配置。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能设计测试到发布的流水线,并为失败发布写出有证据的停止与回退条件。

本篇重点加练

迁移兼容与应用回退共同决定恢复是否可执行,不能把切回旧镜像视为全部系统回滚。

  • 迁移兼容
  • 应用回滚边界

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    流水线包含测试、打包与部署三个 job,环境分别在每个 job 中重新构建。分析“已测试制品”和“实际部署制品”如何被识别。

    提交内容
    流水线依赖图
    怎样算完成
    • 区分 job 与 step 作用域
    • 每个阶段记录输入及输出制品

    练到的知识点流水线 job 与 step · 构建一次

  2. 作业 02设计评审

    应用拟新增必填数据库字段,旧版本还在处理请求。设计不可变制品标识与数据库迁移兼容检查,不默认应用回滚能撤销数据变化。

    提交内容
    版本与迁移矩阵
    怎样算完成
    • 覆盖新旧代码共存
    • 标明不可逆数据操作

    练到的知识点不可变制品 · 迁移兼容

  3. 作业 03代码推演

    实例启动后端口已监听但缓存预热未完成,停止时仍有三个在途请求。画出就绪、存活、接流与停机的状态变化。

    提交内容
    生命周期状态图
    怎样算完成
    • 区分探针用途
    • 在途请求有完成或拒绝规则

    练到的知识点就绪与存活 · 优雅停机

  4. 作业 04代码推演

    新版本新增可空字段后开始写新枚举值,旧版本仍能连接数据库。分别检查存储层兼容与旧业务逻辑读取兼容。

    提交内容
    新旧读写矩阵
    怎样算完成
    • 区分表结构合法和业务可解释
    • 包含旧版未知枚举

    练到的知识点迁移兼容

综合应用

  1. 作业 05面试场景

    发布后错误率略升且订单异步完成时间明显变长,应用健康检查正常。制定暂停、继续与回退需要的证据,并说明回退的资源边界。

    提交内容
    发布决策与告警矩阵
    怎样算完成
    • 包含低样本或数据缺失
    • 检查数据库及消息格式兼容

    练到的知识点指标与告警 · 应用回滚边界

  2. 作业 06编程实践

    用本地脚本模拟构建、测试、制品校验和部署到临时目录,加入测试失败、摘要不符和就绪失败三种情况,保留阶段账本。

    提交内容
    脚本、制品与执行记录
    怎样算完成
    • 不连接真实发布环境
    • 模拟器未验证集群调度能力须注明

    练到的知识点流水线 job 与 step · 构建一次 · 不可变制品 · 迁移兼容 · 就绪与存活 · 优雅停机 · 指标与告警 · 应用回滚边界

  3. 作业 07设计评审

    发布影响数据库、缓存内容和消息格式,应用镜像可以快速回退。为三类持久状态写出恢复责任与可逆性判断。

    提交内容
    回退边界及状态清单
    怎样算完成
    • 不默认自动清空数据
    • 明确未恢复状态的业务影响

    练到的知识点应用回滚边界

  4. 作业 08编程实践

    在本地小程序中保存旧版样本与新版写入样本,分别用两版读取器验证迁移前后可读性,生成兼容性报告。

    提交内容
    代码、样本与版本报告
    怎样算完成
    • 不连接真实部署系统
    • 报告语义差异而非仅解析成功

    练到的知识点迁移兼容 · 应用回滚边界

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    团队要发布同时涉及接口、表结构和消费者的功能。制定可分阶段验证的发布顺序及每阶段回退条件。

    提交内容
    发布计划与兼容验收表
    怎样算完成
    • 涵盖在途消息与旧实例
    • 每项决策有可观察证据

    练到的知识点构建一次 · 不可变制品 · 迁移兼容 · 就绪与存活 · 优雅停机 · 指标与告警 · 应用回滚边界

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    产品要求发布失败后五分钟恢复服务,但部分新业务动作不可撤销。重新定义“恢复”是恢复代码、可用性还是业务一致性,并提出验收分层。

    提交内容
    恢复目标与限制评审
    怎样算完成
    • 每层有独立证据
    • 不可撤销动作保留补救边界

    练到的知识点迁移兼容 · 应用回滚边界

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗