Gateway API 实战:在单节点 Kubernetes 上使用 Envoy Gateway 跑通 HTTPRoute、Header 匹配和金丝雀分流

一、前言

本文会在一个单节点 Kubernetes 测试环境中安装 Envoy Gateway,然后通过 Gateway API 完成几组 HTTP 路由实验:

复制代码
1. 安装 Envoy Gateway
2. 创建 GatewayClass
3. 部署 app-v1 / app-v2 两个测试服务
4. 创建 Gateway 和 HTTPRoute
5. 验证基础路由:/ -> app-v1
6. 验证 Path 路由:/api -> app-v1,/web -> app-v2
7. 验证 Header 匹配:带 x-env: canary 的请求 -> app-v2
8. 验证 backendRefs.weight 金丝雀分流:app-v1 90%,app-v2 10%

二、实验环境与整体链路

本文实验环境是一个单节点 Kubernetes 测试集群。实验中使用的主要资源如下:

复制代码
Gateway Controller:Envoy Gateway
Envoy Gateway 安装命名空间:envoy-gateway-system
测试业务命名空间:gateway-demo
GatewayClass:eg
Gateway:app-gateway
测试域名:app.example.local
后端服务:
  - app-v1
  - app-v2

由于本文是单节点 Kubernetes 测试环境,没有云厂商 LoadBalancer,也没有安装 MetalLB,所以 Envoy Gateway 创建出来的 LoadBalancer 类型 Service 的 EXTERNAL-IP 会是 <pending>

因此本文使用 kubectl port-forward 的方式在本机验证 Gateway API 的转发链路。

最终请求链路如下:

复制代码
curl
  -> kubectl port-forward
  -> Envoy Gateway 数据面 Service
  -> Envoy Proxy 数据面 Pod
  -> Gateway
  -> HTTPRoute
  -> Service
  -> Pod

三、安装 Envoy Gateway

3.1 拉取 Helm Chart

首先拉取 Envoy Gateway 的 Helm Chart:

复制代码
helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.8.1

如果网络可以正常访问 Docker Hub,这一步会在当前目录生成类似下面的文件:

复制代码
gateway-helm-v1.8.1.tgz

如果测试机无法访问 Docker Hub,也可以在能访问外网的机器上下载后再上传到测试机。


3.2 使用 Helm 安装 Envoy Gateway

执行安装命令:

复制代码
helm install envoygateway ./gateway-helm-v1.8.1.tgz \
  -n envoy-gateway-system \
  --create-namespace

这里需要注意两个名字:

复制代码
envoygateway
  是 Helm release 名称。

envoy-gateway-system
  是 Envoy Gateway 安装所在的命名空间。

Helm release 名称和后面创建的 GatewayClass 名称不是一回事。

也就是说,这里的 envoygateway 只是 Helm 管理这次安装的 release 名字;后面我们创建的 GatewayClass eg 是 Gateway API 里的资源名字。


3.3 验证 Envoy Gateway Pod

执行:

复制代码
kubectl -n envoy-gateway-system get pod

实验输出如下:

复制代码
NAME                                      READY   STATUS      RESTARTS   AGE
envoy-gateway-6f954cd9dd-49zm7            1/1     Running     0          26s
envoygateway-gateway-helm-certgen-z9xzq   0/1     Completed   0          30s

这里有两个对象需要理解:

复制代码
envoy-gateway-xxx
  Envoy Gateway Controller,状态 Running,说明控制平面正常运行。

envoygateway-gateway-helm-certgen-xxx
  Helm 安装过程中用于生成证书的 Job,Completed 是正常状态。

只要 envoy-gateway-xxx1/1 Running,说明 Envoy Gateway 控制平面已经正常起来了。


3.4 验证 Gateway API CRD

执行:

复制代码
kubectl get crd | grep gateway.networking.k8s.io

实验输出如下:

复制代码
backendtlspolicies.gateway.networking.k8s.io
gatewayclasses.gateway.networking.k8s.io
gateways.gateway.networking.k8s.io
grpcroutes.gateway.networking.k8s.io
httproutes.gateway.networking.k8s.io
listenersets.gateway.networking.k8s.io
referencegrants.gateway.networking.k8s.io
tcproutes.gateway.networking.k8s.io
tlsroutes.gateway.networking.k8s.io
udproutes.gateway.networking.k8s.io

这说明 Gateway API 相关 CRD 已经安装到集群中。

对于 Gateway API 来说,CRD 非常关键。只有 CRD 存在,Kubernetes 才认识下面这些资源:

复制代码
GatewayClass
Gateway
HTTPRoute
ReferenceGrant
GRPCRoute
TCPRoute
TLSRoute
UDPRoute

本文主要使用这三个核心资源:

复制代码
GatewayClass
Gateway
HTTPRoute

四、创建 GatewayClass

4.1 GatewayClass 的作用

GatewayClass 是集群级资源,用来表示一类 Gateway 由哪个 Gateway Controller 处理。

可以这样理解:

复制代码
GatewayClass = 选择使用哪种 Gateway Controller

本文使用 Envoy Gateway,所以 GatewayClass 的 controllerName 写成:

复制代码
gateway.envoyproxy.io/gatewayclass-controller

这是 Envoy Gateway 对应的 controllerName。


4.2 创建 GatewayClass YAML

创建文件:

复制代码
cat > 00-gatewayclass.yaml <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
EOF

应用:

复制代码
kubectl apply -f 00-gatewayclass.yaml

检查:

复制代码
kubectl get gatewayclass

实验输出:

复制代码
NAME   CONTROLLER                                      ACCEPTED   AGE
eg     gateway.envoyproxy.io/gatewayclass-controller   True       5s

这里重点看:

复制代码
ACCEPTED=True

这说明 Envoy Gateway Controller 已经接受了这个 GatewayClass。后续 Gateway 里写:

复制代码
gatewayClassName: eg

就表示这个 Gateway 交给 Envoy Gateway 处理。


五、部署 app-v1 / app-v2 测试服务

为了隔离实验资源,先创建一个测试命名空间。后续 Deployment、Service、Gateway、HTTPRoute 都放在这个命名空间中。

复制代码
kubectl create ns gateway-demo

为了测试不同路由规则,这里部署两个简单 HTTP 服务:

复制代码
app-v1 返回:hello from app-v1
app-v2 返回:hello from app-v2

5.1 创建测试应用 YAML

创建文件:

复制代码
cat > 01-apps.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-v1
  namespace: gateway-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-v1
  template:
    metadata:
      labels:
        app: app-v1
    spec:
      tolerations:
        - key: "node-role.kubernetes.io/control-plane"
          operator: "Exists"
          effect: "NoSchedule"
        - key: "node-role.kubernetes.io/master"
          operator: "Exists"
          effect: "NoSchedule"
      containers:
        - name: app
          image: hashicorp/http-echo:1.0
          args:
            - "-text=hello from app-v1"
          ports:
            - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  name: app-v1
  namespace: gateway-demo
spec:
  selector:
    app: app-v1
  ports:
    - name: http
      port: 8080
      targetPort: 5678
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-v2
  namespace: gateway-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-v2
  template:
    metadata:
      labels:
        app: app-v2
    spec:
      tolerations:
        - key: "node-role.kubernetes.io/control-plane"
          operator: "Exists"
          effect: "NoSchedule"
        - key: "node-role.kubernetes.io/master"
          operator: "Exists"
          effect: "NoSchedule"
      containers:
        - name: app
          image: hashicorp/http-echo:1.0
          args:
            - "-text=hello from app-v2"
          ports:
            - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  name: app-v2
  namespace: gateway-demo
spec:
  selector:
    app: app-v2
  ports:
    - name: http
      port: 8080
      targetPort: 5678
EOF

5.2 应用测试服务

执行:

复制代码
kubectl apply -f 01-apps.yaml

检查 Deployment、Pod、Service:

复制代码
kubectl -n gateway-demo get deploy,pod,svc

实验输出:

复制代码
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/app-v1   1/1     1            1           15s
deployment.apps/app-v2   1/1     1            1           15s

NAME                          READY   STATUS    RESTARTS   AGE
pod/app-v1-6dbdb7dc7-947vw    1/1     Running   0          15s
pod/app-v2-6f5444c457-7l25x   1/1     Running   0          15s

NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/app-v1   ClusterIP   10.103.180.185   <none>        8080/TCP   15s
service/app-v2   ClusterIP   10.96.61.62      <none>        8080/TCP   15s

继续检查后端端点:

复制代码
kubectl -n gateway-demo get endpoints

实验输出:

复制代码
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME     ENDPOINTS           AGE
app-v1   10.244.0.252:5678   15s
app-v2   10.244.0.253:5678   15s

六、创建 Gateway

6.1 Gateway 的作用

Gateway 用来描述具体的入口配置,例如:

复制代码
使用哪个 GatewayClass
监听哪个端口
使用什么协议
匹配哪个 hostname
允许哪些 Route 绑定

本文创建一个名为 app-gateway 的 Gateway,监听 HTTP 80 端口,并匹配 app.example.local


6.2 创建 Gateway YAML

创建文件:

复制代码
cat > 02-gateway.yaml <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: app-gateway
  namespace: gateway-demo
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      port: 80
      protocol: HTTP
      hostname: "app.example.local"
      allowedRoutes:
        namespaces:
          from: Same
EOF

应用:

复制代码
kubectl apply -f 02-gateway.yaml

6.3 Gateway 字段解释

复制代码
gatewayClassName: eg

表示这个 Gateway 使用前面创建的 GatewayClass eg

也就是交给 Envoy Gateway Controller 处理。

复制代码
listeners:
  - name: http

定义一个 listener,名字叫 http。后面的 HTTPRoute 可以通过:

复制代码
sectionName: http

精确绑定到这个 listener。

复制代码
port: 80
protocol: HTTP

表示这个 listener 处理 HTTP 80 端口流量。

复制代码
hostname: "app.example.local"

表示这个 listener 只匹配 Host 为 app.example.local 的请求。

复制代码
allowedRoutes:
  namespaces:
    from: Same

表示只允许和 Gateway 同 namespace 的 Route 绑定。因为这个 Gateway 在 gateway-demo 命名空间,所以只有 gateway-demo 命名空间里的 HTTPRoute 可以绑定到它。


6.4 检查 Gateway

执行:

复制代码
kubectl -n gateway-demo get gateway

实验输出:

复制代码
NAME          CLASS   ADDRESS   PROGRAMMED   AGE
app-gateway   eg                False        2m1s

这里可以看到:

复制代码
ADDRESS 为空
PROGRAMMED=False

这不一定表示 Gateway YAML 写错。继续查看详情:

复制代码
kubectl -n gateway-demo describe gateway app-gateway

关键状态如下:

复制代码
Reason:  Accepted
Status:  True
Type:    Accepted

Reason:  AddressNotAssigned
Status:  False
Type:    Programmed

这说明:

复制代码
Gateway 已经被 Envoy Gateway 接受。
但是 Gateway 没有分配到外部地址。

由于本文是单节点 Kubernetes,没有云厂商 LoadBalancer,也没有 MetalLB,所以 Envoy Gateway 创建出来的 LoadBalancer Service 无法获得 EXTERNAL-IP。

因此这里的 Programmed=False 是因为:

复制代码
Reason: AddressNotAssigned
Message: No addresses have been assigned to the Gateway

这不是 Gateway YAML 错误。再看 listener 状态:

复制代码
Listeners:
  Attached Routes: 0
  Programmed=True
  Accepted=True
  ResolvedRefs=True

这说明:

复制代码
Gateway 的 listener 配置已经成功翻译并发送到数据面。
只是当前还没有 HTTPRoute 绑定到这个 listener。

七、创建基础 HTTPRoute:/ 转发到 app-v1

7.1 HTTPRoute 的作用

HTTPRoute 用来描述 HTTP 请求如何匹配,以及匹配后转发到哪个后端 Service。

可以简单理解为:

复制代码
Gateway 负责入口。
HTTPRoute 负责路由规则。
Service 负责后端访问入口。
Pod 才是真正处理请求的应用实例。

本文先创建一个最简单的 HTTPRoute:

复制代码
app.example.local/
  -> app-v1:8080

7.2 创建基础 HTTPRoute YAML

创建文件:

复制代码
cat > 03-route-basic.yaml <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route-basic
  namespace: gateway-demo
spec:
  parentRefs:
    - name: app-gateway
      sectionName: http
  hostnames:
    - "app.example.local"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: app-v1
          port: 8080
EOF

应用:

复制代码
kubectl apply -f 03-route-basic.yaml

7.3 HTTPRoute 字段解释

复制代码
parentRefs:
  - name: app-gateway
    sectionName: http

表示这条 HTTPRoute 绑定到:

复制代码
Gateway app-gateway 的 http listener

这里的 sectionName: http 对应 Gateway 中 listener 的名字:

复制代码
listeners:
  - name: http

hostnames:
  - "app.example.local"

表示这条 HTTPRoute 只匹配 Host 为 app.example.local 的请求。

复制代码
matches:
  - path:
      type: PathPrefix
      value: /

表示匹配所有以 / 开头的路径。

复制代码
backendRefs:
  - name: app-v1
    port: 8080

表示匹配成功后,转发到 app-v1 这个 Service 的 8080 端口。


7.4 检查 HTTPRoute

执行:

复制代码
kubectl -n gateway-demo get httproute

实验输出:

复制代码
NAME              HOSTNAMES               AGE
app-route-basic   ["app.example.local"]   24s

继续查看详情:

复制代码
kubectl -n gateway-demo describe httproute app-route-basic

重点看:

复制代码
Reason: Accepted
Status: True
Type: Accepted

Reason: ResolvedRefs
Status: True
Type: ResolvedRefs

这说明:

复制代码
Accepted=True
  HTTPRoute 已经成功绑定到 Gateway。

ResolvedRefs=True
  HTTPRoute 引用的后端 Service 已经成功解析。

也就是说,app-route-basic 已经成功绑定到 app-gateway,并且能够找到后端 app-v1:8080


7.5 查看 Gateway 上绑定的 Route 数量

执行:

复制代码
kubectl -n gateway-demo describe gateway app-gateway | grep Attached

实验输出:

复制代码
Attached Routes:  1

说明 app-route-basic 已经成功挂到 app-gatewayhttp listener 上。


八、理解 Envoy Gateway 控制面和数据面

创建 Gateway 之后,再查看 Envoy Gateway 命名空间中的 Pod:

复制代码
kubectl -n envoy-gateway-system get pod

实验输出:

复制代码
NAME                                                       READY   STATUS    RESTARTS   AGE
envoy-gateway-6f954cd9dd-49zm7                             1/1     Running   0          68m
envoy-gateway-demo-app-gateway-c2617110-5df694555c-smmfb   2/2     Running   0          83s

这里很多初学者容易迷惑:为什么多了一个 envoy-gateway-demo-app-gateway 开头的 Pod?

这不是重复安装了 Envoy Gateway,而是 控制面数据面 的区别。


8.1 envoy-gateway-xxx 是控制面

复制代码
envoy-gateway-6f954cd9dd-49zm7

这个 Pod 是 Helm 安装时创建的 Envoy Gateway Controller。它的职责是:

复制代码
监听 GatewayClass
监听 Gateway
监听 HTTPRoute
监听 Service / EndpointSlice
把 Gateway API 资源翻译成 Envoy 配置
创建和管理 Envoy 数据面 Deployment / Service

它本身一般不直接承接业务 HTTP 请求。


8.2 envoy-gateway-demo-app-gateway-xxx 是数据面

复制代码
envoy-gateway-demo-app-gateway-c2617110-5df694555c-smmfb

这个 Pod 是 Envoy Gateway 根据我们创建的 Gateway app-gateway 自动创建出来的数据面 Envoy Proxy Pod。可以拆开理解这个名字:

复制代码
envoy
gateway-demo
app-gateway
c2617110
随机 Pod 后缀

其中:

复制代码
gateway-demo
  是 Gateway 所在 namespace

app-gateway
  是 Gateway 名字

所以看到这个名字,就可以判断:

复制代码
这是 gateway-demo 命名空间里的 app-gateway 对应的数据面 Envoy Pod。

真正处理业务 HTTP 请求的是这个数据面 Envoy Pod。


8.3 是否每创建一个 Gateway 都会创建一个新 Pod

默认情况下,Envoy Gateway 通常会为每个 Gateway 创建一套独立的 Envoy Proxy 数据面资源。

也就是说,创建一个 Gateway,Envoy Gateway Controller 监听到它后,默认会创建一套对应的数据面资源,例如:

复制代码
Deployment
Pod
Service

不过 Envoy Gateway 也支持合并模式,可以把多个 Gateway 的 listener 合并到同一套 Envoy Proxy fleet 中。本文没有配置合并模式,所以使用的是默认模式。

可以简单记住:

复制代码
默认模式:
  一个 Gateway -> 一套独立 Envoy 数据面

合并模式:
  多个 Gateway -> 可以合并到一套 Envoy 数据面

8.4 控制面 Service 和数据面 Service 的区别

执行:

复制代码
kubectl -n envoy-gateway-system get svc

可以看到类似:

复制代码
NAME                                      TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)
envoy-gateway                             ClusterIP      10.97.106.78   <none>        18000/TCP,18001/TCP,18002/TCP,19001/TCP,9443/TCP
envoy-gateway-demo-app-gateway-c2617110   LoadBalancer   10.99.157.94   <pending>     80:31738/TCP

这里也有两个 Service:

复制代码
envoy-gateway
  控制面 Service,给 Envoy Gateway Controller 自己使用。

envoy-gateway-demo-app-gateway-c2617110
  数据面 Service,给外部流量进入 Gateway 使用。

业务流量应该打到数据面 Service,而不是控制面 Service。所以后面 port-forward 的对象是:

复制代码
service/envoy-gateway-demo-app-gateway-c2617110

而不是:

复制代码
service/envoy-gateway

九、使用 port-forward 验证基础路由

9.1 为什么需要 port-forward

由于本文是单节点 Kubernetes,没有云厂商 LoadBalancer,也没有安装 MetalLB,所以数据面 Service 的 EXTERNAL-IP 是:

复制代码
<pending>

这表示外部地址没有分配成功。为了在本机验证 Gateway API 转发链路,可以使用 kubectl port-forwardport-forward 可以理解成:

复制代码
在本机端口和 Kubernetes 集群内部某个 Pod/Service 端口之间建立一条临时转发通道。

9.2 执行 port-forward

先设置变量:

复制代码
export ENVOY_SERVICE=envoy-gateway-demo-app-gateway-c2617110

执行端口转发:

复制代码
kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8888:80

这条命令的含义是:

复制代码
把当前机器的 127.0.0.1:8888
转发到 envoy-gateway-system 命名空间下
envoy-gateway-demo-app-gateway-c2617110 这个 Service 的 80 端口。

格式可以理解为:

复制代码
本地端口:目标端口

所以:

复制代码
8888:80

表示:

复制代码
本机 8888 -> 集群内目标 Service/Pod 的 80

该命令会一直占用当前终端。不要关闭这个终端。


9.3 curl 验证基础路由

重新开一个终端,执行:

复制代码
curl -s -H "Host: app.example.local" http://127.0.0.1:8888/

实验输出:

复制代码
hello from app-v1

这说明请求已经成功到达 app-v1。这里必须带:

复制代码
-H "Host: app.example.local"

原因是 Gateway 和 HTTPRoute 都配置了:

复制代码
hostname: "app.example.local"
hostnames:
  - "app.example.local"

如果直接访问:

复制代码
curl http://127.0.0.1:8888/

请求里的 Host 会是:

复制代码
127.0.0.1:8888

它不匹配 app.example.local,路由就可能不会命中。


9.4 基础路由链路

基础路由成功后,请求链路如下:

复制代码
curl -H "Host: app.example.local" http://127.0.0.1:8888/
  |
  v
kubectl port-forward
  |
  v
Envoy Gateway 数据面 Service
  |
  v
Envoy 数据面 Pod
  |
  |  Envoy 根据 Gateway listener 和 HTTPRoute 规则匹配请求
  v
Service app-v1:8080
  |
  v
app-v1 Pod:5678
  |
  v
hello from app-v1

十、实验一:Path 路由

基础路由已经跑通后,开始测试 Path 路由。目标:

复制代码
/api -> app-v1
/web -> app-v2

为了避免多个 HTTPRoute 同时存在导致测试结果不清晰,先删除基础 Route:

复制代码
kubectl -n gateway-demo delete httproute app-route-basic

10.1 创建 Path 路由 YAML

创建文件:

复制代码
cat > 04-route-path.yaml <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route-path
  namespace: gateway-demo
spec:
  parentRefs:
    - name: app-gateway
      sectionName: http
  hostnames:
    - "app.example.local"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: app-v1
          port: 8080
    - matches:
        - path:
            type: PathPrefix
            value: /web
      backendRefs:
        - name: app-v2
          port: 8080
EOF

应用:

复制代码
kubectl apply -f 04-route-path.yaml

检查:

复制代码
kubectl -n gateway-demo get httproute
kubectl -n gateway-demo describe httproute app-route-path

10.2 测试 Path 路由

测试 /api

复制代码
curl -s -H "Host: app.example.local" http://127.0.0.1:8888/api

输出:

复制代码
hello from app-v1

测试 /web

复制代码
curl -s -H "Host: app.example.local" http://127.0.0.1:8888/web

输出:

复制代码
hello from app-v2

说明:

复制代码
/api 成功转发到 app-v1
/web 成功转发到 app-v2

这里使用的是:

复制代码
type: PathPrefix

表示按路径前缀匹配。例如:

复制代码
/api
/api/
/api/v1

都属于 /api 前缀。


十一、实验二:Header 匹配

接下来测试基于请求头的匹配。目标:

复制代码
普通请求 -> app-v1
带 x-env: canary 的请求 -> app-v2

这类能力可以用于测试版本访问、灰度验证等场景。先删除上一条 Path Route:

复制代码
kubectl -n gateway-demo delete httproute app-route-path

11.1 创建 Header 匹配 YAML

创建文件:

复制代码
cat > 05-route-header.yaml <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route-header
  namespace: gateway-demo
spec:
  parentRefs:
    - name: app-gateway
      sectionName: http
  hostnames:
    - "app.example.local"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
          headers:
            - type: Exact
              name: x-env
              value: canary
      backendRefs:
        - name: app-v2
          port: 8080
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: app-v1
          port: 8080
EOF

这里把带 Header 的规则写在前面,把普通默认规则写在后面,便于理解和排查。带 x-env: canary 的请求会匹配第一条规则,普通请求会匹配后面的默认规则。

应用:

复制代码
kubectl apply -f 05-route-header.yaml

检查:

复制代码
kubectl -n gateway-demo get httproute
kubectl -n gateway-demo describe httproute app-route-header

11.2 测试普通请求

执行:

复制代码
curl -s -H "Host: app.example.local" http://127.0.0.1:8888/

输出:

复制代码
hello from app-v1

普通请求没有带 x-env: canary,所以命中第二条规则,转发到 app-v1


11.3 测试带 Header 的请求

执行:

复制代码
curl -s -H "Host: app.example.local" \
     -H "x-env: canary" \
     http://127.0.0.1:8888/

输出:

复制代码
hello from app-v2

这说明带有:

复制代码
x-env: canary

的请求命中了第一条规则,并被转发到 app-v2。这里需要注意:

复制代码
同一个 matches 中的 path 和 headers 是共同匹配条件。

也就是说,请求需要同时满足:

复制代码
PathPrefix /
Header x-env=canary

才会命中第一条规则。


十二、实验三:金丝雀分流

最后测试基于权重的流量分配。目标:

复制代码
app-v1 90%
app-v2 10%

先删除 Header Route:

复制代码
kubectl -n gateway-demo delete httproute app-route-header

12.1 创建金丝雀分流 YAML

创建文件:

复制代码
cat > 06-route-canary.yaml <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app-route-canary
  namespace: gateway-demo
spec:
  parentRefs:
    - name: app-gateway
      sectionName: http
  hostnames:
    - "app.example.local"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: app-v1
          port: 8080
          weight: 90
        - name: app-v2
          port: 8080
          weight: 10
EOF

应用:

复制代码
kubectl apply -f 06-route-canary.yaml

检查:

复制代码
kubectl -n gateway-demo get httproute
kubectl -n gateway-demo describe httproute app-route-canary

12.2 测试金丝雀分流

执行 50 次请求:

复制代码
for i in {1..50}; do
  curl -s -H "Host: app.example.local" http://127.0.0.1:8888/
done | sort | uniq -c

实验输出:

复制代码
45 hello from app-v1
 5 hello from app-v2

这说明流量大致按照 90/10 分到了 app-v1app-v2。这里要注意,weight 是相对权重,不是必须加起来等于 100。例如:

复制代码
app-v1 weight=90
app-v2 weight=10

和:

复制代码
app-v1 weight=9
app-v2 weight=1

表达的比例都近似是:

复制代码
app-v1 90%
app-v2 10%

另外,请求次数较少时,结果不一定严格等于 90/10。如果想让统计结果更稳定,可以把请求次数增加到 200 次:

复制代码
for i in {1..200}; do
  curl -s -H "Host: app.example.local" http://127.0.0.1:8888/
done | sort | uniq -c

十三、常见问题总结

13.1 为什么 curl 必须加 Host header

因为 Gateway 里配置了:

复制代码
hostname: "app.example.local"

HTTPRoute 里配置了:

复制代码
hostnames:
  - "app.example.local"

所以 curl 测试时要带:

复制代码
-H "Host: app.example.local"

否则请求 Host 是:

复制代码
127.0.0.1:8888

不会匹配 app.example.local


13.2 为什么 port-forward 到数据面 Service,不是 envoy-gateway Service

Envoy Gateway 安装后会有控制面 Service:

复制代码
envoy-gateway

创建 Gateway 后又会出现数据面 Service:

复制代码
envoy-gateway-demo-app-gateway-c2617110

它们的作用不同:

复制代码
envoy-gateway
  是控制面 Service,给 Envoy Gateway Controller 自己使用。

envoy-gateway-demo-app-gateway-c2617110
  是数据面 Service,给外部流量进入 Gateway 使用。

业务流量应该进入数据面 Service,而不是控制面 Service。所以正确的 port-forward 是:

复制代码
kubectl -n envoy-gateway-system port-forward service/envoy-gateway-demo-app-gateway-c2617110 8888:80

十四、总结

本文在单节点 Kubernetes 测试环境中完成了 Envoy Gateway 和 Gateway API 的基础实战。

完成的内容包括:

复制代码
1. 使用 Helm 安装 Envoy Gateway
2. 检查 Gateway API CRD
3. 创建 GatewayClass
4. 部署 app-v1 / app-v2 两个测试服务
5. 创建 Gateway
6. 创建基础 HTTPRoute
7. 使用 port-forward 本机访问 Envoy Gateway 数据面
8. 验证基础路由:/ -> app-v1
9. 验证 Path 路由:/api -> app-v1,/web -> app-v2
10. 验证 Header 匹配:x-env: canary -> app-v2
11. 验证金丝雀分流:app-v1 90%,app-v2 10%

通过这次实验,可以把 Gateway API 的核心链路串起来:

复制代码
GatewayClass
  -> Gateway
  -> HTTPRoute
  -> Service
  -> Pod

也可以把 Envoy Gateway 的控制面和数据面关系理清楚:

复制代码
envoy-gateway-xxx
  -> 控制面 Controller,负责监听 Gateway API 资源并管理 Envoy 配置

envoy-gateway-demo-app-gateway-xxx
  -> 数据面 Envoy Proxy,负责真正接收和转发业务流量

最终请求链路如下:

复制代码
真实流量链路:

curl
  -> port-forward
  -> Envoy 数据面 Service
  -> Envoy 数据面 Pod
  -> Service
  -> Pod

配置匹配逻辑:

Gateway listener
  -> HTTPRoute host/path/header 匹配
  -> backendRefs 选择后端 Service

这篇文章只是 Gateway API 的普通 HTTP 服务实验。后续可以继续把后端 app-v1/app-v2 替换成 vLLM 模型服务,通过 Gateway API 暴露 /v1/chat/completions 接口。