k8s学习(二)容器编排与k8s作业管理

pod内的网络共享

  • pod是一组共享了资源的容器,所有容器共享同一个network namespace,并可以声明共享同一个Volume
  • pod内多个容器是对等关系,所以pod内引入了一个中间容器,叫infra容器(汇编语言写的,占用极少的资源,永远处于暂停状态的容器,解压大小为100-200kb),在pod中,infra容器是第一个被创建的,其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。
    在这里插入图片描述
  • 对于pod内的不同容器来说
    • 不同容器可以通过localhost通信
    • 不同容器看到的网络设备一致,和infra内看到的也一样
    • 一个pod只有一个IP地址,网络资源也是一个pod一份
    • pod的生命周期只与infra容器一致,与pod内其他容器无关
    • 一个pod流量的进出,都是通过infra容器完成的。如果要为 k8s 开发一个网络插件时,应该重点考虑的是如何配置这个 Pod 的 Network Namespace,而不是每一个用户容器如何使用你的网络配置
  • 如果网络插件需要在容器里安装某些包或者配置才能完成的话,是不可取的:Infra 容器镜像的 rootfs 里几乎什么都没有,没有随意发挥的空间。当然,这同时也意味着你的网络插件完全不必关心用户容器的启动与否,而只需要关注如何配置 Pod,也就是 Infra 容器的 Network Namespace 即可。

pod内的存储共享

  • k8s 项目只要把所有 Volume 的定义都设计在 Pod 层级即可。这样,一个 Volume 对应的宿主机目录对于 Pod 来说就只有一个,Pod 里的容器只要声明挂载这个 Volume,就一定可以共享这个 Volume 对应的宿主机目录

容器设计模式(用Pod的设计思想来指导容器编排)

  • pod配置中的重要字段
    • NodeSelector:将pod与node绑定,配置了NodeSelector后,该pod只能被调度到label上被打了对应标签的node节点上
    • NodeName:被赋值后,pod就会被认定为已经经过了调度,调度的结果会给这个字段赋值。在调试阶段,如果直接给NodeName赋值,则会跳过kube-scheduler调度,直接在目标的node上部署,流程为
      • kube-scheduler监听api-server上的pod的NodeName字段是否为空 -> 为空则从k8s节点中根据资源定义属性(quota资源量、NodeSelector、taint)选择合适的调度节点,并把Node名填到pod的NodeName字段上 -> apiserver根据nodename字段的主机名,通知对应节点上的kubelet组件来读取对应pod资源定义-> kubelet从apiserver读取对应pod资源定义清单,根据资源清单中定义的属性,把对应pod运行起来 -> kubelet把pod的状态反馈给api-server,由api-server持久化到etcd中(节点状态的汇报需要等到kubelet下一个定时周期,因此api-server上节点状态的更新存在一定的延迟
    • HostAliases:定义了 Pod 的 hosts 文件(比如 /etc/hosts),如果要设置 hosts 文件里的内容,一定要通过这种方法。否则,如果直接修改了 hosts 文件的话,在 Pod 被删除重建之后,kubelet 会自动覆盖掉被修改的内容
    • shareProcessNamespace:是否要共享PID namespace。凡是跟容器的 Linux Namespace 相关的属性,也一定是 Pod 级别的。这个原因也很容易理解:Pod 的设计,就是要让它里面的容器尽可能多地共享 Linux Namespace,仅保留必要的隔离和限制能力
      apiVersion: v1
      kind: Pod
      metadata:
        name: nginx
      spec:
        shareProcessNamespace: true
        containers:
        - name: nginx
          image: nginx
        - name: shell
          image: busybox
          stdin: true
          tty: true
      
      • stdin:容器的标准输入保持打开,以便允许用户与容器进行交互。这允许用户向容器发送输入,例如键盘输入
      • tty: Docker 分配一个终端(TTY)设备,以便在容器内部显示命令行界面。
      • hostPID/hostIPC/hostNetwork:共享宿主机的 Network、IPC 和 PID Namespace,Pod 里的所有容器,会直接使用宿主机的网络(hostNetwork)、直接与宿主机进行 IPC 通信(hostIPC)、看到宿主机里正在运行的所有进程(hostPID)。
      • container
        • Image 镜像
        • command:启动命令
        • workingDir:工作目录
        • Ports:容器端口号
        • VolumesMount:数据卷挂载
        • ImagePullPolicy:镜像拉取策略
          • 默认值Always:每次创建 Pod 都重新拉取一次镜像,当远程仓库的镜像信息与本地相同则跳过;当远程仓库的镜像信息与本地不同则拉取
          • ifnotpresent:优先使用本地,当本地不存在,则拉取远程仓库镜像;当本地存在,无论远程仓库镜像信息与本地是否相同都使用本地的
          • Never :从来不拉取新镜像
        • Lifecycle:定义的是 Container Lifecycle Hooks,作用是在容器状态发生变化时触发一系列“钩子”
          • postStart:在容器启动后立刻执行一个指定的操作。需要明确的,postStart 定义的操作虽然是在 Docker 容器 ENTRYPOINT 执行之后,但它并不严格保证顺序。也就是说,在 postStart 启动时,ENTRYPOINT 有可能还没有结束。如果 postStart 执行超时或者错误,k8s 会在该 Pod 的 Events 中报出该容器启动失败的错误信息,导致 Pod 也处于失败的状态
          • preStop:preStop 发生在容器被杀死之前(比如,收到了 SIGKILL 信号)。而需要明确的是,preStop 操作的执行,是同步的。所以,它会阻塞当前的容器杀死流程,直到这个 Hook 定义操作完成之后,才允许容器被杀死
      • 生命周期(在status.phase中),可能有以下几个状态
        • Pending:Pod 的 YAML 文件已经提交给了 k8s,API 对象已经被创建并保存在 Etcd 当中。但是,这个 Pod 里有些容器因为某种原因而不能被顺利创建,如资源不足、程序启动失败、依赖不满足
        • Running:Pod 已经调度成功,跟一个具体的节点绑定。它包含的容器都已经创建成功,并且至少有一个正在运行中。
        • Succeeded:Pod 里的所有容器都正常运行完毕,并且已经退出了。如job
        • Failed:Pod 里至少有一个容器以不正常的状态(非 0 的返回码)退出。这个状态的出现,意味着要 Debug 这个容器的应用,比如查看 Pod 的 Events 和日志
        • Unknown:异常状态,意味着 Pod 的状态不能持续地被 kubelet 汇报给 kube-apiserver,这很有可能是主从节点(Master 和 Kubelet)间的通信出现了问题。
    • conditions(pod.status中的状态)
      • PodScheduled
      • Ready
      • Initialized
      • Unschedulable

Volume

  • projected Volume:在 k8s 中,有几种特殊的 Volume,它们存在的意义不是为了存放容器里的数据,也不是用来进行容器和宿主机之间的数据交换。这些特殊 Volume 的作用是为容器提供预先定义好的数据。到目前为止,k8s 支持的 Projected Volume 一共有四种:
  • Secret:存放加密的键值对(默认是base64加密存放,在真正的生产环境中需要在 k8s 中开启 Secret 的加密插件,增强数据的安全性。)
    • configmap:与Secret类似,唯一区别是不加密
    • downward api:让 Pod 里的容器能够直接获取到这个 Pod API 对象本身的信息,不过Downward API 能够获取到的信息,一定是 Pod 里的容器进程启动之前就能够确定下来的信息。而如果想要获取 Pod 容器运行后才会出现的信息,比如,容器进程的 PID,那就肯定不能使用 Downward API 了,而应该考虑在 Pod 里定义一个 sidecar 容器。
    • serviceAccountToken:Service Account 的授权信息和文件保存在它所绑定的一个特殊的 Secret 对象里的。这个特殊的 Secret 对象,就叫作 ServiceAccountToken
      • serviceAccount:一种“服务账户”,它是 k8s 进行权限分配的对象。比如,Service Account A,可以只被允许对 Kubernetes API 进行 GET 操作,而 Service Account B,则可以有 Kubernetes API 的所有操作权限。
      • k8s 提供了一个默认“服务账户”(default Service Account)。并且,任何一个运行的 Pod,都可以直接使用这个默认的 Service Account,而无需显示地声明挂载(依靠projected Volume)。一旦 Pod 创建完成,容器里的应用就可以直接从这个默认 ServiceAccountToken 的挂载目录里访问到授权信息和文件,这种把 Kubernetes 客户端以容器的方式运行在集群里,然后使用 default Service Account 自动授权的方式,被称作“InClusterConfig”。

健康检查和恢复机制

  • 定义一个健康检查“探针”(LivenessProbe)。这样,kubelet 就会根据这个 Probe 的返回值决定这个容器的状态,而不是直接以容器镜像是否运行(来自 Docker 返回的信息)作为依据。
  • 当pod为unhealthy状态时,会根据restartPolicy确认pod的恢复机制,需要注意,Pod 的恢复过程,永远都是发生在当前节点上,而不会跑到别的节点上。事实上,一旦一个 Pod 与一个节点(Node)绑定,除非这个绑定发生了变化(pod.spec.node 字段被修改),否则它永远都不会离开这个节点。这也就意味着,如果这个宿主机宕机了,这个 Pod 也不会主动迁移到其他节点上去(如果要调度到其他节点上,需要修改deployment)
    • restartPolicy
      • 默认为Always,只要容器不在运行状态,就自动重启容器
      • OnFailure: 只在容器异常时才自动重启容器
      • Never: 从来不重启容器
    • restart policy和pod状态对应关系
      • 只要 Pod 的 restartPolicy 指定的策略允许重启异常的容器(比如:Always),那么这个 Pod 就会保持 Running 状态,并进行容器重启。否则,Pod 就会进入 Failed 状态
      • 对于包含多个容器的 Pod,只有它里面所有的容器都进入异常状态后,Pod 才会进入 Failed 状态。在此之前,Pod 都是 Running 状态
      • readinessProbe,探针检查结果成功与否,决定了这个 Pod 是不是能被通过 Service 的方式访问到,而并不影响 Pod 的生命周期

控制器模型

  • Pod 这个看似复杂的 API 对象,实际上就是对容器的进一步抽象和封装而已。k8s中所有控制器都遵循一个通用的编排模式:即控制循环,循环对比实际状态(status)和期望状态(spec),然后执行动作到达期望状态
    • 实际状态来源:kubelet通过心跳汇报、监控系统中保存的应用数据、控制器自己收集
    • 期望状态来源:创建deployment创建的yaml
  • deployment控制器模型实现
    • 从etcd中获取到携带了selecctor中matchLabels标签的pod,并获取状态(实际状态)
    • deployment的yaml中定义了期望状态
    • deployment对两个状态作对比,根据结果对pod进行写操作。这个阶段也成reconcile,调谐循环或同步循环,最终结果是某种对象的写操作
  • 被控制对象的定义来源于模板template字段

副本与水平扩展

  • pod的水平扩容/伸缩,滚动更新都依赖的是replicaset
    • 一个 ReplicaSet 对象,是由副本数目的定义和一个 Pod 模板组成的,定义就是deployment的一个子集
    • Deployment 控制器实际操纵的,正是这样的 ReplicaSet 对象,而不是 Pod 对象
    • 滚动更新的好处:
      • 在滚动更新过程中,如果新版本pod有问题启动不起来,那么“滚动更新”就会停止,从而允许开发和运维人员介入。而在这个过程中,由于应用本身还有旧版本的 Pod 在线,所以服务并不会受到太大的影响。
    • 为了保证服务连续性,在任何时间窗口内,只有指定比例的 Pod 处于离线状态。同时,它也会确保只有指定比例的新 Pod 被创建出来
  • 通过多个 ReplicaSet 对象,Kubernetes 项目就实现了对多个“应用版本”的描述。
    • 之所以使用ReplicaSet对象来管理pod对象,而不是deployment亲自管理pod对象;原因就在于通过ReplicaSet对象管理,可以实现通过一个Deployment对象来管理应用程序多个版本的更新和回退。 即抽象出ReplicaSet对象这个概念,可以将Deployment对象与pod对象进行解耦,使得Deployment对象可以更加关注于对应用程序版本更新/回退的管控,而弹性伸缩的细节就可以让ReplicaSet对象来协助管理。 如果Deployment对象只需要提供pod弹性伸缩的功能,则本质上是不需要ReplicaSet对象的。

statefulset

  • deployment这种控制器模型不能覆盖所有的应用编排场景。因为对deployment来说,一个应用的所有 Pod,是完全一样的。所以,它们互相之间没有顺序,也无所谓运行在哪台宿主机上。需要的时候,Deployment 就可以通过 Pod 模板创建新的 Pod;不需要的时候,Deployment 就可以“杀掉”任意一个 Pod
  • 在实际场景中(尤其是分布式应用),多个实例间往往存在依赖关系,比如:主从关系、主备关系,在这种实例之间不对等的关系情况下,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application)
  • statefulset设计
    • 拓扑状态:应用的多个实例之间不是完全对等的关系。这些应用实例必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。
    • 存储状态:多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。

statefulset拓扑状态保持

  • headless service
    • 集群内可以通过访问svc访问到pod,访问svc有两种方式
      • 通过svc的虚拟IP
      • 通过DNS(svc.namespace.svc.cluster.local),该方式还可以分为两个方法
        • Normal Service。这种情况下,访问解析到的是 svc 这个 Service 的 虚拟IP,后面的流程就跟方式一一致
        • Headless Service。这种情况下访问解析到的直接就是 svc 代理的某一个 Pod 的 IP 地址。这里的区别在于,Headless Service 不需要分配一个虚拟IP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址,适用于分布式集群场景
          • 直接访问每个 Pod:在分布式存储系统中,通常需要直接与每个存储节点进行通信,而不是通过负载均衡器或代理,Headless Service 可以通过 DNS 解析直接获取每个Pod的 IP 地址列表,使你可以直接与每个节点建立连接
          • 简化服务发现:分布式存储系统通常需要对存储节点进行动态的服务发现,以便将请求路由到可用的节点。使用 Headless Service,你可以通过 DNS 查询获得存储节点的 IP 地址,无需复杂的服务发现机制,从而简化了服务发现的过程
          • 无需额外负载均衡层:在一些分布式存储系统中,每个存储节点本身就具备负载均衡和故障转移的能力。通过直接访问每个存储节点,你可以利用存储系统自身的负载均衡机制,无需引入额外的负载均衡层,从而减少了系统的复杂性和性能开销
        • 灵活的部署和扩展:使用 Headless Service,每个存储节点都可以以独立的 Pod 形式部署和扩展。这使得分布式存储系统可以根据需求动态地添加或删除存储节点,而无需影响整体服务的可用性和性能。
      • Headless service 与 service vip最本质的区别就是
        • headless service -->> coredns -->> podip ,dns直接代理所有的pod。pod之间可以互相访问,这样对于一些集群类型的应用就可以解决互相之间身份识别的问题了
        • service -->> coredns -->> vip(clusterip) -->> 通过iptables规则负载pod -->> real server (podip)
      • headless service代理的所有pod的ip地址,都会被绑定到DNS记录中,只要有pod、svc名称就能找到对应pod的ip地址
  • sts的yaml,相比于deployment,多了serviceName字段,这个字段的作用,就是告诉 sts 控制器,在执行控制循环的时候,使用 serivceName字段值的 这个 Headless Service 来保证 Pod 的“可解析身份”。
  • sts会给它管理的所有 Pod 进行编号,从 0 开始累加,与 sts的每个 Pod 实例一一对应,绝不重复。同时创建也是严格按照编号顺序进行。比如,在 web-0 进入到 Running 状态、并且Conditions成为 Ready 之前,web-1 会一直处于 Pending 状态
  • 通过headless service可以做到直接通过域名访问的方式直接访问指定的pod,而不是访问通过VIP访问到svc,再由svc负载均衡到某一个pod。k8s为每个pod通过名称+编号的形式固定了pod的拓扑关系,也为每个pod提供了唯一的访问入口

statefulset存储状态保持

  • 如果每个应用自己申请声明volume,有几个问题
    • Volume的每个字段难以理解
    • 安全问题,类似如IP、秘钥文件路径这些直接暴露给开发人员不安全
  • 因此引入了pvc和pv,降低了使用持久化Volume的门槛,有了pvc后声明使用pv只需要两步
    • 定义一个pvc,声明想要Volume的属性,如**存储大小、读写模式(读写模式包括:
      • readyWriterOnce(RWO):只能在一个节点上被读写,不能跨节点声明
      • readOnlyMany(ROX):只读,可以被多个节点挂载声明
      • readWriteMany(RWX):可读写,可以被多个节点挂载声明
      • readyWriterOncePod(RWOP):可读写,只能被一个pod声明**
    • 在应用中,声明使用pvc
  • k8s 中 PVC 和 PV 的设计,实际上类似于“接口”和“实现”的思想。开发者只要知道并会使用“接口”,即:PVC;而运维人员则负责给“接口”绑定具体的实现,即:PV。
  • PVC 与 PV 的绑定得以实现的前提是系统里存在创建好了符合条件的 PV;或者,k8s 集群运行在公有云上,这样 k8s 就会通过 Dynamic Provisioning 的方式,自动为你创建与 PVC 匹配的 PV

statefulset原理

  • StatefulSet 的控制器直接管理的是 Pod(pod的ownerReference就是sts)。这是因为,StatefulSet 里的不同 Pod 实例,不再像 ReplicaSet 中那样都是完全一样的,而是有了细微区别的。比如,每个 Pod 的 hostname、名字(序号不仅表示了启动顺序,也标志不同的网络标识)等都是不同的,而 StatefulSet 区分这些实例的方式,就是通过在 Pod 的名字里加上事先约定好的编号。
  • k8s 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器(coreDNS)中生成带有同样编号的 DNS 记录。只要 StatefulSet 能够保证这些 Pod 名字里的编号不变,那么 Service 里类似于 web-0.nginx.default.svc.cluster.local 这样的 DNS 记录也就不会变,而这条记录解析出来的 Pod 的 IP 地址,则会随着后端 Pod 的删除和再创建而自动更新
  • StatefulSet 为每一个 Pod 分配并创建一个同样编号的 PVC(通过volumeClaimTemplates)。这样,Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有一个独立的 Volume。
  • sts的滚动更新是从后往前进行的,原因为
    • 有状态应用的依赖关系:StatefulSet 中的有状态应用通常存在依赖关系,例如主从复制、分布式存储等。以数据库为例,如果先更新了前面的 Pod,那么后面的 Pod 在更新时,外部请求可能会连接到一个已经更新的 Pod,导致数据不一致或中断服务。因此,从后往前的更新顺序可以确保更新的 Pod 在进行连接和同步时始终连接到稳定的旧版本 Pod
    • 稳定的网络标识符:有状态应用通常会使用稳定的网络标识符(如主机名或 DNS 记录)来标识彼此。当一个有状态应用的 Pod 更新时,会保留其稳定的网络标识符,以便其他应用或服务能够继续与其进行通信。从后往前的更新顺序可以确保更新的 Pod 在更新过程中能够保持其稳定的网络标识符,避免应用之间的连接中断。
    • 数据迁移和持久化存储:有状态应用通常需要迁移数据或使用持久化存储。从后往前的更新顺序可以使得更新的 Pod 在更新过程中能够正确地迁移数据或重新连接到持久化存储,确保数据的完整性和可用性。
  • sts的滚动更新还能做到更精细的控制,如金丝雀分布、灰度发布
    • 金丝雀发布是一种将新版本或功能引入到现有生产环境中的策略,但只在一小部分用户或系统上进行测试和评估。它的目标是在真实环境中评估新版本或功能的性能、稳定性和用户体验,以便在全面部署之前及时发现和解决任何问题
    • 灰度发布是**一种逐步引入新版本或功能的策略,通过逐渐增加受影响的用户或系统来降低风险。**它的目标是在控制风险的同时,逐步将新版本或功能推广给更多的用户或系统,以确保其稳定性和可用性。
  • sts中rollingUpdate中partition字段值控制可以实现灰度发布(只有序号大于等于partition的pod才会被更新,序号小于partition参数的pod不会被更新)

daemonset

  • 管理的daemon pod特点
    • Pod 运行在 k8s 集群里的每一个节点(Node)上
    • 每个节点上只有一个这样的 Pod 实例
    • 当有新的节点加入集群后,该 Pod 会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。
  • 常用场景
    • 网络插件Agent 组件用来处理这个节点上的容器网络
    • 存储插件的 Agent 组件用来在这个节点上挂载远程存储目录,操作容器的 Volume 目录
    • 各种监控组件和日志组件负责这个节点上的监控信息和日志搜集
  • DaemonSet 开始运行的时机很多时候比整个k8s集群出现的时机都早(如网络插件),控制流程
    • daemonset controller从etcd中获取到node列表,基于pod的label selector遍历所有node节点是否在节点上有这个pod
    • 没有则启动
    • 有这种 Pod,但是数量大于 1,那就说明要把多余的 Pod 从这个 Node 上删除掉
    • 正好只有一个这种 Pod,那说明这个节点是正常的
  • 创建时需要指定node节点,采用nodeSelector(deprecated),或者nodeAffinity
  • daemonset在向集群提交创建pod时,还会给pod添加上tolerations,使调度能够容忍一些taint,达到需求目的
  • 该字段能够让该pod在未就绪node节点上部署,尽管节点未就绪可能会导致pod启动失败,但daemonset会一直尝试直到pod启动成功
    apiVersion: v1
    kind: Pod
    metadata:
      name: with-toleration
    spec:
      tolerations:
      - key: node.kubernetes.io/unschedulable
        operator: Exists
        effect: NoSchedule
    

Job与CronJob

  • Deployment、StatefulSet,以及 DaemonSet主要编排的是Long running work
  • job controller工作原理
    • Job Controller 控制的对象直接就是 Pod
    • 其次,Job Controller 在控制循环中Reconcile操作,是根据实际在 Running 状态 Pod 的数目成功退出的 Pod 的数目,以及 parallelism(最大并发)completions (需要成功运行完成的数量)参数的值共同计算出在这个周期里,应该创建或者删除的 Pod 数目,然后调用 Kubernetes API 来执行这个操作
  • 三种常用的用法
    • 外部管理器 +Job 模板
    • 拥有固定任务数目的并行 Job:只关心最后是否有指定数目(completions)个任务成功退出,不关心并行度
    • 指定并行度(parallelism),但不设置固定的 completions 的值
  • CronJob:CronJob是job对象的控制器,yaml中最关键就是jobTemplate(CronJob与Job的关系类似于Deployment与replicaset的关系)
  • 由于定时任务的特殊性,很可能某个 Job 还没有执行完,另外一个新 Job 就产生了,需要通过concurrencyPolicy 字段来定义具体的处理策略
    • Allow(默认情况):Job 可以同时存在
    • Forbid:不会创建新的 Pod,该创建周期被跳过
    • Replace:新产生的 Job 会替换旧的、没有执行完的 Job
  • startingDeadlineSeconds:如果job创建失败就会被标记为“miss”。当DeadlineSeconds范围内,miss 的数目达到 100 时,那么 CronJob 会停止再创建这个 Job

k8s声明式与命令式

  • 对现有api对象的修改(Patch)称为声明式API,比如apply、set、edit,api-server在响应声明式请求的时候,一次能处理多个写操作,并具备merge能力
    • 例如istio给被治理服务的sidecar注入
  • 替换现有对象是一种命令式操作,比如replace,api-server在响应命令式请求时,只能处理一个写请求,否则可能产生冲突
  • 每一个API对象在etcd里的的资源路径(定义一个资源对象):资源对象组->资源版本->资源名称,资源对象创建过程:
    • 提交资源对象的yaml信息到api-server,apiserver过滤请求,前置工作(如授权、超时处理、审计)
    • 请求进入MUX和Routes过程,完成url和handler绑定,找到对应Cronjob类型定义
    • apiserver根据CronJob定义,使用yaml定义创建CronJob对象,在这个过程中,会进行convert操作,即把用户提交的yaml转成superVerson对象(API 资源类型所有版本的字段全集)
  • Admission和Validation操作,验证通过的api对象会存入apiserver里的registry
  • apiserver 把验证过的 API 对象转换成用户最初提交的版本,进行序列化操作,并调用 Etcd 的 API 把它保存起来

添加一个自定义对象

添加一个自定义对象流程

  • 在GOAPTH路径下,创建结构项目(也可以自定义一个yaml,然后通过operator自动生成)
$GOPATH/src/github.com/<your-name>/k8s-controller-custom-resource
.
├── controller.go
├── crd
│   └── network.yaml
├── example
│   └── example-network.yaml
├── main.go
└── pkg
    └── apis
        └── samplecrd //groupname
            ├── register.go
            └── v1 // version
                ├── doc.go
                ├── register.go ///放置后面要用到的全局变量
                └── types.go  // crd struct
  • 通过代码生成工具code-generator为自定义资源生成client、informer、lister
├── controller.go
├── crd
│   └── network.yaml
├── example
│   └── example-network.yaml
├── main.go
└── pkg
    ├── apis
    │   └── samplecrd
    │       ├── constants.go
    │       └── v1
    │           ├── doc.go
    │           ├── register.go
    │           ├── types.go
    │           └── zz_generated.deepcopy.go //自动生成的 DeepCopy 代码文件
    └── client
        ├── clientset
        ├── informers
        └── listers

添加一个自定义对象控制器

  • 声明式 API不像命令式 API有着明显的执行逻辑。使得基于声明式 API 的业务功能实现,往往需要通过控制器模式来“监视”API 对象的变化,然后以此来决定实际要执行的工作。
import (
    "fmt"
)

func main() {

    stopCh := signals.SetupSignalHandler()

    cfg, err := clientcmd.BuildConfigFromFlags(masterUrl, kubeconfig)

    kubeClient, err := kubernetes.NewForConfig(cfg)

    networkCleint, err := clientset.NewForConfig(cfg)

    networkInformerFactory := informers.NewSharedInformerFactory(networkClient, ...)
    
    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())
	}
    
}
  • 自定义对象控制器流程
    • 根据Master 配置(APIServer 的地址端口和 kubeconfig 的路径),创建一个 Kubernetes 的 client(kubeClient)和 Network 对象的 client(networkClient)
    • 创建一个叫作 InformerFactory的工厂,并使用它生成一个 Network 对象的 Informer,传递给控制器
    • 启动Informer,然后执行 controller.Run,启动自定义控制器

自定义控制器原理

  • informer的两个职责,本质上就是一个带有本地缓存和索引机制的、可以注册eventHandler的client
    • 同步本地缓存
    • 根据事件类型,触发事先注册好的 ResourceEventHandler
      在这里插入图片描述
  • 控制器首先从apiserver里获取关心的对象Informer 与 API 对象是一一对应的,在创建informer的时候,传递一个client,正是这个client与apiserver建立了连接,负责维护连接的则是informer使用的reflector(使用内置的ListAndWatch方法获取和监听资源)
  • 在ListAndWatch监听下,当监视的自定义资源对象发生变化,reflector会收到“事件通知”,这时该增量变化会被放进Delta FIFO队列中
  • 发生变化的包含:
    • Create:k8s对象的创建流程为:
      • 判断对象的 resourceVersion 是否合法,如果 resourceVersion != 0,则抛出错误
      • 对待处理对象做一些预处理:把 resourceVersion 和 selfLink 置为空
      • 对待处理对象进行编码,转换成二进制,进而转换成可被 ETCD 接受的格式
      • 判断 key 是否已存在,如果不存在,则存入 ETCD,否则返回错误信息
      • 记录执行耗时
      • 返回存储好的数据,并将 ETCD 中更新后的 Revision 设置为 resourceVersion
    • Update:k8s对于更新操作提供了update和patch两种方法,但判断冲突的机制是相同的
      • 获取当前更新请求中 obj 对象的 ResourceVersion 值,及服务器端最新 obj 对象 (existing) 的 ResourceVersion 值
      • 如果当前更新请求中 bj 对象的 ResourceVersion 值等于 0,即客户端未设置该值,则判断是否要硬改写 (AllowUnconditionalUpdate),如配置为硬改写策略,将直接更新 obj 对象
      • 如果当前更新请求中 obj 对象的 ResourceVersion 值不等于 0,则判断两个 ResourceVersion 值是否一致,不一致返回冲突错误 (OptimisticLockErrorMsg)
    • Patch
      • 首先判断 patch 的类型,根据类型选择相应的 mechanism
      • 利用 DefaultUpdatedObjectInfo 方法将 applyPatch (应用 Patch 的方法) 添加到 admission chain 的头部
      • 最终还是调用 Update 方法执行更新操作
    • Delete
      • 判断目标对象类型是否正确:是否为指针类型,是否不为 nil
      • 删除之前,先从 ETCD 中获取对应的数据,并判断该删除操作是否满足前置条件
      • 通过比对 ModVersion 判断这段时间内目标对象是否被其他进程 / 线程修改,如果未被修改,则执行删除操作;否则执行 Get 操作,删除失败,打印错误信息,并重新尝试删除
      • 删除成功,返回被删除的数据
  • Informer 会不断地从这个 Delta FIFO Queue 里读取(Pop)增量。每拿到一个增量,Informer 就会判断这个增量里的事件类型,然后创建或者更新本地对象的缓存。这个缓存,在 Kubernetes 里一般被叫作 Store。比如,如果事件类型是添加对象,Informer 就会通过Indexer 的库把这个增量里的 API 对象保存在本地缓存中,并为它创建索引。相反,如果增量的事件类型是 Deleted(删除对象),那么 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 {
        return
      }
      controller.enqueueNetwork(new)
    },
    DeleteFunc: controller.enqueueNetworkForDelete,
 return controller
}
  • work Queue的作用同步informer和reconcile之间的数据
  • eventHandler里定义了3个func,具体操作都是将事件放入workQueue,但入队的不是API对象,而是API对象的key,在reconcile中,则会不断地从这个workQueue里拿到这些 Key,然后开始执行真正的控制逻辑
  • Informer本质上是自定义控制器跟 APIServer 进行数据同步的重要组件,具体来说,informer通过ListWatch把apiserver中的api对象缓存到本地,并负责维护和更新这个缓存(先更新本地缓存,然后触发eventHandler)
  • 在reconcile过程中,每经过 resyncPeriod 指定的时间,Informer 维护的本地缓存都会使用最近一次 LIST 返回的结果强制更新一次,从而保证缓存的有效性。这个缓存强制更新的操作就叫作:resync。

operator

  • 利用k8s的自定义资源来描述想要部署的有状态应用,然后通过在自定义控制器里,根据自定义对象的变化,来完成对应的控制逻辑(应用部署和运维)。
  • Operator 本质上就是一种自定义控制器(Custom Controller)。但是,Operator 与其他自定义控制器还是有一些不同之处:
    • 控制的资源类型不同:其他自定义控制器可能只是控制一些业务逻辑或者某些资源的变化,Operator 则专注于部署和管理有状态应用程序,如数据库、消息队列等复杂的应用。
    • 复杂性和自主性更强:Operator 需要处理应用程序的复杂部署、扩缩容、备份恢复等全生命周期管理,相比之下,其他自定义控制器通常只需要实现一些相对简单的业务逻辑。
    • 面向应用程序的设计:Operator 是以应用程序为中心进行设计的,关注的是应用程序的整体状态和行为,其他自定义控制器则更多地关注某些特定的资源对象及其变化。
  • 但是,crd并不适合所有场景
    • CRD 目前不支持 protobuf,当 API Object 数量 >1K,或者单个对象 >1KB,或者高频请求时,CRD 的响应都会有问题。 所以,CRD 千万不能也不应该被当作数据库使用。
    • 像 k8s ,或者说 Etcd 本身,最佳的使用场景就是作为配置管理的依赖。如果业务需求不能用 CRD 进行建模的时候,比如,需要等待 API 最终返回,或者需要检查 API 的返回值,也是不能用 CRD 的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值