Kubenetes-扩展教程-二-

104 阅读27分钟

Kubenetes 扩展教程(二)

原文:Extending Kubernetes

协议:CC BY-NC-SA 4.0

四、扩展 Kubernetes API

当我看着人类的大脑时,我仍然对它充满敬畏。

—本杰明“本”所罗门卡森

神经外科医生,美国政治家,作家

Kubernetes API 是云原生容器管理系统的大脑;它让你同时感到钦佩、尊重和惊讶。这是一个复杂的 API,有多层、各种资源,幸运的是,还有两个扩展点。本章将着重于通过创建定制资源和 API 聚合来扩展 Kubernetes API。在本章的最后,你将能够创建定制的资源和控制器;也就是说,您将实现一个 Kubernetes 操作符。此外,您将创建和部署扩展 API 服务器,并实际使用聚合 API。

让我们从 Kubernetes API 及其扩展点的概述开始。

kuble API 概述

Kubernetes API 是系统的核心基础。对集群的所有内部和外部操作都是对 API 服务器的请求。因此,Kubernetes 中的所有东西都是一个具有相应动作的 API 对象。官方的版本化参考文档包含了所有的 API 对象以及大量的信息和例子,比如 v1。19 参考

API 是一个基于资源的接口,用于读取、创建、更新或删除资源。kube-apiserver组件通过它的 HTTP REST 端点为 API 提供服务。因此,控制平面、节点或终端用户的每个动作都是对kube-apiserver的一种 HTTP 调用。让我们假设您想要创建一个新的 pod。kubectl create命令向 Kubernetes API 服务器发送一个带有 pod 定义有效负载的请求。该请求是对图 4-1 的引用中提到的/api/v1/namespaces/{namespace}/pods端点的 HTTP POST。

img/503015_1_En_4_Fig1_HTML.jpg

图 4-1

Pod 创建参考

然后,kube-scheduler将 pod 调度到一个节点。正如所料,调度不是命令性的命令,而是声明性的 Kubernetes 资源:Binding。您可以创建一个带有节点和 pod 的Binding请求,如下所示。

apiVersion: v1
kind: Binding
metadata:
  name: pod-to-be-assigned
  namespace: default
target:
  apiVersion: v1
  kind: Node
  name: available-node

Listing 4-1Example Binding

kube-scheduler通过 HTTP POST 请求向/api/v1/namespaces/{namespace}/bindings端点发送绑定资源。然后kubelet在节点上施展魔法,创建容器、附加卷并等待就绪。在此期间,kubelet更新 pod 的状态,它是 Kubernetes 中的一个子资源。状态端点是/api/v1/namespaces/{namespace}/pods/{name}/status,更新通过补丁请求发送。最后,使用kubectl get pods命令列出本地工作站中的 pod。让我们用一些日志来调试这个命令,以检查它的 HTTP 请求。

$ kubectl get pods -v 9
...
* Starting client certificate rotation controller
* curl -k -v -XGET  -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" -H "User-Agent: kubectl/v1.19.0 (darwin/amd64) kubernetes/e199641" 'https://127.0.0.1:55000/api/v1/namespaces/default/pods?limit=500'
* GET https://127.0.0.1:55000/api/v1/namespaces/default/pods?limit=500 200 OK in 19 milliseconds
* Response Headers:
     Cache-Control: no-cache, private
     Content-Type: application/json
...

Listing 4-2Getting pods with additional logs

正如所料,这是一个指向/api/v1/namespaces/default/pods地址的 GET 命令,用于列出默认名称空间中的 pod。如您所知,端点由两个主要部分构成:API 版本和组。

API 版本控制

Kubernetes 中有三个级别的 API 版本,具有以下特征:

  • 稳定:稳定版本的名字是vX,其中X是一个整数,比如v1。正如所料,稳定的 API 端点提供了完善的特性,这些特性将存在于 Kubernetes 的后续版本中。

  • Beta : Beta API 版本有一个包含beta的名字,比如v1beta1。默认情况下,这些特性和资源都经过了很好的测试和启用。但是,对这些 API 的支持在即将发布的版本中可能会过时。因此,您应该在生产中非常小心地使用 beta APIs。

  • Alpha : Alpha API 版本有一个包含alpha的名字,比如v1alpha1。Alpha 特性是新的,可能包含一些错误。更重要的是,Kubernetes 可能会在不考虑向后兼容性的情况下放弃支持或更改 API。因此,您应该只在测试中使用 alpha APIs,而不是在生产中使用。

API 组

API 组打破了 API 服务器的整体结构,可以单独启用或禁用这些组。在 Kubernetes 中,有几个 API 组有两种命名约定:

  • 遗留核心组有apiVersion: v1,由于历史原因位于/api/v1

  • 所有其他组以apiVersion: $GROUP_NAME/$VERSION命名,位于/apis/$GROUP_NAME/$VERSION。例如,使用apps/v1apiVersion如下构造部署对象。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14

Listing 4-3Example deployment

部署的 API 端点是/apis/apps/v1/namespaces/$NAMESPACE/deployments,包括组名和版本。

对 Kubernetes API 的扩展主要集中在两个部分:添加新的端点和为位于端点的资源添加自定义实现逻辑。现在,让我们继续 Kubernetes API 的两个扩展点。

库 API 中的扩展点

Kubernetes API 是一个面向资源的 API,通过创建自定义资源可以进行扩展。当群集启动并运行时,可以动态地添加或删除自定义资源。启用自定义资源时,它具有与本地资源(如 pod)类似的功能。Kubernetes 提供了两种添加定制资源的方法。

自定义资源定义

CustomResourceDefinition (CRD)是一个本地的 Kubernetes API 资源,用于定义自定义资源。在 CRD 中,您用名称、组、版本和模式来表示新的自定义资源。Kubernetes API 服务器为您的定制资源创建一个 REST 端点,并处理 API 操作,如创建、读取、更新和删除。像所有其他 Kubernetes 资源一样,自定义资源实例存储在etcd中。

API 服务器聚合

Kubernetes 中的每个资源都有一个 REST 端点来处理 CRUD 操作。APIService是一个本地 Kubernetes 资源,用于向组、版本和后端端点注册定制资源。您可以声明一个 URL 路径,比如/apis/k8s-extend.io/v1,并让kube-apiserver将请求委派到您的定制后端。

这两种方法的主要区别在于,CRD 通过在 Kubernetes API 中添加新的资源来扩展 Kubernetes API。另一方面,服务器聚合创建了由外部服务器处理的新资源。这两种方法如图 4-2 所示。

img/503015_1_En_4_Fig2_HTML.jpg

图 4-2

库比特 API 扩展

带有 CRDs 的定制资源是存储在 Kubernetes API 中的结构化数据。然而,它们的动力来自于定制控制器。控制器作用于定制资源的状态,并采取诸如创建、删除或更新之类的动作。带有控制器的自定义资源也被称为 CoreOs 在 2016 年首次定义的操作符模式。您可以创建自定义控制器来实现基于存储在自定义资源中的状态的业务逻辑。定制控制器和聚合服务器都需要与 Kubernetes API 服务器通信。因此,您需要开发符合 Kubernetes REST API 的应用。幸运的是,您不需要从头开始实现每个资源和请求,因为客户端库是可用的。

Kubernetes 客户库

Kubernetes 客户端库通过请求和响应来实现本地资源。此外,它们还处理日常任务,如身份验证、凭证发现和kubeconfig读取。Go、Python、Java、Dotnet、JavaScript 和 Haskell 都有官方支持的客户端库。此外,还有许多由社区维护的客户端库,它们覆盖了不同的本地资源和关注领域。

Kubernetes 在官方文档中维护着客户端库的列表;但是,建议使用 Go 或 Python,因为它们拥有最活跃的社区。另外,Kubernetes 及其生态系统是在 Go 语言上开发的;因此,Go 客户端库是无可争议的赢家。在下面的练习中,您将使用 Go 客户端库,即client-go,连接到一个 Kubernetes 集群。

EXERCISE: KUBERNETES GO CLIENT IN ACTION

在本练习中,您将使用client-go为秘密创建一个自定义观察器。我们将从创建依赖文件和源代码开始。然后,我们将借助 Go 中的跨平台选项来构建二进制文件。最后,您将运行自定义观察器并查看它的运行情况。

注意为了继续这个练习,您需要一个正在运行的 Kubernetes 集群和一个kubeconfig来访问。由minikube创建的本地集群足以执行这些步骤。

  1. Create a dependency file go.mod with the following content:

    module secret-watcher
    go 1.14
    require (
          k8s.io/apimachinery v0.19.0
          k8s.io/client-go v0.19.0
    )
    
    

    文件由我们应用的需求组成。第一个是apimachinery,是提供资源定义的库。第二个是client-go库,包括认证、实用程序和客户端命令。

  2. Create a file secret_watcher.go with the following content:

    package main
    
    import (
      "context"
      "flag"
      "fmt"
      "path/filepath"
      "time"
    
      metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
      "k8s.io/client-go/kubernetes"
      "k8s.io/client-go/tools/clientcmd"
      "k8s.io/client-go/util/homedir"
    
      _ "k8s.io/client-go/plugin/pkg/client/auth"
    )
    
    func main() {
    
      // kubeconfig flag
      var kubeconfig *string
      if home := homedir.HomeDir(); home != "" {
        kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) path to the kubeconfig file")
      } else {
        kubeconfig = flag.String("kubeconfig", "", "path to the kubeconfig file")
      }
      flag.Parse()
    
      // create config
      config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
      if err != nil {
        panic(err.Error())
      }
    
      // create client set
      clientset, err := kubernetes.NewForConfig(config)
      if err != nil {
        panic(err.Error())
      }
    
      // watch for secrets
      for {
        secrets, err := clientset.CoreV1().Secrets("").List(context.TODO(), metav1.ListOptions{})
        if err != nil {
          panic(err.Error())
        }
        fmt.Printf("There are %d secrets in the cluster\n", len(secrets.Items))
        time.Sleep(10 * time.Second)
      }
    }
    
    

    这是我们将构建并运行以与集群通信的主文件。如果没有使用默认目录,该函数开始解析kubeconfig标志。然后它使用client-go库读取kubeconfig。随后,创建一个由本地资源客户端组成的clientset。最后,所有的秘密都被列出,并在一个无限循环中打印出计数。

  3. Start a Go build environment in Docker with the following command:

    $ docker run -v "$(pwd)":/go/src/secret-watcher -it onuryilmaz/multi-platform-go-build:1.14-buster bash
    root@e45653990bb6:/go#
    
    

    该命令将挂载当前的工作目录,并在容器内部启动一个交互式 bash。

  4. Run the following command to build the binary:

    $ cd src/secret-watcher/
    $ export GOOS=darwin # for MacOS. Set to linux or windows based on your local operating system
    $ go build -v
    go: downloading k8s.io/apimachinery v0.19.0
    go: downloading k8s.io/client-go v0.19.0
    go: downloading github.com/google/gofuzz v1.1.0
    go: downloading gopkg.in/inf.v0 v0.9.1
    ...
    k8s.io/client-go/kubernetes/typed/storage/v1alpha1
    k8s.io/client-go/kubernetes/typed/storage/v1
    k8s.io/client-go/kubernetes/typed/storage/v1beta1
    k8s.io/client-go/kubernetes
    secret-watcher
    
    

    输出列表检索所有的依赖项,并最终构建二进制文件。使用exit命令从容器退出到本地工作站。

  5. 运行secret-watcher二进制文件,设置kubeconfig标志或者留空以使用默认位置:

    ./secret-watcher
    There are 37 secrets in the cluster
    There are 37 secrets in the cluster
    There are 37 secrets in the cluster
    ...
    
    

secret-watcher应用在一个无限循环中列出集群中的所有秘密,如输出所示。二进制文件的成功运行表明我们可以使用client-go库创建一个定制的 Go 应用。此外,它还与集群进行通信,这表明集群配置、请求和响应工作正常。

在下一节中,我们将使用定制资源和控制器来扩展 Kubernetes API。我们将学习操作符模式的基础知识,然后创建自定义资源来扩展 Kubernetes API。然后我们将理解控制器的概念,并让 Kubernetes 为我们的定制资源和业务逻辑工作。

自定义资源定义和控制器

CustomResourceDefinition (CRD)是在 Kubernetes API 中创建定制资源的简单方法。有了新的资源,Kubernetes API 被扩展来处理 REST 操作和etcd中的存储。这意味着您可以创建、读取、更新或删除定制资源,最重要的是,您可以在它们之上创建自动化。因此,的想法是为 vanilla Kubernetes 中没有实现的业务需求创建定制资源。让我们假设您想要在 Kubernetes 上安装一个集群化和托管的数据库。您将部署机密、卷、配置、状态集和更多 Kubernetes 资源。此外,您希望运行一些业务逻辑,如数据库初始化、迁移或数据库升级。定制资源和控制器是以 Kubernetes 本地方式管理此类应用所遵循的设计模式。让我们从创建一些 CRD 来定义定制资源开始。

CRDs 类似于任何其他 Kubernetes 资源;它们是期望状态的声明性定义。在这种情况下,所需的状态是具有组名、版本、范围、模式和名称的新自定义资源。用于TimeseriesDB资源的示例 CRD 可以被构造如下。

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: timeseriesdbs.extend-k8s.io
spec:
  group: extend-k8s.io
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                dbType:
                  type: string
                replicas:
                  type: integer
            status:
              type: object
              properties:
                stage:
                  type: string
                message:
                  type: string
  scope: Namespaced
  names:
    plural: timeseriesdbs
    singular: timeseriesdb
    kind: TimeseriesDB
    shortNames:
    - tsdb

Listing 4-4TimeseriesDB CRD

在 CRD 的规格中,有四个块:

  • group:多个定制资源可以被分组到一个 Kubernetes API 组中。该字段表示 API 组的名称。

  • 在 Kubernetes 中,资源的版本随着模式的变化而变化。在 CRD,提供了受支持的版本及其schema

  • scope:定制资源实例可以位于Namespace中,也可以位于Cluster中。

  • names:复数、单数和种类字段是资源的名称,用于 REST 端点、资源定义文件和kubectl命令。

CRD 资源的名称在metadata.name中定义,其格式为<plural>.<group>。此外,Kubernetes 将结构化数据存储在带有自定义字段的自定义资源中。字段的结构在模式字段中指定,并且是以 OpenAPI 规范 v3 的形式。0 。现在,将 CRD 保存在一个文件中,并将其部署到集群。

$ kubectl apply -f tsdb-crd.yaml
customresourcedefinition.apiextensions.k8s.io/timeseriesdbs.extend-k8s.io created

Listing 4-5Deployment of CRD

现在,您可以看到timeseriesdbs被添加到集群中的 API 资源。

$ kubectl api-resources --output=name | grep timeseriesdbs
timeseriesdbs.extend-k8s.io

Listing 4-6API resources listing

此外,您可以运行kubectl命令并与 Kubernetes API 交互TimeseriesDB资源。让我们在启用一些日志记录的情况下尝试一下。

$ kubectl get timeseriesdb -v=6 | grep extend-k8s
Config loaded from file:  ...
Starting client certificate rotation controller
GET https://127.0.0.1:55000/api?timeout=32s 200 OK in 18 milliseconds
GET https://127.0.0.1:55000/apis?timeout=32s 200 OK in 6 milliseconds
GET https://127.0.0.1:55000/apis/extend-k8s.io/v1?timeout=32s 200 OK in 11 milliseconds
GET https://127.0.0.1:55000/apis/autoscaling/v1?timeout=32s 200 OK in 10 milliseconds
...
GET https://127.0.0.1:55000/apis/storage.k8s.io/v1beta1?timeout=32s 200 OK in 20 milliseconds
GET https://127.0.0.1:55000/apis/extend-k8s.io/v1/namespaces/default/timeseriesdbs?limit=500 200 OK in 4 milliseconds
No resources found in default namespace.

Listing 4-7kubectl custom resource listing

Note

如果您没有看到 API 检索日志,这是因为kubectl缓存了它们。您可以清除位于$HOME/的缓存目录。kube/cache 并重新运行该命令。

在日志中,kubectl首先连接到apiapis端点,以发现可用的 API 组和版本。API 发现结果被本地缓存,这样在第二次运行时,kubectl将直接调用/apis/extend-k8s.io/v1/namespaces/default/timeseriesdbs

随着 CRD 的创建,Kubernetes API 得到了扩展;因此,API 服务器和客户端工具都可以使用新的资源了。不出所料,TimeseriesDB没有资源。现在,让我们继续在集群中创建定制资源。

或者您将创建的任何自定义资源与本地 Kubernetes 资源(如 pods 或 secrets)并无不同。对于timeseriesdbs.extend-k8s.io CRD 和v1模式,您可以创建以下资源。

apiVersion: extend-k8s.io/v1
kind: TimeseriesDB
metadata:
  name: example-tsdb
spec:
  dbType: InfluxDB
  replicas: 4
status:
  stage: Created
  message: New TimeseriesDB

Listing 4-8TimeseriesDB example

example-tsdb是创建一个有四个副本的 InfluxDB 数据库的定义。status域解释了资源的当前情况。现在,让我们在图 4-3 中直观地匹配 CRD 和示例资源字段。

img/503015_1_En_4_Fig3_HTML.jpg

图 4-3

CRD 和定制资源

您可以使用以下命令将资源部署到集群。

$ kubectl apply -f example-tsdb.yaml
timeseriesdb.extend-k8s.io/example-tsdb created

Listing 4-9Custom resource deployment

您还可以使用 CRD 中定义的shortNames来访问资源。

$ kubectl get tsdb                                                                                                                                             NAME           AGE
example-tsdb   1m

Listing 4-10Custom resource listing

现在,我们有一个定制的资源来管理我们的时间序列数据库。现在的关键问题是,谁将创建和管理四个实例 InfluxDB 到我们的集群中。同样,新版本发布时,谁来升级数据库?换句话说,需要有一个操作员来创建、更新、删除和管理应用。

Kubernetes 中的运算符模式

Kubernetes API 存储并提供定制资源。另一方面,操作员是创建和管理自定义资源中定义的应用的软件扩展。软件操作员背后的动机是取代人类操作员的知识和经验。在传统方法中,运营团队知道如何部署和管理应用。他们观察特定的指标或仪表板,以跟踪整个系统的状态,并在必要时采取行动。在云原生世界中,您被期望使用自动化来处理这样的操作。

运算符是将人类知识和任务实现到代码中的模式。该模式很好地集成到了 Kubernetes 中,因为它遵循了 Kubernetes 中流行的控制器扩展模式。运营商管理生产就绪的云原生应用有四个主要级别:

  • 安装:自动安装应用,并在定制资源中定义所需的状态。

  • 升级:应用的自动化和用户触发升级,用户交互最少。

  • 生命周期管理:决策规则和自动化的应用的初始化、备份和故障恢复。

  • 监控和可伸缩性:监控和分析应用的指标和警报。必要时,采取自动化措施进行扩展、计划和重新平衡。

操作员被部署到带有CustomResourceDefinition和相关控制器的集群中。控制器在 Kubernetes 中作为容器化的应用运行,最常见的是作为部署运行。操作员应用与 Kubernetes API 交互;所以建议使用可以充当 Kubernetes 客户端的编程语言。开源和社区维护的运营商在 OperatorHub 共享,它有 175 个运营商可供使用。如果您计划将一个流行的数据库部署到 Kubernetes,比如 etcd、MongoDB、PostgreSQL 或 CockroachDB,那么您应该检查 OperatorHub 中的操作符。使用有社区支持的现成操作员可以帮助您节省时间和金钱;因此,它是有价值的。

如果您想开发自己的操作符,有两个基本工具需要考虑:

  • Operator SDK: 它是 Operator 框架的一部分,以有效的自动化方式创建 Kubernetes-native 应用。SDK 提供了构建、测试、打包和部署操作者到集群的工具。可以在 Operator SDK 中开发 Go、Ansible 或 Helm 图表。

  • kubebuilder: 这是一个使用 CRDs 构建 Kubernetes APIs 的框架。它关注于创建和部署 Kubernetes API 扩展的速度和降低的复杂性。该工具为 Go 中的自定义资源生成客户端、接口和 webhooks。它还生成将操作员部署到集群所需的资源。接下来,我们将关注 kubebuilder 框架,因为它有两个基本特性:临近 Kubernetes 社区和增强的开发人员体验。

kubebuilder 框架

kubebuilder是一个框架,用于初始化、生成和部署 Kubernetes-native API 扩展代码到集群。框架是包含的电池,这样创建的项目就有了测试环境、部署文件和容器规范。本节将使用框架一步一步地为TimeseriesDB定制资源生成一个项目,并查看它的运行情况。

Note

本节的其余部分将让您接触以下先决条件:Go 版本 1.14+,Docker 版本 17.03+,访问新的 Kubernetes 集群,以及kubectlkustomize

让我们从将kubebuilder二进制文件安装到本地工作站开始。

$ export os=$(go env GOOS)

$ export arch=$(go env GOARCH)


$ curl -L https://go.kubebuilder.io/dl/2.3.1/${os}/${arch} | tar -xz -C /tmp/


$ sudo mv /tmp/kubebuilder_2.3.1_${os}_${arch} /usr/local/kubebuilder


$export PATH=$PATH:/usr/local/kubebuilder/bin

Listing 4-11kubebuilder installation

这些命令为您的操作系统下载二进制文件,并将其安装到您的PATH环境变量中。接下来,是时候创建一个新的 Go 项目了。

$ mkdir -p $GOPATH/src/extend-k8s.io/timeseries-operator
$ cd $GOPATH/src/extend-k8s.io/timeseries-operator
$ kubebuilder init --domain extend-k8s.io

Writing scaffold for you to edit...
Get controller runtime:
  go get sigs.k8s.io/controller-runtime@v0.5.0
go: downloading sigs.k8s.io/controller-runtime v0.5.0
go: downloading k8s.io/apimachinery v0.17.2
...
go fmt ./...
go vet ./...
go build -o bin/manager main.go
Next: define a resource with:
  kubebuilder create api

Listing 4-12Initializing a project

这些命令在GOPATH中创建一个文件夹,然后kubebuilder通过创建一个 scaffold 项目进行初始化。使用以下命令检查文件夹的内容。

$ tree -a
.
├── .gitignore
├── Dockerfile
├── Makefile
├── PROJECT
├── bin
│   └── manager
├── config
│   ├── certmanager
│   │   ├── certificate.yaml
│   │   ├── kustomization.yaml
│   │   └── kustomizeconfig.yaml
│   ├── default
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   ├── manager_webhook_patch.yaml
│   │   └── webhookcainjection_patch.yaml
│   ├── manager
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   ├── rbac

│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   └── role_binding.yaml
│   └── webhook
│       ├── kustomization.yaml
│       ├── kustomizeconfig.yaml
│       └── service.yaml
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── main.go

9 directories, 31 files

Listing 4-13Project structure

用最少的资源初始化项目来构建和运行一个操作符。大部分文件在config文件夹中,格式为kustomize,集成在kubectl中的无模板定制方式。

通过创建资源和控制器将TimeseriesDB API 添加到项目中。

$ kubebuilder create api --group operator --version v1 --kind TimeseriesDB
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing scaffold for you to edit...
api/v1/timeseriesdb_types.go
controllers/timeseriesdb_controller.go
Running make:
make
...bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go

Listing 4-14Adding new API

样板资源和控制器被添加到项目中。让我们首先检查位于/ api/v1/timeseriesdb_types.go的资源定义。

type TimeseriesDBSpec struct {
      ...
      Foo string `json:"foo,omitempty"`
}

type TimeseriesDBStatus struct {
      ...
}

// +kubebuilder:object:root=true

// TimeseriesDB is the Schema for the timeseriesdbs API
type TimeseriesDB struct {
      metav1.TypeMeta   `json:",inline"`
      metav1.ObjectMeta `json:"metadata,omitempty"`

      Spec   TimeseriesDBSpec   `json:"spec,omitempty"`
      Status TimeseriesDBStatus `json:"status,omitempty"`
}

Listing 4-15Boilerplate TimeseriesDB resource

用下面的 Go 结构更新TimeseriesDBSpecTimeseriesDBStatus,删除示例字段并存储实际数据。

// TimeseriesDBSpec defines the desired state of TimeseriesDB
type TimeseriesDBSpec struct {
      DBType   string `json:"dbType,omitempty"`
      Replicas int    `json:"replicas,omitempty"`
}

Listing 4-16TimeseriesDBSpec with actual fields

// TimeseriesDBStatus defines the observed state of TimeseriesDB
type TimeseriesDBStatus struct {
      Status  string `json:"status,omitempty"`
      Message string `json:"message,omitempty"`
}

Listing 4-17TimeseriesDBStatus with actual fields

此外,更改kubebuilder标志,在TimeseriesDB定义之前将status设置为subresource

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

Listing 4-18Status subresource flag

检查位于/controllers/timeseriesdb_controller.go的控制器代码的Reconcile方法。

func (r *TimeseriesDBReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
      _ = context.Background()
      _ = r.Log.WithValues("timeseriesdb", req.NamespacedName)

      // your logic here

      return ctrl.Result{}, nil
}

Listing 4-19Boilerplate TimeseriesDB controller

Kubernetes API 中的TimeseriesDB实例的每个事务都将调用控制器的Reconcile方法。用以下内容更新函数。

func (r *TimeseriesDBReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
      ctx := context.Background()
      log := r.Log.WithValues("timeseriesdb", req.NamespacedName)

      timeseriesdb := new(operatorv1.TimeseriesDB)

      if err := r.Client.Get(ctx, req.NamespacedName, timeseriesdb); err != nil {
            return ctrl.Result{}, client.IgnoreNotFound(err)
      }

      log = log.WithValues("dbType", timeseriesdb.Spec.DBType, "replicas", timeseriesdb.Spec.Replicas)

      if timeseriesdb.Status.Status == "" || timeseriesdb.Status.Message == "" {
            timeseriesdb.Status = operatorv1.TimeseriesDBStatus{Status: "Initialized", Message: "Database creation is in progress"}
            err := r.Status().Update(ctx, timeseriesdb)
            if err != nil {
                  log.Error(err, "status update failed")
                  return ctrl.Result{}, err
            }
            log.Info("status updated")
      }

      return ctrl.Result{}, nil
}

Listing 4-20Updated controller

更新后的协调器方法展示了如何使用由kubebuilder生成的客户端检索TimeseriesDB实例。检索到的对象的字段被打印到输出中。此外,如果状态部分为空,则填充该部分,并在集群中更新资源。

运行make命令,用更新后的TimeseriesDB资源生成客户机和样本文件。

$ make
.../controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go

Listing 4-21Code generation after resource update

kubebuilder平台使用controller-gen工具生成 YAML 格式的实用程序代码和 Kubernetes 资源文件。

将 CRD 安装到集群,并在本地运行控制器。

$ make install run

.../controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
...
kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/timeseriesdbs.operator.extend-k8s.io created
...
.../controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./main.go
...
INFO    controller-runtime.metrics      metrics server is starting to listen    {"addr": ":8080"}
INFO    setup   starting manager
INFO    controller-runtime.manager      starting metrics server {"path": "/metrics"}
INFO    controller-runtime.controller   Starting EventSource    {"controller": "timeseriesdb", "source": "kind source: /, Kind="}
INFO    controller-runtime.controller   Starting Controller     {"controller": "timeseriesdb"}
INFO    controller-runtime.controller   Starting workers        {"controller": "timeseriesdb", "worker count": 1}

Listing 4-22Running the controller locally

在日志中,安装了 CRD,然后控制器代码开始使用 Go 命令。Metrics server、events source、controllers 和 workers 按列出的顺序开始,我们的控制器现在已经准备好,正在等待资源更改。

用以下内容更新位于/config/sample/operator_v1_timeseriesdb.yaml的示例TimeseriesDB实例。

apiVersion: operator.extend-k8s.io/v1
kind: TimeseriesDB
metadata:
  name: timeseriesdb-sample
spec:
  dbType: Prometheus
  replicas: 5

Listing 4-23Example TimeseriesDB instance

在另一个终端中,将示例自定义资源部署到集群。

$ kubectl apply -f config/samples/
timeseriesdb.operator.extend-k8s.io/timeseriesdb-sample created

Listing 4-24Deploying example TimeseriesDB

instance

在运行控制器的终端中,您现在应该看到下面两行。

INFO    controllers.TimeseriesDB        status updated  {"timeseriesdb": "default/timeseriesdb-sample", "dbType": "Prometheus", "replicas": 5}
DEBUG   controller-runtime.controller   Successfully Reconciled {"controller": "timeseriesdb", "request": "default/timeseriesdb-sample"}

Listing 4-25Controller logs after custom resource creation

它显示了创建资源时调用了控制器的Reconcile方法。让我们检查状态更新是否正确。

$ kubectl describe timeseriesdb timeseriesdb-sample
Name:         timeseriesdb-sample
Namespace:    default
...
Spec:
  Db Type:   Prometheus
  Replicas:  5
Status:
  Message:  Database creation is in progress
  Status:   Initialized
Events:     <none>

Listing 4-26Custom resource status

控制器更新状态字段,并显示控制器不处于只读模式,它可以对资源进行更改。

在使用kubebuilder框架从头开始创建一个操作符之后,您对如何使用定制资源和控制器来扩展 Kubernetes API 有了一个印象。值得一提的是,在开发运营商的同时,有三个要点需要考虑:

  • 声明式 : Kubernetes 有一个声明式 API,它的资源应该是一样的。您的定制资源和它们周围的控制器应该只读取spec和更新status字段。如果您发现自己改变了控制器中的spec,您需要修改您的定制资源和控制器逻辑。

  • 幂等:控制器做的改变应该是幂等的,原子的。当操作员窗格在协调过程中重新启动时,它可以防止您从头开始创建完整的数据库。

  • 抵抗错误:控制器在集群内部或外部创建资源,因此,它容易出现错误、超时或取消。您需要考虑控制器的每个动作及其潜在的故障。Kubernetes-native 方法是使用回退策略重试,更新资源的状态,并在必要时发布事件。

接下来,我们将继续 Kubernetes API 中的第二个扩展点,以创建由外部服务器处理的新资源。

聚合的 API 和扩展服务器

聚合层用额外的 API 扩展了 Kubernetes,以提供超出 Kubernetes API 服务器所提供的内容。CRD 和 operator 模式的主要区别在于新资源不存储在 Kubernetes API 中。资源的请求被定向到外部服务器,并收集响应。虽然这种方法增加了灵活性,但也增加了操作的复杂性。通过聚合扩展 Kubernetes API 有三个基本要素:

  • 聚合层:该层在kube-apiserver内部运行,代理新 API 类型的请求。

  • APIService 资源:新的 API 类型由APIService资源动态注册。

  • 扩展 API 服务器:扩展 API 服务器响应聚合层上代理的请求。

我们可以在图 4-4 中说明一个请求的流程,该流程从 Kubernetes API 开始,到扩展 API 服务器结束。传入请求的旅程从用户的身份验证和授权开始。然后,聚合层将请求定向到扩展 API 服务器。在扩展服务器中,针对 API 服务器对传入的请求进行身份验证。换句话说,扩展 API 服务器检查请求是否来自 Kubernetes API 服务器。然后,扩展服务器向原始用户验证请求的授权。最后,如果请求通过了所有阶段,那么它将被执行并存储在扩展服务器中。请求流显示,用扩展服务器扩展 Kubernetes API 服务器遵循 webhook 设计模式。

img/503015_1_En_4_Fig4_HTML.jpg

图 4-4

聚合 API 请求流

扩展服务器和新资源的动态配置由APIService资源控制。APIService资源由 API 组、版本和扩展服务器的端点组成。为backup.extend-k8s.io组和v1版本扩展 Kubernetes API 的示例APIService资源可以如下构建。

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1.backup.extend-k8s.io
spec:
  version: v1
  group: backup.extend-k8s.io
  groupPriorityMinimum: 2000
  service:
    name: extension-server
    namespace: kube-extensions
  versionPriority: 10
  caBundle: "LS0tL...LS0K"

Listing 4-27Example APIService resource

该定义没有指定自定义资源的实际名称。相反,聚合层重定向所有到达/apis/backup.extend-k8s.io/v1/端点的请求。扩展服务器管理 API 组中的所有定制资源。扩展服务器被指定为 Kubernetes 服务及其名称和命名空间。默认情况下,使用服务的 HTTPS 端口,通过 TLS 处理通信。扩展服务器需要使用由caBundle中指定的 CA 证书签名的证书运行。

一个扩展 API 服务器的开发几乎和开发一个 Kubernetes API 服务器一样复杂,也就是kube-apiserver。对于完整获取的具体例子,参考实现,您可以查看 Kubernetes 的 sample-apiserver 存储库。使用sample-apiserver的推荐方式是派生存储库,修改 API 类型,并频繁地改变基础以跟踪改进和错误修复。

在本节中,我们将使用 apiserver-builder 从头开始生成和部署一个扩展 API 服务器。它是一个用于开发 API 服务器、客户端库和安装资源的完整框架。我们将使用该工具来初始化一个项目,添加定制的资源组和版本、代码生成以及集群部署。

Note

除了正在运行的 Kubernetes 集群之外,本节中的步骤还要求安装以下组件:Go v1.14+、Docker 版本 17.03+和 OpenSSL 1.1.1g+。

首先,让我们安装最新版本的apiserver-boot工具。

$ export os=$(go env GOOS)

$ mkdir -p /tmp/apiserver

$ cd /tmp/apiserver

$ curl  --output /tmp/apiserver/apiserver-builder-alpha.tar.gz -L https://github.com/kubernetes-sigs/apiserver-builder-alpha/releases/download/v2.0.0-alpha.0/apiserver-builder-alpha-v2.0.0-alpha.0-${os}-amd64.tar.gz


$ tar -xf /tmp/apiserver/     apiserver-builder-alpha.tar.gz

$ chmod +x /tmp/apiserver/bin/apiserver-boot


$ mkdir -p /usr/local/apiserver-builder/bin

$ mv /tmp/apiserver/bin/apiserver-boot /usr/local/apiserver-builder/bin/apiserver-boot

$ export PATH=$PATH:/usr/local/apiserver-builder/bin

Listing 4-28apiserver-boot installation

您可以通过运行以下命令来验证安装。

$ apiserver-boot version
Version: version.Version{ApiserverBuilderVersion:"8f12f3e43", KubernetesVendor:"kubernetes-1.19.2", GitCommit:"8f12f3e43cb0a75c82e8a6b316772a230f5fd471", BuildDate:"2020-11-04-20:35:32", GoOs:"darwin", GoArch:"amd64"}

Listing 4-29apiserver-boot version check

让我们从在GOPATH中创建一个文件夹开始。

$ mkdir -p $GOPATH/src/extend-k8s.io/timeseries-apiserver

$ cd $GOPATH/src/extend-k8s.io/timeseries-apiserver

Listing 4-30Go project initialization

使用apiserver-boot创建一个 scaffold API 服务器。

$ apiserver-boot init repo --domain extend-k8s.io
Writing scaffold for you to edit...

Listing 4-31API server initialization

现在,用下面的命令检查生成的文件。

$ tree -a
.
├── .gitignore
├── BUILD.bazel
├── Dockerfile
├── Makefile
├── PROJECT
├── WORKSPACE
├── bin
├── cmd
│   ├── apiserver
│   │   └── main.go
│   └── manager
│       └── main.go -> ../../main.go
├── go.mod
├── hack
│   └── boilerplate.go.txt
├── main.go
└── pkg
    └── apis
        └── doc.go

7 directories, 12 files

Listing 4-32Folder structure

生成的代码是一个基本的 API 服务器,带有工具,如Makefilebazel文件。

使用以下命令向扩展服务器添加自定义资源和控制器。

$ apiserver-boot create group version resource --group backup --version v1 --kind TimeseriesDBBackup
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing scaffold for you to edit...
controllers/backup/timeseriesdbbackup_controller.go

Listing 4-33Adding custom resource

运行代码生成工具,确保新资源和控制器按预期工作。

$  make generate
...controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."

Listing 4-34Code generation for new resource and controller

首先,导出$REPOSITORY 环境变量作为 Docker 存储库,然后为扩展服务器构建容器映像。

$ apiserver-boot build container --image $REPOSITORY/timeseries-apiserver:v1

Will build docker Image from directory /var/folders/nn/.../T/apiserver-boot-build-container656172154
Writing the Dockerfile.

Building binaries for Linux amd64.
CGO_ENABLED=0
GOOS=linux
GOARCH=amd64
go build -o /var/folders/nn/.../T/apiserver-boot-build-container656172154/apiserver cmd/apiserver/main.go
go build -o /var/folders/nn/.../T/apiserver-boot-build-container656172154/controller-manager cmd/manager/main.go

Building the docker Image using /var/folders/nn/.../T/apiserver-boot-build-container656172154/Dockerfile.

docker build -t $REPOSITORY     /timeseries-apiserver:v1 /var/folders/nn/../T/apiserver-boot-build-container656172154
Sending build context to Docker daemon  102.2MB
Step 1/5 : FROM ubuntu:14.04
 ---> df043b4f0cf1
Step 2/5 : RUN apt-get update
 ---> Using cache
 ---> 60dfe53c07c6
Step 3/5 : RUN apt-get install -y ca-certificates
 ---> Using cache
 ---> ff5f3be9ac8d
Step 4/5 : ADD apiserver .
 ---> 3778467102f7
Step 5/5 : ADD controller-manager .
 ---> 32672f63fd9b
Successfully built 32672f63fd9b
Successfully tagged $REPOSITORY/timeseries-apiserver:v1

Listing 4-35Container build

将 Docker 容器推到注册表中,以便在 Kubernetes 集群中使用。

$ docker push $REPOSITORY/timeseries-apiserver:v1

Listing 4-36Container push

使用以下命令部署扩展 API 服务器。

$ apiserver-boot run in-cluster --name timeseriesdb-api --namespace default --image $REPOSITORY/timeseries-apiserver:v1 --build-image=false

openssl req -x509 -newkey rsa:2048 -addext basicConstraints=critical,CA:TRUE,pathlen:1 -keyout config/certificates/apiserver_ca.key -out config/certificates/apiserver_ca.crt -days 365 -nodes -subj /C=un/ST=st/L=l/O=o/OU=ou/CN=timeseriesdb-api-certificate-authority
Generating a RSA private key
.+++++
.................................+++++
writing new private key to 'config/certificates/apiserver_ca.key'
...

Adding APIs:
      backup.v1
...

kubectl apply -f config
deployment.apps/timeseriesdb-api-apiserver created
secret/timeseriesdb-api created
service/timeseriesdb-api created
apiservice.apiregistration.k8s.io/v1.backup.extend-k8s.io created
deployment.apps/timeseriesdb-api-controller created
statefulset.apps/etcd created
service/etcd-svc created
clusterrole.rbac.authorization.k8s.io/timeseriesdb-api-apiserver-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/timeseriesdb-api-apiserver-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/timeseriesdb-api-apiserver-auth-delegator created
clusterrole.rbac.authorization.k8s.io/timeseriesdb-api-controller created
clusterrolebinding.rbac.authorization.k8s.io/timeseriesdb-api-controller created

Listing 4-37Deployment of extension server

该命令处理一系列自动化操作,以创建 TLS 证书、添加 API 和部署一长串 Kubernetes YAML 文件。现在,是时候检查定制资源 API 是否已启用并按预期运行了。

让我们从描述新的APIService资源开始。

$ kubectl describe apiservice v1.backup.extend-k8s.io
Name:         v1.backup.extend-k8s.io
Namespace:
...
Status:
  Conditions:
      ...
    Message:               all checks passed
    Reason:                Passed
    Status:                True
    Type:                  Available
Events:                    <none>

Listing 4-38APIService status

Message字段表示所有检查通过,同时APIService列为Available。在example-tsdb-backup.yaml中创建一个包含以下内容的示例TimeseriesDBBackup资源。

apiVersion: backup.extend-k8s.io/v1
kind: TimeseriesDBBackup
metadata:
  name: example-tsdb-backup

Listing 4-39Example TimeseriesDBBackup

将示例资源部署到集群并检索回来。

$ kubectl apply -f example-tsdb-backup.yaml
timeseriesdbbackup.backup.extend-k8s.io/example-tsdb-backup created

$ kubectl get TimeseriesDBBackups
NAME                  CREATED AT
example-tsdb-backup   2021-07-28T09:05:00Z

Listing 4-40Create and read of the custom resource

输出显示,我们可以与 Kubernetes API 进行交互,以获得由 aggregated API 扩展的定制资源。

最后一步是更进一步检查etcd中扩展资源的物理数据。Kubernetes API 服务器使用etcd作为它的数据库。类似地,扩展 API 服务器与其数据库交互来存储定制资源。etcd部署在扩展 API 服务器旁边,它应该有一个 pod 正在运行并且可以访问。

$ kubectl exec -it etcd-0 -- sh
/ # ETCDCTL_API=3 etcdctl get --prefix /registry
/registry/sample-apiserver/backup.extend-k8s.io/timeseriesdbbackups/example-tsdb-backup
{"kind":"TimeseriesDBBackup","apiVersion":"backup.extend-k8s.io/v1","metadata":{"name":"example-tsdb-backup","uid":"...","creationTimestamp":"...","annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"backup.extend-k8s.io/v1\",\"kind\":\"TimeseriesDBBackup\",\"metadata\":{\"annotations\":{},\"name\":\"example-tsdb-backup\"}}\n"}},"spec":{},"status":{}}

Listing 4-41Access to etcd

来自etcd的数据显示扩展 API 服务器存储了TimeseriesDBBackup个实例。此外,使用kubectl表明 Kubernetes API 是用 aggregated API 方法扩展的。

与使用 CRDs 相比,创建和部署独立的服务器来处理 API 请求并不简单。然而,用聚合服务器扩展 Kubernetes API 比 CRDs 有以下两个好处:更灵活的资源验证检查和对客户机的协议缓冲支持。另一方面,在部署扩展服务器时,有三个要点需要考虑:

  • 新的故障点:聚合 API 服务器根据业务逻辑和需求自行运行。它应该设计得很好,操作时要非常小心,不要在集群中出现单点故障。

  • 存储:聚合的 API 服务器可以选择如何存储数据。应该考虑初始化、备份、恢复和存储容量。

  • 安全和审计:聚合服务器的认证、授权和审计设置应该与主 API 服务器保持一致。

关键要点

  • Kubernetes API 是系统的核心结构,集群上的所有操作都作为对 API 服务器的请求来处理。

  • CustomResourceDefinition (CRD)是向 Kubernetes API 添加新的定制资源的简单方法。它处理由 CRDs 创建的资源的 REST 操作和存储。

  • 当自定义资源与自定义控制器相结合时,就有可能将人类的知识和任务实现为代码,即操作符模式。

  • 聚合层用额外的 API 扩展了 Kubernetes,以提供超出 Kubernetes API 服务器所提供的内容。Kubernetes API 代理聚合 API 资源的请求。

在下一章中,我们将通过运行多个调度器和开发定制的调度器来扩展 Kubernetes 调度。

五、调度扩展

行动表达轻重缓急。

—圣雄甘地

印度律师、政治家、社会活动家、作家

调度器是 Kubernetes 的核心部分,用于将工作负载分配给集群中的节点。分配操作基于集群管理员和操作员设置的优先级和规则。本章将着重于通过创建定制调度程序和开发扩展来扩展 Kubernetes 调度程序。在本章的最后,您将在集群中同时运行多个调度程序。此外,您将通过创建调度程序扩展程序来干预调度决策。

让我们从 Kubernetes 调度器及其扩展点的概述开始。

库调度程序概述

Kubernetes 调度程序在控制平面中运行,并将 pod 分配给节点。默认行为是平衡节点的资源利用率,同时应用集群中资源的规则和优先级。调度器的原理遵循 Kubernetes 的控制器设计模式。它会观察新创建的 pod,并在集群中找到最佳节点。

让我们通过从头创建一个多节点集群和一个 pod 来看看 Kubernetes 调度程序的运行情况。

$  minikube start --nodes 5

Listing 5-1Starting a multi-node local cluster

该命令将创建一个包含五个节点的本地集群,您可以使用以下命令列出这些节点。

$ kubectl get nodes
NAME           STATUS   ROLES    AGE     VERSION
minikube       Ready    master   7m17s   v1.19.2
minikube-m02   Ready    <none>   5m43s   v1.19.2
minikube-m03   Ready    <none>   4m10s   v1.19.2
minikube-m04   Ready    <none>   2m22s   v1.19.2
minikube-m05   Ready    <none>   34s     v1.19.2

Listing 5-2Node listing

现在,让我们创建一个 pod 并等待它运行。

$ kubectl run nginx-1 --image=nginx
pod/nginx-1 created
$ kubectl get pods -w
NAME      READY   STATUS               RESTARTS     AGE
nginx-1   0/1     Pending              0            0s
nginx-1   0/1     Pending              0            0s
nginx-1   0/1     ContainerCreating    0            0s
nginx-1   1/1     Running              0            16s

Listing 5-3Pod creation

你将在几秒钟内看到PendingContainerCreatingRunning阶段。调度的关键步骤是Pending。它表明 Kubernetes API 接受了 pod,但是它还没有被安排到集群节点。让我们检查事件,以找到任何与调度相关的信息。

$ kubectl get events
...
2m54s       Normal   Scheduled                 pod/nginx-1         Successfully assigned default/nginx-1 to minikube-m05
...

Listing 5-4Event listing

您将看到kube-scheduler已经选择了minikube-m05节点。让我们深入了解一下kube-scheduler的内部结构,了解更多关于决策是如何做出的。

调度框架

调度框架是 Kubernetes 调度器的架构。它是一个可插入的框架,插件在其中实现调度特性。框架中的顺序工作流有多个步骤,如图 5-1 所示。工作流程主要分为调度绑定两种。调度的重点是寻找最佳节点,而绑定处理 Kubernetes API 操作来完成调度。

img/503015_1_En_5_Fig1_HTML.jpg

图 5-1

调度框架

每个步骤都有不言自明的名称,但是有一些要点需要考虑:

  • QueueSort:在kube-scheduler的等待队列中对待调度的 pod 进行排序。

  • PreFilter:检查与调度周期相关的 pod 的条件和信息。

  • Filter:过滤节点,通过使用插件和调用外部调度程序扩展程序,为 pod 找到合适的节点列表。

  • PostFilter:如果没有可行节点,则运行可选步骤。在一个典型的场景中,PostFilter将导致其他 pod 的抢占,从而为调度打开一些空间。

  • PreScore:为评分插件创建一个可共享状态。

  • Score/Prioritize:通过调用各个评分插件和调度器扩展程序,对过滤后的节点进行排名。

  • 综合多个来源的得分,计算出最终排名。具有最高加权分数的节点将赢得 pod。

  • Reserve/Unreserve:通知插件关于所选节点的信息是一个可选步骤。

  • Permit:批准、拒绝或暂停(超时)调度决策。

  • PreBind:在将 pod 绑定到节点之前,执行所需的任何工作,例如提供网络卷并安装它。

  • Bind:该步骤只由一个插件处理,因为它需要将决策发送给 Kubernetes API。

  • PostBind:通知装订循环结果的可选信息步骤。

一个插件可以在多个工作流点注册并执行调度子任务。虽然框架和插件创建了一个开放的架构,但是所有的插件都被编译成了kube-scheduler二进制文件。您可以从参考文档中查看可用插件的列表。如果你想改变控制平面中运行的kube-scheduler的配置,这是终极知识。

我们已经看到了调度程序的运行,并对其架构有所了解。现在我们将继续定义扩展点以及如何使用它们。

扩展点

您可以用四种主要方法定制或扩展 Kubernetes 调度程序。

第一种方式是克隆修改上游kube-scheduler代码。然后,您需要编译、封装和运行,而不是在控制平面中部署kube-scheduler。然而,这并不那么简单,而且需要付出巨大的努力,在下一个版本中调整上游代码的变化。

第二种方式是为kube-scheduler内部的调度框架开发插件。这不是第一种方法 hacky ,但是同样,它需要和第一种方法一样多的努力,因为您需要更新、编译和维护上游kube-scheduler存储库的变更。

第三种方法是在集群中运行一个单独的调度程序和默认的调度程序。在PodSpec中有一个特定的字段来定义调度器:schedulerName。如果该字段为空,则将其设置为default-scheduler,并由kube-scheduler处理。因此,可以运行第二个调度程序,并在schedulerName字段中指定它。然后,自定义调度程序会将 pod 分配给节点。这种方法实现了控制器 Kubernetes 设计模式。它将监视具有特定schedulerName的 pod,并为它们分配一个节点。

第四种也是最后一种方法是开发和运行调度程序扩展程序。调度器扩展器是外部服务器,Kubernetes 调度器在调度框架的特定步骤调用它们。这种方法类似于调度框架插件,但是扩展程序是带有 HTTP 端点的外部服务。因此,扩展程序实现了 webhook Kubernetes 设计模式。

前两个扩展方法不是真正的扩展点,因为它们修改了 vanilla Kubernetes 组件。因此,在这一章中,我们将关注最后两种方式:多调度器和调度器扩展程序。我们可以在图 5-2 中说明这两种方法与 Kubernetes 调度程序的交互。

img/503015_1_En_5_Fig2_HTML.jpg

图 5-2

立方调度程序扩展点

三个阶段与调度程序扩展程序交互:FilterPrioritizeBind。因此,使用扩展器在kube-scheduler的规则内操作是有益的。如果您正在寻求更大的灵活性,选择运行自定义调度程序是明智的。自定义调度程序是外部应用,因此它们不限于调度框架的流程和请求。

在接下来的小节中,您将了解这两种方法的细节,并看到它们的实际应用。

配置和管理多个调度程序

Kubernetes scheduler 以其精细的架构和丰富的配置功能将 pod 分配给节点。但是,如果默认计划程序不符合您的要求,可以创建一个新的计划程序并同时运行它们。多调度器的基本思想是基于 pod 规范中的一个字段:schedulerName。如果指定了字段,则 pod 由相应的调度程序调度。另一方面,如果没有设置,默认调度程序将调度 pod。

让我们从运行minikube start --nodes 5创建一个多节点集群开始,如果您还没有启动和运行集群的话。然后,您可以创建一个 pod 并检查它是否为schedulerName

$ kubectl run nginx-by-default-scheduler --image=nginx

$ kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
nginx-by-default-scheduler   1/1     Running   0          99s

$ kubectl get pods nginx-by-default-scheduler -o jsonpath="{.spec.schedulerName}"
default-scheduler

Listing 5-5Pod with the default scheduler

当您在没有指定schedulerName字段的情况下创建时,它由缺省值填充,然后由缺省调度程序分配。现在,让我们创建另一个由定制调度程序处理的 pod。

$ kubectl run nginx-by-custom-scheduler --image=nginx --overrides='{"spec":{"schedulerName":"custom-scheduler"}}'
pod/nginx-by-custom-scheduler created

$ kubectl get pods nginx-by-custom-scheduler
NAME                        READY   STATUS    RESTARTS   AGE
nginx-by-custom-scheduler   0/1     Pending   0          16s

Listing 5-6Pod with a custom scheduler

pod 处于Pending状态,因为没有调度程序来处理它。现在是时候为集群部署第二个调度程序来处理schedulerName字段等于custom-scheduler的 pod 了。

custom-scheduler中,我们将禁用上游调度程序中启用的所有 beta 功能。调度程序将在kube-scheduler旁边的kube-system名称空间中运行。创建一个名为kube-scheduler-custom.yaml的文件,内容如下。

apiVersion: v1
kind: Pod
metadata:
  name: kube-scheduler-custom
  namespace: kube-system
spec:
  containers:
  - name: kube-scheduler-custom
    image: k8s.gcr.io/kube-scheduler:v1.19.0
    command:
    - kube-scheduler
    - --kubeconfig=/etc/kubernetes/scheduler.conf
    - --leader-elect=false
    - --scheduler-name=custom-scheduler
    - --feature-gates=AllBeta=false
    volumeMounts:
    - mountPath: /etc/kubernetes/scheduler.conf
      name: kubeconfig
      readOnly: true
  nodeName: minikube
  restartPolicy: Always
  volumes:
  - hostPath:
      path: /etc/kubernetes/scheduler.conf
      type: FileOrCreate
    name: kubeconfig

Listing 5-7Custom scheduler pod definition

pod 是运行k8s.gcr.io/kube-scheduler:v1.19.0映像并将kubeconfig附加为只读卷的坦率定义。以下三个标志定义了自定义计划程序的功能:

  • leader-elect=false在运行调度程序之前禁用领导者选举阶段,因为只有一个自定义调度程序实例会运行。

  • scheduler-name=custom-scheduler定义调度程序的名称。

  • feature-gates=AllBeta=false禁用所有测试版功能。

使用kubectl apply -f kube-scheduler-custom.yaml文件创建部署并检查 pod 状态。

$  kubectl -n kube-system get pods kube-scheduler-custom
NAME                    READY   STATUS    RESTARTS   AGE
kube-scheduler-custom   1/1     Running   0          24s

Listing 5-8Custom scheduler pod in the cluster

现在,检查我们的 pod 的状态,它卡在 Pending 中。

$ kubectl get pods nginx-by-custom-scheduler
NAME                        READY   STATUS    RESTARTS   AGE
nginx-by-custom-scheduler   1/1     Running   0          42s

Listing 5-9Pod assignment

pod 处于Running阶段,这意味着定制调度程序可以完美地工作。创建定制调度器可能不是每个云工程师日常工作的一部分,因为默认的 Kubernetes 调度器在大多数情况下都工作得很好。然而,当您需要实现更复杂的需求时,您将创建您的定制调度程序并将其部署到集群中。让我们假设您想要创建一个调度程序来最小化成本。在您的自定义调度程序中,您可能需要首先将 pod 分配给最便宜的节点。相反,您可以创建一个自定义调度程序,在选择节点时考虑监视指标。在这种情况下,您可能需要将 pod 分发到节点,以最小化系统中的总延迟。然而,最小化成本或优化延迟取决于外部系统,而不在默认的 Kubernetes 调度程序的范围内。

运行多个调度器并用它们想要的调度器标记 pod 是一种简单的 Kubernetes-native 方法。最重要的部分是开发一个防弹调度程序。创建和操作自定义调度程序时,有三个关键点需要考虑:

  • Kubernetes API 兼容性 : Scheduler 与 Kubernetes API 交互,以观察 pod、检索节点列表并创建绑定。因此,您需要开发与 Kubernetes API 版本兼容的定制调度程序。如果您使用的是官方客户端库,幸运的是,您只需要使用正确的版本。

  • 高可用性:如果您的调度程序停止运行或出现故障,将导致 pod 处于挂起状态。因此,您的应用将不会在集群中运行。因此,您需要将应用设计为高可用性运行。

  • 配合默认调度器:如果集群中有不止一个决策者,您需要小心冲突的决策。例如,默认调度程序控制资源请求和限制。如果您的自定义计划程序在不考虑群集资源的情况下填充节点,默认计划程序的窗格可能会移动到其他节点。因此,您的定制调度程序应该与默认调度程序配合良好,并避免决策冲突。

在下面的练习中,您将使用kubebuilder从头开始创建一个定制调度程序。此外,您将在集群中运行它,并将一些 pod 分配给节点。

EXERCISE: DEVELOPING A CUSTOM SCHEDULER WITH KUBEBUILDER

在本练习中,您将使用kubebuilder创建一个定制的混沌调度器。本质上,调度程序是监视集群中的 pod 的控制器。因此,您将创建一个控制器并实现协调方法。最后,您将运行控制器并看到它的运行。

注意剩下的练习是基于kubebuilder的,它需要以下先决条件:kubebuilder v2.3.1,Go 版本 v1.14+,访问一个 Kubernetes 集群,以及kubectl

  • 检索节点列表。

  • 随机选择一个节点。

  • 创建一个包含节点和 pod 的绑定资源。

  • 将绑定资源发送给 Kubernetes API。

  1. Initialize the project structure with the following commands:

    $ mkdir -p $GOPATH/src/extend-k8s.io/chaos-scheduler
    $ cd $GOPATH/src/extend-k8s.io/chaos-scheduler
    $ kubebuilder init
    Writing scaffold for you to edit...
    Get controller runtime:
    $ go get sigs.k8s.io/controller-runtime@v0.5.0
    Update go.mod:
    $ go mod tidy
    Running make:
    $ make
    .../bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
    go fmt ./...
    go vet ./...
    go build -o bin/manager main.go
    Next: define a resource with:
    $ kubebuilder create api
    
    

    这些命令创建一个文件夹,并用样板代码引导项目。

  2. Create controller for watching the pods with the following command:

    $ kubebuilder create api --kind Pod --group core --version v1
    Create Resource [y/n]
    n
    Create Controller [y/n]
    y
    Writing scaffold for you to edit...
    controllers/pod_controller.go
    Running make:
    $ make
    .../bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
    go fmt ./...
    go vet ./...
    go build -o bin/manager main.go
    
    

    由于 pods 已经是 Kubernetes 资源,选择no跳过Create Resource提示。但是,接受第二个提示,为 pod 资源生成一个控制器。

  3. 打开位于controllers文件夹的pod_controlller.go。你会看到两个功能SetupWithManagerReconcileSetupWithManager是控制器启动时调用的函数。Reconcile是由集群中每个观察到的变化调用的函数。

    Change the SetupWithManager function with the following content:

    func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error {
    
          filter := predicate.Funcs{
                CreateFunc: func(e event.CreateEvent) bool {
                      pod, ok := e.Object.(*corev1.Pod)
                      if ok {
                            if pod.Spec.SchedulerName == "chaos-scheduler" && pod.Spec.NodeName == "" {
                                  return true
                            }
                            return false
                      }
                      return false
                },
                UpdateFunc: func(e event.UpdateEvent) bool {
                      return false
                },
                DeleteFunc: func(e event.DeleteEvent) bool {
                      return false
                },
          }
    
          return ctrl.NewControllerManagedBy(mgr).
                For(&corev1.Pod{}).
                WithEventFilter(filter).
                Complete(r)
    }
    
    

    它添加了一个过滤器来观察带有schedulerName chaos-scheduler和空nodeName的 pod 的创建事件。

    Change the Reconcile function with the following content:

    func (r *PodReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
          ctx := context.Background()
          log := r.Log.WithValues("pod", req.NamespacedName)
    
          nodes := new(corev1.NodeList)
          err := r.Client.List(ctx, nodes)
          if err != nil {
                return ctrl.Result{Requeue: true}, err
          }
    
          node := nodes.Items[rand.Intn(len(nodes.Items))].Name
          log.Info("scheduling", "node", node)
    
          binding := new(corev1.Binding)
          binding.Name = req.Name
          binding.Namespace = req.Namespace
          binding.Target = corev1.ObjectReference{
                Kind:       "Node",
                APIVersion: "v1",
                Name:       node,
          }
    
          err = r.Client.Create(ctx, binding)
          if err != nil {
                return ctrl.Result{Requeue: true}, err
          }
    
          return ctrl.Result{}, nil
    }
    
    

    更新后的Reconcile功能执行以下操作:

选择一个随机节点是调度器制造混乱的基本部分。它将测试 Kubernetes 在动荡和意外条件下的能力和恢复力。

混沌工程是一种在不断变化的情况下(即混沌)对系统进行实验的常用方法。该方法对大规模和分布式应用进行试验,以建立对弹性和复原力的信心。

将以下库添加到pod_controlller.go的导入列表中:

  1. Start the controller with the following command:

    $ make run
    ../bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
    go fmt ./...
    go vet ./...
    ../bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
    go run ./main.go
    INFO      controller-runtime.metrics    metrics server is starting to listen    {"addr": ":8080"}
    INFO      setup          starting manager
    INFO      controller-runtime.manager    starting metrics server           {"path": "/metrics"}
    INFO      controller-runtime.controller    Starting EventSource      {"controller": "pod", "source": "kind source: /, Kind="}
    INFO      controller-runtime.controller    Starting Controller      {"controller": "pod"}
    INFO      controller-runtime.controller    Starting workers   {"controller": "pod", "worker count": 1}
    
    

    如日志所示,控制器启动并等待集群中 pod 的事件。

  2. 在另一个终端,创建一个由chaos-scheduler :

    $ kubectl run nginx-by-chaos-scheduler --image=nginx --overrides='{"spec":{"schedulerName":"chaos-scheduler"}}'
    pod/nginx-by-chaos-scheduler created
    
    

    安排的 pod

  3. Check the logs of controller started in Step 4:

    ...
    INFO      controllers.Pod      scheduling      {"pod": "default/nginx-by-chaos-scheduler", "node": "minikube-m02"}
    DEBUG      controller-runtime.controller    Successfully Reconciled    {"controller": "pod", "request": "default/nginx-by-chaos-scheduler"}
    
    

    额外的日志行表示自定义计划程序分配了 pod。

  4. 检查在步骤 5 中启动的 pod 的状态:

    $ kubectl get pods nginx-by-chaos-scheduler
    NAME                      READY  STATUS    RESTARTS  AGE
    nginx-by-chaos-scheduler  1/1    Running   0         34s
    
    
"math/rand"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"

混沌调度程序将新的 pod 分配给一个节点,它正在运行。它显示了使用kubebuilder从头开始开发的定制调度程序可以完美地工作。

在下一节中,我们将使用第二个扩展点来扩展 Kubernetes 调度程序:调度程序扩展程序。scheduler extender 方法将作为 webhooks 工作,并干扰调度框架阶段。

调度程序扩展器

调度器扩展器是外部的 webhooks,用于在调度框架的不同阶段调整调度决策。框架有多个阶段寻找合适的节点,每一步都调用编译成kube-scheduler的插件。在四个特定的阶段,它还调用调度器扩展程序:过滤器评分/优先化抢占,以及绑定。来自 webhooks 的响应与调度器插件的结果相结合。因此,调度程序扩展程序方便了对kube-scheduler的扩展,而无需深入其源代码。

在本节中,将介绍配置细节和扩展器 API。最后,您将开发一个 scheduler extender webhook 服务器,并在 Kubernetes 集群中运行它。

配置详细信息

Kubernetes 调度程序连接到外部进程,因此它应该知道在哪里连接和评估响应。配置通过一个模式为KubeSchedulerConfiguration的文件传递。最低配置如下所示。

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: /etc/kubernetes/scheduler.conf

Listing 5-10Minimal KubeSchedulerConfiguration

Note

KubeSchedulerConfiguration的全面细节可在参考文件中获得。

您也可以按如下方式向KubeSchedulerConfiguration添加扩展器。

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: /etc/kubernetes/scheduler.conf
extenders:
- urlPrefix: http://localhost:8888/
  filterVerb: filter
  ignorable: true
  weight: 1
- urlPrefix: http://localhost:8890/
  filterVerb: filter
  prioritizeVerb: prioritize
  bindVerb: bind
  ignorable: false
  weight: 1

Listing 5-11Extenders in KubeSchedulerConfiguration

在前面的例子中,两个扩展器在localhost:8888localhost:8890上运行。第一个仅用于过滤节点,当它失败时,它不会阻止调度。然而,第二个是在框架的过滤、评分和绑定阶段调用的。此外,它是不可忽略的,所以如果 webhook 不可达或失败,pod 的调度将被卡在Pending

Note

您可以在源代码中检查扩展器配置的字段,因为它不是 Kubernetes API 文档的一部分。

在用扩展器信息配置了kube-scheduler之后,现在让我们深入研究它们之间的交互。

调度程序扩展器 API

Kubernetes scheduler 使用与其阶段相关的数据对扩展程序进行 HTTP 调用,并期望得到结构化的响应。在 scheduler extender 中,您需要用 JSON 请求和响应来实现这些调用。最重要的优点是,您可以用独立于 Kubernetes 二进制文件的任何语言来开发扩展器。

过滤器

Filter webhooks 接收以下数据作为参数。

type ExtenderArgs struct {
      // Pod being scheduled
      Pod *v1.Pod
      // List of candidate nodes where the pod can be
      // scheduled; to be populated only if
      // Extender.NodeCacheCapable == false
      Nodes *v1.NodeList
      // List of candidate node names where the pod
      // can be scheduled; to be populated only if
      // Extender.NodeCacheCapable == true
      NodeNames *[]string
}

Listing 5-12ExtenderArgs data structure

它只是由一个 pod 和一个基于扩展器中缓存状态的节点或节点名称列表组成。作为响应,下面的数据结构被发送回来。

type ExtenderFilterResult struct {
      // Filtered set of nodes where the pod can be scheduled
      // only if Extender.NodeCacheCapable == false
      Nodes *v1.NodeList
      // Filtered set of nodes where the pod can be scheduled
      // only if Extender.NodeCacheCapable == true
      NodeNames *[]string
      // Filtered out nodes where the pod can't be scheduled
      // and the failure messages
      FailedNodes FailedNodesMap
      // Error message indicating failure
      Error string
}

type FailedNodesMap map[string]string

Listing 5-13ExtenderFilterResult data structure

响应由 pod 的过滤节点组成。此外,不可调度的节点作为FailedNodes与它们的消息一起被发回。最后,如果过滤由于任何原因失败,还有一个Error字段。

优先考虑

优先化 webhooks 接收相同的数据结构ExtenderArgs,就像过滤器 webhooks 一样。webhook 应该为节点创建分数,以分配 pod 并发回以下数据结构。

type HostPriorityList []HostPriority

type HostPriority struct {
      // Name of the host
      Host string
      // Score associated with the host
      Score int64
}

Listing 5-14HostPriorityList data structure

来自 webhook 的分数被添加到由其他扩展器和 Kubernetes scheduler 插件计算的分数中。调度框架为 pod 分配选择具有最高分数的节点。

先取

当 Kubernetes 将 pod 调度到节点时,在集群中找到合适的节点并不总是可能的。在这种情况下,抢占逻辑被触发以从节点中驱逐一些 pod。如果抢占成功,pod 将被调度到节点,被驱逐的将找到新家。在抢占过程中,调度器还使用以下数据结构调用启用的 webhooks。

type ExtenderPreemptionArgs struct {
      //pod being scheduled
      Pod *v1.Pod
      // Victims map generated by scheduler preemption phase
      // Only set NodeNameToMetaVictims if
      // Extender.NodeCacheCapable == true.
      // Otherwise, only set NodeNameToVictims.
      NodeNameToVictims     map[string]*Victims
      NodeNameToMetaVictims map[string]*MetaVictims
}

type Victims struct {
       // a group of pods expected to be preempted.
      Pods             []*v1.Pod
      // the count of violations of PodDisruptionBudget
      NumPDBViolations int64
}

type MetaVictims struct {
       // a group of pods expected to be preempted.
      Pods             []*v1.Pod
      // the count of violations of PodDisruptionBudget
      NumPDBViolations int64
}

Listing 5-15ExtenderPreemptionArgs data structure

数据由一个 pod 和一个潜在节点图组成,这些节点上有Victims。作为响应,webhook 发送以下数据。

type ExtenderPreemptionResult struct {
      NodeNameToMetaVictims map[string]*MetaVictims
}

Listing 5-16ExtenderPreemptionResult data structure

webhook 评估抢占的节点和单元,并发送回潜在的受害者。

约束

绑定调用用于委托节点和 pod 分配。当它被实现时,与 Kubernetes API 交互以进行绑定就成了扩展器的责任。Webhooks 接收以下数据作为参数。

type ExtenderBindingArgs struct {
      // PodName is the name of the pod being bound
      PodName string
      // PodNamespace is the namespace of the pod being bound
      PodNamespace string
      // PodUID is the UID of the pod being bound
      PodUID types.UID
      // Node selected by the scheduler
      Node string
}

Listing 5-17ExtenderBindingArgs data structure

作为响应,如果在绑定期间发生错误,它将返回。

type ExtenderBindingResult struct {
      // Error message indicating failure
      Error string
}

Listing 5-18ExtenderBindingResult data structure

在下面的练习中,您将从头开始创建一个 scheduler extender,并实际使用它。扩展器将干扰调度框架决策和 pod 到节点的分配。

EXERCISE: DEVELOPING AND RUNNING A SCHEDULER EXTENDER

在本练习中,您将创建一个定制的 chaos scheduler 扩展器,并在 Kubernetes 集群中运行它。您将在 Go 中开发一个 HTTP web 服务器,因为调度程序扩展程序原则上是 webhook 服务器。此外,您将在 minikube 中配置kube-scheduler以连接到您的调度程序扩展器。

注意剩下的练习是基于在 Go 中编写一个 web 服务器,它需要以下先决条件:Docker、minikube 和kubectl

  1. 使用以下命令在minikube中启动多节点集群:

  2. Create a pod definition for scheduler extender with the name kube-scheduler-extender.yaml under manifests folder with the following content:

    apiVersion: v1
    kind: Pod
    metadata:
      labels:
        component: kube-scheduler-extender
        tier: control-plane
      name: kube-scheduler-extender
      namespace: kube-system
    spec:
      containers:
      - image: DOCKER_REPOSITORY/k8s-scheduler-extender:v1
        name: kube-scheduler-extender
      hostNetwork: true
    
    

    注意不要忘记将DOCKER_REPOSITORY更改为步骤 8 中设置的环境变量。

  3. 使用以下命令将当前工作目录挂载到minikube节点:

    img/503015_1_En_5_Figa_HTML.png

  4. In another terminal, SSH into the minikube node and copy the manifests with the following commands, and restart the kubelet:

    $ minikube ssh
    docker@minikube:~$ sudo su
    
    root@minikube:/home/docker# cp /etc/k8s-scheduler-extender/manifests/kube-scheduler-extender.yaml /etc/kubernetes/manifests/kube-scheduler-extender.yaml
    
    root@minikube:/home/docker# cp /etc/k8s-scheduler-extender/manifests/kube-scheduler-config.yaml /etc/kubernetes/kube-scheduler-config.yaml
    
    root@minikube:/home/docker# cp /etc/k8s-scheduler-extender/manifests/kube-scheduler.yaml /etc/kubernetes/manifests/kube-scheduler.yaml
    
    root@minikube:/home/docker# systemctl restart kubelet
    
    

    在复制步骤中,您已经将清单和配置文件添加到了kubelet查找的位置。在最后一步中,您已经重启了kubelet来加载新文件并使用它们。您可以退出 minikube 节点并在本地工作站上继续。

  5. 创建一个有 25 个副本的部署,并观察集群中的事件:

    $ kubectl create deployment nginx --image=nginx --replicas=25
    deployment.apps/nginx created
    $  kubectl get events --field-selector reason=FailedScheduling
    LAST SEEN   TYPE      REASON             OBJECT                       MESSAGE
    ...
    3m23s       Warning   FailedScheduling   pod/nginx-6799fc88d8-qnn9p   0/5 nodes are available: 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube-m02: coin is tails, 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube-m03: coin is tails, 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube-m04: coin is tails, 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube-m05: coin is tails, 1 nginx-6799fc88d8-qnn9p cannot be scheduled to minikube: coin is tails.
    ...
    
    

    如果您运气好,所有五个节点都有 tails,那么 pod 也会有类似的事件。如果您没有幸运地看到该事件,您还可以检查调度程序扩展器的日志:

    $ kubectl -n kube-system logs -f kube-scheduler-extender-minikube
    ...
    time=".." level=info msg="Flipped the coin and it is heads"
    time=".." level=info msg="Flipped the coin and it is heads"
    time=".." level=info msg="Flipped the coin and it is tails"
    time=".." level=info msg="Flipped the coin and it is heads"
    time=".." level=info msg="Flipped the coin and it is tails"
    time=".." level=info msg="Rolled the dice and it is 1"
    time=".." level=info msg="Rolled the dice and it is 10"
    time=".." level=info msg="Rolled the dice and it is 1"
    ...
    
    
  • 命令标志config

  • 名为kube-scheduler-config的卷

  • kube-scheduler-config的卷安装

  1. Create the following folder structure in your Go environment:

    $ mkdir -p cd $GOPATH/src/extend-k8s.io/k8s-scheduler-extender
    
    $ cd $GOPATH/src/extend-k8s.io/k8s-scheduler-extender
    
    $ mkdir -p cmd manifests pkg/filter pkg/prioritize
    
    $ tree -a
    
    .
    ├── cmd
    ├── manifests
    └── pkg
        ├── filter
        └── prioritize
    
    5 directories, 0 files
    
    

    文件夹结构是创建 Go 应用的主流方式。在下面的步骤中,您将在每个目录中创建文件。

  2. Create a file flip.go in pkg/filter folder with the following content:

    package filter
    
    import (
          "math/rand"
          "time"
    
          "github.com/sirupsen/logrus"
    )
    
    const (
          HEADS = "heads"
          TAILS = "tails"
    )
    
    var coin []string
    
    func init() {
          rand.Seed(time.Now().UnixNano())
          coin = []string{HEADS, TAILS}
    }
    
    func Flip() string {
    
          side := coin[rand.Intn(len(coin))]
          logrus.Info("Flipped the coin and it is ", side)
          return side
    }
    
    

    函数Flip返回正面或反面来随机过滤节点。

    Create a file filter.go in pkg/filter folder with the following content:

    package filter
    
    import (
          "fmt"
    
          corev1 "k8s.io/api/core/v1"
          extenderv1 "k8s.io/kube-scheduler/extender/v1"
    )
    
    func Filter(args extenderv1.ExtenderArgs) extenderv1.ExtenderFilterResult {
    
          filtered := make([]corev1.Node, 0)
          failed := make(extenderv1.FailedNodesMap)
    
          pod := args.Pod
    
          for _, node := range args.Nodes.Items {
    
                side := Flip()
                if side == HEADS {
                      filtered = append(filtered, node)
                } else {
                      failed[node.Name] = fmt.Sprintf("%s cannot be scheduled to %s: coin is %s", pod.Name, node.Name, side)
                }
          }
    
          return extenderv1.ExtenderFilterResult{
                Nodes: &corev1.NodeList{
                      Items: filtered,
                },
                FailedNodes: failed,
          }
    
    }
    
    

    Filter函数通过接收ExtenderArgs作为参数和ExtenderFilterResult作为响应来实现调度程序扩展器调用的逻辑。

  3. Create a file roll.go in pkg/prioritize folder with the following content:

    package prioritize
    
    import (
          "math/rand"
          "time"
    
          "github.com/sirupsen/logrus"
          extenderv1 "k8s.io/kube-scheduler/extender/v1"
    )
    
    func init() {
          rand.Seed(time.Now().UnixNano())
    }
    
    func Roll() int64 {
    
          number := rand.Int63n(extenderv1.MaxExtenderPriority + 1)
          logrus.Info("Rolled the dice and it is ", number)
    
          return number
    
    }
    
    

    Roll function imitates rolling dice to find a score for the nodes. Create a file prioritize.go in pkg/prioritize folder with the following content:

    package prioritize
    
    import (
          extenderv1 "k8s.io/kube-scheduler/extender/v1"
    )
    
    func Prioritize(args extenderv1.ExtenderArgs) extenderv1.HostPriorityList {
    
          hostPriority := make(extenderv1.HostPriorityList, 0)
    
          for _, node := range args.Nodes.Items {
                hostPriority = append(hostPriority, extenderv1.HostPriority{
                      Host:  node.Name,
                      Score: Roll(),
                })
          }
    
          return hostPriority
    
    }
    
    

    Prioritize函数实现调度器扩展器调用来接收ExtenderArgs并将HostPriorityList发送回kube-scheduler

  4. Create a main.go file under cmd folder with the following content:

    package main
    
    import (
          "encoding/json"
          "log"
          "net/http"
    
          "github.com/gorilla/mux"
          "github.com/extend-k8s.io/k8s-scheduler-extender/pkg/filter"
          "github.com/extend-k8s.io/k8s-scheduler-extender/pkg/prioritize"
          "github.com/sirupsen/logrus"
    
          extenderv1 "k8s.io/kube-scheduler/extender/v1"
    )
    
    func main() {
          r := mux.NewRouter()
    
          r.HandleFunc("/", homeHandler)
          r.HandleFunc("/filter", filterHandler)
          r.HandleFunc("/prioritize", prioritizeHandler)
    
          log.Fatal(http.ListenAndServe(":8888", r))
    }
    
    func filterHandler(w http.ResponseWriter, r *http.Request) {
    
          args := extenderv1.ExtenderArgs{}
          response := extenderv1.ExtenderFilterResult{}
    
          if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
                response.Error = err.Error()
          } else {
                response = filter.Filter(args)
          }
    
          w.Header().Set("Content-Type", "application/json")
          if err := json.NewEncoder(w).Encode(response); err != nil {
                logrus.Error(err)
                return
          }
    
    }
    
    func prioritizeHandler(w http.ResponseWriter, r *http.Request) {
    
          args := extenderv1.ExtenderArgs{}
          response := make(extenderv1.HostPriorityList, 0)
    
          if err := json.NewDecoder(r.Body).Decode(&args); err == nil {
                response = prioritize.Prioritize(args)
          }
    
          w.Header().Set("Content-Type", "application/json")
          if err := json.NewEncoder(w).Encode(response); err != nil {
                logrus.Error(err)
                return
          }
    }
    
    func homeHandler(w http.ResponseWriter, r *http.Request) {
    
          w.Write([]byte("scheduler extender is running!"))
    }
    
    

    它是带有 HTTP 处理程序的 webhook 服务器的入口点,用于过滤和区分调用的优先级。默认情况下,服务器将在 8888 端口上运行。

  5. 在根文件夹中创建一个go.mod文件来设置依赖版本:

    module github.com/extend-k8s.io/k8s-scheduler-extender
    
    go 1.14
    
    require (
          github.com/gorilla/mux v1.8.0
          github.com/sirupsen/logrus v1.6.0
          k8s.io/api v0.19.0
          k8s.io/kube-scheduler v0.19.0
    )
    
    
  6. 在根文件夹中创建一个Dockerfile来构建容器镜像,步骤如下:

    FROM golang:1.14-alpine as builder
    ADD . /go/src/github.com/extend-k8s.io/k8s-scheduler-extender
    WORKDIR /go/src/github.com/extend-k8s.io/k8s-scheduler-extender/cmd
    RUN go build -v
    
    FROM alpine:latest
    COPY --from=builder /go/src/github.com/extend-k8s.io/k8s-scheduler-extender/cmd/cmd /usr/local/bin/k8s-scheduler-extender
    CMD ["k8s-scheduler-extender"]
    
    
  7. 现在,您可以使用以下命令构建并推送调度程序扩展器的 Docker 映像:

    Note Set DOCKER_REPOSITORY environment variable according to your Docker repository.

    $ docker build -t $DOCKER_REPOSITORY/k8s-scheduler-extender:v1 .
    Step 1/7 : FROM golang:1.14-alpine as builder
    ...
    Step 7/7 : CMD ["k8s-scheduler-extender"]
     ---> Running in 6655404206c1
    Removing intermediate container 6655404206c1
     ---> 0ce4bb201541
    Successfully built 0ce4bb201541
    Successfully tagged $DOCKER_REPOSITORY/k8s-scheduler-extender:v1
    
    $ docker push $DOCKER_REPOSITORY/k8s-scheduler-extender:v1
    The push refers to repository [docker.io/$DOCKER_REPOSITORY/k8s-scheduler-extender]
    ...
    v1: digest: sha256:0e62a24a4b9e9e0215f5f02e37b5f86d9235ee950e740069f80951e370ae5b34 size: 739
    
    
  8. Create a Kubernetes scheduler configuration file with the name kube-scheduler-config.yaml under manifests folder:

    apiVersion: kubescheduler.config.k8s.io/v1beta1
    kind: KubeSchedulerConfiguration
    clientConnection:
      kubeconfig: /etc/kubernetes/scheduler.conf
    extenders:
    - urlPrefix: http://localhost:8888/
      filterVerb: filter
      prioritizeVerb: prioritize
      weight: 1
    
    

    这是一个将被传递给kube-scheduler的简单配置,它用端点定义了您的扩展器的位置。

  9. Create a Kubernetes scheduler pod file to replace the default pod definition of kube-scheduler. Set the filename kube-scheduler.yaml under manifests folder with the following content:

    apiVersion: v1
    kind: Pod
    metadata:
      creationTimestamp: null
      labels:
        component: kube-scheduler
        tier: control-plane
      name: kube-scheduler
      namespace: kube-system
    spec:
      containers:
      - command:
        - kube-scheduler
        - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
        - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
        - --bind-address=127.0.0.1
        - --kubeconfig=/etc/kubernetes/scheduler.conf
        - --leader-elect=false
        - --port=0
        - --config=/etc/kubernetes/kube-scheduler-config.yaml
        image: k8s.gcr.io/kube-scheduler:v1.19.0
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 8
          httpGet:
            host: 127.0.0.1
            path: /healthz
            port: 10259
            scheme: HTTPS
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 15
        name: kube-scheduler
        resources:
          requests:
            cpu: 100m
        startupProbe:
          failureThreshold: 24
          httpGet:
            host: 127.0.0.1
            path: /healthz
            port: 10259
            scheme: HTTPS
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 15
        volumeMounts:
        - mountPath: /etc/kubernetes/scheduler.conf
          name: kubeconfig
          readOnly: true
        - mountPath: /etc/kubernetes/kube-scheduler-config.yaml
          name: kube-scheduler-config
          readOnly: true
      hostNetwork: true
      priorityClassName: system-node-critical
      volumes:
      - hostPath:
          path: /etc/kubernetes/scheduler.conf
          type: FileOrCreate
        name: kubeconfig
      - hostPath:
          path: /etc/kubernetes/kube-scheduler-config.yaml
          type: FileOrCreate
        name: kube-scheduler-config
    status: {}
    
    

    它添加了三个部分来使用步骤 9 中的kube-scheduler-config.yaml

$ minikube start --kubernetes-version v1.19.0 --nodes 5

这表明kube-scheduler配置正确,并且连接到调度程序扩展器 webhook。webhook 通过投掷硬币来随机过滤节点。此外,它通过掷骰子给节点打分。换句话说,scheduler extender 在调度过程中产生了一些随机性和混乱。

开发和运行调度程序扩展程序非常简单,因为您可以扩展现有的默认调度程序的功能,而无需重新编译二进制文件。此外,您可以用任何想要的编程语言创建扩展程序。但是,最好在以下问题上保持谨慎,因为您会生成 Kubernetes 控制平面组件的接触点:

  • 配置:使用静态文件为 Kubernetes 调度程序定义扩展程序。因此,请确保文件位置及其内容是正确的。此外,确保该文件不会因集群升级而被覆盖或删除。

  • 性能:像所有的 webhooks 一样,扩展器作为外部进程运行。连接到另一个服务器并检索响应在时间上是很昂贵的。确保 webhook 尽可能快地提供响应,并且控制平面组件可以访问它。

  • 缓存不一致:可以在扩展器中为节点信息启用缓存。如果您的节点不经常改变,或者调度决策不那么重要,那么将节点信息缓存在扩展器中是有益的。另一方面,如果您总是需要关于节点的最新信息,可以禁用缓存,使用 Kubernetes 调度程序发送给您的数据。

关键要点

  • Kubernetes 调度器是在集群上分配工作负载的控制平面组件。

  • Kubernetes 调度程序在优先级和规则集中选择最佳节点。

  • 通过在集群中运行多个调度器,可以扩展调度决策。

  • 调度框架是 Kubernetes 调度器的可插拔架构,并且可以通过 webhooks 进行扩展。

在下一章中,我们将通过开发和运行存储、网络和设备插件来扩展 Kubernetes 与基础设施的交互。