独立的草稿、未来与过期包含开关,通向构建产物和单独的部署步骤。

Hugo 发布选项决定哪些页面进入产物

Hugo 在构建运行时判断哪些页面应当渲染。文章元数据提供发布时间与草稿状态,项目配置和命令参数提供包含规则,构建时钟提供判断所用的时间。生成目录一旦部署,在下次产物替换之前不会自行变化。因此,日期走到午夜不等于触发发布;没有写 draft: true 的文件,也不会自动被当成草稿。这两个边界决定了排期内容能否按预期上线。 元数据和构建选项分别起作用 在默认规则下,标记为 draft: true 的页面不进入产物;发布时间在未来的页面也会被排除,已经过了失效时间的页面同样如此。Hugo 根据 front matter 配置解析各类日期,显式的 publishDate 可以不同于页面显示的 date。对必须精确到某个边界的排期,应使用包含时区的完整时间戳,避免本机和构建服务器把同一个日期解释为不同时间。 --buildDrafts 包含草稿,--buildFuture 包含未来内容,--buildExpired 包含已失效内容,短选项分别是 -D、-F 和 -E。这些开关处理不同条件:一篇发布时间尚未到达的草稿,需要同时允许草稿和未来内容才能提前进入产物。项目配置或环境配置也能启用相同行为,所以只看文章里的标记不足以判断实际发布结果,还必须检查本次构建的有效选项。 本站明确设置了 buildFuture: true,用于保留包含未来日期文章的既有公开目录。因此,本站这些文章的日期不是保密截止时间,也不能用 Hugo 的默认排期规则推断它们尚未公开。修改发布策略之前,需要把配置和预期 URL 清单一起比较,否则关闭一个看似普通的开关,就可能让大量已经存在的地址消失。介绍默认机制与解释本站事实,应当采用各自对应的证据。 预览产物仍然可以被公开 预览开关只改变输出包含什么,不会给产物增加禁止部署的属性。如果流水线上传了用 -D -F 生成的目录,草稿和未来页面一样可以被访问。因此,预览应使用独立输出位置,正式发布则根据计划中的选项重新构建。Git 发布标签只是标识源码历史,既不会改变 Hugo 的包含规则,也不能证明某个生成目录已经经过审阅。交付记录需要关联源码版本、构建选项和实际产物。 按时发布还需要在发布时间之后执行新的构建与部署。反过来,把已经公开的页面重新标记为草稿,也不会撤回服务器上的旧 HTML 或缓存副本。需要重建、替换被服务的产物,再检查公共地址。Hugo 通常不会自动删除输出目录里的所有遗留文件,因此独立的新目录或合适的清洁构建,是证明被排除页面确实不存在的必要步骤。导航里不显示链接,远不足以证明内容已经下线。 ...

三月 18, 2028 · 2 分钟 · XB Field Notes
RollingUpdate 的新旧版本生命线有重叠。Recreate 在 Deployment 升级期间,旧版结束到新版启动之间留出无服务副本的空档。

Kubernetes 滚动更新逐步替换 Pod

先选择发布的形状,再调整速度。新旧版本能否同时处理请求?能否安全共享存储和任务所有权?如果新版本起不来,如何恢复?这些问题决定 Deployment 应该用 RollingUpdate 重叠运行,还是用 Recreate 先停旧版、再起新版。姊妹篇讨论滚动的节奏和现场观察;本篇讨论兼容性、停机与恢复边界。 先检查兼容性与所有权,再选择版本重叠。Recreate 只改变升级顺序;回滚仍需经过演练的镜像、配置与数据恢复方案。 逐步替换,还是先停后起 RollingUpdate 是 Deployment 的默认策略。surge 和 unavailable 两个预算控制替换过程,允许新旧 ReplicaSet 同时存在。就绪状态参与可用副本的计算,但不代表业务逻辑正确,更不代表每个请求都成功。不能因为看到 Ready,就推断订单、支付或后台任务已经通过发布验收。 ...

十一月 27, 2027 · 2 分钟 · XB Field Notes
release 标签移到镜像 two;既有容器和 latest 仍选择镜像 one。

镜像标签可以移动,既有容器仍保留原先选择的镜像

镜像标签是与镜像引用关联的可读名字。如果仓库策略没有禁止重新指派,同一个标签之后可以标识另一个产物。v1.4 和 latest 的区别首先是命名惯例,而不是前者天然不可变。要复现部署,需要记录实际选择的产物,以及影响行为的平台和运行配置。只保存一个看起来像版本号的名字,并不足以证明当时运行的是哪些内容。 原文仅在本地操作的历史样例,本次未重跑;不表示执行了仓库操作。需记录所选产物与平台;摘要对象类型、内容可用性和验证仍然重要。 坏掉的样子 latest 不会要求仓库寻找最新上传镜像或最高语义版本,它是在引用省略标签时采用的惯例默认值。发布另一个版本,不会自动移动 latest;必须有人或自动化更新这个具体引用。如果把每次 push 都解释成自动更新 latest,就会错误理解发布流程,甚至误判为什么一个节点仍然取得旧版本。 标签的本地解析结果,也可能不同于仓库当前结果。拉取策略、本地镜像是否存在,以及编排工具的行为,共同决定何时查询仓库。因此,两台机器运行相同可读标签,仍可能使用不同产物。仅记录命令文本,会留下实际取回什么、是否保留缓存引用等不确定性;排查时应读取所选产物身份。 移动标签不会改写已经创建的容器所选择的镜像。重启该容器,会保留原配置的镜像和可写状态,不会按照标签的新目标重新创建实例。如果更新流程只是重新打标签,然后 restart 旧容器,命令可能成功,应用却仍运行旧内容。标签更新、容器替换和容器重启需要分别判断,不能混成同一个动作。 快速验证 先取得固定基础镜像,再用 Python 和 Docker 构建器运行脚本。实测环境是 Docker 29.3.1、Linux arm64。所有标签使用独特的本地名字,不向仓库推送任何内容;容器关闭网络,清理只删除实验自己的容器与标签。因此可以实际验证移动行为,又不改变已有发布引用: ...

八月 24, 2027 · 1 分钟 · XB Field Notes
轻量引用直接指向提交;附注引用先指向标签对象,再由它指向提交。

git tag 把一次发布固到提交

发布标签为某个源码版本提供一个容易交流的名字。它与分支不同,创建新提交时不会自动前进。但 Git 并没有从物理上禁止标签变化:拥有相应权限的人仍能替换或删除标签引用。可靠的发布约定来自几件事共同作用:记录对象标识、坚持发布名称不可变,以及限制谁能够更新发布引用。不能仅凭“用了标签”就断言任何人拿到的代码一定相同。 轻量标签直接指向提交;附注标签经独立标签对象指向目标。标签不随新提交前进,但发布名称稳定仍需权限与策略。签名信任、制品来源和远端分发是不同检查。 两种标签 轻量标签是直接指向某个对象的引用,通常指向提交。附注标签则指向单独的标签对象,其中保存目标、打标签者身份、时间、说明,以及可选的签名。打标签的人并不一定是提交作者;有了这些元数据,也不意味着附注已经经过密码学签名。发布记录需要说明谁作出发布决定,而不是把提交作者与发布者混为一谈。 对于附注标签,git rev-parse refs/tags/v2.4.1 得到标签对象标识,git rev-parse 'refs/tags/v2.4.1^{commit}' 则解引用到目标提交。当说明或签名身份也是发布证据的一部分时,应同时记录二者。只比较提交会漏掉标签留言或签名者变化;只比较可读名称,则连源码是否一致也无法确定。对象标识与名称各自承担不同的核对职责。 为什么给发布签名 git tag -v 使用配置的签名后端及本地信任材料验证签名标签。密码学验证成功之后,还需要确认该密钥确实属于项目发布策略认可的人。一个陌生人的有效签名,并不能证明维护者批准了这次发布。密钥的来源、授权范围和撤销状态,都应由实际发布流程明确管理,而不是靠命令里出现“成功”两个字代替。 源码标签有签名,也不能直接证明下载的二进制就是从该源码构建的。制品校验和、构建来源记录及构建过程,需要补上这条独立关系。另外,不要把验签命令接到 head 后面,再依赖管道最后一项的退出码:只截取几行输出可能掩盖验签失败。发布检查应保留并判断验签命令自身的结果,显示多少行只是界面选择。 移动要格外小心 修正尚未分享的本地标签,与替换消费者已经获取的标签,是两种不同操作。标签一旦分发,应优先使用新版本名称,并记录旧版本被撤回的原因。修补安全缺陷也不要求重写旧标签;保留原引用反而有助于识别受影响部署。服务端的引用保护应阻止未经授权的发布标签更新与删除,不能只靠客户端的习惯维持这个约定。 如果特殊情况下确实批准替换,应公布准确的新旧对象标识,并只协调那个明确的引用。把所有标签一起强制获取,会覆盖其他无关的本地引用,不应成为常规修复建议。消费者需要知道自己持有的是哪一个对象,不能让两个不同提交在没有说明的情况下继续共享同一个发布名称。 坏掉的样子 latest 这类浮动名称可能在两次构建之间解析到不同提交。即便界面保留方便阅读的名字,也应为每个制品和每次部署记录真正解析到的提交标识。同名不保证两次构建一致,而固定提交也不会自动固定外部依赖或编译器版本。源码定位与可复现构建有关,却不能互相替代。 另一种故障是发布标签只存在于某个本地克隆。普通分支推送不一定传输它;--follow-tags 处理的是从推送提交可达、符合条件的附注标签,不是本地全部标签。应使用 git ls-remote --tags 核对准确的远端引用及对象标识,不能对版本号作模糊子串匹配,否则可能把另一个版本当成目标。远端可见只能证明分发,不能证明构建正确或签名可信。 快速验证 在 POSIX 系统上用 Python 3 及 Git 运行下面的实验。它创建独立临时仓库,使用示例身份,为演示明确关闭签名,并且不配置远端。所有提交和标签都位于这个临时仓库,不会给当前项目打标签;退出后由临时目录管理器清理。命令通过参数列表调用,不需要拼接或解释来自外部的 Shell 文本。 ...

六月 11, 2027 · 1 分钟 · 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
从 1.0.0 开始,公共接口的不兼容变化提升主版本,兼容功能增加提升次版本,兼容修复提升补丁版本。版本号传达判断,不能证明应用升级安全。

语义化版本让兼容性承诺更明确

依赖版本有价值的前提,是发布者与使用者对公共契约有共同认识。语义化版本为变化提供一套表达:从 1.0.0 开始,不兼容的公共接口变化需要提升主版本,兼容的功能增加需要提升次版本,兼容的问题修复需要提升补丁版本。数字归纳的是发布者对这一版的判断,并不会自行检查实现,也不能保证应用升级之后必然正确运行。理解承诺边界,比只记住三个数字更有用。 先声明兼容性覆盖哪些内容 公共接口可以包括导出的函数、正式说明的命令参数、配置格式、响应字段和明确约定的错误行为。边界应写得足够清楚,才能判断一次变化是否违约。保持契约不变的内部重构,与删除已经承诺支持的参数,不属于同一类变化。修正原先违反文档契约的行为,可能仍是兼容的缺陷修复,即便有人曾经观察并依赖那个错误表现。仍需在变更说明中解释,并验证受影响的调用方。 语义化版本规范要求:把公共功能标记为废弃时,应提升次版本;之后真正移除功能,则需要按不兼容变化处理。已经发布的某个版本,其内容不应继续变化,修正应通过另一个新版本交付。在相同版本标签背后替换文件,会破坏追溯:两个依赖声明完全相同的安装过程,可能得到不同的字节,排查问题时也无法再靠版本号明确区分。 主版本为零表示初始开发阶段,公共接口不应被视为稳定。维护者可以为这一阶段声明额外约定,但使用者不能默认所有零主版本更新都兼容。预发布版本的优先级低于对应的正式版本;加号之后的构建元数据不参与优先级比较。因此,两个版本在排序意义上相等,不意味着它们的包内容、来源或文件摘要相同。兼容性、选择顺序与产物身份是不同问题。 坏掉的样子 标错的次版本即使满足依赖范围,也可能破坏已声明的契约。标号正确的发布,也可能暴露应用对内部实现细节的依赖。升级时仍应读变更说明,运行有代表性的集成验证;需要重复解析依赖时保留锁文件。锁文件记录当时选择了哪些版本,不能替这些版本证明行为正确,更不是来源真实性的证明。版本范围允许安装,只说明解析条件满足了。 包管理器的范围语法又是另一层规则。语义化版本规范定义版本及优先级,具体工具决定约束怎样筛选候选。在 node-semver 中,插入符通常允许最左非零分量右侧的变化,所以 ^0.2.3 不接受 0.3.0,^0.0.3 也不接受 0.0.4。三个分量完整的波浪号范围,通常只允许同一次版本线内的补丁更新。不能把所有插入符都理解成“只要主版本不变就行”。 预发布筛选还有附加规则。宽泛的稳定版本范围,不会自动接纳数值更高的测试版;明确允许某个预发布三元组后,才可能接纳该三元组后续的预发布版本。应验证实际解析器,而不是用普通字符串排序推断升级关系,也不要把数值更大直接等同于可被当前范围选择。不同生态的约束语法不能仅凭相似外观相互套用。 按已声明的公共接口变化分类,再分别验证解析器与受影响应用。语义化版本传达兼容性判断;范围或锁文件不证明行为正确或来源真实。固定 semver@7.7.2 示例仍在断言前受阻。本次绘图未重跑示例。 快速验证 原始文章报告安装尝试在解析器断言执行前失败;本次源码审核未重试安装,完整固定版本示例仍未验证。这里没有把其他版本库的行为算成该示例的通过证据。 将代码保存为 semver.py,在已有 Node 和 npm 的环境执行 python3 semver.py。实验需要访问公共 npm 仓库。脚本在临时目录安装 semver@7.7.2,隔离配置和缓存,关闭包安装脚本,再调用这个库真实的比较函数。它不会修改当前项目的依赖声明或锁文件;结束时临时安装目录随上下文清理。 from pathlib import Path from tempfile import TemporaryDirectory import json import os import subprocess with TemporaryDirectory() as directory: root = Path(directory) user_config = root / 'user.npmrc' global_config = root / 'global.npmrc' user_config.write_text('') global_config.write_text('') env = {'PATH': os.environ['PATH'], 'LC_ALL': 'C', 'NPM_CONFIG_USERCONFIG': str(user_config), 'NPM_CONFIG_GLOBALCONFIG': str(global_config), 'NPM_CONFIG_CACHE': str(root / 'cache')} subprocess.run(['npm', 'install', '--ignore-scripts', '--no-audit', '--no-fund', '--package-lock=false', '--registry=https://registry.npmjs.org', 'semver@7.7.2'], cwd=root, env=env, check=True, capture_output=True) script = r''' const s = require('semver'); const assert = require('node:assert/strict'); const cases = [ ['1.9.0', '^1.2.3', true], ['2.0.0', '^1.2.3', false], ['1.2.9', '~1.2.3', true], ['1.3.0', '~1.2.3', false], ['0.2.9', '^0.2.3', true], ['0.3.0', '^0.2.3', false], ['0.0.4', '^0.0.3', false], ['1.3.0-beta.1', '^1.2.3', false], ['1.3.0-beta.2', '^1.3.0-beta.1', true] ].map(([version, range, expected]) => { const accepted = s.satisfies(version, range); assert.equal(accepted, expected); return {version, range, accepted}; }); assert.equal(s.compare('1.2.3+build.1', '1.2.3+build.2'), 0); assert.equal(s.lt('1.3.0-beta.2', '1.3.0'), true); assert.equal(s.valid('1.02.3'), null); console.log(JSON.stringify({library: require('semver/package.json').version, cases, metadata_precedence_equal: true, prerelease_before_release: true, leading_zero_rejected: true})); ''' result = subprocess.run(['node', '-e', script], cwd=root, env=env, capture_output=True, text=True, check=True) print(json.dumps(json.loads(result.stdout))) 九个范围案例应当全部符合断言:^1.2.3 接受 1.9.0,拒绝 2.0.0;波浪号拒绝下一次版本;两个零主版本范围更加收敛。稳定范围拒绝 1.3.0-beta.1,明确指定预发布的范围则接受 1.3.0-beta.2。输出保留版本、范围与布尔结果,便于逐项看清究竟是哪条条件在起作用。 ...

二月 13, 2027 · 1 分钟 · 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