ingress-nginx 退役四个月后:一半的 K8s 集群还在裸奔——Gateway API 迁移的完整工程实战
2026 年 1 月 29 日,Kubernetes 指导委员会(Steering Committee)和安全响应委员会(Security Response Committee)罕见地联名发了一篇博客。措辞之直白,在 Kubernetes 官方博客的历史上都不多见:
"To be abundantly clear: choosing to remain with Ingress NGINX after its retirement leaves you and your users vulnerable to attack. None of the available alternatives are direct drop-in replacements. This will require planning and engineering time. Half of you will be affected. You have two months left to prepare."
翻译过来就是:继续用 ingress-nginx,你和你的用户就是在等着被打。没有任何一个替代品是无痛平替。这需要规划和工程时间。你们中的一半会受影响。你还剩两个月。
现在是 2026 年 7 月底。那"两个月"过去四个月了。
按照官方引用的 Datadog 内部研究数据,约 50% 的云原生环境依赖 ingress-nginx。这个数字意味着,此刻仍有海量集群跑着一个不再发布任何版本、不再修任何 bug、不再打任何安全补丁的南北向流量入口。它不会自己崩,Pod 状态还是 Running,kubectl get ingress 也照样返回结果——正因为如此,很多团队"在被入侵之前根本不会知道自己受影响"。
这篇文章不写鸡汤,只写工程。我们从退役的技术根因讲起,拆开 Gateway API 的资源模型,把 ingress2gateway 的自动迁移边界摸清楚,逐条对照 annotation 到底怎么翻译,最后给出一套可以在生产上分批灰度、随时回滚的切换方案。
一、先做体检:你到底受不受影响
在讨论方案之前,先花 30 秒确认现状。官方给的检测命令是:
kubectl get pods --all-namespaces \
--selector app.kubernetes.io/name=ingress-nginx
但这条命令只覆盖标准 Helm chart 安装。实际生产环境里,很多集群是通过云厂商组件、KubeSphere/Rancher 这类平台、或者魔改 YAML 装进去的,label 未必标准。更可靠的排查方式是同时看三个维度:
# 1) 看 IngressClass 上挂的 controller 标识
kubectl get ingressclass -o custom-columns=\
'NAME:.metadata.name,CONTROLLER:.spec.controller'
# 期望看到:k8s.io/ingress-nginx ← 这就是社区版 ingress-nginx
# 2) 统计到底有多少条 Ingress 依赖它
kubectl get ingress -A -o json | jq -r '
.items[]
| select(.spec.ingressClassName=="nginx"
or (.metadata.annotations["kubernetes.io/ingress.class"]=="nginx"))
| "\(.metadata.namespace)/\(.metadata.name)"' | wc -l
# 3) 最关键:把 annotation 面盘出来
kubectl get ingress -A -o json | jq -r '
.items[].metadata.annotations // {}
| keys[]
| select(startswith("nginx.ingress.kubernetes.io/"))' \
| sort | uniq -c | sort -rn
第三条命令的输出,才是决定你迁移工作量的真正指标。我在几个中等规模集群(200~600 条 Ingress)上跑过,典型分布长这样:
412 nginx.ingress.kubernetes.io/ssl-redirect
287 nginx.ingress.kubernetes.io/proxy-body-size
203 nginx.ingress.kubernetes.io/rewrite-target
198 nginx.ingress.kubernetes.io/proxy-read-timeout
91 nginx.ingress.kubernetes.io/use-regex
64 nginx.ingress.kubernetes.io/backend-protocol
47 nginx.ingress.kubernetes.io/canary
31 nginx.ingress.kubernetes.io/auth-url
22 nginx.ingress.kubernetes.io/configuration-snippet
11 nginx.ingress.kubernetes.io/cors-allow-origin
这张表就是你的迁移清单。 排在前面的都好办,排在后面的 configuration-snippet、auth-url 才是真正会让你加班的东西。后面第五节会逐条给映射方案。
二、它为什么必须死:不是没人爱,是修不好了
很多人第一反应是"这么多人用的项目,怎么会没人维护"。这个问题值得认真回答,因为它决定了你不该指望有人 fork 出一个"社区续命版"来救你。
2.1 一两个人的项目,撑着半个云原生世界
官方声明里那句话很扎心:过去几年,这个项目"solely by one or two people working in their free time"——只有一两个人用业余时间在维护。而它承载的是全球一半云原生环境的南北向入口。
这不是"缺人",这是结构性的维护赤字。一个安全边界组件,代码量大、攻击面广、用户基数巨大、下游兼容包袱极重,却没有专职团队。任何一个 CVE 报过来,响应窗口都是以周计的。
2.2 真正的死因:配置模板拼接这条路走死了
ingress-nginx 的架构本质是一个配置生成器:控制器 watch Ingress/Service/Endpoints/Secret/ConfigMap,在内存里构建模型,然后渲染出一份 nginx.conf,再让 nginx 重载。为了避免频繁 reload,它在数据面塞了 lua-nginx-module,把 upstream 变更这类高频改动走 Lua 动态更新。
这个设计在 2017 年是聪明的。但它埋了一颗雷:annotation 的值最终会被拼接进 nginx 配置文本。
于是就有了 2025 年那组被称为 IngressNightmare 的漏洞(其中 CVE-2025-1974 是 CVSS 9.8 的未认证 RCE):攻击者能通过 admission webhook 的路径,把恶意配置片段注入 nginx 配置,进而在 controller Pod 里执行代码——而这个 Pod 通常有读取集群内全部 Secret 的权限。
2026 年 2 月又披露了新的一批,其中 CVE-2026-24512 是配置注入:rules.http.paths.path 这个 Ingress 字段可以被用来往 nginx 配置里注入内容。还有一个更阴的:如果配置了包含 HTTP 401/403 的默认自定义错误后端,而这个后端没有正确遵守 X-Code 响应头,那么带 auth-url 注解的 Ingress 即使认证失败也可能被访问——认证形同虚设。
官方声明里的这句话是全文技术含量最高的一句:
"the flexibility Ingress NGINX was designed with, that was once a boon, has become a burden that cannot be resolved."
曾经的灵活性,变成了无法化解的负债。 只要"用户可控字符串 → 拼接进配置文本 → 由特权进程解析执行"这条链路还在,同类漏洞就会源源不断。修不完,也没法从架构上根治——除非重写。
2.3 那个说好的接班人,自己先没了
2024 年 12 月,社区在 kubernetes-sigs 下开了一个新项目 InGate,定位是"an Ingress & Gateway API Controller",被寄予厚望——用现代架构重写一个同时支持两套 API 的控制器,给 ingress-nginx 用户一条平滑退路。
结果:这个项目现在躺在 kubernetes-retired/ingate,仓库描述前面挂着 [EOL] 标记,最后一次推送停在 2026 年 6 月 30 日,724 个 star 就此定格。
接班人比前任先走一步。 这件事的信号非常明确:不要等社区给你一个"官方无痛替代品",那个东西不会出现了。 官方声明里"None of the available alternatives are direct drop-in replacements"这句话,是字面意思。
三、Gateway API:不是换个壳,是换了一套权责模型
3.1 Ingress 的原罪:一个资源装下所有人的诉求
Ingress 最大的问题不是功能少,而是它把三类完全不同的人的配置塞进了同一个 YAML:
- 集群管理员关心:用哪个负载均衡器实现、监听哪些端口、证书从哪来
- 网关运维关心:域名归属、TLS 策略、跨命名空间授权
- 应用开发关心:我这个服务的路径怎么路由、超时多少、怎么灰度
在 Ingress 里,这些全在一个对象上。想给开发者放开"改自己服务的路由",就得把整个 Ingress 的写权限给出去——而 Ingress 上还挂着证书引用和全局注解。RBAC 在这里是失效的。
而"表达能力不足"的部分,社区的解法是加 annotation。加到最后,每家实现的 annotation 完全不兼容,nginx.ingress.kubernetes.io/* 有一百多个,换一个控制器等于全量重写。annotation 事实上变成了没有 schema、没有校验、没有版本管理的影子 API。
3.2 Gateway API 的三层拆分
Gateway API 由 SIG-Network 主导,2023 年底 GA。它把 Ingress 的职责拆成了三层:
| 资源 | 职责 | 归谁管 |
|---|---|---|
GatewayClass | 声明网关实现类型(相当于 StorageClass) | 基础设施提供方 |
Gateway | 一个具体的网关实例:监听端口、协议、TLS、允许哪些 Route 接入 | 集群/平台运维 |
HTTPRoute / GRPCRoute / TCPRoute / UDPRoute / TLSRoute | 具体的路由规则 | 应用开发者 |
ReferenceGrant | 跨命名空间引用的显式授权 | 被引用方命名空间的属主 |
关键在于这是有向的、需要双向同意的模型:Gateway 通过 allowedRoutes 声明"我允许哪些命名空间的哪些 Route 挂到我身上",Route 通过 parentRefs 声明"我要挂到哪个 Gateway"。两边都点头,路由才生效。跨命名空间引用 Service 或 Secret,还需要目标命名空间里存在一条 ReferenceGrant。
这套机制解决了 Ingress 时代一个真实的安全问题:A 团队在自己的命名空间里创建一条 Ingress,抢占了 B 团队的域名。在 Gateway API 里,域名归属由 Gateway 的 listener 定义,Route 只能在 Gateway 允许的 hostname 集合内做细分。
3.3 一个完整的例子
先看基础设施层,由平台团队维护:
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: eg
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-gw
namespace: infra
spec:
gatewayClassName: eg
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com" # 域名归属在这一层锁死
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: wildcard-example-com
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true" # 只有打了标签的 ns 能接入
kinds:
- kind: HTTPRoute
- kind: GRPCRoute
再看应用层,由业务团队自己维护,权限只需要本命名空间的 HTTPRoute:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: orders
namespace: shop
spec:
parentRefs:
- name: prod-gw
namespace: infra
sectionName: https
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /api/v1/orders
headers:
- name: x-api-version
value: "2" # 原生支持 header 匹配,不用 annotation
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: X-Route-Origin
value: gateway-api
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /orders # 结构化重写,不是正则
timeouts:
request: 30s
backendRequest: 25s
retry:
attempts: 3
codes: [502, 503, 504]
backoff: 100ms
backendRefs:
- name: orders-v2
port: 8080
weight: 90
- name: orders-canary
port: 8080
weight: 10 # 灰度是一等公民
对比一下:这份 YAML 在 ingress-nginx 时代要靠 rewrite-target + use-regex + canary + canary-weight + proxy-read-timeout + 一坨 configuration-snippet 才能拼出来,而且换个控制器全部作废。现在它们都是有 schema、有校验、跨实现可移植的字段。
3.4 Gateway API 的当前状态(截至 2026-07)
最新版本是 v1.6.1(2026-07-16 发布),v1.6.0 在 6 月 29 日发布。这个版本有几个值得注意的变化:
- TCPRoute 和 UDPRoute 双双 GA,进入
v1版本,v1alpha2标记废弃。这意味着 Gateway API 现在能正经承接四层流量了,不再是"只能做 HTTP"。 XBackendGEP 进入 Experimental,为后端资源的统一抽象铺路。- HTTPRoute 重试校验收紧:
retry.codes必须唯一,retry.attempts必须 ≥ 1。 - BackendTLSPolicy 可以和其他 Route 类型组合使用了,不再局限于 HTTPRoute。
- CA 引用数量上限从 8 提到 16,Gateway infrastructure 对象允许 16 个 annotation。
- ListenerSet 相关的一致性测试补齐(跨命名空间 Route 默认不允许等行为被明确)。
- 文档从 MkDocs 迁到 Docsy,并上线了一个配置向导
/wizard。
另外一个信号:实验性的 SessionPersistence API 移除了 idleTimeout 字段。如果你在做会话保持相关的方案,注意这个 breaking change。
升级建议:CRD 装 v1.6.1,但业务 YAML 里 apiVersion 用 gateway.networking.k8s.io/v1,只用 Standard 通道的字段。Experimental 通道的东西(ListenerSet、XBackend、部分 Policy)可以试,但别放核心链路。
四、ingress2gateway:能帮你干 70%,剩下 30% 是你的活
社区提供了官方迁移工具 ingress2gateway,最新版本 v1.2.0(2026-07-07)。它的定位很清楚:把 Ingress 资源翻译成 Gateway API 资源,包括一部分厂商 annotation 的转换。
4.1 装和跑
# 方式一:Go install
go install github.com/kubernetes-sigs/ingress2gateway@v1.2.0
# 方式二:Homebrew
brew install ingress2gateway
最基本的用法是直接读集群:
# 全集群转换,输出 Gateway API YAML
ingress2gateway print --all-namespaces > gw-api-generated.yaml
# 只转某个 namespace,并指定 provider(识别 nginx annotation)
ingress2gateway print \
--namespace shop \
--providers ingress-nginx \
--output-format yaml > shop-routes.yaml
# 从文件转(CI 里做 diff 检查很有用)
ingress2gateway print \
--input-file ./manifests/ingress.yaml \
--providers ingress-nginx
一个非常实用的技巧:把它接进 CI,对 Ingress 目录做持续转换并 diff,这样在迁移过渡期里,任何人新增的 Ingress 都会立刻暴露"这条规则转不过去"的问题:
#!/usr/bin/env bash
# ci/check-gateway-migratable.sh
set -euo pipefail
OUT=$(mktemp)
ingress2gateway print \
--input-file ./deploy/ingress/ \
--providers ingress-nginx > "$OUT" 2>/tmp/i2g.err || true
# 工具对无法转换的 annotation 会输出警告到 stderr
if grep -qiE 'unsupported|not supported|ignoring' /tmp/i2g.err; then
echo "❌ 检测到无法自动迁移的 Ingress 配置:"
grep -iE 'unsupported|not supported|ignoring' /tmp/i2g.err
echo "请手工补 HTTPRoute/Policy,或联系平台团队。"
exit 1
fi
echo "✅ 全部 Ingress 可自动转换为 Gateway API"
4.2 它的能力边界
必须说清楚:ingress2gateway 是个翻译器,不是迁移方案。 它能可靠处理的是:
- host / path 规则 →
HTTPRoute.rules[].matches - TLS Secret 引用 →
Gateway.listeners[].tls.certificateRefs - backend service/port →
backendRefs - 一部分标准化程度高的 nginx annotation(重写、canary 权重等)
它处理不了的是:
configuration-snippet/server-snippet:这是任意 nginx 配置文本,没有语义等价物auth-url/auth-signin外部认证:需要换成各实现自己的SecurityPolicy/AuthenticationFilter- 复杂正则重写:
rewrite-target: /$2配合use-regex的捕获组语义 - 各种
proxy-*缓冲、限流、mTLS 后端等,落在实现特定的 Policy 上 - ConfigMap 里的全局 nginx 调优参数(
worker-processes、keep-alive等)
所以正确用法是:先跑工具拿到 70% 的骨架,然后拿第一节那张 annotation 统计表逐项对账。
五、Annotation 逐条翻译对照表
这是全文最实用的一节。以下映射基于 Gateway API v1.6 的 Standard 字段 + Envoy Gateway v1.8 的 Policy 扩展(其他实现的 Policy 名字不同,但思路一致)。
5.1 能直接映射到 Gateway API 标准字段的
| ingress-nginx annotation | Gateway API 等价物 |
|---|---|
rewrite-target: / (前缀替换) | filters[].urlRewrite.path.type: ReplacePrefixMatch |
ssl-redirect: "true" | 单独一条 HTTP listener 的 HTTPRoute,挂 RequestRedirect filter(scheme: https, statusCode: 301) |
canary + canary-weight | backendRefs[].weight |
canary-by-header / canary-by-header-value | matches[].headers(不同 header 走不同 rule) |
canary-by-cookie | matches[].headers 匹配 Cookie,或实现侧的 header 匹配扩展 |
use-regex + 正则 path | matches[].path.type: RegularExpression(属于 Extended 支持,需确认实现) |
proxy-read-timeout | timeouts.backendRequest |
proxy-send-timeout | timeouts.backendRequest |
| (整体请求超时) | timeouts.request |
backend-protocol: GRPC | 换用 GRPCRoute |
backend-protocol: HTTPS | BackendTLSPolicy |
cors-allow-origin 等一组 | filters[].cors(v1.6 已明确 allowCredentials 语义) |
upstream-hash-by | 实现侧的 BackendTrafficPolicy 一致性哈希 |
service-upstream | Gateway API 默认走 Endpoint,需要走 ClusterIP 时用实现侧配置 |
ssl-redirect 的写法值得单独给个完整例子,因为它是出现频率最高的注解:
# Gateway 上开两个 listener
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: prod-gw
namespace: infra
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
hostname: "*.example.com"
allowedRoutes: { namespaces: { from: All } }
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs: [{ kind: Secret, name: wildcard-example-com }]
allowedRoutes: { namespaces: { from: All } }
---
# 一条全局跳转 Route,替代所有 ssl-redirect annotation
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: force-https
namespace: infra
spec:
parentRefs:
- name: prod-gw
sectionName: http # 只挂在 80 端口
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
412 条 ssl-redirect annotation,被一条 HTTPRoute 干掉了。 这就是从"每条 Ingress 各自声明"到"平台统一策略"的收益。
5.2 需要落到实现侧 Policy 的
以 Envoy Gateway 为例,proxy-body-size 和限流这类要写成 BackendTrafficPolicy / ClientTrafficPolicy:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: body-size-and-conn
namespace: infra
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: prod-gw
connection:
bufferLimit: 32768
# 对应 nginx.ingress.kubernetes.io/proxy-body-size: 50m
# (具体字段名以所用版本文档为准,思路是 Policy 附着到 Gateway/Route)
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: orders-limits
namespace: shop
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: orders
rateLimit:
type: Local
local:
rules:
- limit: { requests: 1000, unit: Second }
circuitBreaker:
maxParallelRequests: 512
maxPendingRequests: 128
healthCheck:
active:
timeout: 1s
interval: 5s
unhealthyThreshold: 3
healthyThreshold: 2
type: HTTP
http: { path: /healthz, expectedStatuses: [200] }
这里有个必须提前想清楚的架构问题:Policy 是各实现自定义的 CRD,它们不可移植。也就是说,你在 Gateway API 这一层拿回了可移植性,但在 Policy 这一层又交回去了一部分。
我的建议是:把 Policy 收拢到平台团队,业务只写 Route。 业务侧的 HTTPRoute 保持 100% 标准,Policy 由平台以模板方式统一下发。这样将来换实现时,需要重写的只有平台维护的那几十个 Policy,而不是几百个业务 YAML。
5.3 configuration-snippet:没有银弹,只能拆
这是最难的一类。configuration-snippet 里通常塞的是:
# 典型的 snippet 内容
more_set_headers "X-Frame-Options: SAMEORIGIN";
if ($request_uri ~* "^/admin") {
return 403;
}
set $upstream_keepalive on;
proxy_set_header X-Real-Port $remote_port;
处理套路是分类拆解:
- 改 header 的 →
RequestHeaderModifier/ResponseHeaderModifierfilter,直接标准化 - 按条件拒绝的 → 拆成独立的 HTTPRoute rule +
RequestRedirect/实现侧的 direct response,或者干脆前移到 WAF - 调 proxy 参数的 → 对应实现的 Policy
- 注入业务逻辑的(见过在 snippet 里写 Lua 做鉴权的)→ 老老实实拆成一个 sidecar 或独立服务,用
ExtAuth挂上去
值得注意的是:在 ingress-nginx 后期版本里,snippet 因为安全原因已经默认被禁用了(需要显式在 ConfigMap 里开 allow-snippet-annotations: "true")。如果你还在大量依赖 snippet,说明你的集群大概率也开着这个开关——这本身就是一个应该立刻处理的风险点。
5.4 外部认证:auth-url 的现代写法
# Envoy Gateway 的 SecurityPolicy,替代 auth-url / auth-signin
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: ext-auth
namespace: shop
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: admin-console
extAuth:
http:
backendRefs:
- name: authz-svc
port: 9000
path: /verify
headersToBackend:
- x-user-id
- x-user-role
对比 auth-url 的好处在于:认证失败的行为由 ext_authz 协议定义,而不是靠后端服务"记得"返回正确的 X-Code 头。前面提到的那个 CVE(错误后端不遵守 X-Code 导致认证被绕过),在这个模型下从根上不存在。
六、选型:三条路,别选错
路线 A:继续用 Ingress API,换一个还在维护的实现
适用:Ingress 数量大、annotation 用得浅、团队没有余量做架构改造、有明确的一年内交付压力。
候选与现状(截至 2026-07-31):
| 实现 | 最新版本 | 特点 |
|---|---|---|
| Traefik | v3.7.9(2026-07-24) | 同时支持 Ingress / IngressRoute CRD / Gateway API,社区活跃 |
| NGINX Gateway Fabric | v2.6.7(2026-07-15) | F5 官方,nginx 数据面,Gateway API 优先 |
| APISIX Ingress Controller | 2.1.0(2026-06-01) | 插件生态丰富,Ingress + Gateway API 双支持 |
| 云厂商托管网关 | — | ACK/TKE/EKS 各自的 APIG 方案,通常提供 annotation 兼容层 |
注意:换实现不等于零改动。nginx.ingress.kubernetes.io/* 这个 annotation 前缀是 ingress-nginx 专有的,换到 Traefik 就得改成 traefik.ingress.kubernetes.io/*,且语义不完全一致。云厂商的兼容层通常覆盖度最高,但你换来的是供应商锁定。
这条路的本质是把问题往后推。 Ingress API 本身没有被废弃(networking.k8s.io/v1 仍然稳定),但整个生态的投入重心已经彻底转向 Gateway API。两年后你还得再迁一次。
路线 B:迁到 Gateway API
适用:想一次到位、有平台团队、需要多租户权责隔离、需要正经的流量治理(灰度/镜像/重试/熔断)。
| 实现 | 最新版本 | 数据面 | 定位 |
|---|---|---|---|
| Envoy Gateway | v1.8.3(2026-07-22) | Envoy | Gateway API 原生,Policy 体系完整 |
| Istio | 1.30.3(2026-07-16) | Envoy | 已有服务网格时的自然延伸,支持 GAMMA(网格内路由) |
| Cilium | v1.20.0(2026-07-29) | eBPF + Envoy | 网络插件与网关一体,四层走 eBPF |
| NGINX Gateway Fabric | v2.6.7 | nginx | 想保留 nginx 数据面行为时的选择 |
| Traefik | v3.7.9 | 自研 Go | 轻量,中小集群友好 |
选型的关键判据不是"谁功能多",而是你的团队能不能运维它的数据面。Envoy 的调试心智负担明显高于 nginx:xDS 配置、cluster/listener/route 三层抽象、config_dump 的读法,都是要学的。如果你的 SRE 团队对 nginx 熟到能背 error_log 格式,但从没看过 Envoy 的 access log,那 NGF 可能比 Envoy Gateway 更适合作为第一步。
路线 C:借退役的机会,重构入口架构
适用:本来就有 mesh 或多集群规划、AI 推理流量占比在上升。
一个正在快速变化的场景是 LLM 推理网关。传统 HTTP 网关的负载均衡假设是"请求耗时基本均匀",而推理请求的耗时可以差两个数量级,还涉及 KV cache 亲和性、模型多版本路由、token 级别的限流。Gateway API 生态里已经有针对这个场景的扩展方向,Higress、APISIX、Envoy AI Gateway 等项目都在这个赛道上投入。
如果你的集群同时承载传统业务和 AI 推理,这次退役是把"业务网关"和"AI 网关"分开规划的好时机——它们的流量特征、限流维度、可观测指标都不一样,硬塞进一个网关只会互相拖累。
七、生产切换:怎么做到不停服、可回滚
理论讲完了,讲怎么落地。核心原则只有一条:新旧双栈并行,用流量比例做灰度,任何时刻都能一键切回。
阶段 0:并行部署,零流量
新网关和 ingress-nginx 完全独立部署,各自持有独立的 LoadBalancer IP:
# 装 Gateway API CRD(Standard 通道)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.1/standard-install.yaml
# 装 Envoy Gateway
helm install eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.8.3 -n envoy-gateway-system --create-namespace
# 确认 GatewayClass 就绪
kubectl get gatewayclass eg -o jsonpath='{.status.conditions[?(@.type=="Accepted")].status}'
此时集群里跑着两套入口,DNS 还全指向老的。这个阶段可以持续几周,没有任何风险。
阶段 1:影子验证
把生成的 HTTPRoute 全部 apply 上去,然后用请求镜像做无损验证。Gateway API 的 RequestMirror filter 正好干这个——不过要注意,镜像要配在老网关那边把流量复制给新网关的后端,或者更简单的做法:直接对新网关的 IP 打压测流量,用真实的 host 头。
# 拿到新网关的地址
NEW_GW=$(kubectl get svc -n envoy-gateway-system \
-l gateway.envoyproxy.io/owning-gateway-name=prod-gw \
-o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}')
# 用真实域名的 Host 头,逐条比对新旧响应
while read -r host path; do
old=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $host" "https://$OLD_GW$path" -k)
new=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $host" "https://$NEW_GW$path" -k)
[ "$old" = "$new" ] || echo "MISMATCH $host$path old=$old new=$new"
done < routes.txt
这个脚本我强烈建议跑全量。最常见的坑就是重写规则的边界行为不一致:rewrite-target: /$2 这种带捕获组的正则,翻译成 ReplacePrefixMatch 之后,对于 /api/ 和 /api 这种带不带尾斜杠的情况,行为可能不同。
另一个高频坑:ingress-nginx 的路径匹配默认是前缀匹配且不区分是否在路径分段边界上,而 Gateway API 的 PathPrefix 明确规定必须在 / 分段边界匹配。也就是说 /foo 这条 PathPrefix 规则,在 Gateway API 下不会匹配 /foobar,但在某些 ingress-nginx 配置下会。这个差异会让一批"以前能访问"的 URL 突然 404。
阶段 2:DNS 权重灰度
最稳的切流方式是在 DNS 层做权重,而不是改 Ingress。
api.example.com A <OLD_GW_IP> weight=95
api.example.com A <NEW_GW_IP> weight=5
按 5% → 20% → 50% → 100% 推进,每档观察至少一个完整业务周期(含峰值时段)。回滚就是把权重打回 0,DNS TTL 设短一点(60s)。
如果你的入口前面有云厂商的 SLB/ALB,更好的做法是在 LB 后端组里同时挂新旧两组节点,用 LB 的权重做灰度——生效速度比 DNS 快得多,回滚是秒级。
阶段 3:关键指标看什么
切流期间必须盯的四组指标:
# 1) 新旧网关的 5xx 率对比(这是唯一的红线指标)
sum(rate(envoy_http_downstream_rq_xx{envoy_response_code_class="5"}[5m]))
/ sum(rate(envoy_http_downstream_rq_total[5m]))
# 2) P99 延迟,注意 TLS 握手开销的变化
histogram_quantile(0.99,
sum by (le) (rate(envoy_http_downstream_rq_time_bucket[5m])))
# 3) 上游连接池:Envoy 的连接复用行为和 nginx 差别很大
envoy_cluster_upstream_cx_active
envoy_cluster_upstream_cx_connect_fail
# 4) 配置收敛延迟:Route 变更到数据面生效的时间
envoy_control_plane_connected_state
第 3 条特别值得展开。nginx 和 Envoy 的上游连接管理模型不同:nginx 的 keepalive 连接池是 per-worker 的,Envoy 是 per-cluster 且默认 HTTP/1.1 也会做连接复用。切过去之后,后端服务看到的连接数分布会变,如果你的后端有基于连接数的限流或者连接池配置(比如数据库连接池按上游连接数估算的),可能会踩坑。
第 4 条是架构层面的本质改善:ingress-nginx 的模型是"改配置 → 渲染 nginx.conf → reload",reload 期间有 worker 进程新旧共存,长连接会被拖着;Envoy 走 xDS 增量推送,配置变更不需要重启进程,也不会断连接。在一个每天发布几十次的集群里,这个差别是实打实的可用性提升。
阶段 4:清理
流量 100% 切走并稳定运行两周后:
# 先缩容不删除,留一周后悔窗口
kubectl scale deploy ingress-nginx-controller -n ingress-nginx --replicas=0
# 确认没有残留引用
kubectl get ingress -A -o json | jq -r '
.items[] | select(.spec.ingressClassName=="nginx")
| "\(.metadata.namespace)/\(.metadata.name)"'
# 最后再删
helm uninstall ingress-nginx -n ingress-nginx
八、几个容易翻车的细节
1. WebSocket 和长连接。 ingress-nginx 默认对 Upgrade 请求做透传,超时受 proxy-read-timeout 控制。迁到 Gateway API 后,timeouts.backendRequest 会作用在整个请求上——如果你设了 30s,WebSocket 连接会在 30 秒后被掐断。正确做法是给 WebSocket 路径单独一条 rule,不设 backendRequest 超时,或者用实现侧的 idle timeout。
2. 大文件上传。 proxy-body-size: 0(不限制)在 Envoy 侧没有直接等价物,Envoy 默认有 buffer 上限。需要显式配置流式转发,否则大文件上传会 413。
3. 客户端真实 IP。 X-Forwarded-For 的处理逻辑在两边不同。ingress-nginx 靠 use-forwarded-headers + proxy-real-ip-cidr,Envoy Gateway 用 ClientTrafficPolicy.clientIPDetection。这个配错了不会报错,只会让你的风控和日志里的 IP 全是网关 IP,非常隐蔽。
4. 证书管理。 cert-manager 对 Gateway API 的支持是通过 Gateway 的 annotation(cert-manager.io/cluster-issuer)实现的,和 Ingress 的用法类似但不完全一样。迁移时要确认自动续期链路是通的,别等证书过期才发现。
5. 别在过渡期让两个网关同时持有同一个域名的证书签发权。 ACME HTTP-01 挑战会打架,导致续期失败。过渡期建议用 DNS-01,或者把证书签发固定在一侧。
6. ReferenceGrant 忘了配。 跨命名空间引用 Service 或 TLS Secret 时,没有 ReferenceGrant 会静默失败——Route 的 status 里会有 ResolvedRefs: False,但如果你不看 status,表现就是"路由不生效但也没报错"。养成 apply 之后看 status 的习惯:
kubectl get httproute orders -n shop -o jsonpath='{range .status.parents[*]}{.conditions[*].type}={.conditions[*].status} {end}'
九、总结:这不是一次升级,是一次架构还债
回头看整件事,ingress-nginx 的退役其实讲了三个道理。
第一,"用户多"不等于"可持续"。 半个云原生世界跑在一两个志愿者的业余时间上,这个状态持续了好几年。开源基础设施的可持续性问题,不会因为使用量大而自动解决——反而使用量越大,维护者的压力和责任越不对等。你依赖的每一个开源组件,都值得问一句:它有几个活跃维护者?
第二,把用户输入拼进配置文本,这条路在安全上没有未来。 ingress-nginx 死于设计而非死于失误。任何"用户可控字符串 → 模板渲染 → 特权进程解析"的架构,最终都会走到同一个死胡同。Gateway API 用强类型 CRD 替代 annotation 字符串,本质上就是在这一层做了根治。
第三,接班人也会死。 InGate 从立项到 EOL 一年半,724 个 star 归零。不要把迁移计划建立在"社区会给我一个无痛方案"的假设上。
给还没动手的团队一份最小行动清单:
- 今天:跑那三条排查命令,确认自己在不在那 50% 里,把 annotation 统计表拉出来
- 本周:装 Gateway API v1.6.1 的 CRD(Standard 通道),跑一遍
ingress2gateway print,看看能自动转多少 - 两周内:选定实现,在非生产集群跑通一条完整链路(含 TLS、鉴权、灰度)
- 一个月内:并行部署到生产,跑全量影子比对
- 两个月内:DNS/LB 权重灰度,按 5/20/50/100 推进
- 三个月内:下线 ingress-nginx
四个月前官方说"你还剩两个月"。现在已经是负两个月了。Pod 还在 Running 不代表你安全,只代表还没被打到。
早点动手,别等到某个凌晨被 oncall 电话叫醒的时候,才想起这篇文章。