声明式API与Kubernetes编程范式

Istio项目

Dynamic Admission Control

在 Kubernetes 项目中,当一个 Pod 或者任何一个 API 对象被提交给 APIServer 之后,在K8S项目正式处理之前会通过 项目里一组被称为 Admission Controller 的代码实现 Admission 的功能 来 进行一些初始化工作(如自动为所有 Pod 加上某些标签(Labels)。)

如果想要添加一些自己的规则到 Admission Controller(需要重新编译并重启 APIServer)。

所以,k8s提供了一种**“热插拔”**式的 Admission 机制,它就是 Dynamic Admission Control,也叫作:Initializer

用户提交的Pod中只有一个容器:myapp-pod

apiVersion: v1
kind: Pod
metadata:name: myapp-podlabels:app: myapp
spec:containers:- name: myapp-containerimage: busyboxcommand: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']

Istio 项目要做的,就是在这个 Pod YAML 被提交给 Kubernetes 之后,在它对应的 API 对象里自动加上 Envoy 容器的配置,使这个对象变成如下所示的样子:

apiVersion: v1
kind: Pod
metadata:name: myapp-podlabels:app: myapp
spec:containers:- name: myapp-containerimage: busyboxcommand: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']- name: envoyimage: lyft/envoy:845747b88f102c0fd262ab234308e9e22f693a1command: ["/usr/local/bin/envoy"]...

如何做到呢

Istio 编写一个用来为 Pod“自动注入”Envoy 容器的 Initializer。

Istio 会将这个 Envoy 容器本身的定义,以 ConfigMap 的方式保存在 Kubernetes 当中。

apiVersion: v1
kind: ConfigMap
metadata:name: envoy-initializer
data:config: |containers:- name: envoyimage: lyft/envoy:845747db88f102c0fd262ab234308e9e22f693a1command: ["/usr/local/bin/envoy"]args:- "--concurrency 4"- "--config-path /etc/envoy/envoy.json"- "--mode serve"ports:- containerPort: 80protocol: TCPresources:limits:cpu: "1000m"memory: "512Mi"requests:cpu: "100m"memory: "64Mi"volumeMounts:- name: envoy-confmountPath: /etc/envoyvolumes:- name: envoy-confconfigMap:name: envoy

Initializer 需要把envoy的相关字段merge到用户提交的Pod内容中,(使用PATCH API实现,时声明式API的主要能力)

Istio 将一个编写好的 Initializer,作为一个 Pod 部署在 Kubernetes 中

apiVersion: v1
kind: Pod
metadata:labels:app: envoy-initializername: envoy-initializer
spec:containers:- name: envoy-initializerimage: envoy-initializer:0.0.1imagePullPolicy: Always

envoy-initializer:0.0.1 镜像,就是一个事先编写好的“自定义控制器”(Custom Controller)

功能:

for {// 获取新创建的 Podpod := client.GetLatestPod()// Diff 一下,检查是否已经初始化过if !isInitialized(pod) {// 没有?那就来初始化一下doSomething(pod)}
}
//在 Initializer 控制器的工作逻辑里,它首先会从 APIServer 中拿到这个 ConfigMap
//把这个 ConfigMap 里存储的 containers 和 volumes 字段,直接添加进一个空的 Pod 对象里:
//Kubernetes 的 API 库,为我们提供了一个方法,使得我们可以直接使用新旧两个 Pod 对象,生成一个 TwoWayMergePatch
func doSomething(pod) {cm := client.Get(ConfigMap, "envoy-initializer")newPod := Pod{}newPod.Spec.Containers = cm.ContainersnewPod.Spec.Volumes = cm.Volumes// 生成 patch 数据patchBytes := strategicpatch.CreateTwoWayMergePatch(pod, newPod)// 发起 PATCH 请求,修改这个 pod 对象client.Patch(pod.Name, patchBytes)
}

Kubernetes 还允许你通过配置,来指定要对什么样的资源进行这个 Initialize 操作

apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:name: envoy-config
initializers:// 这个名字必须至少包括两个 "."- name: envoy.initializer.kubernetes.iorules:- apiGroups:- "" // 前面说过, "" 就是 core API Group 的意思apiVersions:- v1resources:- pods

一旦这个 InitializerConfiguration 被创建,Kubernetes 就会把这个 Initializer 的名字,加在所有新创建的 Pod 的 Metadata 上

apiVersion: v1
kind: Pod
metadata:initializers:pending:- name: envoy.initializer.kubernetes.ioname: myapp-podlabels:app: myapp
...

每一个新创建的 Pod,都会自动携带了 metadata.initializers.pending 的 Metadata 信息

这个 Metadata,正是接下来 Initializer 的控制器判断这个 Pod 有没有执行过自己所负责的初始化操作的重要依据

当在 Initializer 里完成了要做的操作后,一定要metadata.initializers.pending 标志清除掉。

声明式API的工作原理

总结

K8s中,一个API对象在etcd中完整的路径是由Group(API组)、Version(API版本)、Resource(API资源类型)3个部分决定的。

核心 API 对象

对于 Kubernetes 里的核心 API 对象,比如:Pod、Node 等,是不需要 Group 的(即:它们 Group 是“”)。所以,对于这些 API 对象来说,Kubernetes 会直接在 /api 这个层级进行下一步的匹配过程

非核心 API 对象

对于 CronJob 等非核心 API 对象来说,Kubernetes 就必须在 /apis 这个层级里查找它对应的 Group,进而根据“batch”这个 Group 的名字,找到 /apis/batch,Kubernetes 会进一步匹配到 API 对象的版本号。

ps:同一种 API 对象可以有多个版本,这正是 Kubernetes 进行 API 版本化管理的重要手段

APIServer

资源操作的唯一入口,接受用户输入的命令,提供认证、授权、API注册和发现等机制

代码生成功能(Gengo、code-generator)

由于同时要兼顾性能、API 完备性、版本化、向后兼容等很多工程化指标,所以 Kubernetes 团队在 APIServer 项目里大量使用了 Go 语言的代码生成功能,来自动化诸如 Convert、DeepCopy 等与 API 资源相关的操作

CRD

Kubernetes v1.7 之后,得益于一个全新的 API 插件机制:CRD(Custom Resource Definition),允许用户在 Kubernetes 中添加一个跟 Pod、Node 类似的、新的 API 资源类型,即:自定义 API 资源。。

举例:

声明如下一个API对象

apiVersion: samplecrd.k8s.io/v1
kind: Network
metadata:name: example-network
spec:cidr: "192.168.0.0/16"gateway: "192.168.0.1"

可以看到,API 组是 samplecrd.k8s.io,版本是v1, 资源类型是Network

想要让K8s认识这个API,需要用到CR(Custom Resource)

声明一个CRD

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:name: networks.samplecrd.k8s.io
spec:group: samplecrd.k8s.ioversion: v1names:kind: Networkplural: networksscope: Namespaced

在这个CRD中,指定了这个 group: samplecrd.k8s.io version: v1

指定了这个 CR 的资源类型叫作 Network,复数(plural)是 networks。

声明了它的 scope 是 Namespaced,即:我们定义的这个 Network 是一个属于 Namespace 的对象,类似于 Pod。

使k8s认识字段

让 Kubernetes“认识”这种 YAML 文件里描述的“网络”部分,比如“cidr”(网段),“gateway”(网关)这些字段的含义。

张哥代码

代码生成简介

register.go

位置:pkg/apis/samplecrd

用来放置后面要用到的全局变量


package samplecrdconst (GroupName = "samplecrd.k8s.io"Version   = "v1"
)
doc.go
// +k8s:deepcopy-gen=package// +groupName=samplecrd.k8s.io
package v1

+k8s:deepcopy-gen=package 意思是,请为整个 v1 包里的所有类型定义自动生成 DeepCopy 方法;而+groupName=samplecrd.k8s.io,则定义了这个包对应的 API 组的名字

这些定义在 doc.go 文件的注释,起到的是全局的代码生成控制的作用,所以也被称为 Global Tags

types.go

定义一个 Network 类型到底有哪些字段(比如,spec 字段里的内容)

package v1
...
// +genclient
// +genclient:noStatus
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// Network describes a Network resource
type Network struct {// TypeMeta is the metadata for the resource, like kind and apiversionmetav1.TypeMeta `json:",inline"`// ObjectMeta contains the metadata for the particular object, including// things like...//  - name//  - namespace//  - self link//  - labels//  - ... etc ...//metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec networkspec `json:"spec"`
}
// networkspec is the spec for a Network resource
type networkspec struct {Cidr    string `json:"cidr"`Gateway string `json:"gateway"`
}// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// NetworkList is a list of Network resources
type NetworkList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata"`Items []Network `json:"items"`
}

Network 类型定义方法跟标准的 Kubernetes 对象一样,包括 TypeMeta(API 元数据)和 ObjectMeta(对象元数据)字段

其中的 Spec 字段,就是需要我们自己定义的部分。所以,在 networkspec 里,定义了 Cidr 和 Gateway 两个字段。

??除了定义 Network 类型,你还需要定义一个 NetworkList 类型,用来描述一组 Network 对象应该包括哪些字段。??

(在 Kubernetes 中,获取所有 X 对象的 List() 方法,返回值都是List 类型,而不是 X 类型的数组。这是不一样的。)

+genclient 的意思是:请为下面这个 API 资源类型生成对应的 Client 代码(这个 Client,我马上会讲到)。而 +genclient:noStatus 的意思是:这个 API 资源类型定义里,没有 Status 字段。否则,生成的 Client 就会自动带上 UpdateStatus 方法。

//+genclient 只需要写在 Network 类型上,而不用写在 NetworkList 上。因为 NetworkList 只是一个返回值类型,Network 才是“主类型”。

一个固定的操作:添加+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object的注释。它的意思是,请在生成 DeepCopy 的时候,实现 Kubernetes 提供的 runtime.Object 接口。否则,在某些版本的 Kubernetes 里,你的这个类型定义会出现编译错误。

register.go

Network 资源类型在服务器端的注册的工作,APIServer 会自动帮我们完成(“registry”的作用就是注册一个类型(Type)给 APIServer)。但与之对应的,我们还需要让客户端也能“知道”Network 资源类型的定义。这就需要我们在项目里添加一个 register.go 文件。它最主要的功能,就是定义了如下所示的 addKnownTypes() 方法:

package v1
...
// addKnownTypes adds our types to the API scheme by registering
// Network and NetworkList
func addKnownTypes(scheme *runtime.Scheme) error {scheme.AddKnownTypes(SchemeGroupVersion,&Network{},&NetworkList{},)// register the type in the schememetav1.AddToGroupVersion(scheme, SchemeGroupVersion)return nil
}

有了这个方法,Kubernetes 就能够在后面生成客户端的时候,“知道”Network 以及 NetworkList 类型的定义了。

总结

Network 对象的定义工作就全部完成了。可以看到,它其实定义了两部分内容:

  • 第一部分是,自定义资源类型的 API 描述,包括:组(Group)、版本(Version)、资源类型(Resource)等。这相当于告诉了计算机:兔子是哺乳动物。
  • 第二部分是,自定义资源类型的对象描述,包括:Spec、Status 等。这相当于告诉了计算机:兔子有长耳朵和三瓣嘴。

接下来要使用 Kubernetes 提供的代码生成工具,为上面定义的 Network 资源类型自动生成 clientset、informer 和 lister

pkg/apis/samplecrd/v1 下面的 zz_generated.deepcopy.go 文件,就是自动生成的 DeepCopy 代码文件。

而整个 client 目录,以及下面的三个包(clientset、informers、 listers),都是 Kubernetes 为 Network 类型生成的客户端库,这些库会在后面编写自定义控制器的时候用到。

创建出这样一个自定义 API 对象,只是完成了 Kubernetes 声明式 API 的一半工作。

接下来的另一半工作是:为这个 API 对象编写一个自定义控制器(Custom Controller)。这样, Kubernetes 才能根据 Network API 对象的“增、删、改”操作,在真实环境中做出相应的响应。比如,“创建、删除、修改”真正的 Neutron 网络。

而这,正是 Network 这个 API 对象所关注的“业务逻辑”。

API编程范式的具体原理

自定义控制器代码:编写main函数、编写自定义控制器信息、以及编写控制器的业务逻辑

编写main函数

var (masterURL  stringkubeconfig string
)func main() {flag.Parse()// set up signals so we handle the first shutdown signal gracefullystopCh := signals.SetupSignalHandler()cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)if err != nil {glog.Fatalf("Error building kubeconfig: %s", err.Error())}kubeClient, err := kubernetes.NewForConfig(cfg)if err != nil {glog.Fatalf("Error building kubernetes clientset: %s", err.Error())}networkClient, err := clientset.NewForConfig(cfg)if err != nil {glog.Fatalf("Error building example clientset: %s", err.Error())}networkInformerFactory := informers.NewSharedInformerFactory(networkClient, time.Second*30)controller := NewController(kubeClient, networkClient,networkInformerFactory.Samplecrd().V1().Networks())go networkInformerFactory.Start(stopCh)if err = controller.Run(2, stopCh); err != nil {glog.Fatalf("Error running controller: %s", err.Error())}
}
//flag
func init() {flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
}

通过散步完成初始化,并启动一个自定义控制器的工作

第一步

var (masterURL  stringkubeconfig string
)cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)kubeClient, err := kubernetes.NewForConfig(cfg)networkClient, err := clientset.NewForConfig(cfg)

根据提供的 Master 配置(APIServer 的地址端口和 kubeconfig 的路径),创建一个 Kubernetes 的 client(kubeClient)和 Network 对象的 client(networkClient)。

第二步

networkInformerFactory := informers.NewSharedInformerFactory(networkClient, time.Second*30)controller := NewController(kubeClient, networkClient,networkInformerFactory.Samplecrd().V1().Networks())

main 函数为 Network 对象创建一个叫作 InformerFactory(即:networkInformerFactory)的工厂,并使用它生成一个 Network 对象的 Informer,传递给控制器。

第三步

 go networkInformerFactory.Start(stopCh)if err = controller.Run(2, stopCh); err != nil {glog.Fatalf("Error running controller: %s", err.Error())}

main 函数启动上述的 Informer,然后执行 controller.Run,启动自定义控制器

自定义控制器的工作原理

这个控制器要做的第一件事,是从 Kubernetes 的 APIServer 里获取它所关心的对象(Network )

(通过 Informer(可以翻译为:通知器)代码库完成)

Informer 与 API 对象是一一对应的,上述代码传递给自定义控制器的,正是一个 Network 对象的 Informer(Network Informer)

在创建这个 Informer 工厂的时候,需要给它传递一个 networkClient。事实上,Network Informer 正是使用这个 networkClient,跟 APIServer 建立了连接。不过,真正负责维护这个连接的,则是 Informer 所使用的 Reflector 包。

ListAndWatch

Reflector 使用的是一种叫作ListAndWatch的方法,来**“获取”“监听”**这些 Network 对象实例的变化。

  1. 一旦 APIServer 端有新的 Network 实例被创建、删除或者更新,Reflector 都会收到“事件通知”。这时,该事件及它对应的 API 对象这个组合,就被称为增量(Delta),它会被放进一个 Delta FIFO Queue(即:增量先进先出队列)中。

  2. Informe 会不断地从这个 Delta FIFO Queue 里读取(Pop)增量。每拿到一个增量,Informer 就会判断这个增量里的事件类型,然后创建或者更新本地对象的缓存。这个缓存,在 Kubernetes 里一般被叫作 Store

​ 2.1. 如果事件类型是 Added(添加对象),那么 Informer 就会通过一个叫作 Indexer 的库把这个增量里的 API 对象保存在本地缓存 中,并为它创建索引。相反地,如果增量的事件类型是 Deleted(删除对象),那么 Informer 就会从本地缓存中删除这个对象

同步本地缓存的工作,是 Informer 的第一个职责,也是它最重要的职责。

​ 2.2. 根据这些事件的类型,触发事先注册好的 ResourceEventHandler。这些 Handler,需要在创建控制器的时候注册给它对应的 Informer。

编写控制器的定义

func NewController(kubeclientset kubernetes.Interface,networkclientset clientset.Interface,networkInformer informers.NetworkInformer) *Controller {...controller := &Controller{kubeclientset:    kubeclientset,networkclientset: networkclientset,networksLister:   networkInformer.Lister(),networksSynced:   networkInformer.Informer().HasSynced,workqueue:        workqueue.NewNamedRateLimitingQueue(...,  "Networks"),...}networkInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: controller.enqueueNetwork,UpdateFunc: func(old, new interface{}) {oldNetwork := old.(*samplecrdv1.Network)newNetwork := new.(*samplecrdv1.Network)if oldNetwork.ResourceVersion == newNetwork.ResourceVersion {// Periodic resync will send update events for all known Networks.// Two different versions of the same Network will always have different RVs.return}controller.enqueueNetwork(new)},DeleteFunc: controller.enqueueNetworkForDelete,})return controller
}

使用创建的两个 client(kubeclientset 和 networkclientset)和Informer 初始化了自定义控制器

这个自定义控制器中还有一个工作队列(work queue) WorkQueue(k8s中有很多工作队列的实现)。负责同步 Informer 和控制循环之间的数据。

为 networkInformer 注册了三个 Handler(AddFunc、UpdateFunc 和 DeleteFunc),分别对应 API 对象的“添加”“更新”和“删除”事件。而具体的处理操作,都是将该事件对应的 API 对象加入到工作队列中。实际上入队的是他们的Key,即该API对象的<namespace>/<name>,后面的控制循环,则会不断地从这个工作队列里拿到这些 Key,然后开始执行真正的控制逻辑

Informer

Informer,其实就是一个带有本地缓存和索引机制的、可以注册 EventHandler 的 client。它是自定义控制器跟 APIServer 进行数据同步的重要组件

Informer 通过一种叫作 ListAndWatch 的方法,把 APIServer 中的 API 对象缓存在了本地,并负责更新和维护这个缓存。

其中,ListAndWatch 方法的含义是:首先,通过 APIServer 的 LIST API“获取”所有最新版本的 API 对象;然后,再通过 WATCH API 来“监听”所有这些 API 对象的变化。

而通过监听到的事件变化,Informer 就可以实时地更新本地缓存,并且调用这些事件对应的 EventHandler 了。

此外,在这个过程中,每经过 resyncPeriod 指定的时间,Informer 维护的本地缓存,都会使用最近一次 LIST 返回的结果强制更新一次,从而保证缓存的有效性。在 Kubernetes 中,这个缓存强制更新的操作就叫作:resync

需要注意的是,这个定时 resync 操作,也会触发 Informer 注册的“更新”事件。但此时,这个“更新”事件对应的 Network 对象实际上并没有发生变化,即:新、旧两个 Network 对象的 ResourceVersion 是一样的。在这种情况下,Informer 就不需要对这个更新事件再做进一步的处理了。

这也是为什么我在上面的 UpdateFunc 方法里,先判断了一下新、旧两个 Network 对象的版本(ResourceVersion)是否发生了变化,然后才开始进行的入队操作。

以上,就是 Kubernetes 中的 Informer 库的工作原理了。

控制循环(Control Loop)部分

也正是 main 函数最后调用 controller.Run() 启动的“控制循环

func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {...if ok := cache.WaitForCacheSync(stopCh, c.networksSynced); !ok {return fmt.Errorf("failed to wait for caches to sync")}...for i := 0; i < threadiness; i++ {go wait.Until(c.runWorker, time.Second, stopCh)}...return nil
}

可以看到,启动控制循环的逻辑非常简单:

首先,等待 Informer 完成一次本地缓存的数据同步操作;
然后,直接通过 goroutine 启动一个(或者并发启动多个)“无限循环”的任务。
而这个“无限循环”任务的每一个循环周期,执行的正是我们真正关心的业务逻辑。

编写自定义控制器的业务逻辑

func (c *Controller) runWorker() {for c.processNextWorkItem() {}
}func (c *Controller) processNextWorkItem() bool {obj, shutdown := c.workqueue.Get()...err := func(obj interface{}) error {...if err := c.syncHandler(key); err != nil {return fmt.Errorf("error syncing '%s': %s", key, err.Error())}c.workqueue.Forget(obj)...return nil}(obj)...return true
}

在这个执行周期里(processNextWorkItem)

首先从工作队列里出队(workqueue.Get)了一个成员( Key(Network 对象的:namespace/name)).

func (c *Controller) syncHandler(key string) error {namespace, name, err := cache.SplitMetaNamespaceKey(key)...network, err := c.networksLister.Networks(namespace).Get(name)if err != nil {if errors.IsNotFound(err) {glog.Warningf("Network does not exist in local cache: %s/%s, will delete it from Neutron ...",namespace, name)glog.Warningf("Network: %s/%s does not exist in local cache, will delete it from Neutron ...",namespace, name)// FIX ME: call Neutron API to delete this network by name.//// neutron.Delete(namespace, name)return nil}...未结束

然后,在 syncHandler 方法中,我使用这个 Key,尝试从 Informer 维护的缓存中拿到了它所对应的 Network 对象。

(使用 networksLister 来尝试获取这个 Key 对应的 Network 对象。这个操作,其实就是在访问本地缓存的索引。)

(在 Kubernetes 的源码中,你会经常看到控制器从各种 Lister 里获取对象,比如:podLister、nodeLister 等等,它们使用的都是 Informer 和缓存机制。)

如果控制循环从缓存中拿不到这个对象(即:networkLister 返回了 IsNotFound 错误),那就意味着这个 Network 对象的 Key 是通过前面的“删除”事件添加进工作队列的。所以,尽管队列里有这个 Key,但是对应的 Network 对象已经被删除了。

需要调用 Neutron 的 API,把这个 Key 对应的 Neutron 网络从真实的集群里删除掉。

而如果能够获取到对应的 Network 对象,就执行控制器模式里的对比“期望状态”和“实际状态”的逻辑

 return err}glog.Infof("[Neutron] Try to process network: %#v ...", network)// FIX ME: Do diff().//// actualNetwork, exists := neutron.Get(namespace, name)//// if !exists {//   neutron.Create(namespace, name)// } else if !reflect.DeepEqual(actualNetwork, network) {//   neutron.Update(namespace, name)// }return nil
}

自定义控制器“千辛万苦”拿到的这个 Network 对象,正是 APIServer 里保存的“期望状态”,即:用户通过 YAML 文件提交到 APIServer 里的信息。当然,在我们的例子里,它已经被 Informer 缓存在了本地。

然后通过Neutron API 来查询实际的网络情况

比如,可以先通过 Neutron 来查询这个 Network 对象对应的真实网络是否存在。

  • 如果不存在,这就是一个典型的“期望状态”与“实际状态”不一致的情形。这时,我就需要使用这个 Network 对象里的信息(比如:CIDR 和 Gateway),调用 Neutron API 来创建真实的网络。
  • 如果存在,那么,我就要读取这个真实网络的信息,判断它是否跟 Network 对象里的信息一致,从而决定我是否要通过 Neutron 来更新这个已经存在的真实网络。

这样,我就通过对比“期望状态”和“实际状态”的差异,完成了一次调协(Reconcile)的过程。

总结

在实际应用中,除了控制循环之外的所有代码,实际上都是 Kubernetes 为你自动生成的,即:pkg/client/{informers, listers, clientset}里的内容。

而这些自动生成的代码,就为我们提供了一个可靠而高效地获取 API 对象“期望状态”的编程库。

所以,接下来,作为开发者,你就只需要关注如何拿到“实际状态”,然后如何拿它去跟“期望状态”做对比,从而决定接下来要做的业务逻辑即可。
me)
// }

return nil
}


自定义控制器“千辛万苦”拿到的这个 Network 对象,**正是 APIServer 里保存的“期望状态”**,即:用户通过 YAML 文件提交到 APIServer 里的信息。当然,在我们的例子里,它已经被 Informer 缓存在了本地。然后通过Neutron API 来查询**实际的网络情况**比如,可以先通过 Neutron 来查询这个 Network 对象对应的真实网络是否存在。- 如果不存在,这就是一个典型的“期望状态”与“实际状态”不一致的情形。这时,我就需要使用这个 Network 对象里的信息(比如:CIDR 和 Gateway),调用 Neutron API 来创建真实的网络。
- 如果存在,那么,我就要读取这个真实网络的信息,判断它是否跟 Network 对象里的信息一致,从而决定我是否要通过 Neutron 来更新这个已经存在的真实网络。这样,我就通过对比“期望状态”和“实际状态”的差异,完成了一次调协(Reconcile)的过程。# 总结在实际应用中,除了控制循环之外的所有代码,实际上都是 Kubernetes 为你自动生成的,即:pkg/client/{informers, listers, clientset}里的内容。而这些自动生成的代码,就为我们提供了一个可靠而高效地获取 API 对象“期望状态”的编程库。所以,接下来,作为开发者,你就只需要关注如何拿到“实际状态”,然后如何拿它去跟“期望状态”做对比,从而决定接下来要做的业务逻辑即可。

深入剖析Kubernetes--第五章:声明式API与Kubernetes编程范式相关推荐

  1. api接口怎么写_面向声明式API编程(DAP)

    DAP是Mars-java 最近提出的一个新的开发方式,全称 Declarative API Programming, 提倡后端为一个独立的整体,不应该是为前端服务的,所以当前端需要接口的时候,只需要 ...

  2. 《Spring系列》第15章 声明式事务(一) 基础使用

    一.ACID特性 ⑴ 原子性(Atomicity)原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库 ...

  3. 声明式API replica controller vs replica set 对比

    1.在命令式API中,你可以直接发出服务器要执行的命令,例如: "运行容器"."停止容器"等. 在声明性API中,你声明系统要执行的操作,系统将不断向该状态驱动 ...

  4. Kubernetes 第五章 YAML

    对于kubernetes 资源可以使用加载 yaml 标记语言的方式,进行自定义: YAML(/ˈjæməl/,尾音类似camel骆驼)是一个可读性高,用来表达数据序列化的格式.YAML参考了其他多种 ...

  5. 【C】【笔记】《C语言深度剖析》第五章 内存管理

    本文为个人读书笔记,仅供记录学习过程中遇到的日后需要留意的问题,如有相关版权问题请及时通知作者. 野指针 也称悬垂指针,指向已经不存在的对象.要杜绝野指针,.定义指针变量的同时就初始化为NULL,用完 ...

  6. KCL:声明式的云原生配置策略语言

    楔子: 以蚂蚁集团典型的建站场景为例,在接入 Kusion 后,用户侧配置代码减少到 5.5%,用户面对的 4 个平台通过接入统一代码库而消减,在无其他异常的情况下交付时间从 2 天下降到 2 小时- ...

  7. 通过 React Hooks 声明式地使用 setInterval

    2019独角兽企业重金招聘Python工程师标准>>> 本文由云+社区发表 作者:Dan Abramov 接触 React Hooks 一定时间的你,也许会碰到一个神奇的问题: se ...

  8. 04.声明式服务调用:Spring Cloud Feign(Greenwich.SR2)

    1.Feign是什么 Feign是整合了Ribbon与Hystrix外,还提供了声明式的Web服务客户端定义方式.采用了声明式API接口的风格,将Java Http客户端绑定到它的内部.Feign的首 ...

  9. Kubernetes 学习总结(23)—— 2022 年 Kubernetes 的 5 个趋势

    前言 Kubernetes 在成长,使用它的团队也在成长.早期采用者现在已经进入了自己的领域,能够基于经验和云原生生态系统的增长,以新的方式扩展 Kubernetes 的核心功能."我们将继 ...

最新文章

  1. 精灵盛典电脑模拟器服务器怎么修改,精灵盛典ios电脑版
  2. 如何将NSString转换为NSNumber
  3. Linux服务器数据备份
  4. treemap怎么保证有序_干货!208道面试题教你怎么通过面试!
  5. php怎么使得字体滚动,滚动文字+字体特效代码(全集)
  6. oracle 中sql的分类,Oracle数据库语言分类
  7. inner join 与 left join 、right join之间的区别
  8. java写顾客购买的商品总价格_成交总金额=商品价格×商品件数-总优惠额。 如果一个顾客,购买的商品一口价为5元,购买的商品件数为4,总物流运费4元,满20送3,请问成交的金额是()。...
  9. iOS开发之解析XML格式数据
  10. solr处理oracle中CLOB、BLOB
  11. WPF高级教程(三)XAML
  12. 解决虚拟机-虚拟网络配置没有桥接模式,本地没有虚拟网卡
  13. Python编写尼姆游戏
  14. 我的世界服务器增加刷怪率,我的世界:最简易刷怪空间,别再暴殄天物了!
  15. 爱阅书香之书源制作 POST请求方式
  16. nvm use出现乱码
  17. outlook2007 配置
  18. 根据概率密度函数生成随机数的代码
  19. 华为荣耀8电信卡显示无服务器,华为荣耀手机实现双电信卡双待双通,5步告诉你真相...
  20. 红黑树系列1——红黑树的建立

热门文章

  1. LeetCode 14.最长公共前缀(字符串)
  2. spark ui job和stage的dag图查看过去运行的任务,查不到,分析源码解决问题
  3. pythongui库推荐_八款常用的 Python GUI 开发框架推荐
  4. 猎豹浏览器使用评测(2)-一款很轻的极简绿色浏览器
  5. Vue 使用echarts 地图自定义图标和修改图标样式,点击切换图标
  6. 反射创建实例时出现异常 class *** cannot access a member of class *** with modifiers
  7. 图片转成base64格式上传至数据库
  8. FFMpeg-9、给视频添加实时时间水印drawtext filters+中文水印显示问题
  9. 用js写卡牌游戏(五)
  10. cx_Oracle.DatabaseError: ORA-01036: 非法的变量名/编号