Go K8s Gateway API生產實戰:從Ingress遷移的6個關鍵步驟

云原生

當Ingress成為瓶頸:K8s流量管理的至暗時刻

凌晨2點,維運團隊緊急上線一個灰度發布。Ingress的nginx注解改了3遍,nginx.ingress.kubernetes.io/canary 配置始終不生效。更頭痛的是,3個團隊共用一個Ingress控制器,任何注解變更都可能互相覆蓋。最終灰度發布回滾,上線延遲6小時。

這不是個例。Ingress的注解地獄、跨命名空間限制、多團隊協作衝突,已經成為K8s流量管理的最大痛點。Kubernetes Gateway API作為Ingress的繼任者,透過角色化設計、可擴展路由和原生流量切分,徹底解決了這些問題。本文將從6個關鍵步驟出發,帶你完成從Ingress到Gateway API的生產級遷移。


核心概念速查

資源 角色 職責 類比
GatewayClass 基礎設施提供商 定義控制器類型和參數 IngressClass
Gateway 集群維運 定義入口監聽器和基礎設施 Ingress控制器本身
HTTPRoute 應用開發者 定義HTTP路由規則 Ingress規則
TCPRoute 應用開發者 定義TCP路由規則 Ingress無原生支援
GRPCRoute 應用開發者 定義gRPC路由規則 Ingress無原生支援
ReferenceGrant 集群維運 允許跨命名空間引用 無對應
ParentRef 應用開發者 Route綁定到Gateway Ingress的ingressClassName

目錄

  1. 問題分析:Ingress的5大挑戰
  2. 步驟1:GatewayClass與基礎設施配置
  3. 步驟2:Gateway資源定義
  4. 步驟3:HTTPRoute路由規則配置
  5. 步驟4:Ingress到Gateway API遷移腳本(Go)
  6. 步驟5:金絲雀發布與流量切分
  7. 步驟6:多集群網關配置
  8. 5大常見陷阱
  9. 10大錯誤排查
  10. 進階優化技巧
  11. 方案對比:Ingress vs Gateway API vs 自建網關
  12. 推薦工具
  13. 總結與延伸閱讀

問題分析:Ingress的5大挑戰

挑戰1:注解不一致。Nginx Ingress用nginx.ingress.kubernetes.io/rewrite-target,Traefik用traefik.ingress.kubernetes.io/rewrite-target,注解不相容導致控制器遷移成本極高。

挑戰2:跨命名空間路由。Ingress的backend只能引用同命名空間的Service,跨命名空間流量必須用ExternalName Service hack,安全且不可控。

挑戰3:多集群網關。Ingress設計上只考慮單集群,多集群場景需要額外引入Federation或自建控制面。

挑戰4:金絲雀發布複雜。Nginx Ingress的Canary注解需要建立兩個Ingress物件,權重配置不直觀,且與常規路由混在一起難以管理。

挑戰5:協議支援有限。Ingress只支援HTTP/HTTPS,TCP/UDP需要額外配置,gRPC需要注解hack,WebSocket支援因控制器而異。


步驟1:GatewayClass與基礎設施配置

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: production-gateway-class
  annotations:
    gateway.networking.k8s.io/controller: nginx
spec:
  controllerName: k8s.io/nginx-gateway-controller
  parametersRef:
    group: ""
    kind: ConfigMap
    name: gateway-params
    namespace: gateway-system
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: gateway-params
  namespace: gateway-system
data:
  workerProcesses: "auto"
  maxConnections: "10240"
  proxyBufferSize: "16k"

GatewayClass由基礎設施團隊管理,定義控制器類型和全域參數。不同環境可以使用不同的GatewayClass,實現開發/預發/生產的隔離。


步驟2:Gateway資源定義

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: production-gateway
  namespace: gateway-system
  annotations:
    cert-manager.io/issuer: letsencrypt-prod
spec:
  gatewayClassName: production-gateway-class
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              shared-gateway-access: "true"
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - name: wildcard-tls
            namespace: gateway-system
      allowedRoutes:
        namespaces:
          from: All
  addresses:
    - type: IPAddress
      value: 10.0.1.100

Gateway由維運團隊管理,定義監聽器、TLS終止和允許路由的命名空間。allowedRoutes實現了精細化的命名空間存取控制,替代了Ingress的隱式全域存取。


步驟3:HTTPRoute路由規則配置

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-service-route
  namespace: app-team-a
spec:
  parentRefs:
    - name: production-gateway
      namespace: gateway-system
      sectionName: https
  hostnames:
    - "api.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /v2
        - headers:
            - type: Exact
              name: X-API-Version
              value: "2"
      backendRefs:
        - name: api-v2-service
          port: 8080
          weight: 90
        - name: api-v2-canary-service
          port: 8080
          weight: 10
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: api-v1-service
          port: 8080

HTTPRoute由應用團隊管理,支援路徑匹配、標頭匹配、權重分配。parentRefs綁定到Gateway的特定listener,weight實現原生流量切分。


步驟4:Ingress到Gateway API遷移腳本(Go)

package main

import (
    "context"
    "fmt"
    "os"

    networkingv1 "k8s.io/api/networking/v1"
    gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

type IngressMigrator struct {
    clientset    *kubernetes.Clientset
    gatewayClass string
    gatewayName  string
    gatewayNS    string
}

func NewIngressMigrator(kubeconfig, gatewayClass, gatewayName, gatewayNS string) (*IngressMigrator, error) {
    config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
    if err != nil {
        return nil, fmt.Errorf("build kubeconfig: %w", err)
    }
    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        return nil, fmt.Errorf("create clientset: %w", err)
    }
    return &IngressMigrator{
        clientset:    clientset,
        gatewayClass: gatewayClass,
        gatewayName:  gatewayName,
        gatewayNS:    gatewayNS,
    }, nil
}

func (m *IngressMigrator) Migrate(ctx context.Context, namespace string) ([]*gatewayv1.HTTPRoute, error) {
    ingresses, err := m.clientset.NetworkingV1().Ingresses(namespace).List(ctx, metav1.ListOptions{})
    if err != nil {
        return nil, fmt.Errorf("list ingresses: %w", err)
    }

    var routes []*gatewayv1.HTTPRoute
    for _, ing := range ingresses.Items {
        route, err := m.convertIngressToHTTPRoute(&ing)
        if err != nil {
            fmt.Fprintf(os.Stderr, "skip ingress %s: %v\n", ing.Name, err)
            continue
        }
        routes = append(routes, route)
    }
    return routes, nil
}

func (m *IngressMigrator) convertIngressToHTTPRoute(ing *networkingv1.Ingress) (*gatewayv1.HTTPRoute, error) {
    route := &gatewayv1.HTTPRoute{
        TypeMeta: metav1.TypeMeta{
            APIVersion: "gateway.networking.k8s.io/v1",
            Kind:       "HTTPRoute",
        },
        ObjectMeta: metav1.ObjectMeta{
            Name:        ing.Name + "-route",
            Namespace:   ing.Namespace,
            Annotations: filterGatewayAnnotations(ing.Annotations),
        },
        Spec: gatewayv1.HTTPRouteSpec{
            ParentRefs: []gatewayv1.ParentReference{
                {
                    Name:      gatewayv1.ObjectName(m.gatewayName),
                    Namespace: (*gatewayv1.Namespace)(&m.gatewayNS),
                },
            },
        },
    }

    for _, rule := range ing.Spec.Rules {
        if rule.Host != "" {
            hostname := gatewayv1.PreciseHostname(rule.Host)
            route.Spec.Hostnames = append(route.Spec.Hostnames, hostname)
        }
        for _, path := range rule.HTTP.Paths {
            matchType := gatewayv1.PathMatchPathPrefix
            if path.PathType != nil && *path.PathType == networkingv1.PathTypeExact {
                matchType = gatewayv1.PathMatchExact
            }
            route.Spec.Rules = append(route.Spec.Rules, gatewayv1.HTTPRouteRule{
                Matches: []gatewayv1.HTTPRouteMatch{
                    {
                        Path: &gatewayv1.HTTPPathMatch{
                            Type:  &matchType,
                            Value: &path.Path,
                        },
                    },
                },
                BackendRefs: []gatewayv1.HTTPBackendRef{
                    {
                        BackendRef: gatewayv1.BackendRef{
                            BackendObjectReference: gatewayv1.BackendObjectReference{
                                Name: gatewayv1.ObjectName(path.Backend.Service.Name),
                                Port: (*gatewayv1.PortNumber)(&path.Backend.Service.Port.Number),
                            },
                        },
                    },
                },
            })
        }
    }
    return route, nil
}

func filterGatewayAnnotations(annotations map[string]string) map[string]string {
    filtered := make(map[string]string)
    for k, v := range annotations {
        if k == "kubernetes.io/ingress.class" || k == "nginx.ingress.kubernetes.io/rewrite-target" {
            continue
        }
        filtered[k] = v
    }
    return filtered
}

func main() {
    kubeconfig := os.Getenv("KUBECONFIG")
    if kubeconfig == "" {
        kubeconfig = clientcmd.RecommendedHomeFile
    }

    migrator, err := NewIngressMigrator(kubeconfig, "production-gateway-class", "production-gateway", "gateway-system")
    if err != nil {
        fmt.Fprintf(os.Stderr, "init migrator: %v\n", err)
        os.Exit(1)
    }

    routes, err := migrator.Migrate(context.Background(), "default")
    if err != nil {
        fmt.Fprintf(os.Stderr, "migrate: %v\n", err)
        os.Exit(1)
    }

    for _, route := range routes {
        fmt.Printf("Generated HTTPRoute: %s/%s\n", route.Namespace, route.Name)
    }
    fmt.Printf("Total: %d routes migrated\n", len(routes))
}

步驟5:金絲雀發布與流量切分

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-canary-route
  namespace: app-team-a
spec:
  parentRefs:
    - name: production-gateway
      namespace: gateway-system
  hostnames:
    - "api.example.com"
  rules:
    - matches:
        - headers:
            - type: Exact
              name: X-Canary
              value: "true"
      backendRefs:
        - name: api-v2-service
          port: 8080
          weight: 100
    - backendRefs:
        - name: api-v1-service
          port: 8080
          weight: 95
        - name: api-v2-service
          port: 8080
          weight: 5

Gateway API原生支援基於Header和權重的流量切分,無需Nginx的Canary注解。從5%流量開始,逐步提升到100%,完成金絲雀發布。


步驟6:多集群網關配置

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: multi-cluster-gateway-class
spec:
  controllerName: k8s.io/nginx-gateway-controller
  parametersRef:
    group: multicluster.x-k8s.io
    kind: ServiceImport
    name: multi-cluster-params
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: cross-cluster-route
  namespace: app-team-a
spec:
  parentRefs:
    - name: production-gateway
      namespace: gateway-system
  hostnames:
    - "api.example.com"
  rules:
    - backendRefs:
        - name: api-service-cluster-east
          port: 8080
          weight: 70
        - name: api-service-cluster-west
          port: 8080
          weight: 30

多集群網關透過ServiceImport和Multi-Cluster Service API實現跨集群流量分發,權重控制實現地域親和和故障轉移。


5大常見陷阱

❌ 陷阱1:Gateway和Route放在同一命名空間 ✅ Gateway放在gateway-system,Route放在應用命名空間,透過allowedRoutes控制存取權限。角色分離是Gateway API的核心設計。

❌ 陷阱2:忽略ReferenceGrant配置跨命名空間引用 ✅ 跨命名空間引用Service或Secret時,必須在目標命名空間建立ReferenceGrant,否則路由不會生效。

❌ 陷阱3:直接刪除Ingress再建立Gateway ✅ 應先建立Gateway和HTTPRoute,驗證路由生效後再刪除Ingress,實現平滑遷移。

❌ 陷阱4:HTTPRoute的weight總和不為100 ✅ weight是相對權重,總和不需要為100,但建議保持總和為100以便理解和計算。

❌ 陷阱5:GatewayClass沒有指定parametersRef ✅ 生產環境務必透過parametersRef配置控制器參數,避免使用預設值導致效能瓶頸。


10大錯誤排查

錯誤現象 可能原因 排查指令 解決方案
HTTPRoute狀態為空 parentRef指向的Gateway不存在 kubectl get gateway -A 檢查Gateway名稱和命名空間
路由不生效 allowedRoutes未允許Route命名空間 kubectl describe gateway 新增命名空間標籤或改為All
TLS憑證未載入 certificateRefs引用的Secret不存在 kubectl get secret -n gateway-system 建立TLS Secret
跨命名空間路由失敗 缺少ReferenceGrant kubectl get referencegrant -A 建立ReferenceGrant允許引用
金絲雀流量未切分 weight配置為0或未設定 kubectl describe httproute 檢查backendRefs的weight值
GatewayClass不可用 控制器Pod未運行 kubectl get pods -n gateway-system 啟動Gateway控制器
TCPRoute不生效 Gateway未監聽TCP埠 kubectl describe gateway 新增TCP協定的listener
GRPCRoute路由失敗 後端Service未啟用gRPC kubectl describe svc 確認Service支援gRPC協定
遷移腳本報404 client-go版本與集群不相容 kubectl version 匹配client-go與K8s版本
多集群路由不均衡 ServiceImport配置錯誤 kubectl get serviceimport -A 檢查Multi-Cluster Service配置

進階優化技巧

1. Gateway基礎設施分離。為不同SLA的服務建立不同GatewayClass,核心服務使用獨佔Gateway實例,避免共享基礎設施的干擾。

2. 路由策略組合。HTTPRoute支援matches巢狀,可以組合路徑、標頭、查詢參數實現精細化路由。例如同時匹配/api/v2路徑和X-Feature-Flag: enabled標頭。

3. 超時和重試策略。透過backendReqTimeout和重試策略注解,在路由層實現超時控制和自動重試,減少應用層程式碼。

4. 觀測性整合。Gateway API支援透過PolicyAttachment整合OpenTelemetry,實現路由級別的Trace和Metrics採集。

5. 漸進式遷移。使用kubectl apply --dry-run=server驗證YAML,搭配kubectl diff預覽變更,確保遷移零風險。


方案對比:Ingress vs Gateway API vs 自建網關

特性 Ingress Gateway API 自建網關
角色分離 ❌ 全部混在一起 ✅ 基礎設施/維運/開發分離 ⚠️ 取決於實作
跨命名空間 ❌ 不支援 ✅ ReferenceGrant ✅ 自由配置
流量切分 ⚠️ 注解hack ✅ 原生weight ✅ 自由配置
TCP/gRPC ❌ 注解hack ✅ 原生支援 ✅ 自由配置
多集群 ❌ 不支援 ⚠️ 需要MCS API ✅ 自由配置
可攜性 ⚠️ 注解不相容 ✅ 標準化API ❌ 完全綁定
學習成本
維運成本
社群支援 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
生產推薦度 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐

推薦工具


總結

Gateway API不是Ingress的簡單升級,而是K8s流量管理的範式轉變。從「一個Ingress打天下」到「角色分離、職責清晰」,從「注解地獄」到「標準化API」,從「單集群」到「多集群原生支援」。6個遷移步驟——GatewayClass定義、Gateway配置、HTTPRoute路由、Go遷移腳本、金絲雀流量切分、多集群網關——涵蓋了生產遷移的完整鏈路。記住:先建立後刪除、漸進式遷移、驗證每一步,才能確保零停機遷移。


延伸閱讀

本站提供瀏覽器本地工具,免註冊即可試用 →

#Kubernetes Gateway API#K8s网关#Ingress迁移#Go#流量管理#2026#云原生