API 网关高可用、升级与迁移治理:从故障域到退出核销
发布窗口里最危险的绿色,不是监控误报,而是旧数据面仍用缓存配置返回 200,控制面却已经无法发布新路由、撤销泄漏凭证或启动替代实例。此时再增加两个代理副本,只扩大了旧状态的服务能力,没有恢复配置权威,也没有保护身份、配额、门户和审计。
迁移现场同样容易被“新网关能转发请求”误导。路由导入成功之后,插件阶段可能改变,限流计数可能重置,Portal 新签发的凭证可能只存在于新侧,DNS 回切也无法撤销已经发生的写请求。可靠迁移不是把入口从 A 指向 B,而是让配置、身份、运行状态、流量和费用依次跨过可验证、可停止、可回退的门槛。
先按状态划分故障,而不是按进程计数
网关通常同时承载配置、身份、运行时计数、API 产品数据和入口基础设施。它们可能位于数据库、etcd、Kubernetes API、对象存储、外部 Secret 系统或托管服务中,也可能只存在于节点缓存。讨论高可用前,先为每种状态写清权威位置、复制方式、恢复点目标、恢复时间目标和验证动作。
| 状态组 | 典型对象 | 权威位置要回答什么 | 恢复后必须证明什么 |
|---|---|---|---|
| 配置事实 | API、Route、Service、Upstream、Policy、Plugin、产品与计划 | Git、管理数据库、etcd、Kubernetes API 或云资源,谁有最终写权限 | 对象数量与摘要一致,引用完整,候选数据面加载目标版本 |
| 身份与密钥 | Consumer、订阅、API Key、OAuth Client、证书、Secret 引用 | 密钥材料能否导出,还是必须重新签发;撤销记录在哪里 | 新凭证成功,旧凭证持续失败,调用方已完成轮换 |
| 运行时状态 | 缓存、限流计数、会话、熔断统计、配置快照 | 切换时是迁移、共享、重置还是接受短时不连续 | 配额不会意外翻倍,重启和断连行为符合既定策略 |
| 产品与证据 | 应用、订阅审批、Portal 内容、分析、审计、告警 | 哪些属于业务记录,哪些只是可重建派生数据 | 消费者关系可对账,合规证据可检索,删除策略可执行 |
| 入口与依赖 | DNS、LB、WAF、证书、私网、数据库、插件外部服务 | 入口由谁变更,证书和网络绑定如何恢复 | 真实消费者路径可达,证书链正确,旧目标不再接流量 |
把故障域画成调用链后,会出现四类不同事故:控制面不可写但数据面仍服务旧配置;控制面接受写入但部分数据面未收敛;某个数据面或可用区失效;共享状态失去多数派或变得不可达。它们分别影响新配置、新实例、已有请求和动态策略,不能用一个“网关可用率”合并。
副本必须跨过真正的相关故障边界。两个 Pod 落在同一节点、两个节点依赖同一可用区 NAT、两个区域共享同一控制面数据库或同一 KMS,仍然只有一个故障域。容量也不能按副本数推导:在丢失一个故障域、连接排空和重试放大同时发生时,剩余数据面、认证依赖、限流存储和上游都要保持预算内。高可用设计至少为“已有请求继续服务、配置可发布、凭据可撤销、新节点可冷启动、证据可查询”分别定义 RTO/RPO;只承诺流量入口的 SLA,会把最关键的控制能力留在恢复目标之外。
一次控制面断连实验至少记录四个结果:已有路由是否继续服务,新配置能否发布,新凭证或撤销能否传播,空节点能否启动并进入就绪。只有第一项成功时,结论只能是“该拓扑的数据面在本次条件下可使用最后配置”,不能写成“控制面故障不影响数据面”。
产品承诺必须绑定具体形态。AWS Regional API 的多可用区运行与跨区域重建责任见 AWS API Gateway 弹性说明;Azure 托管 APIM 与 self-hosted gateway 的宿主、更新和 SLA 责任不同,应分别核对 Azure APIM 可靠性 与 自托管网关支持策略;Apigee 托管多区域实例也不能替代 Apigee hybrid 中由客户管理的 Kubernetes、Cassandra 和入口。自建产品是否保留最后配置、能否冷启动以及动态策略如何退化,必须按目标版本和拓扑演练。
配置权威决定备份到底要备什么
先禁止临时控制台和自动化同时改同一批对象。迁移窗口内只能有一个写入权威:GitOps 仓库、声明式配置制品、管理 API 流水线或产品数据库四者择一。其他入口进入只读或变更冻结;紧急例外必须留下对象 ID、操作者、前后摘要,并回写权威源。双跑是两个数据面执行同一业务意图,不是两个控制面自由接受写入。
一个可恢复包至少包含以下层次:
| 恢复层 | 应保存的内容 | 不能被它替代的内容 |
|---|---|---|
| 业务契约 | OpenAPI、域名、路径、方法、上游与版本映射 | 消费者、密钥、插件运行状态、网络绑定 |
| 网关配置 | 路由、服务、策略、插件、产品、计划的原生导出 | 产品数据库、插件二进制、Portal 与分析数据 |
| 状态存储 | PostgreSQL、etcd、Cassandra 或 Kubernetes/etcd 的一致性备份 | 外部 Secret、DNS、LB、对象存储和 KMS 权限 |
| 执行制品 | 镜像 digest、Helm chart、CRD、插件包、Wasm、配置 schema | 托管产品内部不可导出的状态 |
| 基础设施 | IaC、网络、证书引用、服务账号、区域和容量参数 | 实际密钥材料与第三方合同 |
| 证据与清单 | 版本、校验和、对象数、引用关系、恢复步骤、保留期 | 隔离恢复与真实请求验证 |
备份文件存在不代表可恢复。每个恢复包都应生成机器可核对的清单,例如:
find recovery-pack -type f -print0 \
| sort -z \
| xargs -0 sha256sum > recovery-pack.sha256
sha256sum --check recovery-pack.sha256sha256sum 只证明文件未变化。真正的门禁是在隔离目标恢复同版本,核对对象数量与引用摘要,启动候选数据面,依次发送正确请求、错误凭证、错误路径和无健康上游请求,再从节点配置版本、网关日志与上游日志证明行为。随后再用恢复副本演练目标版本升级;不能恢复的备份不进入变更窗口。
不同产品的权威和恢复单元不可混用:
| 产品或规范 | 权威与备份边界 | 升级和恢复时的关键限制 |
|---|---|---|
| Gateway API / Envoy Gateway | GatewayClass、Gateway、HTTPRoute、ReferenceGrant、Policy CRD、Secret 与 status 在 Kubernetes API;GitOps 和集群备份共同承担恢复责任 | 先核对 Gateway API 与实现 CRD,再升级控制器;恢复后仍要等待调谐、地址与 condition,并发真实请求。参考 Envoy Gateway 安装与升级 |
| Kong Traditional / Hybrid | 数据库原生备份、声明式导出、kong.conf、证书引用、自定义插件和部署清单分别保存 | 数据库 migration 可能不可逆;Hybrid 还要核对 CP/DP 与插件兼容。参考 Kong 备份恢复 与 升级路径 |
| Kong DB-less | 完整声明文件是配置事实源,节点独立加载 | 整份配置解析成功不等于所有节点已收敛;回切依赖仍保留的旧节点与旧声明 |
| APISIX Traditional / Decoupled | 配置位于 etcd;插件代码、证书、Secret 与部署清单另存 | etcd 灾难恢复会创建新的逻辑集群,member 与 revision 假设必须重建;Standalone 不使用这条备份路径。参考 APISIX 部署模式 与 etcd 灾难恢复 |
| KrakenD | 配置、模板、flexible configuration 输入和插件制品由外部版本库保存 | Go 插件与目标二进制和 toolchain 紧耦合,必须重新构建并执行目标发行线的检查,配置 lint 不能证明插件可加载。参考 KrakenD 迁移指南 |
| Tyk / Gravitee | API 定义、Policy、Dashboard/管理数据库、Portal、分析及各自存储需要分别盘点 | 组件、edition、仓库和插件目标不同;不能把 Dashboard 导出等同于平台恢复。分别查看 Tyk 升级指南 与 Gravitee APIM 升级指南 |
| AWS / Azure / Apigee 托管形态 | IaC、受支持的导出、身份和网络映射由客户保存;平台内部备份责任依服务与合同而异 | 导出通常不包含全部 Portal、分析、密钥和网络对象;跨区域仍需重建入口与依赖。Azure 可从 备份恢复说明 核对目标层级限制 |
把兼容性变成一张阻断账本
候选版本启动前,建立从源版本到目标版本的兼容账本。每一行必须有“兼容、需转换、需重建、不可迁移、尚未证明”之一,不能写“理论上兼容”。至少检查数据库 schema、CRD、配置 schema、插件、运行时、外部依赖、客户端协议和可观测字段。
migration_id: gw-change-<id>
source:
topology: <product-and-mode>
version: <exact-version-or-image-digest>
target:
topology: <product-and-mode>
version: <exact-version-or-image-digest>
freeze_owner: <role>
compatibility:
database:
decision: <compatible|convert|rebuild|blocked|unproven>
restore_evidence: <artifact-or-run-id>
backward_readable: <yes|no|unproven>
crds:
decision: <...>
stored_versions: [<version>]
rollback_method: <restore-or-forward-fix>
plugins:
- name: <plugin>
scope: <global|api|route|consumer>
phase_order: <ordered-phases>
runtime: <lua|go|java|wasm|external>
state_backend: <local|database|redis|external>
failure_mode: <fail-open|fail-close|other>
decision: <...>
identities:
exportable: <yes|no|partial>
rotation_owner: <role>
rollback_deadline: <relative-window>数据库迁移首先问“旧二进制还能否读取新 schema”,而不是“迁移命令能否成功”。答案为否时,滚动升级不能把旧节点当作回滚目标;应保留独立旧数据库或隔离恢复点,并把失败后的动作改为前滚修复或入口切回旧集群。任何会写入共享数据库的候选都不得先连接生产库试跑。
兼容账本还要记录写入方向。old reads new 只证明旧程序能读取新 schema,不代表旧程序写出的对象不会破坏新程序;new reads old 也不代表目标版本首次写入后还能降级。数据库、CRD、声明配置和 Portal 状态分别标记 read-old、write-old、read-new、write-new,再给出首次不可逆写入的时间点。波次一旦越过该点,“回滚”只能是入口切回旧集群、从恢复点重建或前滚修复,不能再用替换镜像冒充状态回滚。
CRD 升级涉及集群级 schema、转换 webhook、stored version 和控制器读取能力。只回退 Deployment 镜像不会删除新字段,也不会恢复旧 status。升级前导出 CRD 与自定义资源,检查目标实现支持的 Gateway API profile/feature;标准 HTTPRoute 之外的 Policy CRD、annotation 和实现扩展进入逐项转换,不能以“都支持 Gateway API”跳过。
具体版本还可能规定硬顺序。Envoy Gateway 1.8 将 Gateway API CRD 提升到 1.5.1,控制器会无条件监听 ListenerSet,因此必须先让兼容 CRD 就位,再升级控制器;集群已有云厂商托管 CRD 时,则应保持单一 CRD owner,不能再让 Helm 覆盖另一份。Envoy Gateway 1.8 升级说明与 Helm CRD 管理给出了这一顺序。升级后要检查 CRD bundle version、served/storage version、storedVersions、Policy condition 的 observedGeneration,再用真实请求证明新字段已经进入数据面;仅看到 CRD apply 成功或控制器 Ready 都不够。
插件名相同也不代表语义相同。必须比较作用域、执行阶段与顺序、默认值和单位、状态后端、外部依赖失败语义、错误码与 Header、运行时 ABI、资源消耗、许可证和观测字段。Gateway API 标准 filter、Envoy Gateway Policy CRD 与 Envoy/Wasm 扩展是三层不同能力;APISIX 内置 Lua 插件、自定义 Lua 和 Plugin Runner 也不是同一部署单元。迁移结论只能属于账本中的具体行。
候选环境先证明“能失败”,再证明“能接流量”
候选验证使用从恢复包重建的隔离环境,不连接生产身份库,不向真实上游执行写操作。先保存源侧直接访问基线,再通过 A、B 两个网关发送同一组合成请求。证据至少包含:request ID、入口、命中路由、消费者、配置版本、网关状态码、上游状态码、选中上游、响应摘要和延迟。
通过条件绑定不变量而不是万能百分比:
正确合成身份在 A/B 获得同一业务状态与响应摘要,允许差异必须登记原因;错误、过期和已撤销身份在两侧持续失败,且真实上游没有收到请求;错误路径、无健康上游、策略依赖超时分别停在预期责任层;
所有候选节点报告目标配置版本,真实请求覆盖每个节点;失败注入后剩余容量、连接排空、重试次数和上游负载没有越过容量预算;清理后没有临时凭证、路由、监听、存储或计费对象残留。
演示阈值只能帮助练习。例如“连续三轮响应体一致”不能成为生产标准;生产窗口应使用真实 SLO、请求分布、容量基线和风险等级确定样本量、观察时长与停止阈值。
一个无业务副作用的双跑与切流实验
下面的实验只需要 Docker Compose、curl 和 POSIX shell。它在临时目录创建四个 Nginx 容器:一个合成上游、候选 A、候选 B 和稳定入口。所有宿主端口只绑定 127.0.0.1,探针只有 GET,不携带真实凭证,不访问真实业务或 DNS。它验证响应比对、候选配置失败、单数据面失效、入口切换与回切;它不能证明任何产品的控制面断连、数据库/etcd 恢复、CRD 转换、插件 ABI、分布式限流或 Portal 迁移。
先创建隔离实验目录和配置:
set -eu
LAB="${TMPDIR:-/tmp}/gateway-migration-40"
rm -rf "$LAB"
mkdir -p "$LAB/profiles" "$LAB/evidence"
cd "$LAB"
cat > compose.yaml <<'YAML'
services:
backend:
image: nginx:1.27-alpine
command: ["/bin/sh", "-c", "cp /profiles/backend.conf /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"]
volumes:
- ./profiles:/profiles:ro
networks: [lab]
gateway-a:
image: nginx:1.27-alpine
command: ["/bin/sh", "-c", "cp /profiles/gateway-a.conf /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"]
volumes:
- ./profiles:/profiles:ro
ports: ["127.0.0.1:18441:8080"]
networks: [lab]
gateway-b:
image: nginx:1.27-alpine
command: ["/bin/sh", "-c", "cp /profiles/gateway-b.conf /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"]
volumes:
- ./profiles:/profiles:ro
ports: ["127.0.0.1:18442:8080"]
networks: [lab]
edge:
image: nginx:1.27-alpine
command: ["/bin/sh", "-c", "cp /profiles/edge-a.conf /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"]
volumes:
- ./profiles:/profiles:ro
ports: ["127.0.0.1:18440:8080"]
networks: [lab]
networks:
lab: {}
YAML
cat > profiles/backend.conf <<'NGINX'
server {
listen 8080;
location = /demo {
default_type application/json;
add_header X-Upstream synthetic always;
return 200 '{"service":"synthetic","variant":"stable"}\n';
}
location / { return 404; }
}
NGINX
cat > profiles/gateway-a.conf <<'NGINX'
server {
listen 8080;
location = /demo {
proxy_pass http://backend:8080/demo;
proxy_set_header X-Request-Id $http_x_request_id;
add_header X-Gateway-Candidate candidate-a always;
add_header X-Config-Version config-v1 always;
}
}
NGINX
cat > profiles/gateway-b.conf <<'NGINX'
server {
listen 8080;
location = /demo {
proxy_pass http://backend:8080/demo;
proxy_set_header X-Request-Id $http_x_request_id;
add_header X-Gateway-Candidate candidate-b always;
add_header X-Config-Version config-v1 always;
}
}
NGINX
cat > profiles/gateway-b-bad.conf <<'NGINX'
server {
listen 8080;
location = /other { return 200; }
}
NGINX
cat > profiles/edge-a.conf <<'NGINX'
server {
listen 8080;
location / {
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
proxy_pass http://gateway-a:8080;
}
}
NGINX
cat > profiles/edge-b.conf <<'NGINX'
server {
listen 8080;
location / {
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
proxy_pass http://gateway-b:8080;
}
}
NGINX显式版本 tag 只让示例行为比 latest 更容易理解,tag 仍可能被重写,不能作为供应链复现证据。团队执行时应把 image 改为已验签、已扫描并记录的 digest,保存 docker compose config 与镜像清单。启动后先确认服务与监听,再采集 A/B 基线:
docker compose -p gwmig40 up -d
docker compose -p gwmig40 ps
wait_for_code() {
url="$1"
expected="$2"
attempt=0
streak=0
while [ "$attempt" -lt 50 ]; do
actual="$(curl --silent --output /dev/null --write-out '%{http_code}' "$url" || true)"
if [ "$actual" = "$expected" ]; then
streak=$((streak + 1))
[ "$streak" -ge 3 ] && return 0
else
streak=0
fi
attempt=$((attempt + 1))
sleep 0.1
done
printf 'timeout: %s expected=%s actual=%s\n' "$url" "$expected" "$actual" >&2
return 1
}
wait_for_code http://127.0.0.1:18441/demo 200
wait_for_code http://127.0.0.1:18442/demo 200
for side in a b; do
port=18441
[ "$side" = b ] && port=18442
curl --silent --show-error \
--header "X-Request-Id: lab-baseline-$side" \
--dump-header "evidence/$side.headers" \
--output "evidence/$side.body" \
--write-out '%{http_code}\n' \
"http://127.0.0.1:$port/demo" > "evidence/$side.status"
done
test "$(cat evidence/a.status)" = 200
test "$(cat evidence/b.status)" = 200
cmp evidence/a.body evidence/b.body
grep -i '^X-Upstream: synthetic' evidence/a.headers
grep -i '^X-Upstream: synthetic' evidence/b.headers
grep -i '^X-Config-Version: config-v1' evidence/a.headers
grep -i '^X-Config-Version: config-v1' evidence/b.headers预期是两个状态文件均为 200、cmp 无输出且退出码为 0,两侧都带 X-Upstream: synthetic 和 X-Config-Version: config-v1。X-Gateway-Candidate 应分别是 candidate-a 与 candidate-b,它是预先登记的观测差异,不应被误报为业务差异。
先向候选 B 注入错误路由。这里替换的是容器内临时配置,宿主只读配置和 A 不受影响:
docker compose -p gwmig40 exec -T gateway-b sh -eu -c '
cp /profiles/gateway-b-bad.conf /etc/nginx/conf.d/default.conf
nginx -t
nginx -s reload
' </dev/null
wait_for_code http://127.0.0.1:18442/demo 404
code="$(curl --silent --output evidence/b-bad.body \
--write-out '%{http_code}' http://127.0.0.1:18442/demo)"
test "$code" = 404
test "$(curl --silent --output /dev/null --write-out '%{http_code}' \
http://127.0.0.1:18441/demo)" = 200证据判据不是“B 失败了”,而是 B 对 /demo 稳定返回 404、A 仍返回 200,错误被隔离在候选侧。恢复 B 后重新运行基线比对:
docker compose -p gwmig40 exec -T gateway-b sh -eu -c '
cp /profiles/gateway-b.conf /etc/nginx/conf.d/default.conf
nginx -t
nginx -s reload
' </dev/null
wait_for_code http://127.0.0.1:18442/demo 200再把稳定入口从 A 切到 B。nginx -t 是配置接收证据,切换后的响应 Header 才是流量接管证据:
curl --silent --dump-header evidence/edge-before.headers \
--output evidence/edge-before.body http://127.0.0.1:18440/demo
grep -i '^X-Gateway-Candidate: candidate-a' evidence/edge-before.headers
docker compose -p gwmig40 exec -T edge sh -eu -c '
cp /profiles/edge-b.conf /etc/nginx/conf.d/default.conf
nginx -t
nginx -s reload
' </dev/null
for attempt in $(seq 1 50); do
curl --silent --dump-header evidence/edge-after.headers \
--output evidence/edge-after.body http://127.0.0.1:18440/demo
grep -qi '^X-Gateway-Candidate: candidate-b' evidence/edge-after.headers && break
sleep 0.1
done
grep -i '^X-Gateway-Candidate: candidate-b' evidence/edge-after.headers
cmp evidence/edge-before.body evidence/edge-after.body停止 B 模拟数据面失效。由于 edge 的上游地址仍存在但连接无法建立,入口应在 proxy_connect_timeout 到期后返回 504,而 A 的直连探针仍成功;这证明入口不会自动回切,也证明旧侧仍具备接管条件:
docker compose -p gwmig40 stop gateway-b
edge_code="$(curl --silent --output evidence/edge-b-down.body \
--connect-timeout 3 --max-time 10 \
--write-out '%{http_code}' http://127.0.0.1:18440/demo || true)"
test "$edge_code" = 504
test "$(curl --silent --output /dev/null --write-out '%{http_code}' \
http://127.0.0.1:18441/demo)" = 200
docker compose -p gwmig40 exec -T edge sh -eu -c '
cp /profiles/edge-a.conf /etc/nginx/conf.d/default.conf
nginx -t
nginx -s reload
' </dev/null
wait_for_code http://127.0.0.1:18440/demo 200若 edge_code 不是 504,先查看 docker compose -p gwmig40 logs edge gateway-b,不要为了得到预期数字改写证据。预期 edge 日志包含 upstream timed out ... while connecting to upstream;若是立即拒绝连接,Nginx 可能返回 502,二者都属于失败,却指向不同的网络证据。proxy_connect_timeout 限制建立上游连接可占用入口 worker 多久,proxy_read_timeout 限制已连接上游两次读取之间的等待;这里的 2s/5s 只是实验值。生产值要小于端到端延迟预算,并和客户端超时、重试次数、上游尾延迟共同计算,否则故障时大量挂起连接会先耗尽容量。真实 LB 也可能返回其他网关错误码或重试到健康节点,生产判据应绑定目标入口实现。
四次 exec 都使用 -T 并把标准输入接到 /dev/null。这在交互终端里不显眼,却决定整段命令能否作为脚本或经管道执行:若让 compose exec 继承脚本 stdin,它可能消费后续命令,表现为 Nginx reload 成功后脚本提前结束。reload 后旧 worker 也可能短暂处理已有连接,因此 wait_for_code 要求连续三次命中目标状态,入口切换则轮询目标候选 Header;CI 应保留总次数上限,并以最终探针而不是 exec 退出码判断新配置是否真正接管请求。
保存需要的脱敏证据后执行精确清理:
cd "$LAB"
docker compose -p gwmig40 down --volumes --remove-orphans
test -z "$(docker ps -aq --filter label=com.docker.compose.project=gwmig40)"
test -z "$(docker network ls -q --filter label=com.docker.compose.project=gwmig40)"
test -z "$(docker volume ls -q --filter label=com.docker.compose.project=gwmig40)"
cd "${TMPDIR:-/tmp}"
rm -rf "$LAB"不要使用无项目过滤的 docker system prune。若清理断言失败,先用同一 label 列出对象并确认归属;实验结束后 18440、18441、18442 均不应继续监听。共享环境还要撤销临时身份、删除测试路由和日志索引,并按云资源标签核对账单。
滚动升级的核心是限制混合版本窗口
滚动升级只适合目标产品明确支持的混合版本组合,而且共享 schema、CRD 和插件能被窗口内所有节点读取。推荐顺序不是固定“先控制面”或“先数据面”,而是服从产品升级文档和兼容矩阵;例如 Kong Hybrid 的 CP/DP 有专门兼容关系,而 Envoy Gateway 要先处理兼容的 Gateway API/CRD。跨产品复制顺序会制造事故。
以 self-managed Kong Hybrid 为例,CP 只接受同 major 的 DP,且 DP 的 minor 不能高于 CP;因此升级链通常先升级 CP,再升级 DP。配置中的自定义插件还必须同时存在于 CP 与 DP,插件 major 一致,DP 的 minor 不能高于 CP。兼容检查失败时,旧 DP 可能继续转发最后配置,却收不到新配置;这正是需要同时观察节点版本和 config hash 的原因。Kong Hybrid 兼容关系与 Kong 升级顺序说明了这些限制。托管 Konnect、传统数据库模式和 DB-less 的规则不同,不能把这条顺序机械外推。
一个可审计的滚动过程包含以下状态:
冻结管理写入,记录源配置版本、对象摘要、数据面节点与容量基线。从恢复包启动一个不接生产流量的候选,完成正反探针与插件加载验证。放入最小真实流量前,确认剩余旧节点具有 N-1 容量,入口支持连接排空,长连接有单独策略。
每替换一个节点,都核对版本、配置 hash、就绪、请求命中、错误率、尾延迟、上游尝试次数和资源水位。任一节点出现配置不收敛、插件加载失败、上游放大或证据缺口,立即停止继续替换;是否回退取决于 schema 是否仍向后可读。全部节点升级后继续保留旧入口与恢复点,直到回滚窗口关闭,再解冻写入并归档证据。
每个波次都要预先写下停止线,而不是看到异常后临时讨论。至少包含:候选 5xx 与策略拒绝增量、p99 延迟增量、认证依赖错误、上游尝试放大、未收敛节点数、配置 hash 滞后时间、CPU/内存/连接水位和影子差异率。阈值来自该 API 的 SLO 与容量基线,不使用全平台平均数稀释少数 Consumer 的失败。任一硬停止线触发时先把入口权重冻结在当前波次,保留故障证据,再决定排空候选、切回旧入口还是前滚修复;自动化不得继续“完成 rollout”来追求 Deployment 绿色。
一次有意义的反向实验是让候选节点缺失一个已启用插件或只安装不兼容版本。预期结果不是请求随机失败,而是候选在接流量前被兼容门禁隔离,控制面记录无法下发或加载配置,旧节点仍服务已批准配置。若候选通过 Ready 并接到流量后才返回 500,说明就绪探针、配置收敛门禁或入口注册顺序存在缺口。恢复时先补齐同一制品摘要并确认 config hash 收敛,再重新放入最小波次,不能靠重试掩盖插件身份不一致。
“Pod Ready”只说明就绪探针通过。“管理 API 成功”只说明写入入口接受请求。升级门禁必须再证明每个数据面加载目标版本,并让合成请求覆盖不同节点。WebSocket、SSE、上传下载和大请求体会让旧连接持续留在被排空节点;需要按协议统计活动连接,而不是只等固定分钟数。
双跑比较业务语义,不复制真实副作用
双跑前将请求分为四类:可安全重放的只读请求;带时间、随机数或个性化字段但可归一化比较的请求;依赖会话或配额、只能使用合成身份的请求;支付、下单、发信、扣减、异步发布等禁止镜像的写请求。默认只允许第一类进入影子链路。
候选 B 应连接隔离上游、只读副本或专用 mock,并阻断外发邮件、消息、Webhook 和计费。认证 Header 不直接复制到另一信任域;使用独立合成消费者或由受控代理重新签发短期实验身份。日志只保存必要 Header、状态码、摘要和关联标识,禁止落盘真实 Token、Cookie 与请求体。
比对器至少输出这些字段:
{
"request_id": "synthetic-<id>",
"source": {"status": 200, "route": "demo-v1", "upstream": "blue", "config": "a-17"},
"candidate": {"status": 200, "route": "demo-v1", "upstream": "blue", "config": "b-09"},
"body_digest_equal": true,
"allowed_header_diff": ["date", "server", "x-gateway-candidate"],
"policy_decision_equal": true,
"external_side_effects": 0
}响应体先做协议感知的归一化,再计算摘要;JSON 字段顺序、时间戳和追踪标识不能直接制造假差异。另一方面,不能为了追求相同而删除认证结论、路由、上游选择和缓存 Header。总体 200 比例相同也不能掩盖某个消费者全部 403,差异应按 API、方法、消费者、策略和上游分层。
DNS 与入口切流要同时管理新连接和旧连接
正式切流前降低 TTL 只能影响尚未缓存或重新解析的客户端,不能驱逐已有连接,也不能保证所有递归解析器严格按期过期。先通过 LB 权重、API mapping、Ingress/Gateway 或私网路由做可观测的小流量,再决定是否改变 DNS;只有 DNS 是唯一入口时,才把权威记录切换作为主要动作。
切流顺序应保持单调:
冻结双侧管理写入,确认 A/B 配置和身份摘要仍可解释。验证 B 的证书、SNI、WAF、私网、健康检查、容量与日志链,不以内部探针代替消费者路径。按调用方、Header、区域或权重逐步导入新连接;持续比较网关与上游证据。
停止放量后仍保留 A,并追踪 A 的请求率、长连接、异步回调和 DNS 查询趋势。回滚窗口关闭前,不在 B 接受无法反向同步的独占消费者、凭证、配额或管理写入。
回切时先停止继续向 B 放量,再把新连接导向 A;不要先关闭 B,否则在途连接会被强制中断。若 B 已产生新凭证、订阅、撤销、配额变化或外部副作用,必须先冻结并完成反向同步或业务补偿。DNS 指回 A 只恢复入口,不会恢复身份和状态。
回滚需要拆成四个动作:流量回切改变新连接去向,二进制回退替换执行程序,配置回退恢复已验证对象,状态恢复从恢复点或补偿链重建数据库、CRD、凭据和计数。四者的可行窗口不同,也不必同时发生。若新版本已经写入旧版本不可读的 schema,可以把流量切回仍连接旧状态库的 A,却不能让旧二进制连接已迁移的 B 状态库;若旧 Credential 已完成撤销,任何回滚都不应让它复活。变更单应明确当前允许哪一种回滚,并在跨过不可逆写入后自动关闭错误选项。
回滚窗口应由三类约束共同决定:DNS 与长连接的最长滞留,数据库/CRD/插件的向后兼容期限,以及业务能够同步或补偿 B 侧新状态的期限。窗口结束条件不是“观察了一段时间”,而是 A 流量归零、B 行为稳定、关键差异关闭、恢复点仍可用且 owner 明确批准。超过窗口后,把旧侧称为灾备或归档环境,不能继续承诺即时无损回切。
消费者、凭证与 Portal 必须单独迁移
配置导出往往不包含可恢复的密钥明文,托管平台也可能只允许创建新凭证。为每个消费者建立迁移状态:未联系 -> 新凭证已签发 -> 双凭证验证 -> 调用方已切换 -> 旧凭证已撤销 -> 旧侧无调用。凭证不通过工单正文、日志或批量 CSV 明文传递;由目标 Secret/KMS 或一次性领取机制交付。
轮换顺序是先创建新凭证并验证,再让消费者切换,观察旧凭证使用,最后撤销旧凭证并持续发送负向探针。若产品不能并存双凭证,安排消费者级窗口,不要全局同时替换。OAuth client、mTLS 证书、Webhook 签名密钥和上游服务账号分别有不同信任方,不能用 API Key 清单覆盖。
Portal 迁移至少对账用户、应用、订阅、产品/计划、审批状态、配额、凭证引用、同意记录和退订状态。分析与审计按保留政策归档,不为了“历史完整”把不再允许处理的个人数据无限复制。旧 Portal 进入只读后,要阻止新注册、新订阅和新凭证签发;否则旧入口即使没有代理流量,仍会制造无法回切的身份分叉。
把迁移门禁接进项目交付链
团队仓库可以保留一套与产品无关的迁移证据结构,具体导出、恢复和探针命令由适配器实现:
gateway-migration/
manifest.yaml # 源/目标版本、拓扑、owner、回滚期限
compatibility.yaml # 数据库、CRD、插件、策略和身份账本
probes/
cases.yaml # 正确、拒绝、错误路由和依赖失败用例
normalize.yaml # 允许忽略的动态字段及理由
scripts/
export.sh # 只读导出,不打印秘密
restore-isolated.sh # 只允许隔离目标
compare.sh # 输出逐请求差异
cleanup.sh # 按标签与对象 ID 精确清理
evidence/
.gitkeep # 实际证据进入受控制品库,不提交敏感日志Pull Request 先运行 schema 校验、引用完整性、插件制品校验和静态策略检查;临时环境再执行隔离恢复、正反探针、候选失败注入和清理断言。生产变更单只引用受控证据制品及摘要,不复制 Token、数据库备份或完整访问日志。流水线身份应只能读取源配置、创建带过期标签的临时资源和写入证据库,不能同时拥有生产入口切换与密钥管理全权。
失败注入也进入项目回归集:控制面网络阻断、单数据面停止、共享状态只读或不可达、错误插件配置、无健康上游、候选入口证书错误。每个用例写明预期停止层、允许的旧行为、禁止的新行为和恢复动作。无法在隔离环境安全模拟的数据库多数派丢失或云区域故障,使用厂商演练能力和正式变更 runbook,不在共享开发集群直接制造。
成本停止条件要早于技术放量
双跑成本包含 A/B 数据面与控制面、数据库/etcd/Redis/Cassandra、备份与日志、跨区或跨云出口、LB/DNS/WAF、影子流量的上游调用、产品订阅和双倍值班。每项在开始前指定预算 owner、最大双跑期限、延长审批和超限动作。
不要把“新侧单位请求成本下降”作为唯一判断。迁移期还要计入配置转换、插件重写、消费者支持、审计保留、跨域流量、凭证双维护和未来退出成本。关键不可迁移项、差异率、回切能力或单位成本在期限内不能收敛时,应停止放量并回到选型,而不是长期养两套平台。
旧侧退出按可证明的顺序完成:
入口:DNS、LB、WAF、API mapping、Ingress/Gateway 和私网路由不再指向旧目标,观察窗口内旧访问日志为零。身份:撤销旧 PAT、管理 Token、API Key、服务账号和证书,调用方侧确认旧凭证不再使用。配置:旧控制面转只读,导出最终快照、对象摘要、审计与删除批准。
数据:按政策归档或删除 Portal、订阅、分析、日志、Trace、缓存、备份和对象存储,并保存删除边界证明。基础设施:删除实例、数据库、etcd、Redis、磁盘、PVC、快照、LB、IP、NAT、私网连接、DNS 和测试后端。供应链:删除插件仓库凭证、镜像、CI secret、Webhook、Marketplace 订阅、license 注册和旧支持联系人。
费用:跨一个完整账单观察周期核对 usage、账单与成本导出;每笔残留费用都能映射到资源和 owner。
控制台看不到资源不是零成本证明,删除实例也不代表快照、日志索引、IP、NAT、私网端点和支持订阅停止计费。退出证据应与迁移证据使用同一个 migration_id,直到费用 owner 签署核销。
团队职责按不可兼任动作拆分
| 角色 | 负责的决定与证据 | 不应独自完成的动作 |
|---|---|---|
| API owner | 契约、上游副作用、业务探针、允许差异和补偿 | 批准平台数据库迁移或删除全部审计数据 |
| 平台 owner | 拓扑、容量、配置发布、候选集群、滚动与入口排空 | 自行批准业务语义差异和身份风险 |
| 安全/身份 owner | 管理权限、消费者凭证、证书、Secret、撤销与日志脱敏 | 直接修改业务路由来通过验证 |
| 数据/产品 owner | Portal、订阅、分析、保留与个人数据删除 | 以历史保留为由无限复制敏感数据 |
| SRE/值班 owner | SLO、故障注入、停止阈值、切流与回切执行 | 在缺少业务批准时接受不可逆副作用 |
| 财务/采购 owner | 订阅、许可、双跑预算、账单观察与退出条款 | 用合同承诺替代技术恢复演练 |
| 变更批准人 | 核对门禁证据,批准放量、关闭回滚窗口和销项 | 同时作为所有证据的唯一生产者 |
交接材料不只写联系人,还要写替补、权限取得方式、证据位置、停止口令和升级路径。切流当班人员必须能在不等待原作者的情况下找到旧入口、恢复点、候选摘要、凭证冻结动作和成本资源清单。
上线与退出的最终判据
控制面、数据面、状态存储、入口和策略依赖的故障域已分别演练,结论绑定具体产品、版本与拓扑。配置只有一个写入权威;恢复包包含配置、状态、制品、基础设施与证据清单,并完成同版本隔离恢复。数据库、CRD、插件、策略、身份和观测字段均有兼容决定;unproven 项不能进入放量。
候选通过正确请求和失败反例;每个数据面加载目标配置版本,错误能停在预期责任层。双跑只使用允许重放的合成或安全请求,差异按 API、消费者、策略和上游分层,外部副作用为零。滚动、DNS/LB 切流、长连接排空和回切都绑定证据与停止阈值;回滚窗口由状态兼容性决定。
Portal、订阅、消费者和凭证完成对账、轮换与撤销,旧侧不再接受独占管理写入。旧入口、凭证、数据、基础设施、供应链和费用全部核销;残留项有 owner、期限和可查询证据。
当上述任一项缺失时,正确状态不是“迁移基本完成”,而是保持冻结、停止放量或继续双跑。高可用的最终证明也不是副本数量,而是故障被隔离后,团队仍能解释正在运行哪一版状态、下一步允许做什么,以及怎样安全退出。
