编程 ingress-nginx 退役四个月后:一半的 K8s 集群还在裸奔——Gateway API 迁移的完整工程实战

2026-07-31 04:53:01 +0800 CST views 6

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 状态还是 Runningkubectl 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-snippetauth-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"。
  • XBackend GEP 进入 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 里 apiVersiongateway.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-processeskeep-alive 等)

所以正确用法是:先跑工具拿到 70% 的骨架,然后拿第一节那张 annotation 统计表逐项对账。


五、Annotation 逐条翻译对照表

这是全文最实用的一节。以下映射基于 Gateway API v1.6 的 Standard 字段 + Envoy Gateway v1.8 的 Policy 扩展(其他实现的 Policy 名字不同,但思路一致)。

5.1 能直接映射到 Gateway API 标准字段的

ingress-nginx annotationGateway API 等价物
rewrite-target: / (前缀替换)filters[].urlRewrite.path.type: ReplacePrefixMatch
ssl-redirect: "true"单独一条 HTTP listener 的 HTTPRoute,挂 RequestRedirect filter(scheme: https, statusCode: 301
canary + canary-weightbackendRefs[].weight
canary-by-header / canary-by-header-valuematches[].headers(不同 header 走不同 rule)
canary-by-cookiematches[].headers 匹配 Cookie,或实现侧的 header 匹配扩展
use-regex + 正则 pathmatches[].path.type: RegularExpression(属于 Extended 支持,需确认实现)
proxy-read-timeouttimeouts.backendRequest
proxy-send-timeouttimeouts.backendRequest
(整体请求超时)timeouts.request
backend-protocol: GRPC换用 GRPCRoute
backend-protocol: HTTPSBackendTLSPolicy
cors-allow-origin 等一组filters[].cors(v1.6 已明确 allowCredentials 语义)
upstream-hash-by实现侧的 BackendTrafficPolicy 一致性哈希
service-upstreamGateway 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;

处理套路是分类拆解

  1. 改 header 的RequestHeaderModifier / ResponseHeaderModifier filter,直接标准化
  2. 按条件拒绝的 → 拆成独立的 HTTPRoute rule + RequestRedirect/实现侧的 direct response,或者干脆前移到 WAF
  3. 调 proxy 参数的 → 对应实现的 Policy
  4. 注入业务逻辑的(见过在 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):

实现最新版本特点
Traefikv3.7.9(2026-07-24)同时支持 Ingress / IngressRoute CRD / Gateway API,社区活跃
NGINX Gateway Fabricv2.6.7(2026-07-15)F5 官方,nginx 数据面,Gateway API 优先
APISIX Ingress Controller2.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 Gatewayv1.8.3(2026-07-22)EnvoyGateway API 原生,Policy 体系完整
Istio1.30.3(2026-07-16)Envoy已有服务网格时的自然延伸,支持 GAMMA(网格内路由)
Ciliumv1.20.0(2026-07-29)eBPF + Envoy网络插件与网关一体,四层走 eBPF
NGINX Gateway Fabricv2.6.7nginx想保留 nginx 数据面行为时的选择
Traefikv3.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 归零。不要把迁移计划建立在"社区会给我一个无痛方案"的假设上。

给还没动手的团队一份最小行动清单:

  1. 今天:跑那三条排查命令,确认自己在不在那 50% 里,把 annotation 统计表拉出来
  2. 本周:装 Gateway API v1.6.1 的 CRD(Standard 通道),跑一遍 ingress2gateway print,看看能自动转多少
  3. 两周内:选定实现,在非生产集群跑通一条完整链路(含 TLS、鉴权、灰度)
  4. 一个月内:并行部署到生产,跑全量影子比对
  5. 两个月内:DNS/LB 权重灰度,按 5/20/50/100 推进
  6. 三个月内:下线 ingress-nginx

四个月前官方说"你还剩两个月"。现在已经是负两个月了。Pod 还在 Running 不代表你安全,只代表还没被打到。

早点动手,别等到某个凌晨被 oncall 电话叫醒的时候,才想起这篇文章。

复制全文 生成海报 Kubernetes 云原生 Gateway API 网关 运维 安全

推荐文章

Vue 3 中的 Fragments 是什么?
2024-11-17 17:05:46 +0800 CST
Java环境中使用Elasticsearch
2024-11-18 22:46:32 +0800 CST
Requests库详细介绍
2024-11-18 05:53:37 +0800 CST
初学者的 Rust Web 开发指南
2024-11-18 10:51:35 +0800 CST
程序员茄子在线接单