欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 本文是《client-go实战》系列的第九篇,前面咱们已经了解了client-go的基本功能,现在要来一次经典的综合实战了,接下来咱们会手写一个kubernetes的controller,其功能是:监听某种资源的变化,一旦资源发生变化(例如增加或者删除),apiserver就会有广播发出,controller使用client-go可以订阅这个广播,然后在收到广播后进行各种业务操作,
  • 本次实战代码量略大,但如果随本文一步步先设计再开发,并不会觉得有太多,总的来说由以下内容构成
  1. 代码整体架构一览
  2. 对着架构细说流程
  3. 全局重点的小结
  4. 编码实战

代码整体架构一览

  • 首先,再次明确本次实战的目标:开发出类似kubernetes的controller那样的功能,实时监听pod资源的变化,针对每个变化做出响应
  • 今天的实战源自client-go的官方demo,其主要架构如下

  • 可能您会觉得上图有些复杂,没关系,接下来咱们细说此图,为后面的编码打好理论基础

对着架构细说流程

  • 首先将上述架构图中涉及的内容进行分类,共有三部分
  1. 最左侧的Kubernetes API Server+etcd是第一部分,它们都是kubernetes的内部组件
  2. 第二部分是整个informer,informer是client-go库的核心模块
  3. 第三部分是WorkQueue和Conrol Loop,它们都是controller的业务逻辑代码
  • 上面三部分合作,就能做到监听资源变化并做出响应
  • 另外,informer内部很复杂也很精巧,后面会有专门的文章去细说,本篇只会提到与controller有关系的informer细节,其余的能不提就不提(不然内容太多,这篇文章写不完了)
  • 分类完毕后,再来聊流程
  1. controller会通过client-go的list&watch机制与API Server建立长连接(http2的stream),只要pod资源发生变化,API Server就会通过长连接推送到controller
  2. API Server推的数据到达Reflector,它将数据写入Delta FIFO Queue
  3. Delta FIFO Queue是个先入先出的队列,除了pod信息还保存了操作类型(增加、修改、删除),informer内部不断从这个队列获取数据,再执行AddFunc、UpdateFunc、DeleteFunc等方法
  4. 完整的pod数据被存放在Local Store中,外部通过Indexer随时可以获取到
  5. controller中准备一个或多个工作队列,在执行AddFunc、UpdateFunc、DeleteFunc等方法时,可以将定制化的数据放入工作队列中
  6. controller中启动一个或多个协程,持续从工作队列中取数据,执行业务逻辑,执行过程中如果需要pod的详细数据,可以通过indexder获取
  • 差不多了,我有种胸有成竹的感觉,迫不及待想写代码,但还是忍忍吧,先规划再动手

编码规划

  • 所谓规划就是把步骤捋清楚,先写啥再写啥,如下图所示

  • 捋顺了,开始写代码吧

编码之一:定义Controller数据结构(controller.go)

  1. type Controller struct {
  2. indexer cache.Indexer
  3. queue workqueue.RateLimitingInterface
  4. informer cache.Controller
  5. }
  • 从上述代码可见Controller结构体有三个成员,indexer是informer内负责存取完整资源信息的对象,queue是用于业务逻辑的工作队列

编码之二:编写业务逻辑代码(controller.go)

  • 业务逻辑代码共有四部分
  1. 把资源变化信息存入工作队列,这里可能按实际需求定制(例如有的数据不关注就丢弃了)
  2. 从工作队列中取出数据
  3. 取出数据后的处理逻辑,这边是纯粹的业务需求了,各人的实现都不一样
  4. 异常处理
  • 步骤1,存入工作队列的操作,留待初始化informer的时候再做,
  • 步骤4,异常处理稍后也有单独段落细说
  • 这里只聚焦步骤2和3:怎么取,取出后怎么用
  • 先写步骤2的代码:从工作队列中取取数据,用名为processNextItem的方法来实现(对每一行代码进行中文注释着实不易,支持的话请点个赞)
  1. func (c *Controller) processNextItem() bool {
  2. // 阻塞等待,直到队列中有数据可以被取出,
  3. // 另外有可能是多协程并发获取数据,此key会被放入processing中,表示正在被处理
  4. key, quit := c.queue.Get()
  5. // 如果最外层调用了队列的Shutdown,这里的quit就会返回true,
  6. // 调用processNextItem的地方发现processNextItem返回false,就不会再次调用processNextItem了
  7. if quit {
  8. return false
  9. }
  10. // 表示该key已经被处理完成(从processing中移除)
  11. defer c.queue.Done(key)
  12. // 调用业务方法,实现具体的业务需求
  13. err := c.syncToStdout(key.(string))
  14. // Handle the error if something went wrong during the execution of the business logic
  15. // 判断业务逻辑处理是否出现异常,如果出现就重新放入队列,以此实现重试,如果已经重试过5次,就放弃
  16. c.handleErr(err, key)
  17. // 调用processNextItem的地方发现processNextItem返回true,就会再次调用processNextItem
  18. return true
  19. }
  • 接下来写业务处理的代码,就是上面调用的syncToStdout方法,常规套路是检查spec和status的差距,然后让status和spec保持一致,(例如spec中指定副本数为2,而status中记录了真实的副本数是1,所以业务处理就是增加一个副本数),这里仅仅是为了展示业务处理代码在哪些,所以就简(fu)化(yan)一些了,只打印pod的名称
  1. func (c *Controller) syncToStdout(key string) error {
  2. // 根据key从本地存储中获取完整的pod信息
  3. // 由于有长连接与apiserver保持同步,因此本地的pod信息与kubernetes集群内保持一致
  4. obj, exists, err := c.indexer.GetByKey(key)
  5. if err != nil {
  6. klog.Errorf("Fetching object with key %s from store failed with %v", key, err)
  7. return err
  8. }
  9. if !exists {
  10. fmt.Printf("Pod %s does not exist anymore\n", key)
  11. } else {
  12. // 这里就是真正的业务逻辑代码了,一般会比较spce和status的差异,然后做出处理使得status与spce保持一致,
  13. // 此处为了代码简单仅仅打印一行日志
  14. fmt.Printf("Sync/Add/Update for Pod %s\n", obj.(*v1.Pod).GetName())
  15. }
  16. return nil
  17. }

编码之三:编写错误处理代码(controller.go)

  • 回顾前面的processNextItem方法内容,在调用syncToStdout执行完业务逻辑后就立即调用handleErr方法了,此方法的作用是检查syncToStdout的返回值是否有错误,然后做针对性处理
  1. func (c *Controller) handleErr(err error, key interface{}) {
  2. // 没有错误时的处理逻辑
  3. if err == nil {
  4. // 确认这个key已经被成功处理,在队列中彻底清理掉
  5. // 假设之前在处理该key的时候曾报错导致重新进入队列等待重试,那么也会因为这个Forget方法而不再被重试
  6. c.queue.Forget(key)
  7. return
  8. }
  9. // 代码走到这里表示前面执行业务逻辑的时候发生了错误,
  10. // 检查已经重试的次数,如果不操作5次就继续重试,这里可以根据实际需求定制
  11. if c.queue.NumRequeues(key) < 5 {
  12. klog.Infof("Error syncing pod %v: %v", key, err)
  13. c.queue.AddRateLimited(key)
  14. return
  15. }
  16. // 如果重试超过了5次就彻底放弃了,也像执行成功那样调用Forget做彻底清理(否则就没完没了了)
  17. c.queue.Forget(key)
  18. // 向外部报告错误,走通用的错误处理流程
  19. runtime.HandleError(err)
  20. klog.Infof("Dropping pod %q out of the queue: %v", key, err)
  21. }
  • 好了,和业务有关的代码已经完成,接下来就是搭建controller框架,把基本功能串起来

编码之四:编写Controller主流程(controller.go)

  • 编写一个完整的Controller,最基本的是构造方法,Controller的构造方法也很简单,保存三个重要的成员变量即可
  1. func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller) *Controller {
  2. return &Controller{
  3. informer: informer,
  4. indexer: indexer,
  5. queue: queue,
  6. }
  7. }
  • 先定义个名为runWorker的简单方法,里面是个无限循环,只要消费消息的processNextItem方法返回true,就无限循环下去
  1. func (c *Controller) runWorker() {
  2. for c.processNextItem() {
  3. }
  4. }
  • 然后是Controller主流程代码,简介清晰,启动informer,开始接受apiserver推送,写入工作队列,然后开启无限循环从工作队列取数据并处理
  1. func (c *Controller) Run(workers int, stopCh chan struct{}) {
  2. defer runtime.HandleCrash()
  3. // 只要工作队列的ShutDown方法被调用,processNextItem方法就会返回false,runWorker的无限循环就会结束
  4. defer c.queue.ShutDown()
  5. klog.Info("Starting Pod controller")
  6. // informer的Run方法执行后,就开始接受apiserver推送的资源变更事件,并更新本地存储
  7. go c.informer.Run(stopCh)
  8. // 等待本地存储和apiserver完成同步
  9. if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
  10. runtime.HandleError(fmt.Errorf("Timed out waiting for caches to sync"))
  11. return
  12. }
  13. // 启动worker,并发从工作队列取数据,然后执行业务逻辑
  14. for i := 0; i < workers; i++ {
  15. go wait.Until(c.runWorker, time.Second, stopCh)
  16. }
  17. <-stopCh
  18. klog.Info("Stopping Pod controller")
  19. }
  • 现在一个完整的Controller已经完成了,接下来编写调用Controller的代码,将其所需的三个对象传入,再调用它的Run方法

编码之五:编写调用Controller的代码(controller_demo.go)

  • 为了能让整个工程的main方法调用Controller,这里新增controller_demo.go方法,里面新增名为ControllerDemo的数据结构,创建Controller对象以及为其准备成员变量的操作都在ControllerDemo.DoAction方法中
  1. package action
  2. import (
  3. v1 "k8s.io/api/core/v1"
  4. "k8s.io/apimachinery/pkg/fields"
  5. "k8s.io/client-go/kubernetes"
  6. "k8s.io/client-go/tools/cache"
  7. "k8s.io/client-go/util/workqueue"
  8. )
  9. type ControllerDemo struct{}
  10. func (controllerDemo ControllerDemo) DoAction(clientset *kubernetes.Clientset) error {
  11. // 创建ListWatch对象,指定要监控的资源类型是pod,namespace是default
  12. podListWatcher := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), "pods", v1.NamespaceDefault, fields.Everything())
  13. // 创建工作队列
  14. queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
  15. // 创建informer,并将返回的存储对象保存在变量indexer中
  16. indexer, informer := cache.NewIndexerInformer(podListWatcher, &v1.Pod{}, 0, cache.ResourceEventHandlerFuncs{
  17. // 响应新增资源事件的方法,可以按照业务需求来定制,
  18. // 这里的做法比较常见:写入工作队列
  19. AddFunc: func(obj interface{}) {
  20. key, err := cache.MetaNamespaceKeyFunc(obj)
  21. if err == nil {
  22. queue.Add(key)
  23. }
  24. },
  25. // 响应修改资源事件的方法,可以按照业务需求来定制,
  26. // 这里的做法比较常见:写入工作队列
  27. UpdateFunc: func(old interface{}, new interface{}) {
  28. key, err := cache.MetaNamespaceKeyFunc(new)
  29. if err == nil {
  30. queue.Add(key)
  31. }
  32. },
  33. // 响应修改资源事件的方法,可以按照业务需求来定制,
  34. // 这里的做法比较常见:写入工作队列,注意删除的时候生成key的方法和新增修改不一样
  35. DeleteFunc: func(obj interface{}) {
  36. // IndexerInformer uses a delta queue, therefore for deletes we have to use this
  37. // key function.
  38. key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
  39. if err == nil {
  40. queue.Add(key)
  41. }
  42. },
  43. }, cache.Indexers{})
  44. // 创建Controller对象,将所需的三个变量对象传入
  45. controller := NewController(queue, indexer, informer)
  46. // Now let's start the controller
  47. stop := make(chan struct{})
  48. defer close(stop)
  49. // 在协程中启动controller
  50. go controller.Run(1, stop)
  51. // Wait forever
  52. select {}
  53. return nil
  54. }

编码之六:main方法中支持(main.go)

  • 然后是整个工程的main方法,里面增加一段代码,支持新增的ControllerDemo,如下图黄框所示

  • 最后,如果您使用的是vscode,记得修改launch.json,如下图黄色箭头,这样main方法运行的时候就会执行Controller的代码了

运行和验证

  • 现在工程目录执行以下命令,获取必要的包
  1. go get k8s.io/apimachinery/pkg/util/diff@v0.25.4
  • 确保kubernetes环境正常,.kube/config配置也能正常使用,然后运行main.go
  • 使用kubectl edit xxx修改kubernetes环境中的pod,例如我这里改的是下图黄色箭头的值

  • 修改完毕保存退出后,运行mian.go的控制台立即有内容输出,如下图黄色箭头,是咱们前面的syncToStdout方法的输入,符合预期

  • 至此,整个Controller已经开发完成了,相信您已经熟悉了informer和kubernetes的controller的基本套路,加上前面的文章打下的基础,再去做kubernetes二次开发,或者operator开发等都能轻松驾驭了

本篇涉及知识点串讲

  • 前几篇的风格,都是抓住一个问题深入研究和实践,但是到了本篇似乎多个知识点同时涌出,并且还要紧密配合完成业务目标,可能年轻的您一下子略有不适应,我这里再次将本次开发中的重点进行总结,经历过一番实战,再来看这些总结,相信您很容易就融会贯通了
  • 先给出数据流视图,结合前面的实战,您应该能一眼看懂

  • 接下来开始梳理重点
  1. 创建一个名为podListWatcher的ListWatch对象,用于对指定资源类型建立监听(本例中监听的资源是pod)
  2. 创建一个名为queue的工作队列,就是个先进先出的内存对象,没啥特别之处
  3. 通过podListWatcher创建一个informer,这个informer的功能对podListWatcher监听的事件作相应
  4. 在创建informer的时候还会返回一个名为indexer的本地缓存,这里面保存了所有pod信息(由于pod的变动全部都会被informer收到,因此indexer中保存了最新的pod信息)
  5. 在新协程中启动informer,这里面对应两件事情:第一,创建Reflector对象,这个Reflector对象会把podListWatcher监听到的数据放入一个DeltaFIFO队列(注意不是步骤2中的工作队列),第二是循环地取出fifo队列中的数据,再调用AddFunc、UpdateFunc、DeleteFunc等方法
  6. 步骤5中提到的AddFunc、UpdateFunc、DeleteFunc可以在创建informer的时候,由业务开发者自定义,一般会再次将key放入工作队列中
  7. 在新协程消费工作队列queue的数据,这里可以根据业务需求写入也任务逻辑代码
  • 基于以上详细描述,再来个精简版,介绍重点对象,如果您对详细描述不感兴趣,可以只看精简版,掌握其中关键即可
  1. podListWatcher:用于监听指定类型资源的变化
  2. queue:工作队列,从里面取出的key,其资源都有事件发生
  3. informer:接受监听到的事件,再调用指定的回调方法
  4. Reflector:informer内部三大对象之一,用于接受事件再写入一个内部fifo队列
  5. DeltaFIFO:informer内部三大对象之二,先入先出队列,还保存了操作类型
  6. indexer:informer内部三大对象之三,这里面保存的是指定资源的完整数据,和apiserver侧保持同步
  7. 接受消息的协程:informer在这个协程中启动,也在这个协程中将数据写入工作队列
  8. 处理工作队列的协程:负责从工作队列中取出数据处理
  9. 工作队列queue和informer内部的fifo是不同的队列,是两回事,为了满足业务需求,我们可以在一个controller中创建多个工作队列,也可以不要工作队列(在informer的三个回调方法中完成业务逻辑)

以下是官方参考信息

源码下载

名称 链接 备注
项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在tutorials/client-go-tutorials文件夹下,如下图红框所示:

  • 写到这里,client-go基本功的学习已经完成了,接下来咱们还要继续深入研究,让这个优秀的库在手中发挥更大的威力,欣宸原创,敬请期待

欢迎关注博客园:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

client-go实战之九:手写一个kubernetes的controller的更多相关文章

  1. 放弃antd table,基于React手写一个虚拟滚动的表格

    缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...

  2. Python+Flask+Gunicorn 项目实战(一) 从零开始,写一个Markdown解析器 —— 初体验

    (一)前言 在开始学习之前,你需要确保你对Python, JavaScript, HTML, Markdown语法有非常基础的了解.项目的源码你可以在 https://github.com/zhu-y ...

  3. 利用SpringBoot+Logback手写一个简单的链路追踪

    目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...

  4. 手写一个简单的ElasticSearch SQL转换器(一)

    一.前言 之前有个需求,是使ElasticSearch支持使用SQL进行简单查询,较新版本的ES已经支持该特性(不过貌似还是实验性质的?) ,而且git上也有elasticsearch-sql 插件, ...

  5. webview的简单介绍和手写一个H5套壳的webview

    1.webview是什么?作用是什么?和浏览器有什么关系? Webview 是一个基于webkit引擎,可以解析DOM 元素,展示html页面的控件,它和浏览器展示页面的原理是相同的,所以可以把它当做 ...

  6. 手写一个最迷你的Web服务器

    今天我们就仿照Tomcat服务器来手写一个最简单最迷你版的web服务器,仅供学习交流. 1. 在你windows系统盘的F盘下,创建一个文件夹webroot,用来存放前端代码.  2. 代码介绍: ( ...

  7. 『练手』手写一个独立Json算法 JsonHelper

    背景: > 一直使用 Newtonsoft.Json.dll 也算挺稳定的. > 但这个框架也挺闹心的: > 1.影响编译失败:https://www.cnblogs.com/zih ...

  8. 教你如何使用Java手写一个基于链表的队列

    在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...

  9. 【spring】-- 手写一个最简单的IOC框架

    1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ...

  10. 只会用就out了,手写一个符合规范的Promise

    Promise是什么 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果.从语法上说,Promise 是一个对象,从它可以获取异步操作的消息.Prom ...

随机推荐

  1. python打包exe总结 pyinstaller py2exe

    Python打包exe 有很多可以用的 如 pyinstaller py2exe cx_Freeze nuitka py2app py0xidizer 其中cx_Freeze没用过 nuitka是把p ...

  2. 如何将mp4文件解复用并且解码为单独的.yuv图像序列以及.pcm音频采样数据?

    一.初始化解复用器 在音视频的解复用的过程中,有一个非常重要的结构体AVFormatContext,即输入文件的上下文句柄结构,代表当前打开的输入文件或流.我们可以将输入文件的路径以及AVFormat ...

  3. 向量数据库Faiss的搭建与使用

    向量数据库Faiss是Facebook AI研究院开发的一种高效的相似性搜索和聚类的库.它能够快速处理大规模数据,并且支持在高维空间中进行相似性搜索.本文将介绍如何搭建Faiss环境并提供一个简单的使 ...

  4. 使用docker安装的tomcat部署activiti-app.war、activiti-admin.war失败(ClassNotFoundException)

    背景 一直以来习惯用docker配置一些本地学习环境,许多教程配置activiti的方式都是通过复制activiti的war包部署在tomcat中,我尝试了一下通过docker的方式遇到了一些不易察觉 ...

  5. git 访问仓库错误

    通过https访问git出现错误, failed: Error in the pull function 尝试将https改为http

  6. CSRF与SSRF

    CSRF与SSRF CSRF(跨站请求伪造) 跨站请求伪造(Cross-site request forgery,CSRF),它强制终端用户在当前对其进行身份 验证后的Web应用程序上执行非本意的操作 ...

  7. Mysql高级3-索引的结构和分类

    一.索引概述 1.1 索引的介绍 索引index:是帮助 Mysql 高效获取数据 的 有序的数据结构,在数据之外,数据库系统维护着的满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据 ...

  8. std::queue 中遇到释放内存错误的问题

    项目上有个需求要用到 std::queue 顺序处理消息事件 简单的示例如下: struct MyEvent { MyEvent() { event_ = CreateEvent(nullptr, 0 ...

  9. NativeBuferring&mdash;&mdash;一种零分配的数据类型[上篇]

    之前一个项目涉及到针对海量(千万级)实时变化数据的计算,由于对性能要求非常高,我们不得不将参与计算的数据存放到内存中,并通过检测数据存储的变化实时更新内存的数据.存量的数据几乎耗用了上百G的内存,再加 ...

  10. 彻底搞懂Vue针对数组和双向绑定(MVVM)的处理方式

    欢迎关注我的博客:https://github.com/wangweianger/myblog Vue内部实现了一组观察数组的变异方法,例如:push(),pop(),shift()等. Object ...