介绍
Kubernetes控制器是一个主动调谐的过程,它会watch一些对象的期望状态,也会watch实际的状态,然后控制器会发送一些指令尝试让对象的当前状态往期望状态迁移。
控制器最简单的实现就是一个循环:
for{
desired := getDesiredState()
current := getCurrentState()
makeChanges(desired,current)
}
Watches 这些都只是这个逻辑的优化。
指南
当你在编写控制器时,有一些准则将有助于确保你得到你需要的结果和性能。
-
一次只操作一个元素,如果你使用了
workqueue.Interface
,你可以将某个资源的变化排成队列,随后将他们弹到多个“worker” gofuncs 中,并保证没有两个gofuncs会同时对同一个元素进行操作。许多控制器必须触发多个资源(我需要“如果 Y 更改,则检查 X”),但几乎所有控制器都可以根据关系将这些资源折叠到“检查此 X”的队列中。例如,ReplicaSet 控制器需要对被删除的 pod 做出反应,但它通过查找相关的 ReplicaSet 并将它们排队来做到这一点。
-
资源之间的随机排序。当控制器对多种类型的资源进行排队时,无法保证资源之间的排序。
不同的watch独立更新。即使使用“created resourceA/X”和“created resourceB/Y”的客观顺序,您的控制器也可以观察到“created resourceB/Y”和“created resourceA/X”。
-
水平驱动,而不是边缘驱动。就像没有一直运行的shell脚本一样,您的控制器可能会在再次运行之前关闭一段不确定的时间。
如果一个API对象出现的标记值为
true
,您不能指望看到它从false
变为true
,只是您现在观察到它是true
。即使是API watch也会遇到这个问题,因此请确保您不要指望看到更改,除非您的控制器还在对象状态中标记了它上次做出的决定信息。 -
使用 SharedInformers。SharedInformers 提供了回调函数来接收特定资源的添加、更新和删除的通知,它们还提供了访问共享缓存和确定缓存何时启动的便利功能。
使用
https://git.k8s.io/kubernetes/staging/src/k8s.io/client-go/informers/factory.go
中的工厂方法来确保你和其他人共享同一个缓存实例。这样我们就大大减少了 APIServer 的连接以及重复的序列化、重复的反序列化、重复的缓存等成本。
你可能会看到其他机制,比如反射器和 DeltaFIFO 驱动控制器。这些都是旧的机制,我们后来用它们来构建 SharedInformers,你应该避免在新控制器中使用它们。
-
永远不要改变原始对象!缓存在控制器之间共享,这意味着如果您改变对象的“副本”(实际上是引用或浅拷贝),您将弄乱其他控制器(不仅仅是您自己的)。
最常见的失败点是制作一个浅拷贝,然后改变一个映射,比如
Annotations
. 用于api.Scheme.Copy
制作深拷贝。 -
等待您的二级缓存。许多控制器具有主要和次要资源。主要资源是您将为其更新的资源
Status
。次要资源是您将要管理(创建/删除)或用于查找的资源。在启动主要同步功能之前,使用该
framework.WaitForCacheSync
功能等待二级缓存。这将确保诸如 ReplicaSet 的 Pod 计数之类的东西不会因已知的过时信息而导致抖动。 -
系统中还有其他参与者。仅仅因为您没有更改对象并不意味着其他人没有。
不要忘记当前状态可能随时改变——仅仅观察期望的状态是不够的。如果您使用不存在所需状态的对象来指示应删除当前状态的事物,请确保您的观察代码中没有错误(例如,在缓存填满之前采取行动)。
-
将错误渗透到顶层以实现一致的重新排队。我们有一个
workqueue.RateLimitingInterface
允许简单的重新排队和合理的退避。
当需要重新排队时,您的主控制器 func 应该返回错误。如果不是,它应该使用utilruntime.HandleError
并返回 nil。这使得审阅者很容易检查错误处理情况,并确信您的控制器不会意外丢失它应该重试的东西。
-
Watches 和 Informers 将“同步”。他们会定期将集群中的每个匹配对象传递给您的
Update
方法。这适用于您可能需要对对象采取额外操作但有时您知道不会有更多工作要做的情况。如果您确定在没有新更改的情况下不需要重新排队项目,您可以比较新旧对象的资源版本。如果它们相同,则跳过重新排队工作。执行此操作时要小心。如果您在失败时跳过重新排队您的项目,您可能会失败,而不是重新排队,然后再也不会重试该项目。
-
如果您的控制器正在协调的主要资源在其状态中支持 ObservedGeneration,请确保在两个字段之间的值不匹配时将其正确设置为 metadata.Generation。
这让客户端知道控制器已经处理了资源。确保您的控制器是负责该资源的主控制器,否则如果您需要通过您自己的控制器传达观察,则需要在资源的状态中创建不同类型的 ObservedGeneration。
-
考虑对导致创建其他资源的资源使用所有者引用(例如,ReplicaSet 导致创建 Pod)。因此,您可以确保一旦您的控制器管理的资源被删除,子资源将被垃圾收集。有关所有者参考的更多信息,请在此处阅读更多信息。
要特别注意收养的方式。当父母或孩子被标记为删除时,您不应该为资源收养孩子。如果您正在为您的资源使用缓存,您可能需要通过直接读取 API 来绕过它,以防您发现所有者引用已为其中一个孩子更新。因此,您可以确保您的控制器不会与垃圾收集器竞争。
示例
package main
import (
"flag"
"fmt"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
"path/filepath"
"time"
)
type Controller struct {
indexer cache.Indexer
queue workqueue.RateLimitingInterface
informer cache.Controller
}
func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller) *Controller {
return &Controller{
indexer: indexer,
queue: queue,
informer: informer,
}
}
func (c *Controller) processNexItem() bool {
// 等待工作队列中有一个新元素
key, quit := c.queue.Get()
if quit {
return false
}
// 告诉队列我们已经完成了处理 此 key 的操作
// 这将为其他 worker 解锁该 key
// 这将确保安全的并行处理,因为永远不会并行处理具有相同key的两个pod
defer c.queue.Done(key)
// 调用包含业务逻辑的方法
err := c.syncToStdout(key.(string))
// 如果在执行业务逻辑期间出现错误,则处理错误
c.handlerErr(err, key)
return true
}
// 控制器的业务逻辑实现
// 在此控制器中,它只是将有关 Pod 的信息打印到 stdout
// 如果发生错误,则简单的返回错误
// 此外重试逻辑不应该成为业务逻辑的一部分
func (c *Controller) syncToStdout(key string) error {
// 从本地存储中获取key对应的对象
obj, exists, err := c.indexer.GetByKey(key)
if err != nil {
klog.Errorf("Fetching object with key %s from store failed with %v", key, err)
return err
}
if !exists {
fmt.Printf("Pod %s does not exists anymore\n", key)
} else {
fmt.Printf("Sync/Add/Update for Pod %s\n", obj.(*v1.Pod).GetName())
}
return nil
}
// 检查是否发生错误,并确保我们稍后重试
func (c *Controller) handlerErr(err error, key interface{}) {
if err == nil {
// 忘记每次成功同步时 key 的#AddRateLimited历史记录。
// 这样可以确保不会因过时的错误历史记录而延迟此key更新的以后处理
c.queue.Forget(key)
return
}
// 如果出现问题,此控制器将重试5次
if c.queue.NumRequeues(key) < 5 {
// 将 key 重新加入到限速队列中
// 根据队列上的速率限制器和重新入队的历史记录,稍后将再次处理该key
c.queue.AddRateLimited(key)
return
}
c.queue.Forget(key)
// 多次重试,我们也无法成功处理该key
runtime.HandleError(err)
klog.Infof("Dropping pod %q out of the queue: %v", key, err)
}
func (c *Controller) Run(thread int, stopCh chan struct{}) {
defer runtime.HandleCrash()
// 停止控制器后关闭队列
defer c.queue.ShutDown()
klog.Info("Starting Pod controller")
// 启动
go c.informer.Run(stopCh)
// 等待所有相关的缓存同步,然后再开始处理队列中的项目
if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
runtime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
return
}
for i := 0; i < thread; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
<-stopCh
klog.Info("Stopping Pod controller")
}
func (c *Controller) runWorker() {
for c.processNexItem() {
}
}
func initClient() (*kubernetes.Clientset, error) {
var err error
var config *rest.Config
var kubeconfig *string
if home := homedir.HomeDir(); home != "" {
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(可选) kubeconfig 文件的绝对路径")
} else {
kubeconfig = flag.String("kubeconfig", "", "kubeconfig 文件的绝对路径")
}
flag.Parse()
// 首先使用 inCluster 模式(需要去配置对应的RBAC权限,默认的sa是default->是没有获取deploy的list权限)
if config, err = rest.InClusterConfig(); err != nil {
// 使用 kubeConfig 文件创建汲取配置 Config 对象
if config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig); err != nil {
panic(err.Error())
}
}
// 通过 rest.Config 对象 创建 Clientset 对象
return kubernetes.NewForConfig(config)
}
func main() {
clientset, err := initClient()
if err != nil {
klog.Fatal(err)
}
// 创建 Pod ListWatcher
podListWatcher := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), "pods", v1.NamespaceDefault, fields.Everything())
// 创建队列
queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
// 在 informer 的帮助下,将工作队列绑定到缓存
// 这样,我们确保无论何时更新缓存,都将 pod key 添加到工作队列中
// 注意: 当我们最终从工作队列中处理元素时,我们可能会看到Pod的版本比响应触发更新的版本新
indexer, informer := cache.NewIndexerInformer(podListWatcher, &v1.Pod{}, 0, cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(obj)
if err == nil {
queue.Add(key)
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
key, err := cache.MetaNamespaceKeyFunc(newObj)
if err == nil {
queue.Add(key)
}
},
DeleteFunc: func(obj interface{}) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err == nil {
queue.Add(key)
}
},
}, cache.Indexers{})
controller := NewController(queue, indexer, informer)
err = indexer.Add(&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "mypod",
Namespace: v1.NamespaceDefault,
},
})
if err != nil {
panic(err)
}
// start controller
stopCh := make(chan struct{})
defer close(stopCh)
go controller.Run(1, stopCh)
select {}
}
======
程序运行结果:
// 如果default有pod,则会输出
Sync/Add/Update for Pod pod名
// 接着会输出
Pod default/mypod does not exists anymore