升级 API 网关,最容易让人产生错觉的地方,是命令看起来太简单:改一个镜像标签,再重建一次容器,似乎就结束了。
但网关位于业务入口。一条路由匹配规则、一个认证插件的默认值,甚至一次更严格的上游证书校验,都可能让“容器正常运行”与“业务正常访问”变成两回事。
本文以 APISIX 3.13.0 → 3.19.0 为版本示例,整理一套可复用的升级流程:准备离线镜像、备份配置、验证兼容性、受控切换,以及失败后的回退。
本文是通用操作教程,不代表某个真实系统已完成升级。目录、服务名、域名和业务路径均为虚构示例,不包含真实部署拓扑、账号或环境配置。示例命令以 Linux、Bash、Docker Compose v2、
jq和 GNU 命令行为前提,执行前必须适配自己的部署。
一、先分清:升级程序,不等于迁移配置
本文讨论的是使用 etcd 保存动态配置的 APISIX 部署。Standalone YAML、API-driven standalone,以及由 Kubernetes 控制器管理的部署,需要采用对应的配置交付方式,不应直接套用后文命令。
可以把这里的网关拆成三层:
| 层次 | 主要内容 | 本次如何处理 |
|---|---|---|
| 程序层 | APISIX 镜像、运行时和插件代码 | 更换并验证 |
| 静态配置层 | config.yaml、环境变量、挂载文件、自定义插件 |
保留现状,按兼容性要求调整 |
| 动态配置层 | etcd 中的路由、上游、消费者、证书及插件配置 | 先备份、再验证,不重新初始化 |
这决定了一个重要原则:
不要把“重新安装网关”的脚本,当成“升级网关”的脚本。
首次安装脚本可能会导入默认路由、覆盖同名对象、更新管理配置。对于已经运行的系统,这些操作往往比换镜像本身更危险。
本次示例只升级 APISIX,不同时升级 etcd,不修改数据卷,也不重跑初始化或全量路由导入。前提是已经确认现有 etcd 版本满足目标 APISIX 的要求;如果不满足,应另行规划依赖升级。
二、跨版本升级,先看兼容性,再看新功能
版本号增加,并不意味着原来的行为全部保持不变。尤其是安全加固:新版本拒绝某些请求,可能恰恰是修复了旧版本过于宽松的行为。
从较早版本跨到新版本时,应覆盖两个版本之间的所有中间版本与补丁发布说明,不能只阅读最终目标版本。下面从官方 3.17、3.18、3.19 发布说明中挑选几个具有代表性的检查项,并非完整兼容清单;3.14–3.16 等中间版本仍需逐一核对。
| 版本 | 代表性变化 | 升级前应检查什么 |
|---|---|---|
| 3.17 | jwt-auth 收紧 claim 校验;hmac-auth 默认签名头包含 date |
Token 内容、签名算法、客户端签名实现 |
| 3.17 | cas-auth 要求新的 cookie.secret 配置 |
是否启用该插件,配置是否满足新 schema |
| 3.18 | 部分请求/响应缓冲插件引入默认大小限制 | 实际启用的插件,以及大请求、大响应的边界行为 |
| 3.18 | openid-connect 收紧 issuer、audience、scope 等校验 |
实际启用的校验项、Bearer 流程和浏览器登录流程 |
| 3.18 | 自定义插件依赖的部分转发头变量行为改变 | 自定义 Lua、NGINX 片段、日志格式和下游解析程序 |
| 3.19 | HTTPS/gRPCS 上游的upstream.tls.verify 开始实际控制证书校验 |
证书链、信任 CA、目标主机名与证书 SAN |
| 3.19 | basic-auth 不再接受空密码 |
Consumer、credential,以及 Secret/环境变量解析后的密码 |
| 3.19 | batch-requests 默认限制单个子响应与总响应大小 |
批量请求是否超过默认的 1 MiB/10 MiB 边界 |
| 3.19 | WebSocket 的部分 Prometheus 指标标签发生变化 | 看板、告警和 recording rule 是否仍能覆盖流量 |
一个特别容易误判的例子:升级后出现 502
3.19 发布说明明确指出:当 HTTPS 或 gRPCS 上游已配置 tls.verify: true 时,新版本会实际验证证书链和主机名。不受信任的证书或名称不匹配,可能导致 TLS 连接失败,表现为 502。
这不等于“新版默认把所有 HTTPS 上游都改成严格校验”。需要结合既有 tls.verify、全局配置和上游 Host 策略判断。
排查顺序应当是:
- 确认失败发生在网关到上游的 TLS 握手,而不是路由不存在。
- 检查上游证书链是否完整、是否被网关信任。
- 核对实际用于校验的主机名与证书 SAN。
- 检查
pass_host、upstream_host与实际访问地址的关系。 - 补齐可信 CA 或修正证书、主机名后重新验证。
不要为了让请求恢复,就默认关闭证书校验。确需临时例外时,也应经过风险评估,并有恢复安全配置的期限。
三、准备离线镜像:把“取包”和“上线”拆开
离线交付的价值,不只是服务器无法访问镜像仓库。它还能让上线窗口只处理已审核、已测试的制品,减少临时下载和标签漂移带来的不确定性。
1. 在联网制品机上准备镜像
以下使用 linux/amd64 作为教学示例,实际必须与目标服务器架构一致。不要根据下载机器的架构推断目标平台。
set -eu
NEW_IMAGE='apache/apisix:3.19.0-debian'
TARGET_PLATFORM='linux/amd64'
ARCHIVE='apisix-3.19.0-debian.tar'
docker pull --platform "$TARGET_PLATFORM" "$NEW_IMAGE"
docker image inspect "$NEW_IMAGE" \
--format '{
{.Id}} {
{.Os}}/{
{.Architecture}} {
{json .RepoDigests}}'
docker save -o "$ARCHIVE" "$NEW_IMAGE"
sha256sum "$ARCHIVE" > "$ARCHIVE.sha256"
记录镜像来源、完整标签、平台、镜像 ID、仓库摘要,以及导出文件的校验值。对可信来源的验证应发生在制品准备阶段,自己计算一个 SHA-256,并不能单独证明来源可信。
这里有三个容易混淆的概念:
- 仓库摘要(RepoDigest):标识仓库中的镜像清单或索引。
- 镜像 ID:Docker 本地镜像的配置摘要。
- tar 文件 SHA-256:验证交付文件是否发生变化。
三者用途不同,不应拿其中一个值代替另一个。
通过受控渠道传输 tar,并通过可信渠道核对校验清单。不要把其他人文章里的哈希值,当成自己制品的校验值。
2. 在目标服务器校验并导入
sha256sum -c apisix-3.19.0-debian.tar.sha256
docker load -i apisix-3.19.0-debian.tar
docker image inspect apache/apisix:3.19.0-debian \
--format '{
{.Id}} {
{.Os}}/{
{.Architecture}}'
docker load 只是导入镜像,不会替换正在运行的容器。
因此可以提前准备制品,再在维护窗口执行真正的切换。不要在这一步顺手清理旧镜像,它是回退的重要依赖。
四、备份必须来自正在运行的系统
代码仓库里的配置,不一定等于生产配置。真正执行升级前,要建立一份可验证的现场基线。
至少包括:
- 当前运行镜像的引用与 ID。
- Compose 项目名、服务名、挂载、网络、启动参数及覆盖文件。
- 正在使用的静态配置、环境变量、证书和自定义插件。
- etcd 快照,以及独立存放的受控副本。
- 动态对象的数量、ID、关键配置和业务验收结果。
备份可能包含密码、Token、证书私钥和业务地址。使用受限目录,按需加密,不放入 Git、工单公开附件或博客素材中。
1. 核对部署身份并保存静态配置
后续服务器命令默认在同一个 Bash 会话中执行。示例假定已有部署位于 /srv/gateway-demo,Compose 项目名为 gateway-demo,网关与配置中心的服务名分别是 gateway 和 etcd。
这些都是虚构名称。必须替换为既有部署的真实值,尤其不能临时换一个 -p 项目名;否则可能创建出另一套资源。
set -eu
umask 077
cd /srv/gateway-demo
dc=(docker compose -p gateway-demo -f compose.yaml --env-file .env)
GW_ID=$("${dc[@]}" ps -q gateway)
ETCD_ID=$("${dc[@]}" ps -q etcd)
test -n "$GW_ID"
test -n "$ETCD_ID"
OLD_IMAGE_REF=$(docker inspect "$GW_ID" --format '{
{.Config.Image}}')
OLD_IMAGE_ID=$(docker inspect "$GW_ID" --format '{
{.Image}}')
docker image inspect "$OLD_IMAGE_ID" >/dev/null
BACKUP_DIR="$PWD/backups/pre-upgrade-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
cp -p compose.yaml .env "$BACKUP_DIR/"
cp -p conf/config.yaml "$BACKUP_DIR/config.yaml"
docker inspect "$GW_ID" > "$BACKUP_DIR/gateway-inspect.json"
printf '%s\n' "$OLD_IMAGE_REF" > "$BACKUP_DIR/old-image-ref.txt"
printf '%s\n' "$OLD_IMAGE_ID" > "$BACKUP_DIR/old-image-id.txt"
printf '备份目录:%s\n' "$BACKUP_DIR"
按实际挂载清单补充备份证书、自定义插件、Secret 配置、加密密钥及其他覆盖文件。上面的三份配置文件只是示例,不能代替完整清单。
若原部署依赖多个 -f 文件、额外 --env-file、profile 或外部环境变量,需要完整复现原来的调用方式。重开终端后,应根据保存的记录恢复变量,而不是直接从中间步骤继续执行。
2. 创建并验证 etcd 快照
以下假设 etcd 容器内已有 etcdctl,且其连接、TLS 与认证参数已经正确配置。示例省略了凭据注入;不是建议关闭 etcd 认证。不符合此前提时,先按实际安全连接方式配置工具。
docker exec "$ETCD_ID" sh -c \
'ETCDCTL_API=3 etcdctl snapshot save /tmp/gateway-pre-upgrade.db'
docker cp "$ETCD_ID:/tmp/gateway-pre-upgrade.db" \
"$BACKUP_DIR/etcd-snapshot.db"
test -s "$BACKUP_DIR/etcd-snapshot.db"
sha256sum "$BACKUP_DIR/etcd-snapshot.db"
然后用与 etcd 版本匹配的工具检查快照状态,例如在具备 etcdutl 的受控工具环境中执行:
etcdutl snapshot status "$BACKUP_DIR/etcd-snapshot.db" -w table
旧版工具可能使用 etcdctl snapshot status。应先检查已安装工具版本和帮助,不能把工具缺失当成“备份已经验证”。
快照文件非空、状态可读,只能说明完成了基础检查。真正的恢复验证是:在隔离目录、隔离网络中恢复快照副本,启动独立 etcd,再让测试网关读取它。
不要把恢复命令指向正在使用的数据目录,也不要让测试集群连接到正式集群。
3. 导出动态对象,用来做差异比较
etcd 快照负责恢复;Admin API 对象导出便于比较。两者用途不同,不能互相替代。
按实际使用情况导出 routes、upstreams、services、consumers、consumer_groups、credentials、plugin_configs、plugin_metadata、global_rules、ssls、stream_routes、secrets 等资源。
注意分页、嵌套凭据资源及版本支持情况,不能只取列表第一页。比较时聚焦对象 ID 与业务配置,排除无关时间戳等动态字段;敏感内容只在受控环境内处理,不打印到公开终端录屏或构建日志。
如果 etcd 被多个系统共享,完整快照也会包含其他系统的数据。恢复范围必须单独设计,不能为了回退一个网关,就覆盖整个共享集群。
五、验证分三层:能解析、能加载、能转发
第一层:静态配置检查
在已导入新镜像的服务器上,用只读挂载做基础检查:
docker run --rm --network none --entrypoint apisix \
-v "$PWD/conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro" \
apache/apisix:3.19.0-debian test
实际配置还引用证书、自定义插件、额外文件或环境变量时,必须同步提供对应的只读挂载和受控环境变量。这个最小示例不能覆盖所有部署。
apisix test 通过,只能证明该测试上下文中的配置检查通过,不能证明 etcd 中的历史路由和插件行为与旧版一致。
第二层:隔离环境加载真实配置副本
使用快照副本与完整静态配置搭建隔离测试环境,检查对象是否能加载,以及是否出现 schema、插件初始化或密钥解密错误。
测试环境不能连接正式 etcd,也不能直接承接正式流量。涉及付款、发消息、写业务数据的请求,应改用测试上游或可控替身,避免演练产生真实副作用。
第三层:回归真实业务行为
至少覆盖以下维度,并保存升级前后的对照结果:
| 测试项 | 不能只看什么 | 还需要验证什么 |
|---|---|---|
| 路由匹配 | 首页能打开 | Host、路径、方法、优先级、尾斜杠 |
| 路径与 Host 改写 | 返回 200 | 上游实际收到的 URI、Host、查询参数 |
| 登录鉴权 | 合法凭据能通过 | 过期、错误、缺失凭据必须按预期拒绝 |
| HTTPS 上游 | TCP 端口可达 | 证书链、SNI、SAN、错误证书应失败 |
| 限流与白名单 | 普通请求成功 | 触发阈值、拒绝请求、真实客户端 IP |
| 文件传输 | 小文件可上传 | 大文件边界、超时、下载完整性与校验值 |
| WebSocket/SSE | 握手成功 | 持续传输、空闲超时、重连、连接排空 |
| TCP/UDP | 服务进程存在 | 实际协议收发、会话保持与超时 |
| 可观测性 | 看板有曲线 | 指标标签、日志结构、告警覆盖是否改变 |
没有完成隔离恢复与业务回归,就不应承诺“直接换镜像一定不影响转发”。
六、维护窗口:只重建 APISIX
对于单实例网关,容器重建期间可能发生短暂中断,已有长连接也可能断开。维护窗口与重试机制可以降低影响,但不能将其称为零停机升级。
1. 切换前再检查一次
- 备份已验证可恢复,旧镜像仍然可用。
- 隔离回归通过,兼容性修改已经在旧、新版本上验证。
- 从最终基线建立到验收结束,冻结控制面写入,包括 Dashboard、Admin API、自动同步任务和控制器。
- 冻结后重新执行第四节的备份与对象导出,更新并记录
BACKUP_DIR,确保最终备份对应切换前状态。如果相较隔离演练又出现配置变更,必须补充回归后才能切换。 - 明确业务验收人、观察时间、错误率/延迟阈值及回退触发条件。
- 涉及 Nginx 文件式 HTTPS 配置时,确认证书与私钥存在、非空、权限正确,挂载位置与配置一致。不要通过删除 443 或关闭 HTTPS 来掩盖缺失证书的问题。
本示例假定既有 compose.yaml 中,网关镜像通过 .env 的 APISIX_IMAGE 控制:
services:
gateway:
image: ${
APISIX_IMAGE:?必须配置 APISIX_IMAGE}
# 其他现有配置保持不变
这只是需要核对的片段,不是用于覆盖现有文件的完整 Compose 配置。如果既有部署不使用这个变量,先确认镜像的真正来源,不要盲目添加变量。
2. 核对基线,修改目标镜像
继续使用前面同一 Bash 会话中的变量。这里先清除同名 Shell 变量,避免它覆盖示例中明确指定的 .env 值;若原部署本来依靠 Shell 注入配置,应按原机制操作,不直接套用。
unset APISIX_IMAGE
test "$(grep -c '^APISIX_IMAGE=' .env)" -eq 1
RESOLVED_IMAGE=$("${dc[@]}" config --format json | jq -er '.services.gateway.image')
test "$(docker image inspect "$RESOLVED_IMAGE" --format '{
{.Id}}')" = "$OLD_IMAGE_ID"
NEW_IMAGE='apache/apisix:3.19.0-debian'
NEW_IMAGE_ID=$(docker image inspect "$NEW_IMAGE" --format '{
{.Id}}')
sed -i "s|^APISIX_IMAGE=.*|APISIX_IMAGE=$NEW_IMAGE|" .env
RESOLVED_IMAGE=$("${dc[@]}" config --format json | jq -er '.services.gateway.image')
test "$RESOLVED_IMAGE" = "$NEW_IMAGE"
"${dc[@]}" config >/dev/null
检查完整 Compose 解析结果时,应避免把包含环境变量和密钥的内容直接打印到日志。上例通过 jq 只取镜像字段;同时不要启用 Shell 的 set -x 跟踪。
3. 只重建目标服务
"${dc[@]}" up -d --no-deps --force-recreate --no-build --pull never gateway
GW_ID=$("${dc[@]}" ps -q gateway)
test -n "$GW_ID"
test "$(docker inspect "$GW_ID" --format '{
{.Image}}')" = "$NEW_IMAGE_ID"
docker exec "$GW_ID" apisix version
"${dc[@]}" ps
几个参数分别解决不同的问题:
--no-deps:不启动依赖服务,将本次操作限定在目标服务。--force-recreate:明确重建容器,而不是仅重启原容器。--no-build:不在维护窗口临时构建。--pull never:只使用已交付的本地镜像,不临时访问镜像仓库。
这些参数不会让不兼容的配置自动变兼容,也不是健康检查或业务验收的替代品。
示例中的检查失败会中止当前 Bash 会话,不会自动回滚。若已经修改 .env,但尚未完成切换,也要从最终备份恢复它,避免后续例行操作意外使用新镜像。
若使用旧版 docker-compose,应事先通过 version 和 up --help 验证参数支持,并改写、演练整套流程;不要在维护窗口临时猜测命令。
七、验收:版本号正确,只是第一项
切换后,先查看进程与近期日志:
docker exec "$GW_ID" apisix version
docker logs --since 10m --tail 200 "$GW_ID"
再通过受控客户端访问事先建立的业务基线。例如下面只是一个虚构健康检查入口,需替换为自己已经存在的验收地址:
curl --fail-with-body --show-error --silent \
--connect-timeout 5 --max-time 15 \
https://gateway.example.com/api/catalog/health
不要使用 -k 跳过 TLS 校验来证明 HTTPS 正常。使用私有 CA 时,应显式提供可信 CA。
健康检查之外,还需要完成:
- 比较动态对象数量、ID、关键配置及实际加载情况。
- 回归登录、认证、上传下载、流式连接和关键接口。
- 比较请求成功率、P95/P99 延迟、上游连接失败、重启次数及资源占用。
- 确认日志和告警仍能工作,避免“看板没有报错,只是指标不再匹配”。
- 检查管理面的实际暴露范围。如果管理接口经过其他入口反向代理,仅限制原始管理端口并不足够。
401、403 并非一律代表失败:非法凭据被拒绝,可能正是预期行为。真正要关注的是原本合法且稳定的请求出现新错误,或非法请求反而被放行。
关键业务失败、持续新增 502、认证链路异常、对象无法加载,或观测指标超过预定阈值时,应按预案回退,不要在正式流量上临时尝试大量配置组合。
八、回退:先判断是程序问题,还是数据问题
情况一:配置保持旧版兼容,只需要退回程序
在控制面冻结、动态配置仍兼容旧版的前提下,可以恢复原 .env,并核对解析出来的镜像确实对应切换前记录的 ID:
cp -p "$BACKUP_DIR/.env" .env
unset APISIX_IMAGE
ROLLBACK_IMAGE=$("${dc[@]}" config --format json | jq -er '.services.gateway.image')
test "$(docker image inspect "$ROLLBACK_IMAGE" --format '{
{.Id}}')" = "$OLD_IMAGE_ID"
"${dc[@]}" config >/dev/null
"${dc[@]}" up -d --no-deps --force-recreate --no-build --pull never gateway
GW_ID=$("${dc[@]}" ps -q gateway)
test "$(docker inspect "$GW_ID" --format '{
{.Image}}')" = "$OLD_IMAGE_ID"
docker exec "$GW_ID" apisix version
随后重新执行业务验收。普通镜像回退不需要清空或恢复 etcd。
如果旧镜像引用已被覆盖,镜像 ID 检查会阻止继续操作。应使用事先保留的旧镜像制品恢复正确引用,不能为了让检查通过就删掉断言。
情况二:已经写入旧版本不支持的配置
此时换回旧镜像,可能仍然无法正常工作。
应先停止相关管理写入,判断受影响对象和数据范围,再按已经演练的恢复方案处理。etcd 快照恢复会把数据退回快照时刻,可能丢失之后的合法变更;共享集群还会涉及其他系统。
因此,不应在通用教程中给出一条“覆盖生产数据目录”的恢复命令。恢复拓扑、成员配置、revision/watch 行为、数据损失范围,以及业务授权,都必须在切换前明确。
回退需要演练的,从来不只是一条命令,而是一条完整的恢复路径。
九、如果业务要求尽量不中断,需要改变什么?
“平滑”不等于承诺任何连接都不会受影响。单实例重建不具备持续承接新请求的冗余能力;如果需要更高连续性,应预先建设多实例与入口切流机制。
一个通用的滚动思路是:
入口负载均衡
├── 网关 A:旧版,继续承接流量
└── 网关 B:摘流 → 升级 → 验证 → 小流量 → 恢复权重
确认 B 稳定后,再按相同步骤处理 A。
这里还有几个不能省略的条件:
- 新旧版本同时运行时,动态配置必须对两者都兼容。
- 摘流后需要等待连接排空,特别是 WebSocket、SSE 和长请求。
- 剩余实例必须能够承担切换期间的流量。
- 实例本地限流计数、缓存、会话和插件状态,需要单独评估。
- 异常时能够及时把流量切回健康实例。
正式滚动阶段可以在验证兼容性后共享配置中心;前面的隔离演练则必须使用副本,二者不要混淆。若选择两套独立配置中心的蓝绿方案,还需要解决配置同步和切回时的一致性问题。
十、把一次升级变成可重复的流程
验收通过后,不要立刻删除旧镜像和备份。按事先约定的观察期,持续确认业务与监控稳定,再解除配置冻结、归档最终镜像标识和验收记录,并按保留策略清理制品。
一份简洁的升级检查清单,可以长期复用:
- [ ] 目标版本及全部跨越版本的兼容变更已核对。
- [ ] 镜像来源、版本、目标平台和校验记录明确。
- [ ] 线上静态配置、动态对象及 etcd 快照已备份。
- [ ] 快照副本已在隔离环境完成恢复验证。
- [ ] 正向、反向及边界业务用例均已回归。
- [ ] 控制面写入已冻结,维护窗口和回退阈值明确。
- [ ] 只重建目标网关,没有改动 etcd 或初始化数据。
- [ ] 新版验收通过,旧镜像与备份仍在保留期内。
- [ ] 最终状态已记录,后续运维使用的配置与实际运行一致。
一次可靠的网关升级,不是“把新版本启动起来”,而是让每一步都有证据、每次切换有边界、每种失败都有退路。