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,跑了两年没出过大事。
推动迁移的真正原因是三个痛到不能再忍的问题:
- 流量路由能力太弱 — 想按 header 灰度发布?Ingress NGINX 需要装一堆 annotation,不同版本之间还不兼容
- 配置分散无法复用 — 每个服务的超时、重试、CORS 策略各自写在不同的 ingress 里,改一个全局策略要改 30 多个文件
- 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——除非你确定需要BackendTLSPolicy或RouteNamespacePolicy。我一开始装了 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 但不知道是哪个服务
延伸思考
如果你也在考虑迁移,有几个方向值得进一步探索:
- GEP-995 服务网格与 Gateway API 融合 — Gateway API 正在向服务网格方向延伸,将来一个 HTTPRoute 可以同时控制南北向和东西向流量。如果你的团队同时用网格和网关,关注这个变化。
- BackendTLSPolicy 实现 mTLS — 如果上游服务之间需要 mTLS,BackendTLSPolicy 可以在 Gateway 层面终结 TLS,不需要每个服务单独配。
- Rate Limiting 的标准接口 — 目前 HTTPRoute 不支持原生的速率限制,但 GEP-1098 正在标准化这个能力。在此之前,需要靠实现层(如 Istio EnvoyFilter)来配置限流。
- Server-Side Apply 与 Gateway API — Gateway API 的控制器模式很适合 SSA,配合
kubectl apply --server-side可以避免配置冲突。但我这次没来得及做,留作后续优化。
最后说一句:Gateway API 不是 Ingress 的替代品,而是对 Kubernetes 流量治理的重新定义。迁移的价值不在于"换了一个新东西",而在于你被迫重新审视了自己的流量模型——这才是最大的收获。

评论已关闭!