同一 07:30 UTC 瞬间分到柏林 09:30(窗口内)与纽约 03:30(窗口外);匹配日历不证明有人值守或已经获准。

变更窗口为人设计,不是为服务器

一次发版既是技术时刻,也是人与协作的时刻。选在什么时候,在于谁会在场看结果。 流量只是算式的一半 选在流量最低的时段发布,用户受影响的面确实最小,但它与人撞了个满怀:凌晨运维最可能不在场、或正在处理别的告警。如果发布在半夜半途失败而无人留意,静默故障可以持续数小时才被人类读取。低流量是好的起点,但它回答的是"流量"而不是"风险"。要在减少用户波及与"是否有人来得及反应"之间权衡,优先选择你确实能观察结果的那个窗口,而不是把少一千个用户看得比回滚能力更重。 尊重人的时钟 窗口要服从的不是服务器,而是人。想一想出了问题谁要行动:值班的 SRE、业务负责人、开发团队,把他们的注意力安排进时段。再想想最近还有哪些变更正在凑热闹——新发布的版本、定时任务、自动迁移——别让它们在同一窗口和你绞成一团,把一次发布变成连环事故。与其选一个"并发更小但没人在场"的时段,不如选一个相关的人清醒、可联系、且尚未超载的时段,这才是真正划算的发布时机。 让窗口成为习惯 固定且有人值守的窗口,可以帮助协调高风险变更,使观察和响应更加可预期。但这并不意味着所有发布都必须攒成一批:小型、自动化、可独立回退的变化,也可以使用持续交付流程。节奏应根据实际风险和响应覆盖决定。把构建、冒烟和金丝雀检查自动化,同时保留明确负责人以及经过演练的恢复路径;日历中的时段应当支持这些保障,而不能取代它们。 坏掉的样子 第一种坏法,是窗口之外的紧急变更没有通道。线上正在着火,值班却被告知等下个窗口,于是有人绕过流程直接推生产,无人盯盘,也无回滚说明。窗口本意是门禁,可如果没有破窗程序——谁来批准、谁来盯着、事后补什么记录——每一次例外都会变成暗箱发布。应在正常安排之外写清紧急通道,并演练到和普通发布一样熟;不能把等待下个日历窗口当作所有事故的处理方式。 第二种坏法,是窗口之内的批量堆积踩踏。四五个服务挤进同一两个小时,仪表盘信号混在一起,错误率一抬头,谁也说不清到底是谁引起的。回滚从一键按钮变成多方谈判,因为回滚其中一个,就要猜另外几个的状态。共享故障信号或依赖的变更应当错开,每个条目进入窗口前,都要写清负责人、健康信号与回退路径。排队是为了让归因清晰,值班才敢果断回滚。 第三种坏法,是跨时区窗口错位。作者写周三上午十点,心里是柏林时间,洛杉矶的同事按本地时间理解,结果在半数响应人熟睡时发布。夏令时切换还会悄悄再挪一小时。窗口必须连时区一起存、按读者本地时间展示,排班时逐一确认真有人在场,才能把这个时段算作有人值守。凡是只写钟点不写时区的窗口,迟早会在某个深夜兑现为误操作。 同一瞬间,不同人的时钟。 依据原文与明确限制静态绘制;未为本图重跑原文夹具。 快速验证 使用 Python 3.9 或更新版本,并确保 zoneinfo 能找到 IANA 时区数据;该模块本身不附带这些数据。把下面程序保存为 change_window_check.py,运行 python3 change_window_check.py。运行时不要使用 -O、-OO 或非零的 PYTHONOPTIMIZE,以免断言被跳过。它只用标准库,把每个窗口都当作时区感知的时间段,结论只有两种:allow 表示满足窗口条件,needs-approval 表示需要审批。演示表包含柏林与纽约的周三上午窗口,起止时间位于同一天。冻结日按明确选择的 UTC 政策日期解释,不取决于调用方用哪个时区表达输入瞬间。坏样本是故意留下的反例,仅作拦截演示。 ...

七月 29, 2027 · 2 分钟 · XB Field Notes
十个期望副本加两个 surge,更新目标为十二;十减一个 unavailable,规划可用预算为九。单独画出的终止尾部可能使总量超过十二。这些预算不能保证不中断。

滚动更新分批换上新版本

滚动更新是一种受控替换,不是服务绝不中断的承诺。它的价值在于引入新副本时可以保留部分旧副本;用户能否平稳完成请求,还取决于资源余量、健康检查是否可信,以及应用如何退出。本文讨论已经选择滚动更新之后怎样调节节奏、怎样观察过程;姊妹篇讨论更靠前的问题:两个版本到底能不能同时运行。 十加二得到十二的目标,十减一得到九的规划预算。正在终止的 Pod、持续就绪与真实请求结果,需要分别核对。 浪涌与缺口 十个期望副本,配 maxSurge: 2,更新目标上限是十二,不是十一。maxUnavailable: 1 给控制器的可用性预算是九个。这不是「每一波最多删除一个旧 Pod」:只要已经有足够的新副本可用,控制器就能继续撤掉多个旧副本。正在终止的 Pod 还可能保留一段时间并继续消耗资源,因此十二也不是所有 Pod 对象或进程数量的绝对上限。 百分比必须先换算整数:surge 向上取整,unavailable 向下取整。三个副本使用默认的百分之二十五,实际就是一个 surge、零个 unavailable。Kubernetes 1.34.0 有一个边界例外:surge 为零、非零的 unavailable 百分比却向下取成零时,控制器会把有效 unavailable 调整为一,让替换能够推进。因此,不能从这个换算得到的零,推断更新一定不会主动减少可用副本。 两个参数都不会替你预留节点资源。选择速度之前,要把资源请求量与可调度容量、命名空间配额、放置约束以及旧 Pod 的退出耗时一起检查。账面上多出的两个位置,并不一定真有节点能承接;新 Pod 全部 Pending 时,调大 surge 也不会凭空生出机器。 ...

六月 7, 2027 · 2 分钟 · XB Field Notes
产物 A 以相同字节从预发布进入生产。各环境分别提供配置;字节一致不能证明行为一致。

让已经验证的产物逐级进入环境

预发布测试说明的是当时实际运行的产物与环境。生产部署前重新构建,会得到另一份输出,需要检查它的身份,或者重新验证它的行为。产物晋级指把已验证的那份文件继续交给下一个环境,并记录这次决定。它减少了一类不确定性,但不会让预发布与生产环境自动等价,也不会免除部署之后的运行检查。交付字节一致,是发布判断的一部分,而不是全部。 明确究竟把什么向前传递 使用不可变的软件包标识或内容摘要定位构建结果。会移动的标签、分支名乃至源码提交,本身都不足以唯一证明部署了哪些字节。同一源码的两次构建,可能因为工具链、依赖解析、嵌入设置或打包步骤而不同。可复现的重建确实可能得到完全相同的字节,但应先比较结果,再把它视为之前通过测试的同一产物,而不是仅凭源码相同推断。 应区分构建阶段与发布阶段。发布组合包含特定产物及该部署需要的配置,运行过程再把这个组合放到真实依赖和数据之上。记录产物身份与合适的配置版本,同时避免泄露秘密。如果前端环境设置在打包时直接编译进 JavaScript,修改设置就会改变字节,也就是生成了另一份需要验证的产物。不能一边修改包内内容,一边继续沿用原包通过测试的身份。 坏掉的样子 某些流水线名义上在晋级产物,部署脚本却悄悄重新编译;另一些流程下载了正确归档,却又覆盖归档内部的配置文件。这两种操作都改变了测试时的输入。应明确哪些设置位于不可变负载之外,并通过应用已有的配置接口读取。校验和如果发生在后续修改之前,就不能继续代表运行时文件;核验时点与文件变化顺序必须一起审查。 即便产物相同,数据库结构、功能开关、依赖版本、权限、资源限制或网络路径变化,也能让行为不同。晋级保证的是字节连续性,不是整个执行上下文都一致。部署后仍要验证关键生产行为;预发布结果无法在生产重现时,要比较真正相关的环境差异,而不是看到摘要相同就认定问题与发布无关。配置也是发布组合的一部分,需要可追溯的变化记录。 回滚同样有前提。旧程序可能无法理解新写入的数据,或者无法适应已经完成的破坏性结构迁移。把指针切回旧产物,并不会撤销这些外部影响。恢复说明应关联数据兼容性、配置和依赖状态;不能反向恢复时,就应准备必要的向前修复。不能把“重新选择旧镜像”描述成所有故障都适用的完整回滚方案。 晋级保留已经测试过的产物身份,不会冻结数据、依赖或配置;选择旧产物也不能撤销不兼容的数据变化。完整原始英中本地文件复制与配置练习已在 2026-10-17 的记录运行中通过;生产部署及其他真实发布系统行为仍未测试。 快速验证 运行记录,2026-10-17:完整原始英文和中文 Python 代码块分别执行一次,环境为 macOS 26.5.1 arm64、Python 3.14.4,使用 -I -B、关闭优化,并保留全部三处原始 assert 语句。每次均输出完全匹配的预期 JSON:产物摘要相同,local-build-41 对应两个不同的环境标签和配置摘要,且明确修改过的副本被拒绝。两次运行均以零状态退出、标准错误为空;生成文件、套接字、线程和各自拥有的临时工作目录均已清理。 每种语言的运行各记录四次成功的回环连接和四条应用层 GET/状态事件:staging-local 的 /artifact、/,随后是 production-simulation 的 /artifact、/,状态均为 200,并与先后启动的两个自有监听器代次匹配。这些记录不是抓包,也不是独立的线上状态测量。本实验仍仅覆盖合成的本地文件复制与配置,没有测试生产部署、注册仓库、秘密、迁移、回滚、签名、可复现构建流水线或真实发布系统的规则执行。 ...

二月 17, 2027 · 2 分钟 · XB Field Notes
运行时开关在已经部署的代码中选择旧路径或新路径。它不能撤销已有影响。

功能开关把发布与暴露解耦

部署代码和向用户开放功能,是不同动作。功能开关可以在代码部署后选择运行路径,支持渐进开放和快速关闭;前提是旧路径仍兼容,配置也真正到达负责求值的进程。开关控制一次决定,不会撤销部署或新行为已经造成的全部效果。讨论回滚时,应明确能停止什么、不能恢复什么,而不是把关闭按钮当作万能撤销。 开关选择后续工作,不能撤销已完成工作。分组数量属于固定本地样例,不能证明请求比例或分布式配置传递。 在明确的门控后部署 发布开关通常默认选择既有路径,让新实现先接受验证。不过部署阶段仍可能变化:导入、启动钩子、表结构迁移、依赖和资源使用,都可能位于被保护分支之外。因此关闭开关不能保证用户完全无感。应同时测试关闭和开启状态的真实产物,包括启动流程,以及两条路径与已有数据的兼容性,避免只看功能入口是否隐藏。 关闭开关可以阻止后续请求进入新路径,却不会撤销已经完成的写入、已发送消息、迁移或在途工作。回滚窗口内应让两条路径都能处理相关数据格式,并定义部分执行后可能需要的修复。紧急关闭只有在传播延迟和实际停止边界符合响应计划时才有效;控制台显示关闭,不代表所有正在运行的操作已经中止。 按含义选择默认行为 对于尚未完成的发布功能,未知配置时退回旧路径可能是安全选择。但这是具体开关的语义,不是任何情况下 false 都安全。关闭保护性检查,或者反转名为 disable-validation 的开关,可能产生相反结果。应明确备用值、配置新鲜度,以及无法求值时的处理,不能只用“失败为关”四个字替代对实际行为的审查。 在合适的操作边界求值一次,并在该操作内部复用决定。多个服务或独立请求之间,需要明确传递兼容决定的方式;每个进程本地求值一次,不会自然形成全局一致性。权限校验也应独立存在。客户端控制显示的开关,不能变成服务器接受受保护操作的授权凭证,否则隐藏按钮并没有真正限制访问。 用稳定身份逐步开放 把稳定用户标识与稳定开关键一起散列,可以形成可重复的分组。改变身份字段、盐值或分桶算法,即使百分比不变,也可能让用户重新分组。用户比例也不等于请求比例:少量重度用户可能产生大量流量。因此解释放量指标前,先记录分配单位,确认观察的是账号、请求还是其他业务实体,不能把几个百分比直接互换。 紧急关闭应覆盖普通分桶选择,并具有经过测试的配置传递路径。SDK 缓存和配置服务故障,可能让部分实例继续使用旧值。应按实际求值的变体观察错误与延迟,不能把控制面修改成功当作每次请求已经切换的证明。放量节奏应依据服务风险和观察延迟决定,不存在对所有业务都适用的固定百分比或等待时间。 坏掉的样子 临时发布开关如果没有删除负责人,会变成长期分支。应指定负责人和退出条件,在回滚窗口结束后删除不再需要的路径。运维开关和权益控制则可能有意长期存在,需要持续测试和维护,不能要求所有开关都很快过期。区分类型,才能既避免死配置堆积,又保留真正需要的长期运行控制。 互相矛盾的开关还可能形成无人测试的组合。可行时集中决定规则,定义优先级,同时验证备用与开启路径。日志只保留足以诊断不一致的非敏感上下文,避免无限保留真实用户标识。本例使用的名字全部是合成测试数据;把这份内存审计列表搬到生产环境前,还需另行设计保留、访问和脱敏规则。 快速验证 把代码保存为 flag_check.py,用 Python 3.10 或更新版本运行。原始实验报告使用 Python 3.14.4;保留的 2026-10-15 审核记录使用 Python 3.12.14 重新执行其决定逻辑。脚本只模拟一个百分比发布开关、紧急关闭、稳定用户分桶和内存审计列表。百分比假定已通过零至一百的输入验证,不包含真实配置下载、SDK 缓存或身份服务。 import hashlib class Flag: """One percentage flag with kill switch, default-closed, and audit log.""" def __init__(self, key, percent): self.key = key self.percent = percent self.killed = False self.audit = [] def _bucket(self, user): digest = hashlib.sha256(f"{self.key}:{user}".encode()).hexdigest() return int(digest, 16) % 100 def evaluate(self, user): if self.killed: decision, reason = False, "killed" elif not user: decision, reason = False, "default-closed" else: hit = self._bucket(user) < self.percent decision, reason = hit, ("in-bucket" if hit else "out-of-bucket") self.audit.append({"user": user, "decision": decision, "reason": reason}) return decision flag = Flag("new-checkout", 20) users = [f"user-{i}" for i in range(200)] # 1. Default closed: a missing user never gets the new path. assert flag.evaluate("") is False assert flag.evaluate(None) is False # 2. Stickiness: the same user gets the same answer twice. first = flag.evaluate("user-1024") second = flag.evaluate("user-1024") assert first == second # 3. Kill switch overrides every bucket. flag.killed = True assert flag.evaluate("user-1024") is False assert flag.evaluate("user-7") is False flag.killed = False # 4. The dial moves exposure for a fixed fixture set. open_20 = sum(flag.evaluate(u) for u in users) flag.percent = 100 open_100 = sum(flag.evaluate(u) for u in users) flag.percent = 0 open_0 = sum(flag.evaluate(u) for u in users) flag.percent = 20 assert open_0 == 0 assert 0 < open_20 < len(users) assert open_100 == len(users) # 5. A different bucketing key disagrees somewhere: the inconsistency to avoid. session_flag = Flag("new-checkout-session", 20) mismatch = [u for u in users if flag.evaluate(u) != session_flag.evaluate(u)] assert mismatch, "expected at least one user where two flag keys disagree" # 6. Audit log holds one entry per evaluation with its reason. assert len(flag.audit) == 2 + 2 + 2 + 200 * 3 + 200 assert len(session_flag.audit) == 200 assert all(set(e) == {"user", "decision", "reason"} for e in flag.audit) print(f"default-closed empty user: off; sticky user-1024: {'on' if first else 'off'} twice") print(f"dial 0/20/100 over 200 users: {open_0}/{open_20}/{open_100}") print(f"killed flag forces off; different flag keys disagree on {len(mismatch)} users, e.g. {mismatch[0]}") print(f"audit entries: flag={len(flag.audit)}, session_flag={len(session_flag.audit)}") print("PASS: default-closed, sticky bucketing, kill switch, dial, and audit log behave as asserted") 在百分之二十时,缺失用户保持关闭,紧急关闭覆盖全部具名用户,同一用户重复求值保持稳定。两百个固定测试用户在零、二十、一百三档,分别有零、三十五、两百人开启。三十五是这组输入的确定结果,不保证任意有限用户集合都精确达到百分之二十,也不说明相同比例的请求会使用新路径。 ...

二月 9, 2027 · 2 分钟 · XB Field Notes
新工作可进入绿色环境,已有会话仍留在蓝色。切回路由不会撤销已接受的写入。

蓝绿部署让两套环境随时待命

蓝绿部署在独立环境中准备新应用版本,同时让旧版本继续服务流量。就绪检查通过后,路由把新的工作导向新环境。保留旧环境可以缩短回滚,因为恢复不必等待重新构建。但这个模式本身并不保证全部依赖都有两份、切换瞬间完成,或每一项数据修改都可以撤销。发布方案必须明确哪些东西被切换,以及哪些状态仍然共享。 路由选择与状态兼容是不同问题。模型解释版本重叠及模式边界,并非真实路由器测量,也不保证恢复安全。 定义切换究竟控制什么 路由机制可能是负载均衡后端、服务选择器,也可能是 DNS。它们的传播与连接语义不同。修改一个控制面对象,不等于所有数据面实例在同一瞬间采用新值。权威配置已经改变后,DNS 缓存与既有连接仍可能访问旧地址,因此配置写入成功只能作为开始观察的时间点,不能直接作为全局完成证据。 应明确路由选择发生在每次请求、每条连接、每个会话,还是队列任务这一层。长连接可能继续留在蓝色环境,新连接则进入绿色;另一些代理会为每个请求重新选择上游。因此,最长客户端连接不一定决定所有部署的排空时间。需要记录实际机制及其超时、终止行为,并知道强制关闭会怎样影响正在执行的工作。 两套版本都应有能力承担分配给自己的流量与依赖,这并不意味着软件版本必须相同,也不意味着整个系统永远精确支付双倍费用。容量可以分阶段准备,数据库或辅助服务也可能共享。节省资源会改变恢复条件:若旧环境已经缩容到很小,切回路由也无法立刻得到原来的完整服务能力。 在重叠窗口内保持数据兼容 共用数据库很常见,但它是设计选择,不是蓝绿部署的定义。使用两个数据库会引入复制、同步和数据对账问题。反转路由不能抹去绿色环境已经接受的写入,也不能安全地凭空补出蓝色环境缺失的事务。应像定义请求流向一样,明确每类数据在切换和回滚时怎样处理。 共用存储时,可以用先扩展再收缩的方式支持版本重叠。先增加兼容字段,部署能容忍两种表示的读写逻辑,必要时回填数据,最后再删除旧结构。仅等待旧 HTTP 连接结束还不够:旧工作进程、定时任务、回滚版本以及外部消费者,都可能仍依赖旧模式。清理条件应覆盖这些实际使用者。 兼容性不仅是列名存在。两个版本可能都能解析字段,却把相同数值解释成不同单位。迁移可能导致查询报错、写入不一致,也可能造成稍后才暴露的数据损坏;不能断言一定损坏,也不能假定总会干净地拒绝操作。宣布回滚安全之前,需要执行具有代表性的读写路径,并检查业务结果而不只是健康检查。 坏掉的样子 切换期间看到两个版本,并不自动说明部署失败。排空、逐步传播或有意分流,都可能产生这种正常现象。只有客户端或共享状态无法容忍重叠时,它才成为故障。可在适当诊断中记录发布标识,然后将错误与路由状态、连接年龄及版本兼容性关联,避免把所有差异都归因于非原子切换。 反过来,同一个客户端连续五十次看到绿色,也不能证明全局原子性。它可能始终访问同一个边缘节点,或复用同一条连接。就绪副本数量也不能证明服务选择器、端点列表、入口控制器以及外部负载均衡器都指向这些副本。需要检查完整请求路径,并从具有代表性的客户端位置验证实际结果。 不要因为切换命令成功,就立即关闭蓝色环境。应预先定义回滚窗口,监控错误和延迟变化,并在不兼容清理前确认旧后台工作已经退出。如果回滚所依赖的状态已经不可逆改变,就需要前向修复或演练过的数据恢复,而不是反复拨动路由。保留一套进程,并不等于保留了所有恢复前提。 快速验证 用 Python 3 执行下面模型,保持断言开启。代码只改变内存字典:两个路由实例分别更新,已有会话保留当初选定的环境,而模式契约决定两个版本能否运行。这是明确写出的模型,并非某个负载均衡器的测量结果,也不是 Kubernetes 集成测试。 routers = {"edge-a": "blue", "edge-b": "blue"} existing = {"session-1": "blue", "session-2": "blue"} routers["edge-a"] = "green" assert set(routers.values()) == {"blue", "green"} routers["edge-b"] = "green" assert set(routers.values()) == {"green"} assert set(existing.values()) == {"blue"} print("new routes green; existing sessions still blue") required = {"blue": {"amount_cents"}, "green": {"amount_cents", "currency"}} def compatible(schema, colour): return required[colour] <= schema schema = {"amount_cents", "currency"} assert all(compatible(schema, colour) for colour in required) routers = dict.fromkeys(routers, "blue") assert compatible(schema, "blue") print("rollback remains schema-compatible") # A destructive replacement can invalidate the old version. required["green"] = {"amount_minor", "currency"} schema = {"amount_minor", "currency"} assert compatible(schema, "green") assert not compatible(schema, "blue") print("routing rollback cannot restore a removed column") 第一个断言暴露了路由实例更新到一半时的混合状态。即使两个实例都选择绿色,模型中的旧会话仍留在蓝色。追加字段后的模式支持切回旧版本,而后来破坏性的字段替换不支持。三行输出分别指出这些边界,不给出并不存在的通用切换时长,也不把进程存活等同于数据兼容。 ...

十一月 2, 2026 · 1 分钟 · XB Field Notes
示例阶段按 1%、10%、50%、100% 逐级放量。继续需要证据;证据不足时暂停。

渐进发布需要证据与恢复路径

渐进发布一边扩大新版本的暴露范围,一边检查行为是否可以接受,可能在所有请求都切换之前发现退化。它不会自动让改动可逆,不保证只有选定比例的用户受到影响,也不一定需要功能开关。流量路由、实例替换和功能暴露是不同控制,可以共同参与一份发布计划。真正需要说明的是谁先接触变化、根据什么证据继续,以及异常发生之后怎样恢复服务。 虚构计数和阈值用于解释决策规则,并非部署实验或生产服务目标。真实发布需要合适群体、新鲜且有代表性的测量、观察时长,以及经确认的恢复路径。 明确人群及其共享依赖 百分比需要分母:请求、用户、租户和实例并不是一回事。百分之一的实例不一定处理百分之一的流量,一个繁忙客户也可能占据大部分请求样本。应选择能够覆盖相关工作负载的群体,而不是把数量很少当成安全证明。一个工作流需要多次请求都落在同一版本时,还要保持一致的分配,避免新旧行为在流程中交替出现。 新旧人群可能共享数据库、队列、缓存或配额。故障金丝雀能够耗尽共同依赖,也可能写入不兼容数据,使旧版本同样受影响。因此,百分之一的放量并不能把事故影响严格限制在百分之一。确定阶段之前,应盘点共享故障路径,尤其注意跨租户状态、公共连接池和异步消息,不能只看入口分流比例就宣布其他用户完全不会感知。 收集足以支持决定的测量 按金丝雀人群拆分错误率和延迟,与合适的对照群体比较,同时保留绝对的服务目标。如果共同依赖让两个版本都坏了,二者指标相近不能证明健康。发布前就应定义观察窗口、最小流量、相关路由,以及测量缺失或过旧时的处理。十个请求没有错误,与大量代表性请求没有错误,证据强度不同;分母不足时不应把零直接解释成通过。 阶段时长应覆盖准备评估的效应,例如启动、缓存预热、长任务、延迟写入及业务周期。三十秒既不是天然足够,也不是天然没有价值。承接一半流量也不能证明满量容量,因为非线性瓶颈可能在后面才出现。满量之后仍需监控,并明确何时算发布完成。观察不是等待一个固定数字过去,而是取得足够新鲜、相关、可解释的证据。 坏掉的样子 只靠定时器的发布,即使没有取得有效测量也会继续晋升。证据不足时应暂停,由明确负责人在约定时间内调查,不能悄悄按成功处理。自动回滚在触发条件与恢复动作都经过验证时可以缩短响应时间,但有些故障需要值班人员判断。自动化是一种执行手段,不会证明恢复操作本身没有风险;暂停也不能无限无人接手。 另一种失败,是认为恢复旧二进制就能恢复旧状态。数据库模式、已持久化格式、已经发出的消息和外部副作用,都可能在流量切回以后继续存在。应保留仍兼容的旧产物,并演练真实恢复路径。如果反向切换不安全,应准备向前修复或业务对账方案,不要展示一个实际上无法恢复服务的回滚按钮。发布可逆性来自这些设计条件,而不是来自阶段名称。 全量平均会掩盖局部退化。假设旧版本错误率为零,处理十分之一请求的金丝雀出现百分之十五错误,摊到全量后只有百分之一点五。算术上的稀释并没有使故障人群变得可以接受。需要同时观察群体拆分、业务结果和整体健康,而且对照组自身也必须正常。不同人群的流量类型差异,还可能让简单比较失去解释力。 快速验证 保存为 rollout_check.py,用 Python 3.10 或更新版本执行 python3 rollout_check.py。2026-10-19 以 Python 3.12.14 完整运行原样程序并通过。下面是使用虚构请求计数的确定性决策模型,不是部署控制器;它不会改变路由,不等待真实遥测,也不实际执行回滚。最小请求数和错误阈值都是教学参数,不应直接作为生产服务目标。 from dataclasses import dataclass STAGES = (1, 10, 50, 100) MIN_REQUESTS = 1000 MAX_ERROR_RATE = 0.02 # Illustrative gate, not an SLO error budget. @dataclass(frozen=True) class Sample: requests: int errors: int def decide(sample): if not 0 <= sample.errors <= sample.requests: raise ValueError("invalid request/error counts") if sample.requests < MIN_REQUESTS: return "PAUSE", None rate = sample.errors / sample.requests return ("ROLLBACK_REQUESTED" if rate > MAX_ERROR_RATE else "PROMOTE"), rate def plan(samples): history = [] for stage in STAGES: decision, rate = decide(samples[stage]) history.append((stage, decision, rate)) if decision != "PROMOTE": break return history healthy = plan({stage: Sample(2000, 10) for stage in STAGES}) faulty = plan({stage: Sample(2000, 300) for stage in STAGES}) missing = plan({stage: Sample(0, 0) for stage in STAGES}) sparse = plan({stage: Sample(100, 0) for stage in STAGES}) assert [row[0] for row in healthy] == list(STAGES) assert all(row[1] == "PROMOTE" for row in healthy) assert faulty == [(1, "ROLLBACK_REQUESTED", 0.15)] assert missing == sparse == [(1, "PAUSE", None)] assert decide(Sample(1000, 20)) == ("PROMOTE", 0.02) assert decide(Sample(1000, 21)) == ("ROLLBACK_REQUESTED", 0.021) for sample in (Sample(-1, 0), Sample(10, -1), Sample(10, 11)): try: decide(sample) except ValueError: pass else: raise AssertionError("invalid sample accepted") # A 10% canary at 15%, with a zero-error control, gives 1.5% fleet errors. fleet_rate = 0.1 * 0.15 + 0.9 * 0.0 assert fleet_rate < MAX_ERROR_RATE for label, history in (("healthy", healthy), ("faulty", faulty), ("missing", missing), ("sparse", sparse)): print(label, history) print(f"fleet dilution: {fleet_rate:.1%}; rollback was requested, not executed") 健康样本会通过四个阶段,故障样本在第一级返回请求回滚。没有测量与只有一百个请求的零错误样本都会暂停。边界断言允许恰好百分之二,拒绝百分之二点一;不合法的请求和错误计数则触发校验失败。这些检查说明代码如何决策,没有证明阈值具有统计合理性,也没有测量不同流量分配是否真正落实。 ...

九月 18, 2026 · 2 分钟 · XB Field Notes