Operator开发

概述

基于k8s做扩展的时候,一种典型方式则是 operator 开发,通过 CRD(CustomResourceDefinition) 来定义自定义资源,开发对应的 controller 来做业务控制,以期实现特定的业务需求。那么其本质是什么?是事件驱动。与我们常见的 C/S 架构不同,operator 开发核心是围绕 CR 的资源变化事件来进行处理。

本文则针对 operator 开发,做一些基础梳理

CRD
image.png

定义

资源,作为 k8s 平台开发的一等公民,有其特定的格式,其详细设计在 https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md 查阅,我们一般的更多关注四个方面:annotation、labels、spec、status。

annotation:注解

一般多用于本资源的 owner 控制器之外的控制器使用,用来表明自身资源的一些非状态熟悉,或者对其他控制器的诉求。

例如,我们的环境中,典型的会在创建 vpcgw 的时候,给其设置如下标签:

k8s.v1.cni.cncf.io/networks: macvlan-conf-1

用来表明我们的 pod 期望加入一个额外的网络,pod本身的owner控制器(scheduler之类)其实并不关注这个字段如何处理,核心是 multus 相关控制器关注到这个注解后,会完成期望的操作。

labels:标签

如名,标签,更多的是提供了查找匹配的能力。

使用非常的广泛,典型的如我们启动pod的时候,一般会设置 app-name 标签,用来标识一簇应用。这样无论是 kubectl 运维,还是 controller 批量查找 都会很方便

spec: 规范(specification)

表明的是资源的预期是什么,类似驴子眼前的萝卜,是整个资源的预期。

例如:deployment 中设置副本数为3,只是代表期望是3,到底是不是3,只能从status中获取。

status:状态

表明的是资源当前的状态是什么,即驴子到底吃到萝卜没。即到底有没有3个副本。

在做 controller 的时候,我们则应当努力使资源处在 spec 所期望的 status 。而这个过程,则称为 调谐(reconcile)。后文说明调谐怎么做。

设计

严格来讲,一个资源对应一个CRD,这个也是我们所建议的高内聚,低耦合中的_高内聚_。那这里就有一个资源设计的问题,即怎么对资源做拆分,设计不同的 CRD?

宽泛来讲,建议与数据模型(数据库表)对齐,如果把不同的资源集中在同一个CRD中,可能会导致CRD过于臃肿,Reconciler 的业务逻辑复杂。但是如果太碎太小,又会导致资源数量太多,浪费资源。

不一而论,此处与数据模型设计异曲同工,全凭业务诉求与团队经验。几个有效的设计经验可以提出来以供参考:

经验一:存在包含关系(父子关系)的资源,拆开为不同的CRD

例如:k8s中,deployment->replicaSet->pod;slb->listener,target 之类

因为这类型的资源其本身除了从属关系外,自身也是具有复杂的业务逻辑,如果设计到一个CRD,那么reconcile 会十分麻烦

经验二:一个资源只有一个owner,只有他可以修改这个资源的所有信息,其他控制器只能修改 annotation

如果存在多个owner,就会出现竞争,在分布式系统中是个很麻烦的事情。但是经常我们又需要多个控制器协同作业,那么,暴露的越少越好。这里k8s提供了annotation字段,留与标记资源的一些信息,与其他控制器协同作业。

Reconciler

基本诉求

如前文,我们期望通过 reconcile 来处理 spec,以期达到某种 status,那么从面向对象的设计出发,则存在一个 调谐器(reconciler)来处理管理整个调谐的声明周期。

我们当前使用 kube-builder 一键生成,框架源码我们暂不赘述,详细的可以单开章节。

核心关注两个基本诉求:

1、怎么监听一个(多个)资源?

2、监听后怎么处理?

诉求一:怎么监听一个(多个)资源

首先,监听一个资源好理解,一个 reconciler 必定是调谐一个特定的资源。那何时需要监听多个资源?

一个典型场景:父子资源(仅以deployment、pod举例,不代表是其具体实现)

假定:deployment 中定义了期望pod有三副本,其状态中记录了副本数。那怎么实现呢?

两种方式:

方式一:pod reconciler 监听到创建出来一个副本后,给deployment的status副本数加一

方式二:deployment reconciler 监听到创建一个副本后,给deployment的status副本数加一

显然,方式一会有一个问题:deployment和pod的reconciler都修改deployment的status,即使是不同的字段也是不被推荐的。因为一个资源的status应当避免由多个控制器写,status代表的是本控制器对资源的处理结果,而并非是其他控制器的处理结果。其他控制器的处理结果则可以写到 annotations 上。所以我们则需要在 deployment reconciler 中,同时监听 deployment 和 pod 两种资源。写法如下:

/*
SLBReconciler 是作为 CRD:slb 的owner控制器存在,用来处理slb资源的变更
其中一个核心业务,是根据slb的配置,创建出 envoy-pod 出来。
同时,在slb中的status,需要记录这些pod的一些信息,以供更好的运维查看
那么也就意味着在slb的业务逻辑中,需要关注两类资源 CRD:slb 以及 k8s.core.Pod
a. For(&networkv1.SLB{}).
For 函数一般的则就是这个控制为谁而工作,此处自然就是 slb
b. Watches(
    &source.Kind{Type: &k8sCoreV1.Pod{}},
    handler.EnqueueRequestsFromMapFunc(r.podTransSlb),
    builder.WithPredicates(pred),
)
Watch 则是用来关注其他资源的变更,此处需要注意,无论怎样,watch后的处理,都尽量转换为 Reconcile 的处理
从其底层代码来看,Watch 是相当于把自己加入到了另一个资源的监听队列上,而另一个资源的变更和本资源的变更
一般是并行的,如果在 Watch.Handler直接处理 slb 的相关字段,那么就会与 For.Reconcile 产生竞争,锁就必不可免了
所以这里的框架设计也是比较优秀的:
func (r *SLBReconciler) podTransSlb(podObject client.Object) []reconcile.Request
这个回调会在Watch的资源发生变更的时候调用,可以看到入参是变化的资源,出参是一个请求切片。
这些请求会直接塞入到当前 reconciler 的事件队列中,此处也就是 slb
这样,就从pod的资源变更转换为了slb的资源变更,队列的处理则是有序的,也就避免了竞争
再者,后文会提到的一个原则:所有的处理逻辑(接口)都应当幂等,也确保了处理逻辑的正确执行。
通过这两者的确保,就完成了 slb-reconciler 对slb、pod两个资源的监听处理

*/

// SetupWithManager sets up the controller with the Manager.
func (r *SLBReconciler) SetupWithManager(mgr ctrl.Manager) error {
    pred := predicate.NewPredicateFuncs(r.podPredicate)
    return ctrl.NewControllerManagedBy(mgr).
        For(&networkv1.SLB{}).
        Watches(
            &source.Kind{Type: &k8sCoreV1.Pod{}},
            handler.EnqueueRequestsFromMapFunc(r.podTransSlb),
            builder.WithPredicates(pred),
        ).
        Complete(r)
}

func (r *SLBReconciler) podTransSlb(podObject client.Object) []reconcile.Request {
    labels := podObject.GetLabels()
    slbName, ok := labels[apis.Labels_Keys_SLB]
    if !ok {
        return nil
    }
    ctx := context.Background()
    log := log.FromContext(context.Background(), "controller", "SLB")
    log.Info("pod update, slb reconcile", "slb", slbName)

    slb := &networkv1.SLB{}
    err := r.Get(ctx, types.NamespacedName{Name: slbName}, slb)
    if err != nil {
        log.Info("pod update, slb reconcile skip, get slb error", "slb", slbName)
        return nil
    }

    return []reconcile.Request{
        {
            NamespacedName: types.NamespacedName{
                Name: slbName,
            },
        },
    }
}

func (r *SLBReconciler) podPredicate(podObject client.Object) bool {
    labels := podObject.GetLabels()
    _, ok := labels[apis.Labels_Keys_SLB]
    if !ok {
        return false
    }

    annos := podObject.GetAnnotations()
    keys := []string{
        apis.Annos_Keys_SLB_InternetVIP,
        apis.Annos_Keys_SLB_IntranetVIP,
        apis.Annos_Keys_SLB_BasicNetworkVIP,
        apis.Annos_Keys_SLB_BasicNetworkVipGw,
        apis.Annos_Keys_SLB_BasicNetworkIP,
        apis.Annos_Keys_SLB_BasicNetworkIpGw,
        apis.Annos_Keys_SLB_OverlaySubnetCIRD,
    }
    for _, key := range keys {
        if _, ok := annos[key]; !ok {
            return false
        }
    }

    return true
}

诉求二:监听后怎么处理

这里则是经验之谈了。

在 bube-builer 创建的代码框架中,我们可以做两种处理方式:

方式一:类 restful 风格

即不使用默认的 reconcile 函数,而是自定义 handler,实现 create、update、delete 函数,处理对应的事件。

方式二:reconcile

即默认方式,在reconcile函数中,通过不同字段的变化来处理。

例如:通过 DeletionTimestamp 是否为0,来处理删除逻辑;通过 Finalizer 的字段来实现对资源的删除保护。通过对spec字段的处理,来直接处理资源。如下,则是一个典型处理逻辑:

func (r *SLBReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx, "controller", "SLB")
    log.Info("SLB Reconcile start", "resource", req.String())

    // .1 get current object
    slb := &networkv1.SLB{}
    err := r.Get(ctx, req.NamespacedName, slb)
    if err != nil {
        if errors.IsNotFound(err) {
            log.Info("SLB deleted", "slb", req.String())
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, err
    }

    // .2 Finalizer
    if r.needFinalizer(ctx, slb) {
        _, err := r.addFinalizer(ctx, slb)
        if err != nil {
            log.Error(err, "Add finalizer fail")
            return ctrl.Result{}, err
        }
        return ctrl.Result{}, nil
    }

    // .3 Delete
    if !slb.DeletionTimestamp.IsZero() {
        log.Info("On remove slb", "slb", slb.Name)

        // a. remove
        err := r.onRemove(ctx, log, slb)
        if err != nil {
            return ctrl.Result{}, err
        }
        return ctrl.Result{}, nil
    }

    // .3 Apply: add and update
    log.Info("On apply slb", "slb", fmt.Sprintf("%+v\n", slb))
    err = r.onApply(ctx, log, slb)
    if err != nil {
        log.Error(err, "apply fail")
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}

但无论是哪一种,都一个点需要额外的注意!!!

**

reconciler 处理的资源是"缓存",而非实时的request/response拿到的数据。

**

基本原则

原则一:一次 reconcile 只更新一次资源
在这里插入图片描述

如上,展示的是 kube-builder 的内部原理,包含了 kube-builder 以及 infomer 机制的部分代码,可以明显的看到,这里存在一个事件队列,一旦存在事件队列就意味着缓存已经产生,我们每次处理的都是当前能看到的资源状态,并非真实的状态,因为有可能还有其他的事件在后面排队等待处理。那基于此,我们看下图,分析下一次reconcile中,更新一次和更新多次有什么区别

6F_FIvSFLJMutDq2UxVVD7rcKFAnraR5TK2tdsiytuw.png

如上图,更新多次与更新一次的主要区别在于

  1. 更新一次的时候,代码逻辑相对复杂,但是比较安全,不会出现(或者说降低了)处理中间态资源的情况

  2. 更新多次的时候,代码逻辑相对简单,但是相对危险,因为相比于更新一次,多次更新会产生无意义版本的资源变化通知,如果带入到了更加复杂的场景:这个 cr 的spec还存在手动修改,或者更上层应用层修改的情况,那么这里的资源变化相对会比一次的复杂很多

所以,建议在一次reconcile的时候,只更新一次status,如果非要多次,那么在更新后请立即返回,避免产生无意义版本资源,下面是一种参考写法:

/**** !!!! 仅做code展示,不要以此理解 slb 的设计 !!! ****/

/*
假定,slb中存在 instance、targetGroup、listener 三种子对象以及一些其他额外的属性
每次更新的时候,这写都可能更新,我们在处理每个逻辑段后,都需要明确,是否更新了 spec
如果更新了,那么立即返回,不然下一次一定是更新冲突。更新冲突:在前文已经描述,不再赘述
*/
func (r *SLBReconciler) onApply(
    ctx context.Context, log logr.Logger, slb *networkv1.SLB,
) error {
    // .1
    objectUpdated, err := r.instanceApply(ctx, log, slb)
    if err != nil {
        log.Error(err, "slb instance apply error")
        return err
    }
    if objectUpdated {
        return nil
    }

    // .2
    objectUpdated, err = r.targetGroupApply(ctx, log, slb)
    if err != nil {
        log.Error(err, "slb target group apply error")
        return err
    }
    if objectUpdated {
        return nil
    }

    // .3
    objectUpdated, err = r.listenerApply(ctx, log, slb)
    if err != nil {
        log.Error(err, "slb listener error")
        return err
    }
    if objectUpdated {
        return nil
    }

    // .x stop/start
    // .x EnvoyPodReplicas

    return nil
}

原则二:不依托 status 做任何事情

道理很简单,当我们收到一个资源变更后,里面的status就已经"过期",是 old-status。基于一个过期的状态,再怎么处理自身都是错的。应当做的是,根据本次的spec处理后,把收集到的 new-status,与 old-status对比,不一致的时候再更新 status。

原则三:先比较再更新

如原则一所展示,资源更新后会生成一个新的版本。而k8s在存储资源的时候,不会比较资源是否发生变化。

那么一旦某次 reconcile 无脑更新资源了,就会生成一个新的资源版本,这个新版本又会触发一次reconcile,如此往复,陷入死循环中。

原则四:所有的处理逻辑(接口)都应当幂等

同样,还是k8s的资源变更机制导致,可能会导致重复的推送某一个资源的变更到reconciler,如果处理逻辑或者接口不幂等,那么处理一定是有问题的。


参考资料

1、k8s api 设计

https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md

2、kube-builder

https://book.kubebuilder.io

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/433246.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

图像超分辨率:Fast Nearest Convolution for Real-Time Efficient Image Super-Resolution

9.Fast Nearest Convolution for Real-Time Efficient Image Super-Resolution 提出一种适用移动端的超分网络 一些tensor op 的推理时间 一些卷积结构的推理时间 网络结构NCNet 主干网络预测的是 残差,什么的残差? 是最近邻插值图像与 ground-truth的…

基于springboot+vue的疫苗发布和接种预约系统

博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战,欢迎高校老师\讲师\同行交流合作 ​主要内容:毕业设计(Javaweb项目|小程序|Pyt…

时间复杂度考点总结

【2022统考真题】下列程序段的时间复杂度是( )。 int sum0; for(int il;i<n;i*2) for(int j0;j<i;j) sum; 这道题容易错选为C,正确答案是B 解答&#xff1a;i1时循环1次&#xff0c;i2时循环2次&#xff0c;i4时循环4次&#xff0c;所以循环次数…

Premiere快速闪光特效视频转场Pr项目模板视频剪辑素材

Premiere转场模板&#xff0c;包含15种快速闪光特效视频转场过渡效果PR项目模板视频剪辑素材&#xff0c;动态效果与动画纹理的组合方式。包含视频教程。提供4K和HD两种版本。来自PR模板网&#xff0c;下载地址&#xff1a;https://prmuban.com/38081.html

微信小程序-4

自定义组件 创建组件 在项目的根目录中&#xff0c;创建 components 文件夹&#xff0c;在里面编写我们的自定义主键&#xff0c;如下所示&#xff1a; 引用组件 局部引用&#xff1a; index.json // 在页面的 .json 文件中&#xff0c;引入组件 {"usingComponents&q…

区间合并(超详细逐步讲解/例题/思路分析/参考代码)

区间合并超详解 区间合并是什么&#xff1f;例1问题描述输入输出数据规模输入输出思路分析代码 例2问题描述输入输出数据规模输入输出思路分析代码 例3问题描述输入输出输入输出思路分析代码 例4问题描述输入输出输入输出参考代码 区间合并是什么&#xff1f; 我们要了解区间合…

下载中心-异步下载

下载中心 文章目录 下载中心一. 概要二. 实现逻辑 下载中心一. 概要二. 实现逻辑三. 主要代码逻辑1.生成任务2.消费任务3.查询方法是如何存入内存中的4.DCGenerateComponent 反射调用查询数据方法 总结 一. 概要 功能概览&#xff1a;将文件下载修改为异步下载&#xff0c;引入…

Ubuntu18.04安装RTX2060显卡驱动+CUDA+cuDNN

Ubuntu18.04安装RTX2060显卡驱动CUDAcuDNN 1 安装RTX2060显卡驱动1.1 查看当前显卡是否被识别1.2 安装驱动依赖1.3 安装桌面显示管理器1.4 下载显卡驱动1.5 禁用nouveau1.6 安装驱动1.7 查看驱动安装情况 2 安装CUDA2.1 查看当前显卡支持的CUDA版本2.2 下载CUDA Toolkit2.3 安装…

1.4 Word2Vec是如何工作的? Word2Vec与LDA 的区别和联系?

1.4 Word2Vec&#xff1a;词嵌入模型之一 场景描述 谷歌2013年提出的Word2Vec是目前最常用的词嵌入模型之一。 Word2Vec实际是一种浅层的神经网络模型,它有两种网络结构&#xff0c;分别是CBOW(Continues Bag of Words)和Skip-gram。 知识点 Word2Vec,隐狄利克雷模型(LDA),…

软件测试之接口测试

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 关注公众号【互联网杂货铺】&#xff0c;回复 1 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1. 什么是接口测试 顾名思义&#xff0c;接口测试是对系统或组…

通信网优岗位真实面经分享!

春招来临&#xff0c;不少网优人已经踏上了面试的征程。网优面试具体涉及哪些环节&#xff1f;主要问题有哪些&#xff1f; 本文收集并整理已经获得高薪offer的优橙学员的相关简历&#xff0c;为正在投递网优岗位的你提供经验&#xff0c;也希望网优人能早日找到满意工作。 通信…

uniapp 滑动页面至某个元素或顶部

直接上代码&#xff1a; uni.pageScrollTo({selector: #top, // 需要返回顶部的元素id或class名称duration: 300 // 过渡时间&#xff08;单位为ms&#xff09; }); 官方文档&#xff1a;

计及电池储能寿命损耗的微电网经济调度(matlab代码)

目录 1 主要内容 储能寿命模型 负荷需求响应 2 部分代码 3 程序结果 4 下载链接 1 主要内容 该程序参考文献《考虑寿命损耗的微网电池储能容量优化配置》模型&#xff0c;以购售电成本、燃料成本和储能寿命损耗成本三者之和为目标函数&#xff0c;创新考虑储能寿命损耗约…

【C++进阶】用哈希表封装unordered_set和unordered_map

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;C航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&#x1…

算法:滑动窗口

文章目录 例题1&#xff1a;长度最小的子数组例题2&#xff1a;无重复字符的最长子串例题3&#xff1a;最大连续1的个数 III例题4&#xff1a;将 x 减到 0 的最小操作数例题5&#xff1a;水果成篮例题6&#xff1a;找到字符串中所有字母异位词例题7&#xff1a;串联所有单词的子…

网络工程技术-学习内容(非技术文)

公共基础双纹线的制作 认识网络环境 (1)ipv4 ipv4地址的构成&#xff0c;分类&#xff0c;子网刻分&#xff0c;超丽素合“ 交换机的基本配置telnet&#xff0c;ssh&#xff0c; web方式三种配置 van. sto.协议 VLAN 端口聚合 三层交换“ 路由器的基本配置《(端口 IP 地址配)《…

msvcp120.dll丢失的解决方法,教你快速解决msvcp120.dll问题

msvcp120.dll是一个在Windows操作系统中至关重要的系统文件&#xff0c;它属于Microsoft Visual C Redistributable Package的一部分。这个动态链接库文件&#xff08;DLL&#xff09;包含了运行某些应用程序所必需的C运行时库函数。当某个程序在运行过程中需要调用这些预先编译…

requests做接口测试

Requests 是用Python语言编写&#xff0c;基于 urllib&#xff0c;采用 Apache2 Licensed 开源协议的 HTTP 库。它比 urllib 更加方便&#xff0c;可以节约我们大量的工作&#xff0c;完全满足 HTTP 测试需求。Requests 的哲学是以 PEP 20 的习语为中心开发的&#xff0c;所以它…

rabbitmq基础(1)

1、背景 能实现消息队列的框架软件有很多&#xff0c;kafka、rabbitmq、RocketMq、activeMq、Redis&#xff08;非专业&#xff09;&#xff0c;各有各的特点和优缺点。但是之前的公司数据需求规模并非很大&#xff0c;所以采用rabbitmq作为消息队列。 2、rabbitMq的基础架构…

工业网关、物联网网关与PLC网关是什么?

网关是什么&#xff1f; 网关是一种用于连接不同网络的网络设备&#xff0c;其作用是实现网络之间的通信和数据交换。它负责将一个网络的数据转发到另一个网络&#xff0c;并且可以进行路由、转换和过滤等处理。通常用于连接局域网和广域网之间&#xff0c;可以是硬件设备或者软…