蓝绿部署在独立环境中准备新应用版本,同时让旧版本继续服务流量。就绪检查通过后,路由把新的工作导向新环境。保留旧环境可以缩短回滚,因为恢复不必等待重新构建。但这个模式本身并不保证全部依赖都有两份、切换瞬间完成,或每一项数据修改都可以撤销。发布方案必须明确哪些东西被切换,以及哪些状态仍然共享。

定义切换究竟控制什么

路由机制可能是负载均衡后端、服务选择器,也可能是 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")

第一个断言暴露了路由实例更新到一半时的混合状态。即使两个实例都选择绿色,模型中的旧会话仍留在蓝色。追加字段后的模式支持切回旧版本,而后来破坏性的字段替换不支持。三行输出分别指出这些边界,不给出并不存在的通用切换时长,也不把进程存活等同于数据兼容。

随后应在隔离部署中,使用真实路由器、代表性流量和可回退的测试数据重复这些检查。分别测量传播、排空与回滚,并保留路由收敛以及应用兼容的证据。前者回答新工作去了哪里,后者回答两套版本能否正确处理当前状态;两类结果不能相互替代。

来源