从实战中上手 Kubernetes Gateway API 的正确姿势

2,430 阅读6分钟

我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。

事件背景

这段时间忙完公司内部的 Serverless 工程模型验收,算是有一点时间腾出来了。实际也不知道要分享什么,实际在设计、实现和验收 Serverless 工程模型过程中有很多可以说。但是最后想了想,还是分享工程中用到的 Kubernetes-sigs 中的好用“工具” -- Gateway API。为什么要说 Gateway API 呢?不妨先说说他给我们带来什么好处:内部发布系统从此不需要在针对 Traefik 和 Istio IngressGateway 两种 Gateway 独立开发接口,同时每次底层 Gateway Pod 版本更新或者业务在 Traefik 和 Istio IngressGateway 之间迁移的时候,还要单独考虑接口的兼容性。 这个赞,一下屏蔽了很多问题,也减少了其他部门开发的难度和心智负担。

这下一不小心就让 Serverless 的 Gateway 统一纳入了到了 Gateway API 管理中,让底层平台和产品以“惊人”的速度上线,而且内部平台小伙伴在实现 Gateway API 调用的时候,Serverless 的 Gateway 接入过程中不用修改任何一行代码就打通了上下游功能关系,瞬间得到了很多开发系统小组的支持和认可。

此时各位看官是不是也有点心动呢?想试试 Kubernetes-sigs 的 Gateway API 呢?

前置知识

Gateway API 介绍

大家一定都知道 Kubernetes 中的 Ingress API 是对集群中服务外部访问进行管理的 API 对象,同时也可以提供负载均衡,SSL 卸载以及虚拟路由等功能。Ingress API 也伴随着 Kubernetes 稳定服役了 5 年之久,但是慢慢随着集群规模的扩大,业务管理的复杂化,Ingress API 渐渐无法满足日益多样化的需求,发现 Ingress API 提供的功能太少且很难被扩展,各个厂商都提供了不同的扩展方式,例如加入不同的 annotation, 这导致应用很难在不同的厂商之间无缝迁移, 并且提供的权限管理模型也过于单一。

于是 Kubernetes 社区便孕育出了新一代的流量管理标准:Gateway API。相较于 Ingress API, Gateway API 的主要改进在于

  1. 提供了比 Ingress API 更丰富的,具有可扩展性的 API 集合,例如基于 Header 的流量匹配,流量加权以及其他在 Ingress API 中需要自定义注释实现的功能。
  2. Gateway API 的设计是角色导向的,它允许不同的团队共享网络基础设施,并且通过角色策略约束不同角色的行为。
  3. Gateway API 也添加了对东西向(Mesh)流量管理的支持,为此各大 Service Mesh 厂商还成立了 GAMMA(Gateway API Mesh Management and Administration)[3]工作小组来探索和讨论在 Gateway API 中处理东西向流量的规范。

Gateway API 核心组件

在理解 Kubernetes Gateway API 是如何工作之前,需要先对其中几个基本资源类型有个大致的了解:

  • GatewayClass: 类似于 Ingress 中的 Ingress Class 对象,定义了一组共享通用配置和行为的 Gateway 集合,一个 GatewayClass 被一个 Controller 控制。
  • Gateway: 描述了流量被分配到集群中服务的方式,可以直接由管理员创建,也可以由控制 GatewayClass 的 Controller 创建。
  • Route: 描述了通过网关的流量如何映射到服务,目前 Gateway API 中提供了五种不同协议的 Route,分别是 HTTPRoute,TLSRoute,TCPRoute,UDPRoute 和 GRPCRoute。(本文重点在 HTTPRoute

核心组件之间逻辑关系:

核心组件结构图

核心组件之间流量关系: 核心组件结构图

GatewayClass

GatewayClass 用大白话说:是一个 Gateway 的提供商,比如:Istio、Traefik、APISIX、Nginx 等等都算是,他们要根据 Kubernetes-sigs 的 Gateway API 的标准和要求,实现一个 Controller,能够让 Gateway 能够依赖 GatewayClass 创建对应的真实网关。

GatewayClass 可以说成模板,生成器都行。

官方文档:gateway-api.sigs.k8s.io/api-types/g…

下面我们用 Istio 1.16.2 举例。看看 Istio 在安装后,默认会生成的一个 GatewayClass 的内容。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: GatewayClass
metadata:
    name: istio
spec:
    controllerName: istio.io/gateway-controller
    description: The default Istio GatewayClass

TIPS: 一般 GatewayClass 不需要人工手动创建。

Gateway

Gateway 用大白话说:是 GatewayClass 的一个衍生物,是通过配置创建出来的一个资源。在创建一个 Gateway 资源的时候,会创建 Gateway 的 Pod,它由 GatewayClass 提供 Gateway 镜像产生(举例:如果是 Istio,产生的 Gateway 就是 IngressGateway 使用的镜像)。

Gateway 可以说成 GatewayClass 的具体实现,一个具体存在的 Pod,负责流量接入以及往后转发。

官方文档:gateway-api.sigs.k8s.io/api-types/g…

举一个 Gateway 配置的例子,看看 Gateway 是如何创建出来的。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
    name: serverless-ingress
    namespace: gateway-api-istio
spec:
    gatewayClassName: istio ## 关联指定的 GatewayClass,名字必须与 GatewayClass 中定义的名称相同
    listeners:
        - allowedRoutes:
              namespaces:
                  from: All ## 允许 Gateway 往所有的 Namespace 的 Pod 转发流量,这个一般是常用模式。
          name: http
          port: 80 ## 网关监听端口
          protocol: HTTP ## 网关使用的协议

产生的 Gateway Pod

$ kubectl get pod -n gateway-api-istio
NAME                                        READY   STATUS    RESTARTS   AGE
serverless-ingress-59cb9b9cdc-vrthr         1/1     Running   0          6d17h

TIPS: Gateway 基本上是使用 Kubernetes-sigs 的 Gateway API 的第一步。

HttpRoute

这里只说 HttpRoute,其他的 Route 模型,小伙伴可以参考官方文档中的内容。目前 HttpRoute 是相对成熟的,而且我们业务主要是 Http 协议,所以对 HttpRoute 做了相对的深入研究。

HttpRoute 用大白话说:真实路由。解决流量在满足特定的转发规则的情况下,如何转发到后端。

HttpRoute Kubernetes-sigs 的 Gateway API 中最核心的部分,所有流量控制和调度都抽象到这里 (没有 Istio 中 VirtualService 和 DestinationRule 之间的复杂的逻辑关系)。

官方文档:gateway-api.sigs.k8s.io/api-types/h…

我们参考下面的例子。这是一个具备灰度流量转发,以及修改 Host 头部的 yaml 文件内容。 主要是体现 HttpRoute 丰富的控制能力,当然这里要多花时间才能理清里面的逻辑关系。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
    name: helloworld-go
    namespace: default
spec:
    hostnames:
        - "*.kt.com"
    parentRefs:
        - group: gateway.networking.k8s.io
          kind: Gateway
          name: serverless-ingress ## HttpRoute 注册到哪个 Gateway,换句话说给哪个网关添加路由
          namespace: gateway-api-istio
    rules:
        - backendRefs:
              - group: ""
                kind: Service
                name: helloworld-go
                port: 80
                weight: 1
          filters:
              - requestHeaderModifier:
                    set:
                        - name: host
                          value: v2-helloworld-go.default
                type: RequestHeaderModifier
          matches:
              - headers:
                    - name: env
                      type: Exact
                      value: canary
                path:
                    type: PathPrefix
                    value: /
        - backendRefs:
              - group: ""
                kind: Service
                name: helloworld-go
                port: 80
                weight: 1
          filters:
              - requestHeaderModifier:
                    set:
                        - name: host
                          value: v1-helloworld-go.default
                type: RequestHeaderModifier
          matches:
              - path:
                    type: PathPrefix
                    value: /

如何部署

如果业务模型是以 Http 协议为主的,基本没有什么 TCP 协议的业务,同时环境里面有多种的 Gateway 模型(Istio、Traefik、APISIX、Nginx 等),或者经常更新 Gateway 版本或者升级的用户,可以考虑使用下 Kubernetes-sigs 的 Gateway API。 当然 Gateway API 部署起来也非常的方便,只需要一个 yaml 文件就可以解决。

使用安装包 standard-install.yaml 直接 apply 就行。

$ kubectl apply -f standard-install.yaml

查看下创建的 Pod

$ kubectl get pod -n gateway-system
NAME                                            READY   STATUS      RESTARTS   AGE
gateway-api-admission-d95fp                     0/1     Completed   0          18d
gateway-api-admission-patch-h9lvt               0/1     Completed   0          18d
gateway-api-admission-server-65cf9bbbc8-f5jtr   1/1     Running     0          18d

gateway-api-admission-d95fpgateway-api-admission-patch-h9lvt 这两个 Pod 没有 Running 是正常的,是因为就是在 gateway-api-admission-server-65cf9bbbc8-f5jtr 启动之前,做了一些任务。实际可以直接删除,因为已经执行过了。我这边资源非常充足,所以不删也行。只要看到 gateway-api-admission-server-65cf9bbbc8-f5jtr 是 Running 状态就行。

有的小伙伴看到这里,Kubernetes-sigs 的 Gateway API 部署就这么简单? 真的就这么简单。本身就是一个协议的抽象层,为什么一定会部署非常复杂呢? 嘿嘿!!!!

虽然我是这么说容易,但是实际部署的过程中还是碰到了一些让人挠头的问题,比较容易让小伙伴的踩坑。 这边部署 Gateway API 环境都是 Istio,所以创建的 Gateway 实际也是一种 Istio IngressGateway,当然下面的问题也是基于 Istio。

问题 1:创建 Gateway Pod 启动失败,image: auto

在部署了 Gateway 的时候,发现 Pod 始终不能到 Running 状态,通过 describe pod 信息看到 Pod 是因为 image:auto 无法拉取到镜像,导致启动失败。

镜像异常

原因是创建 Gateway 资源所在的 Namespace 没有被 Istio 自动注入,导致无法触发到 WebHook ,从而 Gateway 的 image 信息无法正确读取,导致启动失败。

解决办法:只要在 Namespace 上打上一个标签 istio-injection=enabled, 重启 Gateway Pod 就可以了。

官方描述

官方文档:istio.io/latest/docs…

问题 2:Gateway Pod 只有一个,没有对应的 HPA

在部署了 Gateway 的时候,Pod 已正常启动,到了 Running 状态。 但是 Gateway Pod 只有 1 个,明显不能真正满足实际生产需要,多数情况至少需要多个 Pod 一起负载压力。

查看 HPA 相关配置,发现没有对网关做任何 HPA 配置

$ kubectl get hpa -n gateway-api-istio
No resources found in gateway-api-istio namespace.

针对创建出来的 Gateway,手动单独配置 HPA,发现无法正常创建 HPA。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
    name: serverless-ingress
    namespace: gateway-api-istio
spec:
    maxReplicas: 3
    metrics:
        - resource:
              name: cpu
              target:
                  averageUtilization: 80
                  type: Utilization
          type: Resource
    minReplicas: 1
    scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: serverless-ingress

HPA 异常

解决办法:是因为创建 Gateway 的 Deployment 中没有对应的 request/limit 相关资源,需要自己手动补下就行。

如何使用

当你部署完毕了,所有的相关资源都启动正常,就可以部署一个 demo 应用来测试效果了。

我这边使用的 Istio 作为 Gateway API 的实现,那么我也用 Istio 传统 demo 测试应用(httpbin)来做演示。

安装 httpbin

$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/1.16.0/samples/httpbin/httpbin.yaml

刷入 GatewayApi 配置

apiVersion: v1
kind: Namespace
metadata:
    labels:
        istio-injection: enabled ## 这里一定要启动注入,要不然就碰到前面的问题1
    name: istio-ingress
spec:
    finalizers:
        - kubernetes
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
    name: gateway
    namespace: istio-ingress
spec:
    gatewayClassName: istio ## 这里指定使用 Istio GatewayClass
    listeners:
        - name: default
          hostname: "*.example.com"
          port: 80
          protocol: HTTP
          allowedRoutes:
              namespaces:
                  from: All
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
    name: http
    namespace: default
spec:
    parentRefs:
        - name: gateway
          namespace: istio-ingress
    hostnames: ["httpbin.example.com"]
    rules:
        - matches:
              - path:
                    type: PathPrefix
                    value: /get
          backendRefs:
              - name: httpbin
                port: 8000

测试访问:

$ curl -s -I -HHost:httpbin.example.com "http://$INGRESS_HOST/get"
HTTP/1.1 200 OK
server: istio-envoy
...

如何开发

目前 Kubernetes-sigs 的 Gateway API 开发包只支持 Go,用其他语言的小伙伴可能要等等看看社区有没有相关的实现。

用 Go 的小伙伴要开发 Gateway API 相关功能也非常的简单。

  1. 拉取 go package
go get -u sigs.k8s.io/gateway-api
  1. 然后 import 下面的路径
sigs.k8s.io/gateway-api/apis/v1beta1

这里写一个 demo 程序供大家参考:

package main

import (
	"context"
	"flag"
	"fmt"
	"path/filepath"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	gatewayapi "sigs.k8s.io/gateway-api/apis/v1beta1"
)

func main() {
	var kubeconfig *string
	if home := homedir.HomeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}
	flag.Parse()

	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	if err != nil {
		fmt.Println(err)
		return
	}

	dynamicSet, err := dynamic.NewForConfig(config)
	if err != nil {
		fmt.Println(err)
		return
	}

	gwGVR := schema.GroupVersionResource{Version: "v1beta1", Group: "gateway.networking.k8s.io", Resource: "gateways"}

	unstructObj, err := dynamicSet.Resource(gwGVR).Namespace("").List(context.TODO(), metav1.ListOptions{Limit: 100})
	if err != nil {
		fmt.Println(err)
		return
	}

	var gw gatewayapi.Gateway
	for _, item := range unstructObj.Items {
		err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.UnstructuredContent(), &gw)
		if err != nil {
			fmt.Printf("failed to Unstructured Gateway object, error: %s\n", err.Error())
			continue
		}

		fmt.Println("[Gateway] >>>>>>", gw.Name, gw.Namespace)
	}

	httpRouteGVR := schema.GroupVersionResource{Version: "v1beta1", Group: "gateway.networking.k8s.io", Resource: "httproutes"}

	unstructObj, err = dynamicSet.Resource(httpRouteGVR).Namespace("").List(context.TODO(), metav1.ListOptions{Limit: 100})
	if err != nil {
		fmt.Println(err)
		return
	}

	var hr gatewayapi.HTTPRoute
	for _, item := range unstructObj.Items {
		err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.UnstructuredContent(), &hr)
		if err != nil {
			fmt.Printf("failed to Unstructured HTTPRoute object, error: %s\n", err.Error())
			continue
		}

		fmt.Println("[HttpRoute] >>>>>>", hr.Name, hr.Namespace)
	}

	time.Sleep(10 * time.Second)
}

Console 输出:

$ go run 1.go
[Gateway] >>>>>> serverless-ingress gateway-api-istio
[HttpRoute] >>>>>> helloworld-go default

写在最后

Kubernetes-sigs 的 Gateway API 已经完成我们内部的工程模型测试,同时整个技术方案也通过技术委员会的认可,已经作为 2023 年容器专项落实技术之一。在 2023 整个一年中,我们几乎是全部的跟路由相关的平台都会迁移到这套接口上。虽然 Kubernetes-sigs 的 Gateway API 才到了 0.6.1 的版本,却已经达到了我们内部评定的要求,已经基本达到了可用状态,同时也具备了比较完整的生产力。