0 参考资料

课程:《云原生训练营》
书籍:《Kubernetes源码剖析》

1 Kubernetes 核心数据结构

1.1 Group、Version、Resource核心数据结构

Kubernetes将资源再次分组和版本化,形成Group(资源组)​、Version(资源版本)​、Resource(资源)​。Group、Version、Resource核心数据结构如图所示:

  • Group :被称为资源组,在Kubernetes API Server中也可称其为APIGroup。
  • Version :被称为资源版本,在Kubernetes API Server中也可称其为APIVersions。
  • Resource :被称为资源,在Kubernetes API Server中也可称其为APIResource。
  • Kind :资源种类,描述Resource的种类,与Resource为同一级别。

image.png

Kubernetes系统支持多个Group,每个Group支持多个Version,每个Version支持多个Resource,其中部分资源同时会拥有自己的子资源(即SubResource)​。例如,Deployment资源拥有Status子资源。

1.2 kubernetes内置资源

  • kubectl api-versions :列出当前Kubernetes系统支持的资源组和资源版本,其表现形式为<group>/<version>
  • kubectl api-resources :列出当前Kubernetes系统支持的Resource资源列表。

2 client-go

2.1 Client客户端对象

client-go支持4种Client客户端对象与Kubernetes API Server交互的方式,Client交互对象如图所示。
image.png

  • RESTClient是最基础的客户端。RESTClient对HTTP Request进行了封装,实现了RESTful风格的API。ClientSet、DynamicClient及DiscoveryClient客户端都是基于RESTClient实现的。
  • ClientSet在RESTClient的基础上封装了对Resource和Version的管理方法。每一个Resource可以理解为一个客户端,而ClientSet则是多个客户端的集合,每一个Resource和Version都以函数的方式暴露给开发者。ClientSet只能够处理Kubernetes内置资源,它是通过client-gen代码生成器自动生成的。
  • DynamicClient与ClientSet最大的不同之处是,ClientSet仅能访问Kubernetes自带的资源(即Client集合内的资源)​,不能直接访问CRD自定义资源。DynamicClient能够处理Kubernetes中的所有资源对象,包括Kubernetes内置资源与CRD自定义资源。
  • DiscoveryClient发现客户端,用于发现kube-apiserver所支持的资源组、资源版本、资源信息(即Group、Versions、Resources)​。

以上4种客户端:RESTClient、ClientSet、DynamicClient、DiscoveryClient都可以通过kubeconfig配置信息连接到指定的Kubernetes API Server,后面将详解它们的实现。

2.1.1 kubeconfig配置管理

kubeconfig用于管理访问kube-apiserver的配置信息,同时也支持访问多kube-apiserver的配置管理,可以在不同的环境下管理不同的kube-apiserver集群配置,不同的业务线也可以拥有不同的集群。Kubernetes的其他组件都使用kubeconfig配置信息来连接kube-apiserver组件,例如当kubectl访问kube-apiserver时,会默认加载kubeconfig配置信息。
kubeconfig中存储了集群、用户、命名空间和身份验证等信息,在默认的情况下,kubeconfig存放在$HOME/.kube/config路径下。Kubeconfig配置信息如下:

image.png

  • kubeconfig配置信息通常包含3个部分,分别介绍如下。
  • clusters :定义Kubernetes集群信息,例如kube-apiserver的服务地址及集群的证书信息等。
  • users :定义Kubernetes集群用户身份验证的客户端凭据,例如client-certificate、client-key、token及username/password等。
  • contexts :定义Kubernetes集群用户信息和命名空间等,用于将请求发送到指定的集群。

client-go会读取kubeconfig配置信息并生成config对象,用于与kube-apiserver通信,代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func main() {
klog.InitFlags(nil)
flag.Parse()

// set up signals so we handle the shutdown signal gracefully
ctx := signals.SetupSignalHandler()
logger := klog.FromContext(ctx)

cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
if err != nil {
logger.Error(err, "Error building kubeconfig")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}

kubeClient, err := kubernetes.NewForConfig(cfg)
if err != nil {
logger.Error(err, "Error building kubernetes clientset")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}

exampleClient, err := clientset.NewForConfig(cfg)
if err != nil {
logger.Error(err, "Error building kubernetes clientset")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}

kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)
exampleInformerFactory := informers.NewSharedInformerFactory(exampleClient, time.Second*30)

controller := NewController(ctx, kubeClient, exampleClient,
kubeInformerFactory.Apps().V1().Deployments(),
exampleInformerFactory.Samplecontroller().V1alpha1().Foos())

// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(ctx.done())
// Start method is non-blocking and runs all registered informers in a dedicated goroutine.
kubeInformerFactory.Start(ctx.Done())
exampleInformerFactory.Start(ctx.Done())

if err = controller.Run(ctx, 2); err != nil {
logger.Error(err, "Error running controller")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}
}

2.1.2 RESTClient 客户端

RESTClient是最基础的客户端。其他的ClientSet、DynamicClient及DiscoveryClient都是基于RESTClient实现的。RESTClient对HTTP Request进行了封装,实现了RESTful风格的API。它具有很高的灵活性,数据不依赖于方法和资源,因此RESTClient能够处理多种类型的调用,返回不同的数据格式。

2.2 Informer 机制

Informer 机制架构设计:
image.png

Informer架构设计中,有多个核心组件,分别介绍如下:

2.2.1 Reflector

Reflector用于监控(Watch)指定的Kubernetes资源,当监控的资源发生变化时,触发相应的变更事件,例如Added(资源添加)事件、Updated(资源更新)事件、Deleted(资源删除)事件,并将其资源对象存放到本地缓存DeltaFIFO中。

2.2.2 DeltaFIFO

DeltaFIFO可以分开理解,FIFO是一个先进先出的队列,它拥有队列操作的基本方法,例如Add、Update、Delete、List、Pop、Close等,而Delta是一个资源对象存储,它可以保存资源对象的操作类型,例如Added(添加)操作类型、Updated(更新)操作类型、Deleted(删除)操作类型、Sync(同步)操作类型等。
DeltaFIFO本质上是一个先进先出的队列,有数据的生产者和消费者,其中生产者是Reflector调用的Add方法,消费者是Controller调用的Pop方法。下面分析DeltaFIFO的核心功能:生产者方法、消费者方法及Resync机制。

2.2.3 Indexer

Indexer是client-go用来存储资源对象并自带索引功能的本地存储,Reflector从DeltaFIFO中将消费出来的资源对象存储至Indexer。Indexer与Etcd集群中的数据完全保持一致。client-go可以很方便地从本地存储中读取相应的资源对象数据,而无须每次从远程Etcd集群中读取,以减轻Kubernetes API Server和Etcd集群的压力。

Indexer数据结构说明如下:

  • Indexers :存储索引器,key为索引器名称,value为索引器的实现函数。
  • IndexFunc :索引器函数,定义为接收一个资源对象,返回检索结果列表
  • Indices :存储缓存器,key为缓存器名称(在Indexer Example代码示例中,缓存器命名与索引器命名相对应)​,value为缓存数据。Index :存储缓存数据,其结构为K/V。

2.3 WorkQueue

WorkQueue称为工作队列,Kubernetes的WorkQueue队列与普通FIFO(先进先出,First-In,First-Out)队列相比,实现略显复杂,它的主要功能在于标记和去重,并支持如下特性。

  • 有序 :按照添加顺序处理元素(item)​。
  • 去重 :相同元素在同一时间不会被重复处理,例如一个元素在处理之前被添加了多次,它只会被处理一次。
  • 并发性 :多生产者和多消费者。
  • 标记机制 :支持标记功能,标记一个元素是否被处理,也允许元素在处理时重新排队。
  • 通知机制 :ShutDown方法通过信号量通知队列不再接收新的元素,并通知metricgoroutine退出。
  • 延迟 :支持延迟队列,延迟一段时间后再将元素存入队列。
  • 限速 :支持限速队列,元素存入队列时进行速率限制。限制一个元素被重新排队(Reenqueued)的次数。
  • Metric :支持metric监控指标,可用于Prometheus监控。WorkQueue支持3种队列,并提供了3种接口,不同队列实现可应对不同的使用场景,分别介绍如下。
  • Interface :FIFO队列接口,先进先出队列,并支持去重机制。
  • DelayingInterface :延迟队列接口,基于Interface接口封装,延迟一段时间后再将元素存入队列。
  • RateLimitingInterface :限速队列接口,基于DelayingInterface接口封装,支持元素存入队列时进行速率限制。

有状态应用-Operator

基于 CRD 的开发过程

  1. 借助Kubernetes RBAC 和 authentication 机制来保证该扩展资源的 security、access control、authentication 和 multitenancy。
  2. 将扩展资源的数据存储到 Kubernetes 的 etcd 集群
  3. 借助 Kubernetes 提供的 controller 模式开发框架,实现新的 controller,并借助 APIServer 监听 etcd 集群 关于该资源的状态,并定义状态变化的处理逻辑

基于CRD的开发,可以让开发人员扩展添加新功能,更新现有的功能,并且可以自动执行一些管理任务,这些自定义的控制器就像 Kubernetes 原生的组件一样,Operator 直接使用 Kubernetes API 进行开发,就可以根据这些控制器内部编写的自定义规则来监控集群、更改 Pods/Services、对正在运行的应用进行扩缩容。

控制器模式:

image.png

控制器代码示例:
image.png

安装 kubebuilder

下载链接:https://github.com/kubernetes-sigs/kubebuilder/releases

1
mv <your_download_binary> /usr/local/bin/kubebuilder

开发自定义 HTTPClient 资源

创建项目

1
2
3
mkdir httpclient
cd httpclient
kubebuilder init --plugins go/v4 --domain baihl.io --owner "baihl"

查看项目:cat PROJECT :

1
2
3
4
5
6
7
8
9
10
# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
domain: baihl.io
layout:
- go.kubebuilder.io/v4
projectName: httpclient
repo: httpclient
version: "3"

定义 HTTPClient 资源

创建 API、resource、controller:

1
kubebuilder create api --group apps --version v1beta1 --kind HTTPClient

到现在为止,已经定义 HTTPClient 资源,可以进行修改。
image.png

修改 httpclient_types.go

修改 api/v1beta1/httpclient_types.go 填充 HTTPClient 资源类型的 字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// HTTPClientSpec defines the desired state of HTTPClient  
type HTTPClientSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Foo is an example field of HTTPClient. Edit httpclient_types.go to remove/update ServerAddress string `json:"serverAddress,omitempty"`
Count int `json:"count,omitempty"`
}

// HTTPClientStatus defines the observed state of HTTPClient
type HTTPClientStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file Successed int `json:"successed,omitempty"`
Failed int `json:"failed,omitempty"`
}

修改 httpclient_controller.go

定义控制器业务逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func (r *HTTPClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 
log := log.FromContext(ctx)
var httpclient appsv1beta1.HTTPClient
if err := r.Get(ctx, req.NamespacedName, &httpclient); err != nil {
log.Error(err, "Failed to read httpclient")
}

if httpclient.Spec.ServerAddress != "" {
totalCall := httpclient.Status.Failed + httpclient.Status.Succeeded
if totalCall >= httpclient.Spec.Count {
return ctrl.Result{}, nil
}
for i := 0; i < httpclient.Spec.Count; i++ {
log.Info("Calling server,", "times", i)
_, err := http.Get(httpclient.Spec.ServerAddress)
if err != nil {
log.Info("HTTP get failed with error: ", "error", err)
httpclient.Status.Failed = httpclient.Status.Failed + 1
} else {
log.Info("HTTP get succeeded")
httpclient.Status.Succeeded = httpclient.Status.Succeeded + 1
}
}
}

return ctrl.Result{}, nil
}

生成 CRD

在创建项目时,已经自动生成了一个Makefile,可以执行 make help 看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

Usage:
make <target>

General
help Display this help.

Development
manifests Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
generate Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
fmt Run go fmt against code.
vet Run go vet against code.
test Run tests.
lint Run golangci-lint linter
lint-fix Run golangci-lint linter and perform fixes

Build
build Build manager binary.
run Run a controller from your host.
docker-build Build docker image with the manager.
docker-push Push docker image with the manager.
docker-buildx Build and push docker image for the manager for cross-platform support
build-installer Generate a consolidated YAML with CRDs and deployment.

Deployment
install Install CRDs into the K8s cluster specified in ~/.kube/config.
uninstall Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
deploy Deploy controller to the K8s cluster specified in ~/.kube/config.
undeploy Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.

Dependencies
kustomize Download kustomize locally if necessary.
controller-gen Download controller-gen locally if necessary.
envtest Download setup-envtest locally if necessary.
golangci-lint Download golangci-lint locally if necessary.

manifests 和 generate 是针对开发使用,可以自动生成一些逻辑。可直接执行 make build 包含了以上步骤。

1
2
3
4
make build
make docker-build # 构建 自定义资源的 controller 镜像
make docker-push
make deploy

生成的镜像那个如下:
image.png

下边 介绍下 make deploy 的具体执行逻辑:

1
2
3
4
.PHONY: deploy  
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
  1. 进入 config/manager 目录,此目录下存放了 kustomize 的配置文件,kustomize 是一个用于 自定义 Kubernetes 配置的工具,有点像 Helm
  2. 使用Kustomize的edit set image命令来更新controller镜像的引用。
  3. $(KUSTOMIZE) build config/default:使用Kustomize的build命令来生成最终的Kubernetes资源配置文件
  4. 将Kustomize的输出传入 kubectl apply -f -命令,完成控制器在 kubernetes 集群中的部署

部署定义的资源

config/samples 下的 apps_v1beta1_httpclient.yaml 如下:

1
2
3
4
5
6
7
8
9
10
apiVersion: apps.baihl.io/v1beta1  
kind: HTTPClient
metadata:
labels:
app.kubernetes.io/name: httpclient
app.kubernetes.io/managed-by: kustomize
name: httpclient-sample
spec:
serverAddress: http://192.168.34.2:32147/hello
count: 5