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 |
目錄
- 問題分析:Ingress的5大挑戰
- 步驟1:GatewayClass與基礎設施配置
- 步驟2:Gateway資源定義
- 步驟3:HTTPRoute路由規則配置
- 步驟4:Ingress到Gateway API遷移腳本(Go)
- 步驟5:金絲雀發布與流量切分
- 步驟6:多集群網關配置
- 5大常見陷阱
- 10大錯誤排查
- 進階優化技巧
- 方案對比:Ingress vs Gateway API vs 自建網關
- 推薦工具
- 總結與延伸閱讀
問題分析: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 | ❌ 完全綁定 |
| 學習成本 | 低 | 中 | 高 |
| 維運成本 | 中 | 低 | 高 |
| 社群支援 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 生產推薦度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
推薦工具
- JSON格式化工具 — 格式化Gateway API的YAML/JSON配置,快速排查資源定義問題
- 雜湊計算工具 — 計算TLS憑證和ConfigMap校驗值,確保網關配置資料完整性
- cURL轉程式碼工具 — 將cURL測試指令轉為Go程式碼,加速Gateway API客戶端開發
總結
Gateway API不是Ingress的簡單升級,而是K8s流量管理的範式轉變。從「一個Ingress打天下」到「角色分離、職責清晰」,從「注解地獄」到「標準化API」,從「單集群」到「多集群原生支援」。6個遷移步驟——GatewayClass定義、Gateway配置、HTTPRoute路由、Go遷移腳本、金絲雀流量切分、多集群網關——涵蓋了生產遷移的完整鏈路。記住:先建立後刪除、漸進式遷移、驗證每一步,才能確保零停機遷移。
延伸閱讀
本站提供瀏覽器本地工具,免註冊即可試用 →