Kubernetes Gateway API 生产迁移实战:从 Ingress 到 Gateway API 的 30 天记录

2026-05-21 21:11 Kubernetes Gateway API 生产迁移实战:从 Ingress 到 Gateway API 的 30 天记录已关闭评论

Kubernetes Gateway API 生产迁移实战:从 Ingress 到 Gateway API 的 30 天记录

如果你正在考虑把生产环境从 Ingress NGINX 迁移到 Gateway API,我的建议是:现在就做,但别全量切——用混合模式跑一个月再说。

过去 30 天,我把一个日活 50 万的电商平台从 Ingress NGINX 迁移到了 Gateway API(Istio 作为实现)。中间炸了 3 次灰度环境、回滚了 2 次、重构了 1 轮 TLS 配置。这篇文章记录了迁移过程中的每一个关键决策和踩坑记录。

为什么要迁移?不是因为"新"

我们原来的架构很简单:Ingress NGINX + 手动维护的 ingress.yaml,跑了两年没出过大事。

推动迁移的真正原因是三个痛到不能再忍的问题:

  1. 流量路由能力太弱 — 想按 header 灰度发布?Ingress NGINX 需要装一堆 annotation,不同版本之间还不兼容
  2. 配置分散无法复用 — 每个服务的超时、重试、CORS 策略各自写在不同的 ingress 里,改一个全局策略要改 30 多个文件
  3. TLS 证书管理混乱 — 多个 ingress 引用同一个 Secret,谁在用什么证书只能靠 grep

Gateway API 的核心价值不在于"新",而在于它把路由规则从实现解耦了出来。用一个 HTTPRoute 就能定义流量规则,底层可以是 Envoy、HAProxy 或者任何兼容实现。

第一步:环境评估与目标架构

迁移前我做的第一件事不是写 YAML,而是画了一张目标架构图:

用户请求
    │
    ▼
╔══════════════════════════╗
║    Gateway (入口网关)      ║  ← 统一 TLS 终结 + 域名管理
╠══════════════════════════╣
║    HTTPRoute (路由规则)    ║  ← 按 host/path/header 分发
╠══════════════════════════╣
║    |- Service A (v1)    ║
║    |- Service A (v2)    ║  ← 金丝雀发布
║    |- Service B         ║
╚══════════════════════════╝

环境信息:

  • Kubernetes: v1.28(Gateway API 需要 v1.26+)
  • 当前网关: Ingress NGINX v1.8
  • 目标实现: Istio 1.20(内置 Gateway API 支持)
  • 服务数量: 42 个微服务

我选了 Istio 而不是独立部署 Envoy Gateway,因为团队已经在用 Istio 做服务网格,复用 Sidecar 可以减少运维成本。如果你没有网格需求,独立部署 Envoy Gateway 配置更轻量。

第二步:Gateway API 资源安装

Gateway API 不是默认安装的,需要单独装 CRD。这里我踩了第一个坑。

# 安装标准 Gateway API CRD
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml

踩坑 #1: 别用 experimental-install.yaml——除非你确定需要 BackendTLSPolicyRouteNamespacePolicy。我一开始装了 experimental,CRD 版本不兼容导致 Istio 无法正常 reconcile。重装 standard 才解决。

安装完后验证 CRD 是否就绪:

kubectl get crd | grep gateway
# 应该看到: gateways.gateway.networking.k8s.io
#           httproutes.gateway.networking.k8s.io
#           referencegrants.gateway.networking.k8s.io

同时需要启用 Istio 的 Gateway API 支持:

# 安装 Istio 时启用 Gateway API
istioctl install --set profile=default -y \
  --set values.pilot.env.PILOT_ENABLE_GATEWAY_API=true
  
# 如果是已有 Istio 集群,直接更新配置
istioctl upgrade --set values.pilot.env.PILOT_ENABLE_GATEWAY_API=true

第三步:定义 Gateway——入口网关

这是最紧要的一步。Gateway 资源替代了原来 Ingress NGINX 的 nginx-ingress Service + ConfigMap 组合。

原来用 Ingress NGINX 的方式:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.example.com
    - shop.example.com
    secretName: wildcard-tls
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /v1
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 8080

迁移到 Gateway API 后:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: main-gateway
  namespace: istio-system  # 关键:Gateway 可以跨 namespace
spec:
  gatewayClassName: istio
  listeners:
  - name: https-api
    hostname: "api.example.com"
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate
      certificateRefs:
      - name: wildcard-tls
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            gateway-access: "allowed"
  - name: https-shop
    hostname: "shop.example.com"
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate
      certificateRefs:
      - name: wildcard-tls
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            gateway-access: "allowed"
  - name: http-redirect  # 强制 HTTP → HTTPS 跳转
    hostname: "*.example.com"
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: All

关键变化: 原来 Ingress 里每个域名都要单独写 TLS,Gateway 里可以统一管理。allowedRoutes 是全新的安全边界——你可以精确控制哪些 namespace 能挂载路由,避免跨 namespace 的配置污染。

关于 gatewayClassName

istio 这个 gatewayClassName 从哪来的?来自 Istio 部署时自动注册的 GatewayClass 资源:

kubectl get gatewayclass
# NAME     CONTROLLER                    ACCEPTED   AGE
# istio    istio.io/gateway-controller   True       30d

如果用非 Istio 的实现,会看到不同的 GatewayClass(比如 envoy-gateway)。

第四步:编写 HTTPRoute——真正的路由逻辑

这是迁移中最耗时的部分。每个原来的 Ingress 规则都要拆解成对应的 HTTPRoute

基础路由(简单替换)

Ingress:

spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /v1/users
        pathType: Prefix
        backend:
          service:
            name: user-service
            port:
              number: 8080

HTTPRoute:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: user-service-route
  namespace: user-svc
  labels:
    gateway-access: "allowed"   # ← 与 Gateway 的 namespace Selector 匹配
spec:
  parentRefs:
  - name: main-gateway
    namespace: istio-system     # 引用跨 namespace 的 Gateway
    sectionName: https-api      # 只绑定到 https-api 这个 listener
  hostnames:
  - "api.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /v1/users
    backendRefs:
    - name: user-service
      port: 8080

灰度发布(Ingress 做不到的事)

这是 Gateway API 最让我惊艳的特性——原生支持权重路由,不需要任何 annotation 黑魔法。

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: order-service-release
  namespace: order-svc
  labels:
    gateway-access: "allowed"
spec:
  parentRefs:
  - name: main-gateway
    namespace: istio-system
  hostnames:
  - "api.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /v1/orders
    backendRefs:
    - name: order-service-v1
      port: 8080
      weight: 90              # ← 90% 流量
    - name: order-service-v2
      port: 8080
      weight: 10              # ← 10% 流量做灰度

踩坑 #2: weights 加起来不需要等于 100。如果总和是 90+10=100,按比例分配。但如果 sum 不等于 100,Gateway API 会自动归一化。我第一次写的时候写了 80 和 20,以为会报错——结果正常工作,只是变成了 80/20 而已。

Header/Query 路由(真正的灰度发布)

权重还不够,我还需要按用户属性分流:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: order-service-canary
  namespace: order-svc
  labels:
    gateway-access: "allowed"
spec:
  parentRefs:
  - name: main-gateway
    namespace: istio-system
  hostnames:
  - "api.example.com"
  rules:
  - matches:
    - headers:
      - name: "X-User-Group"
        value: "beta"          # 内测用户走 v2
    backendRefs:
    - name: order-service-v2
      port: 8080
  - matches:
    - queryParams:
      - name: "feature"
        value: "new-checkout"  # URL 参数触发新版本
    backendRefs:
    - name: order-service-v2
      port: 8080
  - backendRefs:                # 兜底:默认走 v1
    - name: order-service-v1
      port: 8080

对比原来 Ingress NGINX:

同样的效果,原来需要装 nginx.ingress.kubernetes.io/canary-by-header 之类的 annotation,而且 Ingress NGINX 的 canary 实现有个大坑——不能同时支持按 header + 按权重。一个 ingress 要么是 canary 模式要么不是。Gateway API 的 matches 数组天然支持多条匹配规则,完美解决这个问题。

第五步:TLS 管理的重构

原来 Ingress NGINX 时代,TLS 配置散落在每个 Ingress 里:

# 找"所有"引用了某个证书的 ingress
grep -r "secretName: wildcard-tls" ./ingresses/

迁移到 Gateway API 后,TLS 只在一个地方定义:Gateway 资源的 listeners 里。HTTPRoute 不需要也不应该关心证书。

但有个隐藏的坑——跨 namespace 引用证书

# Gateway 在 istio-system
# 但证书 Secret 在 cert-manager 的 namespace
# Gateway 默认只能引用同 namespace 的 Secret

# 方案 1:把 Secret 复制到 istio-system(不推荐,但最简单)
kubectl get secret wildcard-tls -n cert-manager -o yaml \
  | sed 's/namespace: cert-manager/namespace: istio-system/' \
  | kubectl apply -f -

# 方案 2:使用 ReferenceGrant(推荐)
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-tls-ref
  namespace: cert-manager
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: istio-system
  to:
  - group: ""
    kind: Secret

建议:我最终选了方案 1(复制 Secret),不是 ReferenceGrant 不好——而是我们的 cert-manager 自动签发证书后本来就会 push 到多个 namespace,复制到 istio-system 已经是现有流程的一部分,不需要额外维护 ReferenceGrant。

第六步:平滑迁移——双跑阶段

全量切换风险太高。我的策略是:Ingress 和 Gateway API 并行运行,逐步迁移流量。

阶段 1: 只有 Ingress(旧)
阶段 2: Ingress + Gateway(灰度服务走 Gateway)
阶段 3: 全部走 Gateway,Ingress 保留作为兜底
阶段 4: 下线 Ingress

阶段 2 的实现

DNS 层面,我把部分域名指向了 Gateway 的 Load Balancer,其他域名继续走 Ingress:

# Gateway 的 LB 地址
kubectl get svc -n istio-system istio-gateway-main \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
# 输出: 1.2.3.4

# 修改 DNS,把灰度测试域名指向新 LB
# api-canary.example.com → 1.2.3.4 (Gateway)
# api.example.com        → 5.6.7.8 (Ingress,保持不变)

然后在 Gateway 端配置 api-canary.example.com 的路由,验证通过后把线上域名切过来。

流量镜像(实时验证不伤害用户)

这是 Gateway API 的另一个杀手级特性——流量镜像。把线上请求复制一份发到新集群,但不影响真实响应:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: order-service-mirror
  namespace: order-svc
  labels:
    gateway-access: "allowed"
spec:
  parentRefs:
  - name: main-gateway
    namespace: istio-system
  hostnames:
  - "api.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /v1/orders
    backendRefs:
    - name: order-service-v1       # 返回真实响应
      port: 8080
    filters:
    - type: RequestMirror
      requestMirror:
        backendRef:
          name: order-service-v2   # 流量镜像到 v2,但响应丢弃
          port: 8080

踩坑 #3: 流量镜像不会复制 response,但 v2 如果有写操作(数据库写入),会在镜像流量中真实执行!我镜像期间发现 v2 的数据库里多了测试数据,排查了半天才发现是镜像流量导致的双写。镜像只适合读操作链路验证,写操作的服务不要用镜像。

第七步:监控与验证——如何确认迁移成功

流量切过来之后,我要确认三件事:连通性、延迟、错误率。

1. 连通性验证

#!/bin/bash
# check-routes.sh
GATEWAY_IP="1.2.3.4"
DOMAINS=("api.example.com/healthz" "shop.example.com/healthz")

for domain in "${DOMAINS[@]}"; do
  status=$(curl -o /dev/null -s -w "%{http_code}" \
    -H "Host: ${domain%%/*}" \
    https://$GATEWAY_IP/${domain#*/} \
    --resolve "${domain%%/*}:443:$GATEWAY_IP")
  
  if [ "$status" -eq 200 ]; then
    echo "✅ $domain -> $status"
  else
    echo "❌ $domain -> $status"
  fi
done

2. 延迟对比

# 测试旧网关
curl -w "ingress: %{time_total}s\n" -o /dev/null -s https://old-ingress/api/v1/orders
  
# 测试新网关
curl -w "gateway: %{time_total}s\n" -o /dev/null -s https://new-gateway/api/v1/orders

我的实测数据(50 次采样中位数):

场景 Ingress NGINX Gateway API (Istio) 差异
简单路由 12ms 14ms +2ms(误差范围内)
Header 路由 13ms 15ms +2ms
权重路由 12ms 16ms +4ms(多一次匹配)
流量镜像 N/A 18ms 主链路增加约 0.5ms

Gateway API 延迟比 Ingress NGINX 略高 2-4ms,但对外部请求来说基本不可感知。如果差异超过 10ms,建议检查 Istio 的 Sidecar 配置。

3. 错误率告警

迁移期间我在 Prometheus 配了一条专门的告警:

# PrometheusRule
- alert: GatewayAPIErrorRate
  expr: |
    sum(rate(istio_requests_total{
      reporter="gateway",
      response_code=~"5.."
    }[5m])) / 
    sum(rate(istio_requests_total{
      reporter="gateway"
    }[5m])) > 0.01
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "Gateway API 5xx 率超过 1%"

第八步:回滚方案——没有如果

你一定会遇到需要回滚的情况。关键问题不是"要不要回滚",而是 "多快能回滚"

我的回滚策略分三级:

Level 1: DNS 切回 Ingress
  耗时: 5 分钟(DNS TTL 设了 60s)
  操作: 修改 DNS 记录

Level 2: 直接改回 IngressClass
  耗时: 1 分钟
  操作: kubectl apply 旧的 Ingress 资源

Level 3: 回滚 Gateway API CRD 版本
  耗时: 30 分钟
  操作: kubectl apply -f 旧版 CRD

实际上线时触发了 2 次 Level 2 回滚,都是因为 HTTPRoute 的匹配规则写错了导致某些 API 返回 404。

# 快速回滚脚本
kubectl apply -f backups/ingress/ --recursive
kubectl delete httproutes --all -n problematic-namespace

经验: 每个 HTTPRoute 切到 Gateway 之前,保留原始 Ingress 文件。不要迁移完就删,备份放 backups/ingress/ 目录下,回滚时直接 apply 回来。

踩坑汇总

30 天迁移过程中,最痛的几个坑:

# 症状 根因 解决方案
1 CRD 版本不兼容 Istio 无法 reconcile 装了 experimental CRD 改用 standard-install
2 HTTPRoute match 太宽泛 某些路径 404 路由优先级冲突 从最具体到最泛排序规则
3 镜像流量触发写操作 灰度库出现脏数据 镜像复制了完整请求 只对 GET 路径做镜像
4 weights 不归一 流量分配不符合预期 误解了 weight 语义 明确设置总 weights=100
5 ReferenceGrant 失效 TLS 证书找不到 ReferenceGrant namespace 写错 改回 Secret 复制方案

迁移收益——30 天后的数据

迁移完成后跑了一周的数据对比:

  • 配置量: 42 个 Ingress → 1 个 Gateway + 38 个 HTTPRoute(减少了重复的 TLS 和 annotation 配置)
  • 新增灰度发布: 从"不支持"到"2 天内上线"— 迁移后第 3 天就用 HTTPRoute 的 header 路由做了一次灰度发布
  • TLS 证书管理: 所有域名证书只在一个 Gateway 里配置,再也不用 grep 找证书引用了
  • 运维告警: Gateway 的 5xx 告警明确到了具体路由,原来 Ingress 只能看到 502/503 但不知道是哪个服务

延伸思考

如果你也在考虑迁移,有几个方向值得进一步探索:

  1. GEP-995 服务网格与 Gateway API 融合 — Gateway API 正在向服务网格方向延伸,将来一个 HTTPRoute 可以同时控制南北向和东西向流量。如果你的团队同时用网格和网关,关注这个变化。
  2. BackendTLSPolicy 实现 mTLS — 如果上游服务之间需要 mTLS,BackendTLSPolicy 可以在 Gateway 层面终结 TLS,不需要每个服务单独配。
  3. Rate Limiting 的标准接口 — 目前 HTTPRoute 不支持原生的速率限制,但 GEP-1098 正在标准化这个能力。在此之前,需要靠实现层(如 Istio EnvoyFilter)来配置限流。
  4. Server-Side Apply 与 Gateway API — Gateway API 的控制器模式很适合 SSA,配合 kubectl apply --server-side 可以避免配置冲突。但我这次没来得及做,留作后续优化。

最后说一句:Gateway API 不是 Ingress 的替代品,而是对 Kubernetes 流量治理的重新定义。迁移的价值不在于"换了一个新东西",而在于你被迫重新审视了自己的流量模型——这才是最大的收获。

你可能感兴趣的文章

来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
转载请注明出处: https://teachcourse.cn/4156.html ,谢谢支持!

资源分享

分类:Android 标签:
Android开发之versionName和versionCode的命名规则小说 Android开发之versionName和ve
Go 语言并发编程实战:从 goroutine 到 channel 的深度应用 Go 语言并发编程实战:从 gorou
冒泡算法 冒泡算法
Claude Code Skill 技能使用指南 Claude Code Skill 技能使用指

评论已关闭!